@browserless/screenshot 13.9.16 → 13.9.17

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 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.9.16",
5
+ "version": "13.9.17",
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.9.11",
36
- "@browserless/goto": "13.9.16",
36
+ "@browserless/goto": "13.9.17",
37
37
  "@kikobeats/content-type": "~1.0.4",
38
38
  "@kikobeats/time-span": "~1.0.13",
39
39
  "automad-prism-themes": "~0.3.8",
@@ -70,5 +70,5 @@
70
70
  "timeout": "2m",
71
71
  "workerThreads": false
72
72
  },
73
- "gitHead": "5333e392cef2c9d71503a8d4a43037ea8ef78f06"
73
+ "gitHead": "23a3064ebf0ebc903178c705e223cc70907e256b"
74
74
  }
@@ -0,0 +1,9 @@
1
+ 'use strict'
2
+
3
+ const isolatedRealm = page =>
4
+ typeof page.mainFrame === 'function' ? page.mainFrame().isolatedRealm() : page
5
+
6
+ const evaluateIsolated = (page, pageFunction, ...args) =>
7
+ isolatedRealm(page).evaluate(pageFunction, ...args)
8
+
9
+ module.exports = { evaluateIsolated }
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ const pReflect = require('p-reflect')
7
7
 
8
8
  const isTransientContextLoss = require('./is-transient-context-loss')
9
9
  const isWhiteScreenshot = require('./is-white-screenshot')
10
+ const { evaluateIsolated } = require('./evaluate-isolated')
10
11
  const waitForPrism = require('./pretty')
11
12
  const prettyTimeSpan = require('./time-span')
12
13
  const overlay = require('./overlay')
@@ -22,6 +23,8 @@ const {
22
23
 
23
24
  const timeSpan = require('@kikobeats/time-span')()
24
25
 
26
+ const ELEMENT_CLIP_ATTEMPTS = 2
27
+
25
28
  // No pacing here on purpose: `waitUntilAuto` is a network-idle wait, which on a
26
29
  // live page costs at least its idle window (~500ms) and at most what is left of
27
30
  // the budget. The runaway case was never a fast loop — it was retrying a page
@@ -43,7 +46,7 @@ const captureWithNavigationRetry = async (capture, { page, goto, timeout }) => {
43
46
  }
44
47
 
45
48
  const getPageMeta = page =>
46
- page.evaluate(() => ({
49
+ evaluateIsolated(page, () => ({
47
50
  title: document.title || '',
48
51
  bodyText: document.body ? document.body.innerText || '' : '',
49
52
  url: window.location.href || ''
@@ -63,15 +66,10 @@ const checkPageReady = async (page, { isPageReady, response, screenshot, isWhite
63
66
  return !pageReadyResult.isRejected && !!pageReadyResult.value
64
67
  }
65
68
 
66
- const getBoundingClientRect = element => {
67
- const { top, left, height, width, x, y } = element.getBoundingClientRect()
68
- return { top, left, height, width, x, y }
69
- }
70
-
71
69
  const waitForImagesOnViewport = page =>
72
- page.$$eval('img[src]:not([aria-hidden="true"])', elements =>
70
+ evaluateIsolated(page, () =>
73
71
  Promise.all(
74
- elements
72
+ Array.from(document.querySelectorAll('img[src]:not([aria-hidden="true"])'))
75
73
  .filter(el => {
76
74
  if (el.naturalHeight === 0 || el.naturalWidth === 0) return false
77
75
  const { top, left, bottom, right } = el.getBoundingClientRect()
@@ -86,15 +84,21 @@ const waitForImagesOnViewport = page =>
86
84
  )
87
85
  )
88
86
 
89
- const waitForElement = async (page, element) => {
90
- const screenshotOpts = {}
91
- if (element) {
92
- await page.waitForSelector(element, { visible: true })
93
- screenshotOpts.clip = await page.$eval(element, getBoundingClientRect)
94
- screenshotOpts.fullPage = false
95
- return screenshotOpts
87
+ const readElementClip = async (page, element) => {
88
+ const handle = await page.waitForSelector(element, { visible: true })
89
+ try {
90
+ return await handle.boundingBox()
91
+ } finally {
92
+ await handle.dispose()
96
93
  }
97
- return screenshotOpts
94
+ }
95
+
96
+ const waitForElement = async (page, element, screenshotOpts) => {
97
+ for (let attempt = 0; attempt < ELEMENT_CLIP_ATTEMPTS; attempt++) {
98
+ screenshotOpts.clip = await readElementClip(page, element)
99
+ if (screenshotOpts.clip !== null) break
100
+ }
101
+ screenshotOpts.fullPage = false
98
102
  }
99
103
 
100
104
  const SCREENSHOT_DEFAULT_OPTS = {
@@ -143,10 +147,10 @@ module.exports = ({ goto, ...gotoOpts }) => {
143
147
  const beforeScreenshot = async (page, response, { element, fullPage = false } = {}) => {
144
148
  const timeout = goto.timeouts.action(opts.timeout)
145
149
 
146
- let screenshotOpts = {}
150
+ const screenshotOpts = {}
147
151
  const tasks = [
148
152
  {
149
- fn: () => page.evaluate('document.fonts.ready'),
153
+ fn: () => evaluateIsolated(page, 'document.fonts.ready'),
150
154
  debug: 'beforeScreenshot:fontsReady'
151
155
  },
152
156
  {
@@ -164,9 +168,7 @@ module.exports = ({ goto, ...gotoOpts }) => {
164
168
 
165
169
  if (element && !fullPage) {
166
170
  tasks.push({
167
- fn: async () => {
168
- screenshotOpts = await waitForElement(page, element)
169
- },
171
+ fn: () => waitForElement(page, element, screenshotOpts),
170
172
  debug: 'beforeScreenshot:waitForElement'
171
173
  })
172
174
  }
@@ -181,6 +183,10 @@ module.exports = ({ goto, ...gotoOpts }) => {
181
183
  )
182
184
  )
183
185
 
186
+ if (screenshotOpts.clip === null) {
187
+ throw new Error(`Element \`${element}\` detached before its clip could be read`)
188
+ }
189
+
184
190
  return screenshotOpts
185
191
  }
186
192
 
@@ -4,6 +4,7 @@ const debug = require('debug-logfmt')('browserless:prepare')
4
4
  const pReflect = require('p-reflect')
5
5
 
6
6
  const { waitForDomStability } = require('./wait-for-dom')
7
+ const { evaluateIsolated } = require('./evaluate-isolated')
7
8
 
8
9
  const SCROLL_STEP_MS = 50
9
10
  const OVERFLOW_MIN_PX = 200
@@ -25,14 +26,14 @@ function findTallestOverflowScroller (minPx) {
25
26
  return best
26
27
  }
27
28
 
28
- // page.evaluate only serializes the function it is given, so an in-page helper
29
+ // An evaluate only serializes the function it is given, so an in-page helper
29
30
  // can't be shared across evaluates by reference. Compose the scanner source in
30
31
  // front of `fn` on the Node side (no in-page eval, so page CSP is untouched) to
31
32
  // give every caller the same scanner.
32
33
  const evaluateInPage = (page, fn, ...args) => {
33
34
  const source = `${findTallestOverflowScroller}\nreturn (${fn}).apply(null, arguments)`
34
35
  // eslint-disable-next-line no-new-func
35
- return page.evaluate(new Function(source), ...args)
36
+ return evaluateIsolated(page, new Function(source), ...args)
36
37
  }
37
38
 
38
39
  const waitForOverflowHeight = (page, timeout = OVERFLOW_WAIT_MS) =>
@@ -91,7 +92,7 @@ const expandOverflow = (page, minPx = OVERFLOW_MIN_PX) =>
91
92
 
92
93
  const settleDom = async (page, { idle, timeout }, label) => {
93
94
  const started = Date.now()
94
- const result = await page.evaluate(waitForDomStability, { idle, timeout })
95
+ const result = await evaluateIsolated(page, waitForDomStability, { idle, timeout })
95
96
  debug(label, { ...result, duration: Date.now() - started })
96
97
  }
97
98
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { setTimeout: sleep } = require('node:timers/promises')
4
4
  const isTransientContextLoss = require('./is-transient-context-loss')
5
+ const { evaluateIsolated } = require('./evaluate-isolated')
5
6
 
6
7
  const DEFAULT_QUIET_MS = 300
7
8
  const DEFAULT_POLL_MS = 150
@@ -191,7 +192,7 @@ const waitForReady = async (
191
192
  while (Date.now() < deadline) {
192
193
  let signals
193
194
  try {
194
- signals = await page.evaluate(paintSignals)
195
+ signals = await evaluateIsolated(page, paintSignals)
195
196
  } catch (error) {
196
197
  if (!isTransientContextLoss(page, error)) throw error
197
198
  resets++