@browserless/agent 14.0.1 → 14.0.2

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.
Files changed (2) hide show
  1. package/package.json +3 -2
  2. package/src/snapshot.js +91 -33
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@browserless/agent",
3
3
  "description": "Drive a Puppeteer page toward a natural-language goal using finite decisions.",
4
4
  "homepage": "https://browserless.js.org",
5
- "version": "14.0.1",
5
+ "version": "14.0.2",
6
6
  "main": "src",
7
7
  "author": {
8
8
  "email": "hello@microlink.io",
@@ -47,6 +47,7 @@
47
47
  "test": "ava",
48
48
  "start": "browserless exec examples/wallapop.js --no-headless",
49
49
  "benchmark": "browserless exec scripts/benchmark.js",
50
+ "perf": "node scripts/perf-baseline.js",
50
51
  "compare": "browserless exec scripts/compare.js"
51
52
  },
52
53
  "license": "MIT",
@@ -58,5 +59,5 @@
58
59
  "access": "public"
59
60
  },
60
61
  "types": "src/index.d.ts",
61
- "gitHead": "3a6195884bad55e5b3bd1a9aaf221ceaeb7d3d32"
62
+ "gitHead": "2f8b09a1509de4868c5ca2981ed147af8638968d"
62
63
  }
package/src/snapshot.js CHANGED
@@ -1,4 +1,4 @@
1
- /* global location, scrollX, scrollY, innerWidth, innerHeight, NodeFilter */
1
+ /* global location, scrollX, scrollY, innerWidth, innerHeight */
2
2
  // Adapted from browser-use/jev-ultrafast snapshot.js (MIT). See README.md.
3
3
  module.exports = function snapshot () {
4
4
  return (() => {
@@ -54,9 +54,39 @@ module.exports = function snapshot () {
54
54
  return active
55
55
  }
56
56
  const safe = e => !['password', 'file', 'hidden'].includes(e.type)
57
- const visible = e =>
58
- !cache.closest(e, '[aria-hidden="true"],[inert]') &&
59
- e.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
57
+ const hiddenMemo = new WeakMap()
58
+ const visibleMemo = new WeakMap()
59
+ const hiddenAncestor = element => {
60
+ const seen = []
61
+ for (let current = element; current; current = flatParent(current)) {
62
+ if (hiddenMemo.has(current)) {
63
+ const found = hiddenMemo.get(current)
64
+ for (const item of seen) hiddenMemo.set(item, found)
65
+ return found
66
+ }
67
+ seen.push(current)
68
+ if (current.matches('[aria-hidden="true"],[inert]')) {
69
+ for (const item of seen) hiddenMemo.set(item, current)
70
+ return current
71
+ }
72
+ }
73
+ for (const item of seen) hiddenMemo.set(item, null)
74
+ return null
75
+ }
76
+ const visible = e => {
77
+ if (visibleMemo.has(e)) return visibleMemo.get(e)
78
+ const shown =
79
+ !hiddenAncestor(e) && e.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
80
+ visibleMemo.set(e, shown)
81
+ return shown
82
+ }
83
+ // cache.guard runs again at input time, so it must not reuse this snapshot's memos.
84
+ const visibleNow = e => {
85
+ for (let current = e; current; current = flatParent(current)) {
86
+ if (current.matches('[aria-hidden="true"],[inert]')) return false
87
+ }
88
+ return e.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })
89
+ }
60
90
  const name = (e, seen = new Set()) => {
61
91
  if (!e || seen.has(e)) return ''
62
92
  seen.add(e)
@@ -140,7 +170,7 @@ module.exports = function snapshot () {
140
170
  .map(e => [identity(e), e.value, e.checked, e.selectedIndex, e.disabled, e.readOnly])
141
171
  ]
142
172
  cache.guard = e => {
143
- if (!e?.isConnected || !visible(e)) return null
173
+ if (!e?.isConnected || !visibleNow(e)) return null
144
174
  const scope =
145
175
  cache.closest(e, 'form,dialog,[role="dialog"],article,li,tr,[role="row"]') || flatParent(e)
146
176
  return [
@@ -252,37 +282,65 @@ module.exports = function snapshot () {
252
282
  const words = []
253
283
  const range = document.createRange()
254
284
  let length = 0
255
- for (const root of roots) {
256
- const walker = document.createTreeWalker(
257
- root === document ? document.body : root,
258
- NodeFilter.SHOW_TEXT
259
- )
260
- let node
261
- while ((node = walker.nextNode()) && length < 6000) {
262
- const value = node.textContent.trim()
263
- const parent = node.parentElement || root.host
264
- if (
265
- !value ||
266
- !parent ||
267
- parent.closest('script,style,noscript,template') ||
268
- !visible(parent)
269
- ) {
270
- continue
271
- }
285
+ const TEXT_SKIP = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE'])
286
+ const intersectsViewport = rect =>
287
+ rect.width > 0 &&
288
+ rect.height > 0 &&
289
+ rect.bottom > 0 &&
290
+ rect.top < innerHeight &&
291
+ rect.right > 0 &&
292
+ rect.left < innerWidth
293
+ const outsideViewport = rect =>
294
+ rect.width > 0 &&
295
+ rect.height > 0 &&
296
+ (rect.bottom <= 0 || rect.top >= innerHeight || rect.right <= 0 || rect.left >= innerWidth)
297
+ const fullyInside = rect =>
298
+ rect.top >= 0 &&
299
+ rect.left >= 0 &&
300
+ rect.bottom <= innerHeight &&
301
+ rect.right <= innerWidth &&
302
+ rect.width > 0 &&
303
+ rect.height > 0
304
+ const pushText = (node, skipRect) => {
305
+ if (length >= 6000 || node.nodeType !== 3) return
306
+ const value = node.textContent.trim()
307
+ if (!value) return
308
+ if (!skipRect) {
272
309
  range.selectNodeContents(node)
273
- const r = range.getBoundingClientRect()
274
- if (
275
- r.width > 0 &&
276
- r.height > 0 &&
277
- r.bottom > 0 &&
278
- r.top < innerHeight &&
279
- r.right > 0 &&
280
- r.left < innerWidth
281
- ) {
282
- words.push(value)
283
- length += value.length
310
+ if (!intersectsViewport(range.getBoundingClientRect())) return
311
+ }
312
+ words.push(value)
313
+ length += value.length
314
+ }
315
+ // Off-screen subtrees are skipped. A fixed node inside one of those subtrees is not read.
316
+ const collectText = element => {
317
+ if (length >= 6000 || TEXT_SKIP.has(element.tagName)) return
318
+ const rect = element.getBoundingClientRect()
319
+ if (outsideViewport(rect)) return
320
+ const inside = fullyInside(rect)
321
+ if (!intersectsViewport(rect) || !visible(element)) {
322
+ for (const node of element.childNodes) {
323
+ if (node.nodeType === 1) collectText(node)
284
324
  }
325
+ return
326
+ }
327
+ for (const node of element.childNodes) {
328
+ if (length >= 6000) return
329
+ if (node.nodeType === 3) pushText(node, inside)
330
+ else if (node.nodeType === 1) collectText(node)
331
+ }
332
+ }
333
+ for (const root of roots) {
334
+ if (length >= 6000) break
335
+ if (root === document) {
336
+ if (document.body) collectText(document.body)
337
+ continue
338
+ }
339
+ const host = root.host
340
+ if (host && visible(host)) {
341
+ for (const node of root.childNodes) pushText(node)
285
342
  }
343
+ for (const child of root.children) collectText(child)
286
344
  }
287
345
  const unsupported = []
288
346
  for (const e of queryAll('iframe,canvas,input[type="password"],input[type="file"]')) {