@browserless/screenshot 13.6.3 → 13.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/wait-for-ready.js +114 -5
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@browserless/screenshot",
|
|
3
3
|
"description": "Capture high-quality screenshots of websites with overlay support, device emulation, and automated image optimization.",
|
|
4
4
|
"homepage": "https://browserless.js.org/#/?id=screenshoturl-options",
|
|
5
|
-
"version": "13.6.
|
|
5
|
+
"version": "13.6.5",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"author": {
|
|
8
8
|
"email": "hello@microlink.io",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@browserless/errors": "13.1.7",
|
|
36
|
-
"@browserless/goto": "13.6.
|
|
36
|
+
"@browserless/goto": "13.6.5",
|
|
37
37
|
"@kikobeats/content-type": "~1.0.4",
|
|
38
38
|
"@kikobeats/time-span": "~1.0.12",
|
|
39
39
|
"automad-prism-themes": "~0.3.7",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"svg-gradient": "~1.0.4"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@browserless/test": "13.6.
|
|
55
|
+
"@browserless/test": "13.6.5",
|
|
56
56
|
"ava": "5"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"timeout": "2m",
|
|
72
72
|
"workerThreads": false
|
|
73
73
|
},
|
|
74
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "679c9a8f0db8a6a98bb54fd3d19532ea8a64d76b"
|
|
75
75
|
}
|
package/src/wait-for-ready.js
CHANGED
|
@@ -22,14 +22,72 @@ const { isContextDestroyed } = require('@browserless/errors')
|
|
|
22
22
|
// inside the viewport, and not hidden via CSS — so a blank shell can't pass for
|
|
23
23
|
// content. `text` is the equivalent paint signal for imageless pages: visible
|
|
24
24
|
// characters inside the viewport, counted up to 200 (the threshold consumers
|
|
25
|
-
// rely on)
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
25
|
+
// rely on) and skipping text whose color matches its effective background
|
|
26
|
+
// (invisible on capture), and `fonts` reports whether webfonts finished
|
|
27
|
+
// loading — during a `font-display: block` period text renders invisible,
|
|
28
|
+
// exactly when a capture would be white. `covered` reports whether the counted
|
|
29
|
+
// content hides behind an opaque viewport-covering layer (a fixed white
|
|
30
|
+
// loading overlay passes every DOM signal while a capture stays white):
|
|
31
|
+
// hit-testing catches interactive overlays, and a root-level scan catches
|
|
32
|
+
// `pointer-events: none` layers hit-testing can't see. `viewport` is the
|
|
33
|
+
// in-page viewport height, so consumers can compare it against `height`
|
|
34
|
+
// without relying on `page.viewport()`, which is null under
|
|
35
|
+
// `defaultViewport: null`.
|
|
30
36
|
const snapshot = () => {
|
|
31
37
|
const vw = window.innerWidth || document.documentElement.clientWidth
|
|
32
38
|
const vh = window.innerHeight || document.documentElement.clientHeight
|
|
39
|
+
|
|
40
|
+
// Computed colors resolve to `rgb()`/`rgba()`; anything else is unknown.
|
|
41
|
+
const parseColor = value => {
|
|
42
|
+
const match = /rgba?\(([^)]+)\)/.exec(value || '')
|
|
43
|
+
if (!match) return null
|
|
44
|
+
const parts = match[1].split(',').map(parseFloat)
|
|
45
|
+
return { r: parts[0], g: parts[1], b: parts[2], a: parts.length === 4 ? parts[3] : 1 }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// First opaque background up the ancestor chain; the canvas default (white)
|
|
49
|
+
// when every ancestor is transparent.
|
|
50
|
+
const effectiveBackground = el => {
|
|
51
|
+
for (let node = el; node; node = node.parentElement) {
|
|
52
|
+
const color = parseColor(window.getComputedStyle(node).backgroundColor)
|
|
53
|
+
if (color && color.a >= 0.99) return color
|
|
54
|
+
}
|
|
55
|
+
return { r: 255, g: 255, b: 255, a: 1 }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sameColor = (a, b) =>
|
|
59
|
+
Math.abs(a.r - b.r) < 10 && Math.abs(a.g - b.g) < 10 && Math.abs(a.b - b.b) < 10
|
|
60
|
+
|
|
61
|
+
// A layer only blanks a capture when it is itself painted solid: visible,
|
|
62
|
+
// full opacity, and an opaque background color or a background image.
|
|
63
|
+
const isOpaqueLayer = el => {
|
|
64
|
+
const style = window.getComputedStyle(el)
|
|
65
|
+
if (style.visibility === 'hidden' || parseFloat(style.opacity) < 0.99) return false
|
|
66
|
+
const background = parseColor(style.backgroundColor)
|
|
67
|
+
return (background && background.a >= 0.99) || style.backgroundImage !== 'none'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const coversViewport = el => {
|
|
71
|
+
const rect = el.getBoundingClientRect()
|
|
72
|
+
const width = Math.min(rect.right, vw) - Math.max(rect.left, 0)
|
|
73
|
+
const height = Math.min(rect.bottom, vh) - Math.max(rect.top, 0)
|
|
74
|
+
return width > 0 && height > 0 && width * height >= vw * vh * 0.9
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Up to a handful of counted content samples — enough to hit-test without
|
|
78
|
+
// turning the snapshot into a layout storm. The clamp keeps each probe point
|
|
79
|
+
// inside the viewport; the rect checks below guarantee it stays inside the
|
|
80
|
+
// sampled content's own box.
|
|
81
|
+
const contentPoints = []
|
|
82
|
+
const samplePoint = (el, rect) => {
|
|
83
|
+
if (contentPoints.length >= 4) return
|
|
84
|
+
contentPoints.push({
|
|
85
|
+
el,
|
|
86
|
+
x: Math.max(0, Math.min(vw - 1, rect.left + rect.width / 2)),
|
|
87
|
+
y: Math.max(0, Math.min(vh - 1, rect.top + rect.height / 2))
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
33
91
|
const imgs = document.images
|
|
34
92
|
let images = 0
|
|
35
93
|
let decoded = 0
|
|
@@ -58,6 +116,7 @@ const snapshot = () => {
|
|
|
58
116
|
continue
|
|
59
117
|
}
|
|
60
118
|
painted++
|
|
119
|
+
samplePoint(img, rect)
|
|
61
120
|
}
|
|
62
121
|
// Counting stops at 200 chars, so a text-heavy page costs a handful of nodes,
|
|
63
122
|
// not a full DOM walk; only a near-blank shell walks every text node.
|
|
@@ -86,8 +145,56 @@ const snapshot = () => {
|
|
|
86
145
|
const rect = range.getBoundingClientRect()
|
|
87
146
|
if (rect.width === 0 || rect.height === 0) continue
|
|
88
147
|
if (rect.bottom <= 0 || rect.right <= 0 || rect.top >= vh || rect.left >= vw) continue
|
|
148
|
+
// White-on-white (or any color-on-same-color) text passes every geometry
|
|
149
|
+
// check while a capture shows nothing: don't count it as painted text.
|
|
150
|
+
const color = parseColor(window.getComputedStyle(el).color)
|
|
151
|
+
if (color && (color.a < 0.05 || sameColor(color, effectiveBackground(el)))) continue
|
|
89
152
|
text += value.length
|
|
153
|
+
samplePoint(el, rect)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Is this content sample's paint hidden behind an unrelated opaque,
|
|
157
|
+
// viewport-covering layer? Walk up from the hit-tested element: anything
|
|
158
|
+
// related to the sample (itself, an ancestor, a descendant) paints with it,
|
|
159
|
+
// and the walk stops at the first common ancestor — an ancestor's background
|
|
160
|
+
// always paints below its own descendants.
|
|
161
|
+
const coveredAt = ({ el, x, y }) => {
|
|
162
|
+
const top = document.elementFromPoint(x, y)
|
|
163
|
+
if (!top) return false
|
|
164
|
+
for (let node = top; node && node !== document.documentElement; node = node.parentElement) {
|
|
165
|
+
if (node === el || node.contains(el) || el.contains(node)) return false
|
|
166
|
+
if (coversViewport(node) && isOpaqueLayer(node)) return true
|
|
167
|
+
}
|
|
168
|
+
return false
|
|
90
169
|
}
|
|
170
|
+
|
|
171
|
+
// Only pages the fast path could trust get the (layout-forcing) cover check.
|
|
172
|
+
let covered = false
|
|
173
|
+
if (painted > 0 || text >= 200) {
|
|
174
|
+
covered = contentPoints.some(coveredAt)
|
|
175
|
+
// `elementFromPoint` skips `pointer-events: none` elements, exactly how
|
|
176
|
+
// fading loading overlays are styled — scan root-level layers for one.
|
|
177
|
+
// Overlays mount as direct children of <body>; a deeper scan would cost a
|
|
178
|
+
// full styled DOM walk on every poll for a marginal case.
|
|
179
|
+
if (!covered && document.body) {
|
|
180
|
+
const layers = document.body.children
|
|
181
|
+
for (let i = 0; i < layers.length && !covered; i++) {
|
|
182
|
+
const layer = layers[i]
|
|
183
|
+
const style = window.getComputedStyle(layer)
|
|
184
|
+
if (style.pointerEvents !== 'none') continue
|
|
185
|
+
if (
|
|
186
|
+
style.position !== 'fixed' &&
|
|
187
|
+
style.position !== 'absolute' &&
|
|
188
|
+
style.position !== 'sticky'
|
|
189
|
+
) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
if (contentPoints.some(point => layer.contains(point.el))) continue
|
|
193
|
+
if (coversViewport(layer) && isOpaqueLayer(layer)) covered = true
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
91
198
|
return {
|
|
92
199
|
height: document.documentElement.scrollHeight,
|
|
93
200
|
viewport: vh,
|
|
@@ -95,6 +202,7 @@ const snapshot = () => {
|
|
|
95
202
|
decoded,
|
|
96
203
|
painted,
|
|
97
204
|
text,
|
|
205
|
+
covered,
|
|
98
206
|
fonts: !document.fonts || document.fonts.status === 'loaded',
|
|
99
207
|
complete: document.readyState === 'complete'
|
|
100
208
|
}
|
|
@@ -125,6 +233,7 @@ const waitForReady = async (page, { timeout, quietMs = 300, poll = 150 } = {}) =
|
|
|
125
233
|
decoded: 0,
|
|
126
234
|
painted: 0,
|
|
127
235
|
text: 0,
|
|
236
|
+
covered: false,
|
|
128
237
|
fonts: false,
|
|
129
238
|
complete: false
|
|
130
239
|
}
|