@browserless/screenshot 13.6.8 → 13.6.9
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/README.md +0 -1
- package/package.json +4 -4
- package/src/index.js +88 -82
- package/src/prepare-full-document.js +249 -0
- package/src/wait-for-dom.js +1 -20
- package/src/wait-for-ready.js +30 -98
package/README.md
CHANGED
|
@@ -69,7 +69,6 @@ const buffer = await browserless.screenshot('https://example.com', {
|
|
|
69
69
|
| `element` | `string` | — | CSS selector for element screenshot |
|
|
70
70
|
| `codeScheme` | `string` | `'atom-dark'` | Prism.js theme for code highlighting |
|
|
71
71
|
| `waitUntil` | `string` | `'auto'` | When to consider navigation done |
|
|
72
|
-
| `waitForDom` | `number` | `0` | DOM stability window in ms (idle is `waitForDom / 10`, `0` disables DOM wait) |
|
|
73
72
|
| `isPageReady` | `function` | `({ isWhite }) => !isWhite` | Custom readiness predicate for retry loop |
|
|
74
73
|
| `overlay` | `object` | `{}` | Browser overlay options |
|
|
75
74
|
|
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.9",
|
|
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.6.7",
|
|
36
|
-
"@browserless/goto": "13.6.
|
|
36
|
+
"@browserless/goto": "13.6.9",
|
|
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.9",
|
|
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": "c625834b3b37e036432a0756d67eccc149861ad0"
|
|
75
75
|
}
|
package/src/index.js
CHANGED
|
@@ -9,16 +9,18 @@ const isWhiteScreenshot = require('./is-white-screenshot')
|
|
|
9
9
|
const waitForPrism = require('./pretty')
|
|
10
10
|
const prettyTimeSpan = require('./time-span')
|
|
11
11
|
const overlay = require('./overlay')
|
|
12
|
-
const { waitForDomStability
|
|
13
|
-
const { waitForReady } = require('./wait-for-ready')
|
|
12
|
+
const { waitForDomStability } = require('./wait-for-dom')
|
|
13
|
+
const { waitForReady, paintSignals } = require('./wait-for-ready')
|
|
14
|
+
const {
|
|
15
|
+
expandOverflow,
|
|
16
|
+
scrollFullPageToLoadContent,
|
|
17
|
+
prepareFullDocument,
|
|
18
|
+
resolveScrollTimeout,
|
|
19
|
+
tryHydrateScroll
|
|
20
|
+
} = require('./prepare-full-document')
|
|
14
21
|
|
|
15
22
|
const timeSpan = require('@kikobeats/time-span')()
|
|
16
23
|
|
|
17
|
-
// Retry a page capture (screenshot/pdf) that races with a client-side
|
|
18
|
-
// navigation. When the execution context is destroyed mid-capture, the page is
|
|
19
|
-
// navigating: wait for it to settle via `waitUntilAuto` and retry in-place,
|
|
20
|
-
// bounded by `timeout`, rather than failing the whole request. SPAs (e.g.
|
|
21
|
-
// scribd) navigate client-side after load, so the initial capture often races.
|
|
22
24
|
const captureWithNavigationRetry = async (capture, { page, goto, timeout }) => {
|
|
23
25
|
const elapsed = timeSpan()
|
|
24
26
|
while (true) {
|
|
@@ -32,7 +34,7 @@ const captureWithNavigationRetry = async (capture, { page, goto, timeout }) => {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
const
|
|
37
|
+
const getPageMeta = page =>
|
|
36
38
|
page.evaluate(() => ({
|
|
37
39
|
title: document.title || '',
|
|
38
40
|
bodyText: document.body ? document.body.innerText || '' : '',
|
|
@@ -41,6 +43,18 @@ const getPageSnapshot = page =>
|
|
|
41
43
|
|
|
42
44
|
const defaultIsPageReady = ({ isWhite }) => !isWhite
|
|
43
45
|
|
|
46
|
+
const checkPageReady = async (page, { isPageReady, response, screenshot, isWhite } = {}) => {
|
|
47
|
+
let pageMeta = {}
|
|
48
|
+
if (isPageReady !== defaultIsPageReady) {
|
|
49
|
+
const pageMetaResult = await pReflect(getPageMeta(page))
|
|
50
|
+
pageMeta = pageMetaResult.isRejected ? {} : pageMetaResult.value
|
|
51
|
+
}
|
|
52
|
+
const pageReadyResult = await pReflect(
|
|
53
|
+
isPageReady({ page, response, screenshot, isWhite, isWhiteScreenshot, ...pageMeta })
|
|
54
|
+
)
|
|
55
|
+
return !pageReadyResult.isRejected && !!pageReadyResult.value
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
const getBoundingClientRect = element => {
|
|
45
59
|
const { top, left, height, width, x, y } = element.getBoundingClientRect()
|
|
46
60
|
return { top, left, height, width, x, y }
|
|
@@ -64,41 +78,6 @@ const waitForImagesOnViewport = page =>
|
|
|
64
78
|
)
|
|
65
79
|
)
|
|
66
80
|
|
|
67
|
-
const scrollFullPageToLoadContent = async (page, timeout) => {
|
|
68
|
-
const debug = require('debug-logfmt')('browserless:goto')
|
|
69
|
-
|
|
70
|
-
const duration = debug.duration()
|
|
71
|
-
const result = await page.evaluate(waitForDomStability, {
|
|
72
|
-
idle: timeout / 2 / 2,
|
|
73
|
-
timeout: timeout / 2
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
duration('waitForDomStability', result)
|
|
77
|
-
|
|
78
|
-
await page.evaluate(
|
|
79
|
-
timeout =>
|
|
80
|
-
new Promise(resolve => {
|
|
81
|
-
let currentScrollPosition = 0
|
|
82
|
-
const scrollStep = Math.floor(window.innerHeight)
|
|
83
|
-
const pageHeight = document.body.scrollHeight
|
|
84
|
-
const totalSteps = Math.ceil(pageHeight / scrollStep)
|
|
85
|
-
const stepDelay = timeout / 2 / totalSteps
|
|
86
|
-
const scrollNext = async () => {
|
|
87
|
-
if (currentScrollPosition >= pageHeight) {
|
|
88
|
-
resolve()
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
window.scrollBy(0, scrollStep)
|
|
92
|
-
currentScrollPosition += scrollStep
|
|
93
|
-
setTimeout(scrollNext, stepDelay)
|
|
94
|
-
}
|
|
95
|
-
scrollNext()
|
|
96
|
-
}),
|
|
97
|
-
timeout
|
|
98
|
-
)
|
|
99
|
-
await page.evaluate(() => window.scrollTo(0, 0))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
81
|
const waitForElement = async (page, element) => {
|
|
103
82
|
const screenshotOpts = {}
|
|
104
83
|
if (element) {
|
|
@@ -114,7 +93,6 @@ const SCREENSHOT_DEFAULT_OPTS = {
|
|
|
114
93
|
codeScheme: 'atom-dark',
|
|
115
94
|
overlay: {},
|
|
116
95
|
waitUntil: 'auto',
|
|
117
|
-
waitForDom: DEFAULT_WAIT_FOR_DOM,
|
|
118
96
|
isPageReady: defaultIsPageReady
|
|
119
97
|
}
|
|
120
98
|
|
|
@@ -128,7 +106,6 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
128
106
|
codeScheme = SCREENSHOT_DEFAULT_OPTS.codeScheme,
|
|
129
107
|
overlay: overlayOpts = SCREENSHOT_DEFAULT_OPTS.overlay,
|
|
130
108
|
waitUntil = SCREENSHOT_DEFAULT_OPTS.waitUntil,
|
|
131
|
-
waitForDom = SCREENSHOT_DEFAULT_OPTS.waitForDom,
|
|
132
109
|
isPageReady = SCREENSHOT_DEFAULT_OPTS.isPageReady,
|
|
133
110
|
...opts
|
|
134
111
|
} = {}
|
|
@@ -136,9 +113,17 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
136
113
|
let screenshot
|
|
137
114
|
let response
|
|
138
115
|
|
|
116
|
+
const captureExpanded = (expand, screenshotOpts, timeout) =>
|
|
117
|
+
captureWithNavigationRetry(
|
|
118
|
+
async () => {
|
|
119
|
+
if (expand) await pReflect(expandOverflow(page))
|
|
120
|
+
return page.screenshot(screenshotOpts)
|
|
121
|
+
},
|
|
122
|
+
{ page, goto, timeout }
|
|
123
|
+
)
|
|
124
|
+
|
|
139
125
|
const beforeScreenshot = async (page, response, { element, fullPage = false } = {}) => {
|
|
140
126
|
const timeout = goto.timeouts.action(opts.timeout)
|
|
141
|
-
const waitForDomOpts = resolveWaitForDom(waitForDom)
|
|
142
127
|
|
|
143
128
|
let screenshotOpts = {}
|
|
144
129
|
const tasks = [
|
|
@@ -152,13 +137,6 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
152
137
|
}
|
|
153
138
|
]
|
|
154
139
|
|
|
155
|
-
if (waitForDomOpts) {
|
|
156
|
-
tasks.push({
|
|
157
|
-
fn: () => page.evaluate(waitForDomStability, waitForDomOpts),
|
|
158
|
-
debug: 'beforeScreenshot:waitForDomStability'
|
|
159
|
-
})
|
|
160
|
-
}
|
|
161
|
-
|
|
162
140
|
if (codeScheme && response) {
|
|
163
141
|
tasks.push({
|
|
164
142
|
fn: () => waitForPrism(page, response, { codeScheme, ...opts }),
|
|
@@ -166,12 +144,7 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
166
144
|
})
|
|
167
145
|
}
|
|
168
146
|
|
|
169
|
-
if (fullPage) {
|
|
170
|
-
tasks.push({
|
|
171
|
-
fn: () => scrollFullPageToLoadContent(page, timeout, goto),
|
|
172
|
-
debug: 'beforeScreenshot:scrollFullPageToLoadContent'
|
|
173
|
-
})
|
|
174
|
-
} else if (element) {
|
|
147
|
+
if (element && !fullPage) {
|
|
175
148
|
tasks.push({
|
|
176
149
|
fn: async () => {
|
|
177
150
|
screenshotOpts = await waitForElement(page, element)
|
|
@@ -185,7 +158,7 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
185
158
|
goto.run({
|
|
186
159
|
fn: fn(),
|
|
187
160
|
...opts,
|
|
188
|
-
timeout
|
|
161
|
+
timeout
|
|
189
162
|
})
|
|
190
163
|
)
|
|
191
164
|
)
|
|
@@ -199,34 +172,56 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
199
172
|
let retry = 0
|
|
200
173
|
let isWhite = false
|
|
201
174
|
let isReady = false
|
|
175
|
+
let didHydrateScroll = false
|
|
176
|
+
let didHydrateAttempt = false
|
|
202
177
|
|
|
203
178
|
do {
|
|
204
|
-
screenshot = await captureWithNavigationRetry(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const pageSnapshot = snapshotResult.isRejected ? {} : snapshotResult.value
|
|
212
|
-
const pageReadyResult = await pReflect(
|
|
213
|
-
opts.isPageReady({
|
|
214
|
-
page,
|
|
215
|
-
response: opts.response,
|
|
216
|
-
screenshot,
|
|
217
|
-
isWhite,
|
|
218
|
-
isWhiteScreenshot,
|
|
219
|
-
...pageSnapshot
|
|
220
|
-
})
|
|
179
|
+
screenshot = await captureWithNavigationRetry(
|
|
180
|
+
() =>
|
|
181
|
+
page.screenshot({
|
|
182
|
+
...opts,
|
|
183
|
+
...(opts.fullPage ? { fullPage: false, path: undefined } : {})
|
|
184
|
+
}),
|
|
185
|
+
{ page, goto, timeout }
|
|
221
186
|
)
|
|
222
|
-
|
|
187
|
+
isWhite = await isWhiteScreenshot(screenshot)
|
|
188
|
+
isReady = await checkPageReady(page, {
|
|
189
|
+
isPageReady: opts.isPageReady,
|
|
190
|
+
response: opts.response,
|
|
191
|
+
screenshot,
|
|
192
|
+
isWhite
|
|
193
|
+
})
|
|
223
194
|
|
|
224
195
|
if (isReady || elapsed() >= timeout) break
|
|
225
196
|
|
|
197
|
+
const remaining = timeout - elapsed()
|
|
198
|
+
if (opts.fullPage && !didHydrateAttempt && !isWhite) {
|
|
199
|
+
didHydrateAttempt = true
|
|
200
|
+
const { hydrated, info } = await tryHydrateScroll(page, remaining)
|
|
201
|
+
didHydrateScroll = hydrated
|
|
202
|
+
debug('screenshot:hydrateScroll', { remaining, hydrated, ...info })
|
|
203
|
+
}
|
|
204
|
+
|
|
226
205
|
retry += 1
|
|
227
206
|
await goto.waitUntilAuto(page, { timeout })
|
|
228
207
|
} while (!isReady)
|
|
229
208
|
|
|
209
|
+
if (opts.fullPage) {
|
|
210
|
+
if (isReady) {
|
|
211
|
+
await prepareFullDocument(page, {
|
|
212
|
+
goto,
|
|
213
|
+
timeout: opts.timeout,
|
|
214
|
+
scrolled: didHydrateScroll
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
screenshot = await captureExpanded(
|
|
218
|
+
isReady,
|
|
219
|
+
{ ...opts, fullPage: true },
|
|
220
|
+
resolveScrollTimeout(goto, opts.timeout)
|
|
221
|
+
)
|
|
222
|
+
isWhite = await isWhiteScreenshot(screenshot)
|
|
223
|
+
}
|
|
224
|
+
|
|
230
225
|
return { isWhite, isReady, retry }
|
|
231
226
|
}
|
|
232
227
|
|
|
@@ -239,9 +234,13 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
239
234
|
if (waitUntil !== 'auto') {
|
|
240
235
|
;({ response } = await goto(page, { ...opts, url, waitUntil }))
|
|
241
236
|
const screenshotOpts = await beforeScreenshot(page, response, opts)
|
|
242
|
-
|
|
243
|
-
(
|
|
244
|
-
|
|
237
|
+
if (opts.fullPage) {
|
|
238
|
+
await prepareFullDocument(page, { goto, timeout: opts.timeout })
|
|
239
|
+
}
|
|
240
|
+
screenshot = await captureExpanded(
|
|
241
|
+
opts.fullPage,
|
|
242
|
+
{ ...opts, ...screenshotOpts },
|
|
243
|
+
goto.timeouts.action(opts.timeout)
|
|
245
244
|
)
|
|
246
245
|
debug('screenshot', { waitUntil, duration: timeScreenshot() })
|
|
247
246
|
} else {
|
|
@@ -277,6 +276,13 @@ module.exports = ({ goto, ...gotoOpts }) => {
|
|
|
277
276
|
module.exports.captureWithNavigationRetry = captureWithNavigationRetry
|
|
278
277
|
module.exports.isWhiteScreenshot = isWhiteScreenshot
|
|
279
278
|
module.exports.waitForDomStability = waitForDomStability
|
|
280
|
-
module.exports.resolveWaitForDom = resolveWaitForDom
|
|
281
279
|
module.exports.waitForReady = waitForReady
|
|
280
|
+
module.exports.paintSignals = paintSignals
|
|
281
|
+
module.exports.scrollFullPageToLoadContent = scrollFullPageToLoadContent
|
|
282
|
+
module.exports.expandOverflow = expandOverflow
|
|
283
|
+
module.exports.getPageMeta = getPageMeta
|
|
284
|
+
module.exports.checkPageReady = checkPageReady
|
|
285
|
+
module.exports.tryHydrateScroll = tryHydrateScroll
|
|
286
|
+
module.exports.resolveScrollTimeout = resolveScrollTimeout
|
|
287
|
+
module.exports.prepareFullDocument = prepareFullDocument
|
|
282
288
|
module.exports.SCREENSHOT_DEFAULT_OPTS = SCREENSHOT_DEFAULT_OPTS
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const debug = require('debug-logfmt')('browserless:prepare')
|
|
4
|
+
const pReflect = require('p-reflect')
|
|
5
|
+
|
|
6
|
+
const { waitForDomStability } = require('./wait-for-dom')
|
|
7
|
+
|
|
8
|
+
const SCROLL_STEP_MS = 50
|
|
9
|
+
const OVERFLOW_MIN_PX = 200
|
|
10
|
+
const OVERFLOW_WAIT_MS = 1500
|
|
11
|
+
const PRE_QUIET_MS = 50
|
|
12
|
+
const POST_QUIET_MS = 200
|
|
13
|
+
const SETTLE_MS = 400
|
|
14
|
+
|
|
15
|
+
// The single overflow scanner: the tallest whole-document element that scrolls
|
|
16
|
+
// its own overflow past minPx. Runs in the page.
|
|
17
|
+
function findTallestOverflowScroller (minPx) {
|
|
18
|
+
let best = null
|
|
19
|
+
for (const el of document.querySelectorAll('*')) {
|
|
20
|
+
const style = window.getComputedStyle(el)
|
|
21
|
+
if (style.overflowY !== 'auto' && style.overflowY !== 'scroll') continue
|
|
22
|
+
if (el.scrollHeight <= el.clientHeight + minPx) continue
|
|
23
|
+
if (!best || el.scrollHeight > best.scrollHeight) best = el
|
|
24
|
+
}
|
|
25
|
+
return best
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// page.evaluate only serializes the function it is given, so an in-page helper
|
|
29
|
+
// can't be shared across evaluates by reference. Compose the scanner source in
|
|
30
|
+
// front of `fn` on the Node side (no in-page eval, so page CSP is untouched) to
|
|
31
|
+
// give every caller the same scanner.
|
|
32
|
+
const evaluateInPage = (page, fn, ...args) => {
|
|
33
|
+
const source = `${findTallestOverflowScroller}\nreturn (${fn}).apply(null, arguments)`
|
|
34
|
+
// eslint-disable-next-line no-new-func
|
|
35
|
+
return page.evaluate(new Function(source), ...args)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const waitForOverflowHeight = (page, timeout = OVERFLOW_WAIT_MS) =>
|
|
39
|
+
evaluateInPage(
|
|
40
|
+
page,
|
|
41
|
+
(timeout, minPx) =>
|
|
42
|
+
new Promise(resolve => {
|
|
43
|
+
const started = Date.now()
|
|
44
|
+
let last = 0
|
|
45
|
+
let stable = 0
|
|
46
|
+
const tick = () => {
|
|
47
|
+
const scroll = findTallestOverflowScroller(minPx)
|
|
48
|
+
const height = scroll
|
|
49
|
+
? scroll.scrollHeight
|
|
50
|
+
: (document.scrollingElement || document.documentElement).scrollHeight
|
|
51
|
+
const tall = height > window.innerHeight + minPx
|
|
52
|
+
if (height === last && tall) {
|
|
53
|
+
if (++stable >= 2) return resolve(height)
|
|
54
|
+
} else {
|
|
55
|
+
stable = 0
|
|
56
|
+
last = height
|
|
57
|
+
}
|
|
58
|
+
if (Date.now() - started >= timeout) return resolve(height)
|
|
59
|
+
// Shell pages grow late — poll a bit slower until content appears.
|
|
60
|
+
setTimeout(tick, tall ? 100 : 150)
|
|
61
|
+
}
|
|
62
|
+
tick()
|
|
63
|
+
}),
|
|
64
|
+
timeout,
|
|
65
|
+
OVERFLOW_MIN_PX
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const expandOverflow = (page, minPx = OVERFLOW_MIN_PX) =>
|
|
69
|
+
evaluateInPage(
|
|
70
|
+
page,
|
|
71
|
+
minPx => {
|
|
72
|
+
const scroll = findTallestOverflowScroller(minPx)
|
|
73
|
+
if (!scroll) return false
|
|
74
|
+
let el = scroll
|
|
75
|
+
while (el) {
|
|
76
|
+
const pos = window.getComputedStyle(el).position
|
|
77
|
+
el.style.setProperty('overflow', 'visible', 'important')
|
|
78
|
+
el.style.setProperty('height', 'auto', 'important')
|
|
79
|
+
el.style.setProperty('max-height', 'none', 'important')
|
|
80
|
+
if (pos === 'absolute' || pos === 'fixed') {
|
|
81
|
+
el.style.setProperty('position', 'relative', 'important')
|
|
82
|
+
el.style.setProperty('inset', 'auto', 'important')
|
|
83
|
+
}
|
|
84
|
+
if (el === document.documentElement) break
|
|
85
|
+
el = el.parentElement
|
|
86
|
+
}
|
|
87
|
+
return true
|
|
88
|
+
},
|
|
89
|
+
minPx
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const settleDom = async (page, { idle, timeout }, label) => {
|
|
93
|
+
const started = Date.now()
|
|
94
|
+
const result = await page.evaluate(waitForDomStability, { idle, timeout })
|
|
95
|
+
debug(label, { ...result, duration: Date.now() - started })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const scrollFullPageToLoadContent = async (page, timeout) => {
|
|
99
|
+
const preQuiet = Math.min(PRE_QUIET_MS, Math.floor(timeout / 20))
|
|
100
|
+
const postQuiet = Math.min(POST_QUIET_MS, Math.floor(timeout / 20))
|
|
101
|
+
const scrollBudget = Math.max(0, timeout - preQuiet - postQuiet)
|
|
102
|
+
const started = Date.now()
|
|
103
|
+
|
|
104
|
+
if (preQuiet > 0) {
|
|
105
|
+
await settleDom(page, { idle: preQuiet / 2, timeout: preQuiet }, 'waitForDomStability:pre')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const scroll = await evaluateInPage(
|
|
109
|
+
page,
|
|
110
|
+
(scrollBudget, stepMs, minPx) =>
|
|
111
|
+
new Promise(resolve => {
|
|
112
|
+
const doc = () => document.scrollingElement || document.documentElement
|
|
113
|
+
let root = null
|
|
114
|
+
let pageHeight = doc() ? doc().scrollHeight : 0
|
|
115
|
+
let viewport = window.innerHeight
|
|
116
|
+
let currentScrollPosition = 0
|
|
117
|
+
const scrollStarted = Date.now()
|
|
118
|
+
|
|
119
|
+
const measure = () => {
|
|
120
|
+
if (!root) {
|
|
121
|
+
const overflow = findTallestOverflowScroller(minPx)
|
|
122
|
+
if (overflow) root = overflow
|
|
123
|
+
}
|
|
124
|
+
if (root) {
|
|
125
|
+
pageHeight = root.scrollHeight
|
|
126
|
+
viewport = root.clientHeight || window.innerHeight
|
|
127
|
+
} else {
|
|
128
|
+
const el = doc()
|
|
129
|
+
pageHeight = el ? el.scrollHeight : 0
|
|
130
|
+
viewport = window.innerHeight
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const finish = () => {
|
|
135
|
+
window.scrollTo(0, 0)
|
|
136
|
+
if (root) root.scrollTop = 0
|
|
137
|
+
resolve({
|
|
138
|
+
hasOverflow: !!root,
|
|
139
|
+
pageHeight,
|
|
140
|
+
viewport,
|
|
141
|
+
scrolledPx: currentScrollPosition,
|
|
142
|
+
duration: Date.now() - scrollStarted
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const scrollNext = () => {
|
|
147
|
+
measure()
|
|
148
|
+
const step = Math.max(1, Math.floor(viewport * 0.95))
|
|
149
|
+
if (currentScrollPosition >= pageHeight || Date.now() - scrollStarted >= scrollBudget) {
|
|
150
|
+
return finish()
|
|
151
|
+
}
|
|
152
|
+
if (root) root.scrollBy(0, step)
|
|
153
|
+
else window.scrollBy(0, step)
|
|
154
|
+
currentScrollPosition += step
|
|
155
|
+
setTimeout(scrollNext, stepMs)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
measure()
|
|
159
|
+
// Viewport-sized shell with no overflow yet: brief wait for the SPA
|
|
160
|
+
// scroller before falling through to window scroll / budget exit.
|
|
161
|
+
if (pageHeight <= viewport + 1 && !root) {
|
|
162
|
+
const waitUntil = scrollStarted + Math.min(1000, Math.floor(scrollBudget / 4))
|
|
163
|
+
const waitForRoot = () => {
|
|
164
|
+
measure()
|
|
165
|
+
if (root || Date.now() >= waitUntil) return scrollNext()
|
|
166
|
+
setTimeout(waitForRoot, stepMs)
|
|
167
|
+
}
|
|
168
|
+
return waitForRoot()
|
|
169
|
+
}
|
|
170
|
+
scrollNext()
|
|
171
|
+
}),
|
|
172
|
+
scrollBudget,
|
|
173
|
+
SCROLL_STEP_MS,
|
|
174
|
+
OVERFLOW_MIN_PX
|
|
175
|
+
)
|
|
176
|
+
debug('scrollFullPage', { ...scroll, duration: Date.now() - started })
|
|
177
|
+
|
|
178
|
+
if (postQuiet > 0) {
|
|
179
|
+
await settleDom(
|
|
180
|
+
page,
|
|
181
|
+
{ idle: Math.min(100, postQuiet / 2), timeout: postQuiet },
|
|
182
|
+
'waitForDomStability:post'
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const hydrated = !!(scroll?.hasOverflow && scroll.scrolledPx >= (scroll.viewport || 0))
|
|
187
|
+
return { ...scroll, hydrated, duration: Date.now() - started }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const resolveScrollTimeout = (goto, timeout) =>
|
|
191
|
+
typeof goto.timeouts.goto === 'function'
|
|
192
|
+
? goto.timeouts.goto(timeout)
|
|
193
|
+
: goto.timeouts.action(timeout)
|
|
194
|
+
|
|
195
|
+
// A hydrate scroll spends up to half the remaining budget (capped) scrolling,
|
|
196
|
+
// and the capture that follows still needs time. Below HYDRATE_MIN_BUDGET_MS
|
|
197
|
+
// there isn't enough left for both, so skip it.
|
|
198
|
+
const HYDRATE_MIN_BUDGET_MS = 1000
|
|
199
|
+
const HYDRATE_MAX_SCROLL_MS = 5000
|
|
200
|
+
|
|
201
|
+
const tryHydrateScroll = async (page, remaining) => {
|
|
202
|
+
if (remaining <= HYDRATE_MIN_BUDGET_MS) return { hydrated: false, info: {} }
|
|
203
|
+
const hydrate = await pReflect(
|
|
204
|
+
scrollFullPageToLoadContent(page, Math.min(remaining / 2, HYDRATE_MAX_SCROLL_MS))
|
|
205
|
+
)
|
|
206
|
+
return {
|
|
207
|
+
hydrated: !hydrate.isRejected && !!hydrate.value?.hydrated,
|
|
208
|
+
info: hydrate.isRejected ? {} : hydrate.value
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const prepareFullDocument = async (page, { goto, timeout, scrolled = false } = {}) => {
|
|
213
|
+
const scrollTimeout = resolveScrollTimeout(goto, timeout)
|
|
214
|
+
const elapsed = require('@kikobeats/time-span')({ format: n => Math.round(n) })()
|
|
215
|
+
|
|
216
|
+
if (!scrolled) {
|
|
217
|
+
const height = await pReflect(
|
|
218
|
+
waitForOverflowHeight(page, Math.min(OVERFLOW_WAIT_MS, Math.round(scrollTimeout / 8)))
|
|
219
|
+
)
|
|
220
|
+
debug('prepareFullDocument:overflowHeight', {
|
|
221
|
+
height: height.isRejected ? null : height.value,
|
|
222
|
+
duration: elapsed()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
await scrollFullPageToLoadContent(page, scrollTimeout)
|
|
226
|
+
debug('prepareFullDocument:scroll', { duration: elapsed() })
|
|
227
|
+
} else {
|
|
228
|
+
debug('prepareFullDocument:skipScroll', { duration: elapsed() })
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const settleMs = Math.min(SETTLE_MS, Math.max(0, scrollTimeout - elapsed()))
|
|
232
|
+
if (settleMs > 0 && typeof page.waitForNetworkIdle === 'function') {
|
|
233
|
+
await pReflect(page.waitForNetworkIdle({ idleTime: 200, concurrency: 2, timeout: settleMs }))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const expandResult = await pReflect(expandOverflow(page))
|
|
237
|
+
const expanded = !expandResult.isRejected && expandResult.value
|
|
238
|
+
debug('prepareFullDocument:expandOverflow', { expanded, duration: elapsed() })
|
|
239
|
+
|
|
240
|
+
return { expanded, duration: elapsed(), scrolled }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
expandOverflow,
|
|
245
|
+
scrollFullPageToLoadContent,
|
|
246
|
+
prepareFullDocument,
|
|
247
|
+
resolveScrollTimeout,
|
|
248
|
+
tryHydrateScroll
|
|
249
|
+
}
|
package/src/wait-for-dom.js
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const DEFAULT_WAIT_FOR_DOM = 0
|
|
4
|
-
const WAIT_FOR_DOM_IDLE_RATIO = 10
|
|
5
|
-
|
|
6
|
-
const resolveWaitForDom = waitForDom => {
|
|
7
|
-
const timeout = Number.isFinite(waitForDom) && waitForDom >= 0 ? waitForDom : DEFAULT_WAIT_FOR_DOM
|
|
8
|
-
|
|
9
|
-
if (timeout === 0) return undefined
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
timeout,
|
|
13
|
-
idle: timeout / WAIT_FOR_DOM_IDLE_RATIO
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
3
|
const waitForDomStability = ({ idle, timeout } = {}) =>
|
|
18
4
|
new Promise(resolve => {
|
|
19
5
|
if (!document.body) return resolve({ status: 'no-body' })
|
|
@@ -45,9 +31,4 @@ const waitForDomStability = ({ idle, timeout } = {}) =>
|
|
|
45
31
|
})()
|
|
46
32
|
})
|
|
47
33
|
|
|
48
|
-
module.exports = {
|
|
49
|
-
DEFAULT_WAIT_FOR_DOM,
|
|
50
|
-
WAIT_FOR_DOM_IDLE_RATIO,
|
|
51
|
-
resolveWaitForDom,
|
|
52
|
-
waitForDomStability
|
|
53
|
-
}
|
|
34
|
+
module.exports = { waitForDomStability }
|
package/src/wait-for-ready.js
CHANGED
|
@@ -1,47 +1,21 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
// Navigation-tolerant readiness gate. Resolves once the page has been visually
|
|
4
|
-
// quiet — document height stable, every image decoded, `readyState` complete —
|
|
5
|
-
// for `quietMs`, bounded by `timeout`. Unlike a screenshot poll it takes no
|
|
6
|
-
// screenshots (which are expensive under software rendering, e.g. the GPU-less
|
|
7
|
-
// fleet's llvmpipe) and it survives a client-side navigation: when the
|
|
8
|
-
// execution context is destroyed mid-check the quiet window resets and polling
|
|
9
|
-
// continues instead of throwing (SPAs re-commit their own URL after load).
|
|
10
|
-
|
|
11
3
|
const { setTimeout: sleep } = require('node:timers/promises')
|
|
12
4
|
const { isContextDestroyed } = require('@browserless/errors')
|
|
13
5
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// content. `text` is the equivalent paint signal for imageless pages: visible
|
|
24
|
-
// characters inside the viewport, counted up to 200 (the threshold consumers
|
|
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`.
|
|
36
|
-
const snapshot = () => {
|
|
37
|
-
// A document that is mid-parse or detached can have a null `documentElement`
|
|
38
|
-
// (the same state the text walk below guards against), so every dereference
|
|
39
|
-
// of it in this snapshot goes through `root`.
|
|
6
|
+
const DEFAULT_QUIET_MS = 300
|
|
7
|
+
const DEFAULT_POLL_MS = 150
|
|
8
|
+
|
|
9
|
+
const paintSignals = () => {
|
|
10
|
+
const TEXT_PAINT_CAP = 200
|
|
11
|
+
const MIN_PAINTED_AREA_PX = 256
|
|
12
|
+
const LAZY_IMAGE_VIEWPORTS = 2
|
|
13
|
+
const CONTENT_SAMPLE_LIMIT = 4
|
|
14
|
+
|
|
40
15
|
const root = document.documentElement
|
|
41
16
|
const vw = window.innerWidth || (root ? root.clientWidth : 0)
|
|
42
17
|
const vh = window.innerHeight || (root ? root.clientHeight : 0)
|
|
43
18
|
|
|
44
|
-
// Computed colors resolve to `rgb()`/`rgba()`; anything else is unknown.
|
|
45
19
|
const parseColor = value => {
|
|
46
20
|
const match = /rgba?\(([^)]+)\)/.exec(value || '')
|
|
47
21
|
if (!match) return null
|
|
@@ -49,8 +23,6 @@ const snapshot = () => {
|
|
|
49
23
|
return { r: parts[0], g: parts[1], b: parts[2], a: parts.length === 4 ? parts[3] : 1 }
|
|
50
24
|
}
|
|
51
25
|
|
|
52
|
-
// First opaque background up the ancestor chain; the canvas default (white)
|
|
53
|
-
// when every ancestor is transparent.
|
|
54
26
|
const effectiveBackground = el => {
|
|
55
27
|
for (let node = el; node; node = node.parentElement) {
|
|
56
28
|
const color = parseColor(window.getComputedStyle(node).backgroundColor)
|
|
@@ -62,8 +34,6 @@ const snapshot = () => {
|
|
|
62
34
|
const sameColor = (a, b) =>
|
|
63
35
|
Math.abs(a.r - b.r) < 10 && Math.abs(a.g - b.g) < 10 && Math.abs(a.b - b.b) < 10
|
|
64
36
|
|
|
65
|
-
// A layer only blanks a capture when it is itself painted solid: visible,
|
|
66
|
-
// full opacity, and an opaque background color or a background image.
|
|
67
37
|
const isOpaqueLayer = el => {
|
|
68
38
|
const style = window.getComputedStyle(el)
|
|
69
39
|
if (style.visibility === 'hidden' || parseFloat(style.opacity) < 0.99) return false
|
|
@@ -78,13 +48,9 @@ const snapshot = () => {
|
|
|
78
48
|
return width > 0 && height > 0 && width * height >= vw * vh * 0.9
|
|
79
49
|
}
|
|
80
50
|
|
|
81
|
-
// Up to a handful of counted content samples — enough to hit-test without
|
|
82
|
-
// turning the snapshot into a layout storm. The clamp keeps each probe point
|
|
83
|
-
// inside the viewport; the rect checks below guarantee it stays inside the
|
|
84
|
-
// sampled content's own box.
|
|
85
51
|
const contentPoints = []
|
|
86
52
|
const samplePoint = (el, rect) => {
|
|
87
|
-
if (contentPoints.length >=
|
|
53
|
+
if (contentPoints.length >= CONTENT_SAMPLE_LIMIT) return
|
|
88
54
|
contentPoints.push({
|
|
89
55
|
el,
|
|
90
56
|
x: Math.max(0, Math.min(vw - 1, rect.left + rect.width / 2)),
|
|
@@ -99,11 +65,9 @@ const snapshot = () => {
|
|
|
99
65
|
for (let i = 0; i < imgs.length; i++) {
|
|
100
66
|
const img = imgs[i]
|
|
101
67
|
if (!img.complete) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// beyond it, the fetch never starts.
|
|
106
|
-
if (img.loading === 'lazy' && img.getBoundingClientRect().top > vh * 2) continue
|
|
68
|
+
if (img.loading === 'lazy' && img.getBoundingClientRect().top > vh * LAZY_IMAGE_VIEWPORTS) {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
107
71
|
images++
|
|
108
72
|
continue
|
|
109
73
|
}
|
|
@@ -111,7 +75,7 @@ const snapshot = () => {
|
|
|
111
75
|
decoded++
|
|
112
76
|
if (img.naturalWidth === 0) continue
|
|
113
77
|
const rect = img.getBoundingClientRect()
|
|
114
|
-
if (rect.width * rect.height <
|
|
78
|
+
if (rect.width * rect.height < MIN_PAINTED_AREA_PX) continue
|
|
115
79
|
if (rect.bottom <= 0 || rect.right <= 0 || rect.top >= vh || rect.left >= vw) continue
|
|
116
80
|
if (
|
|
117
81
|
typeof img.checkVisibility === 'function' &&
|
|
@@ -122,21 +86,14 @@ const snapshot = () => {
|
|
|
122
86
|
painted++
|
|
123
87
|
samplePoint(img, rect)
|
|
124
88
|
}
|
|
125
|
-
|
|
126
|
-
// not a full DOM walk; only a near-blank shell walks every text node.
|
|
89
|
+
|
|
127
90
|
let text = 0
|
|
128
|
-
// A document that is mid-parse or detached can have neither `body` nor
|
|
129
|
-
// `documentElement`, and `createTreeWalker` throws on a null root. Text is only
|
|
130
|
-
// a paint signal, so a count that cannot be taken means "no text seen yet": the
|
|
131
|
-
// gate stays conservative and re-polls, rather than failing a render that would
|
|
132
|
-
// otherwise succeed. The same holds mid-walk — a DOM mutating under the walker
|
|
133
|
-
// must not take the capture down with it.
|
|
134
91
|
const textRoot = document.body || root
|
|
135
92
|
const walker = textRoot
|
|
136
93
|
? document.createTreeWalker(textRoot, window.NodeFilter.SHOW_TEXT)
|
|
137
94
|
: { nextNode: () => null }
|
|
138
95
|
try {
|
|
139
|
-
while (text <
|
|
96
|
+
while (text < TEXT_PAINT_CAP) {
|
|
140
97
|
const node = walker.nextNode()
|
|
141
98
|
if (!node) break
|
|
142
99
|
const value = node.nodeValue.trim()
|
|
@@ -156,22 +113,13 @@ const snapshot = () => {
|
|
|
156
113
|
const rect = range.getBoundingClientRect()
|
|
157
114
|
if (rect.width === 0 || rect.height === 0) continue
|
|
158
115
|
if (rect.bottom <= 0 || rect.right <= 0 || rect.top >= vh || rect.left >= vw) continue
|
|
159
|
-
// White-on-white (or any color-on-same-color) text passes every geometry
|
|
160
|
-
// check while a capture shows nothing: don't count it as painted text.
|
|
161
116
|
const color = parseColor(window.getComputedStyle(el).color)
|
|
162
117
|
if (color && (color.a < 0.05 || sameColor(color, effectiveBackground(el)))) continue
|
|
163
118
|
text += value.length
|
|
164
119
|
samplePoint(el, rect)
|
|
165
120
|
}
|
|
166
|
-
} catch {
|
|
167
|
-
// Keep whatever was counted before the DOM shifted underneath the walk.
|
|
168
|
-
}
|
|
121
|
+
} catch {}
|
|
169
122
|
|
|
170
|
-
// Is this content sample's paint hidden behind an unrelated opaque,
|
|
171
|
-
// viewport-covering layer? Walk up from the hit-tested element: anything
|
|
172
|
-
// related to the sample (itself, an ancestor, a descendant) paints with it,
|
|
173
|
-
// and the walk stops at the first common ancestor — an ancestor's background
|
|
174
|
-
// always paints below its own descendants.
|
|
175
123
|
const coveredAt = ({ el, x, y }) => {
|
|
176
124
|
const top = document.elementFromPoint(x, y)
|
|
177
125
|
if (!top) return false
|
|
@@ -182,14 +130,9 @@ const snapshot = () => {
|
|
|
182
130
|
return false
|
|
183
131
|
}
|
|
184
132
|
|
|
185
|
-
// Only pages the fast path could trust get the (layout-forcing) cover check.
|
|
186
133
|
let covered = false
|
|
187
|
-
if (painted > 0 || text >=
|
|
134
|
+
if (painted > 0 || text >= TEXT_PAINT_CAP) {
|
|
188
135
|
covered = contentPoints.some(coveredAt)
|
|
189
|
-
// `elementFromPoint` skips `pointer-events: none` elements, exactly how
|
|
190
|
-
// fading loading overlays are styled — scan root-level layers for one.
|
|
191
|
-
// Overlays mount as direct children of <body>; a deeper scan would cost a
|
|
192
|
-
// full styled DOM walk on every poll for a marginal case.
|
|
193
136
|
if (!covered && document.body) {
|
|
194
137
|
const layers = document.body.children
|
|
195
138
|
for (let i = 0; i < layers.length && !covered; i++) {
|
|
@@ -222,20 +165,13 @@ const snapshot = () => {
|
|
|
222
165
|
}
|
|
223
166
|
}
|
|
224
167
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// hold buys little — below ~300ms it would start trusting coincidences.
|
|
230
|
-
const waitForReady = async (page, { timeout, quietMs = 300, poll = 150 } = {}) => {
|
|
168
|
+
const waitForReady = async (
|
|
169
|
+
page,
|
|
170
|
+
{ timeout, quietMs = DEFAULT_QUIET_MS, poll = DEFAULT_POLL_MS } = {}
|
|
171
|
+
) => {
|
|
231
172
|
if (!Number.isFinite(timeout)) throw new TypeError('timeout must be a finite number')
|
|
232
|
-
// The quiet window must fit within the budget with room to observe it, or the
|
|
233
|
-
// gate could never satisfy its own requirement and would always time out.
|
|
234
173
|
quietMs = Math.min(quietMs, Math.floor(timeout / 2))
|
|
235
174
|
const deadline = Date.now() + timeout
|
|
236
|
-
// Never sleep past the deadline: with a poll larger than the remaining
|
|
237
|
-
// budget, a full-length sleep would overshoot the timeout and steal time
|
|
238
|
-
// from whatever shares the caller's budget (the blank-SPA screenshot poll).
|
|
239
175
|
const nextPoll = () => sleep(Math.min(poll, Math.max(0, deadline - Date.now())))
|
|
240
176
|
let lastHeight = -1
|
|
241
177
|
let quietSince = 0
|
|
@@ -253,14 +189,10 @@ const waitForReady = async (page, { timeout, quietMs = 300, poll = 150 } = {}) =
|
|
|
253
189
|
}
|
|
254
190
|
|
|
255
191
|
while (Date.now() < deadline) {
|
|
256
|
-
let
|
|
192
|
+
let signals
|
|
257
193
|
try {
|
|
258
|
-
|
|
194
|
+
signals = await page.evaluate(paintSignals)
|
|
259
195
|
} catch (error) {
|
|
260
|
-
// Only a client-side navigation tearing down the execution context is
|
|
261
|
-
// absorbed: reset the quiet window and keep polling. Any other evaluate
|
|
262
|
-
// failure is a real error and surfaces instead of masquerading as a
|
|
263
|
-
// timeout.
|
|
264
196
|
if (!isContextDestroyed(error)) throw error
|
|
265
197
|
resets++
|
|
266
198
|
lastHeight = -1
|
|
@@ -269,16 +201,16 @@ const waitForReady = async (page, { timeout, quietMs = 300, poll = 150 } = {}) =
|
|
|
269
201
|
continue
|
|
270
202
|
}
|
|
271
203
|
|
|
272
|
-
last =
|
|
273
|
-
const imagesDecoded =
|
|
274
|
-
const quiet =
|
|
204
|
+
last = signals
|
|
205
|
+
const imagesDecoded = signals.images === 0 || signals.decoded >= signals.images
|
|
206
|
+
const quiet = signals.complete && imagesDecoded && signals.height === lastHeight
|
|
275
207
|
|
|
276
208
|
if (quiet) {
|
|
277
209
|
if (quietSince === 0) quietSince = Date.now()
|
|
278
|
-
if (Date.now() - quietSince >= quietMs) return { ...
|
|
210
|
+
if (Date.now() - quietSince >= quietMs) return { ...signals, resets, timedOut: false }
|
|
279
211
|
} else {
|
|
280
212
|
quietSince = 0
|
|
281
|
-
lastHeight =
|
|
213
|
+
lastHeight = signals.height
|
|
282
214
|
}
|
|
283
215
|
|
|
284
216
|
await nextPoll()
|
|
@@ -287,4 +219,4 @@ const waitForReady = async (page, { timeout, quietMs = 300, poll = 150 } = {}) =
|
|
|
287
219
|
return { ...last, resets, timedOut: true }
|
|
288
220
|
}
|
|
289
221
|
|
|
290
|
-
module.exports = { waitForReady }
|
|
222
|
+
module.exports = { waitForReady, paintSignals }
|