browser-debugger-cli 0.14.0 → 0.16.0

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 (167) hide show
  1. package/.claude/skills/bdg/SKILL.md +3 -2
  2. package/dist/cdp/methodTarget.d.ts +92 -0
  3. package/dist/cdp/methodTarget.js +159 -0
  4. package/dist/cdp/protocol.d.ts +16 -1
  5. package/dist/cdp/protocol.js +21 -0
  6. package/dist/cdp/schema.d.ts +55 -1
  7. package/dist/cdp/schema.js +134 -25
  8. package/dist/cdp/types.d.ts +3 -1
  9. package/dist/commands/cdp.d.ts +38 -1
  10. package/dist/commands/cdp.js +201 -133
  11. package/dist/commands/cleanup.js +21 -4
  12. package/dist/commands/dom/eval.d.ts +2 -1
  13. package/dist/commands/dom/eval.js +6 -21
  14. package/dist/commands/dom/formInteraction.js +8 -4
  15. package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
  16. package/dist/commands/dom/helpers/evalResult.js +59 -0
  17. package/dist/commands/dom/helpers/index.d.ts +4 -4
  18. package/dist/commands/dom/helpers/index.js +3 -3
  19. package/dist/commands/dom/helpers/query.d.ts +2 -2
  20. package/dist/commands/dom/helpers/query.js +2 -2
  21. package/dist/commands/dom/helpers/screenshot.d.ts +21 -26
  22. package/dist/commands/dom/helpers/screenshot.js +50 -668
  23. package/dist/commands/dom/screenshot.js +56 -36
  24. package/dist/commands/helpJson.d.ts +1 -1
  25. package/dist/commands/helpJson.js +3 -3
  26. package/dist/commands/helpTopic.js +10 -4
  27. package/dist/commands/network/har.js +18 -14
  28. package/dist/commands/optionBehaviors.js +24 -9
  29. package/dist/commands/shared/CommandRunner.d.ts +5 -0
  30. package/dist/commands/shared/CommandRunner.js +18 -3
  31. package/dist/commands/shared/interrupt.d.ts +40 -0
  32. package/dist/commands/shared/interrupt.js +73 -0
  33. package/dist/commands/shared/optionTypes.d.ts +3 -0
  34. package/dist/commands/shared/outputFile.d.ts +2 -1
  35. package/dist/commands/shared/outputFile.js +7 -4
  36. package/dist/commands/shared/startHelpers.d.ts +26 -3
  37. package/dist/commands/shared/startHelpers.js +145 -23
  38. package/dist/commands/status.js +3 -1
  39. package/dist/commands/stop.js +2 -1
  40. package/dist/commands/types.d.ts +5 -0
  41. package/dist/connection/cdp.js +1 -16
  42. package/dist/connection/chromeIdentity.d.ts +24 -5
  43. package/dist/connection/chromeIdentity.js +53 -22
  44. package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
  45. package/dist/connection/launcher/flagsBuilder.js +107 -23
  46. package/dist/connection/launcher.d.ts +35 -2
  47. package/dist/connection/launcher.js +99 -12
  48. package/dist/connection/typed-cdp.d.ts +3 -2
  49. package/dist/constants.d.ts +3 -5
  50. package/dist/constants.js +3 -5
  51. package/dist/daemon/SessionController.d.ts +10 -5
  52. package/dist/daemon/SessionController.js +15 -8
  53. package/dist/daemon/ipcServer.js +1 -1
  54. package/dist/daemon/launcher.d.ts +22 -3
  55. package/dist/daemon/launcher.js +45 -8
  56. package/dist/daemon/session/Session.d.ts +5 -1
  57. package/dist/daemon/session/Session.js +9 -8
  58. package/dist/daemon/session/TelemetryStore.d.ts +5 -0
  59. package/dist/daemon/session/TelemetryStore.js +4 -0
  60. package/dist/daemon/session/captureGate.d.ts +59 -0
  61. package/dist/daemon/session/captureGate.js +96 -0
  62. package/dist/daemon/session/chromeConnection.d.ts +16 -1
  63. package/dist/daemon/session/chromeConnection.js +34 -4
  64. package/dist/daemon/session/collectors.d.ts +15 -0
  65. package/dist/daemon/session/collectors.js +39 -2
  66. package/dist/daemon/session/commandRegistry.d.ts +14 -1
  67. package/dist/daemon/session/commandRegistry.js +48 -13
  68. package/dist/daemon/session/downloads.d.ts +32 -0
  69. package/dist/daemon/session/downloads.js +96 -0
  70. package/dist/daemon/session/interactions.d.ts +3 -2
  71. package/dist/daemon/session/interactions.js +7 -2
  72. package/dist/daemon/session/plugins.js +6 -0
  73. package/dist/daemon.js +18520 -17014
  74. package/dist/errors/CommandError.d.ts +2 -0
  75. package/dist/errors/issues.d.ts +1 -1
  76. package/dist/errors/messages.d.ts +81 -0
  77. package/dist/errors/messages.js +198 -6
  78. package/dist/index.js +1446 -1078
  79. package/dist/ipc/client.d.ts +20 -2
  80. package/dist/ipc/client.js +32 -6
  81. package/dist/ipc/protocol/commands.d.ts +36 -2
  82. package/dist/ipc/protocol/commands.js +1 -0
  83. package/dist/ipc/protocol/domTypes.d.ts +24 -1
  84. package/dist/ipc/session/queries.d.ts +3 -0
  85. package/dist/ipc/session/types.d.ts +5 -0
  86. package/dist/ipc/transport/IPCError.d.ts +9 -0
  87. package/dist/ipc/transport/IPCError.js +12 -0
  88. package/dist/ipc/transport/errors.d.ts +2 -1
  89. package/dist/ipc/transport/errors.js +4 -1
  90. package/dist/ipc/transport/index.d.ts +10 -2
  91. package/dist/ipc/transport/index.js +29 -4
  92. package/dist/runtime/dom/actionEffects.d.ts +48 -9
  93. package/dist/runtime/dom/actionEffects.js +269 -34
  94. package/dist/runtime/dom/actionEffectsScripts.d.ts +45 -0
  95. package/dist/runtime/dom/actionEffectsScripts.js +101 -2
  96. package/dist/runtime/dom/captureArea.d.ts +35 -0
  97. package/dist/runtime/dom/captureArea.js +203 -0
  98. package/dist/runtime/dom/elementInfo.d.ts +13 -4
  99. package/dist/runtime/dom/elementInfo.js +12 -3
  100. package/dist/runtime/dom/evalHelpers.d.ts +24 -4
  101. package/dist/runtime/dom/evalHelpers.js +40 -12
  102. package/dist/runtime/dom/formDiscovery.d.ts +1 -1
  103. package/dist/runtime/dom/frames.d.ts +2 -1
  104. package/dist/runtime/dom/frames.js +3 -1
  105. package/dist/runtime/page/bdgWorld.d.ts +9 -0
  106. package/dist/runtime/page/bdgWorld.js +11 -0
  107. package/dist/runtime/page/captureEmulation.d.ts +119 -0
  108. package/dist/runtime/page/captureEmulation.js +189 -0
  109. package/dist/runtime/page/captureScroll.d.ts +24 -0
  110. package/dist/runtime/page/captureScroll.js +124 -0
  111. package/dist/runtime/page/emulation.js +6 -5
  112. package/dist/runtime/page/screenshot.d.ts +41 -0
  113. package/dist/runtime/page/screenshot.js +394 -0
  114. package/dist/runtime/page/userAgent.d.ts +86 -2
  115. package/dist/runtime/page/userAgent.js +154 -33
  116. package/dist/session/paths.d.ts +52 -3
  117. package/dist/session/paths.js +179 -7
  118. package/dist/session/portClaims.d.ts +0 -8
  119. package/dist/session/portClaims.js +1 -22
  120. package/dist/session/sessionList.d.ts +5 -1
  121. package/dist/session/sessionList.js +5 -1
  122. package/dist/telemetry/downloads.d.ts +127 -0
  123. package/dist/telemetry/downloads.js +265 -0
  124. package/dist/telemetry/har/builder.d.ts +12 -1
  125. package/dist/telemetry/har/builder.js +32 -9
  126. package/dist/telemetry/har/sanitize.d.ts +28 -0
  127. package/dist/telemetry/har/sanitize.js +184 -0
  128. package/dist/telemetry/har/sanitizeBody.d.ts +78 -0
  129. package/dist/telemetry/har/sanitizeBody.js +541 -0
  130. package/dist/telemetry/har/types.d.ts +2 -0
  131. package/dist/telemetry/network.d.ts +4 -4
  132. package/dist/telemetry/network.js +38 -4
  133. package/dist/telemetry/networkRetention.d.ts +35 -14
  134. package/dist/telemetry/networkRetention.js +62 -26
  135. package/dist/types.d.ts +9 -14
  136. package/dist/ui/OutputBuilder.d.ts +3 -2
  137. package/dist/ui/OutputBuilder.js +4 -3
  138. package/dist/ui/formatters/cdp.d.ts +32 -9
  139. package/dist/ui/formatters/cdp.js +77 -6
  140. package/dist/ui/formatters/details.js +7 -15
  141. package/dist/ui/formatters/preview.d.ts +2 -0
  142. package/dist/ui/formatters/preview.js +7 -1
  143. package/dist/ui/formatters/sessions.d.ts +3 -2
  144. package/dist/ui/formatters/sessions.js +10 -3
  145. package/dist/ui/formatters/status.js +6 -1
  146. package/dist/ui/formatting.d.ts +7 -0
  147. package/dist/ui/formatting.js +13 -0
  148. package/dist/ui/logging/logger.d.ts +1 -1
  149. package/dist/ui/messages/chrome.d.ts +27 -6
  150. package/dist/ui/messages/chrome.js +78 -12
  151. package/dist/ui/messages/commands.d.ts +71 -3
  152. package/dist/ui/messages/commands.js +98 -3
  153. package/dist/ui/messages/networkMessages.d.ts +50 -5
  154. package/dist/ui/messages/networkMessages.js +50 -6
  155. package/dist/ui/messages/session.d.ts +8 -0
  156. package/dist/ui/messages/session.js +10 -0
  157. package/dist/utils/async.d.ts +3 -2
  158. package/dist/utils/async.js +16 -3
  159. package/dist/utils/atomicFile.d.ts +2 -1
  160. package/dist/utils/atomicFile.js +5 -2
  161. package/dist/utils/directories.d.ts +41 -0
  162. package/dist/utils/directories.js +48 -0
  163. package/dist/utils/http.d.ts +11 -4
  164. package/dist/utils/http.js +5 -3
  165. package/package.json +18 -4
  166. /package/dist/{commands/dom → runtime/page}/screenshotResize.d.ts +0 -0
  167. /package/dist/{commands/dom → runtime/page}/screenshotResize.js +0 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * The area an element screenshot captures: its border box, grown to what
3
+ * the element paints beyond it (overflowing descendants, text, shadows,
4
+ * outline), plus any `--padding`.
5
+ */
6
+ import { TypedCDPConnection } from '../../connection/typed-cdp.js';
7
+ import { CommandError } from '../../errors/index.js';
8
+ import { elementNotVisibleError, elementZeroDimensionsError } from '../../errors/messages.js';
9
+ import { resolveNodeInBdgWorld } from '../page/bdgWorld.js';
10
+ import { createLogger } from '../../ui/logging/index.js';
11
+ import { getErrorMessage } from '../../utils/errors.js';
12
+ import { EXIT_CODES } from '../../utils/exitCodes.js';
13
+ const log = createLogger('dom');
14
+ /** Descendants {@link CONTENT_OVERFLOW_JS} looks at, so a huge element stays cheap */
15
+ const OVERFLOW_SCAN_LIMIT = 2000;
16
+ /**
17
+ * Page-side distances (CSS px, never negative) by which what an element
18
+ * paints reaches beyond its border box on each side: its rendered
19
+ * descendants (uncleared floats, absolutely positioned and transformed
20
+ * children), its text (descenders past a tight line height, read from its
21
+ * scroll size beyond its client size, in transformed px, to the left on an
22
+ * RTL element), and its own outer box shadows and outline (a focus ring).
23
+ * Descendants of an element that clips its overflow (`overflow` other than
24
+ * `visible`) are cut off by it and not counted, nor are fixed ones (they
25
+ * belong to the viewport) or what lies outside the document (skip links at
26
+ * -9999px). Only the shadows and outline count when the element clips its
27
+ * own overflow.
28
+ */
29
+ const CONTENT_OVERFLOW_JS = `function () {
30
+ const view = this.ownerDocument.defaultView;
31
+ const scroller = this.ownerDocument.scrollingElement || this.ownerDocument.documentElement;
32
+ const own = this.getBoundingClientRect();
33
+ const reach = { left: own.left, top: own.top, right: own.right, bottom: own.bottom };
34
+ const page = { left: -view.scrollX, top: -view.scrollY, right: scroller.scrollWidth - view.scrollX, bottom: scroller.scrollHeight - view.scrollY };
35
+ const clips = (style) => style.overflowX !== 'visible' || style.overflowY !== 'visible';
36
+ let budget = ${OVERFLOW_SCAN_LIMIT};
37
+ const walk = (el) => {
38
+ for (const child of el.children) {
39
+ if (--budget < 0) return;
40
+ const style = view.getComputedStyle(child);
41
+ if (style.display === 'none' || style.position === 'fixed') continue;
42
+ const r = child.getBoundingClientRect();
43
+ if (r.width > 0 && r.height > 0 && style.visibility === 'visible') {
44
+ reach.left = Math.min(reach.left, Math.max(r.left, page.left));
45
+ reach.top = Math.min(reach.top, Math.max(r.top, page.top));
46
+ reach.right = Math.max(reach.right, Math.min(r.right, page.right));
47
+ reach.bottom = Math.max(reach.bottom, Math.min(r.bottom, page.bottom));
48
+ }
49
+ if (!clips(style)) walk(child);
50
+ }
51
+ };
52
+ const ownStyle = view.getComputedStyle(this);
53
+ if (!clips(ownStyle)) {
54
+ walk(this);
55
+ const scaleX = this.offsetWidth ? own.width / this.offsetWidth : 1;
56
+ const scaleY = this.offsetHeight ? own.height / this.offsetHeight : 1;
57
+ const wider = Math.max(0, this.scrollWidth - this.clientWidth) * scaleX;
58
+ const taller = Math.max(0, this.scrollHeight - this.clientHeight) * scaleY;
59
+ if (ownStyle.direction === 'rtl') reach.left = Math.min(reach.left, own.left - wider);
60
+ else reach.right = Math.max(reach.right, own.right + wider);
61
+ reach.bottom = Math.max(reach.bottom, own.bottom + taller);
62
+ }
63
+ const ink = { left: 0, top: 0, right: 0, bottom: 0 };
64
+ const grow = (side, amount) => { ink[side] = Math.max(ink[side], amount); };
65
+ for (const layer of ownStyle.boxShadow === 'none' ? [] : ownStyle.boxShadow.split(/,(?![^(]*\\))/)) {
66
+ if (/\\binset\\b/.test(layer)) continue;
67
+ const [x = 0, y = 0, blur = 0, spread = 0] = (layer.replace(/(rgba?|hsla?|color|oklch|lab|lch)\\([^)]*\\)/g, '').match(/-?[\\d.]+px/g) || []).map(parseFloat);
68
+ grow('left', blur + spread - x);
69
+ grow('right', blur + spread + x);
70
+ grow('top', blur + spread - y);
71
+ grow('bottom', blur + spread + y);
72
+ }
73
+ if (ownStyle.outlineStyle !== 'none') {
74
+ const outline = parseFloat(ownStyle.outlineWidth) + parseFloat(ownStyle.outlineOffset);
75
+ ['left', 'top', 'right', 'bottom'].forEach((side) => grow(side, outline));
76
+ }
77
+ return {
78
+ left: Math.max(own.left - reach.left, ink.left),
79
+ top: Math.max(own.top - reach.top, ink.top),
80
+ right: Math.max(reach.right - own.right, ink.right),
81
+ bottom: Math.max(reach.bottom - own.bottom, ink.bottom)
82
+ };
83
+ }`;
84
+ /** Overflow (px) below which the capture keeps to the border box (subpixel rounding) */
85
+ const OVERFLOW_SLACK = 1;
86
+ /** Measurements so far, naming each one's object group (captures may run side by side) */
87
+ let measurements = 0;
88
+ /**
89
+ * Throw an element error with exit code 81.
90
+ *
91
+ * @param err - Message and suggestion
92
+ * @throws CommandError (81)
93
+ */
94
+ function throwInvalid(err) {
95
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
96
+ }
97
+ /**
98
+ * The border box of an element (padding and border included), relative to
99
+ * the viewport.
100
+ *
101
+ * @param cdp - Session connection
102
+ * @param ref - The element
103
+ * @returns Bounds in CSS px
104
+ * @throws CommandError (81) when it is not rendered or has no area
105
+ */
106
+ export async function getElementBounds(cdp, ref) {
107
+ const boxModel = await new TypedCDPConnection(cdp)
108
+ .send('DOM.getBoxModel', ref)
109
+ .catch((error) => {
110
+ log.debug(`No box model: ${getErrorMessage(error)}`);
111
+ return undefined;
112
+ });
113
+ const border = boxModel?.model.border;
114
+ if (!border)
115
+ throwInvalid(elementNotVisibleError());
116
+ const x = border[0] ?? 0;
117
+ const y = border[1] ?? 0;
118
+ const width = (border[2] ?? 0) - x;
119
+ const height = (border[5] ?? 0) - y;
120
+ if (width <= 0 || height <= 0)
121
+ throwInvalid(elementZeroDimensionsError());
122
+ return { x, y, width, height };
123
+ }
124
+ /**
125
+ * Area an element screenshot captures: the border box, grown to the content
126
+ * that overflows it ({@link CONTENT_OVERFLOW_JS}), so floated children are
127
+ * not cropped away.
128
+ *
129
+ * @param cdp - Session connection
130
+ * @param ref - The element
131
+ * @param bounds - Border box (DOM.getBoxModel coordinates)
132
+ * @param padding - Extra space around it (CSS px)
133
+ * @returns The area, or the border box (with the padding) when nothing
134
+ * overflows (or the page cannot be asked)
135
+ */
136
+ export async function captureArea(cdp, ref, bounds, padding) {
137
+ const area = await paintedArea(cdp, ref, bounds);
138
+ return padding > 0
139
+ ? {
140
+ x: area.x - padding,
141
+ y: area.y - padding,
142
+ width: area.width + 2 * padding,
143
+ height: area.height + 2 * padding,
144
+ }
145
+ : area;
146
+ }
147
+ /**
148
+ * The border box grown to what the element paints beyond it
149
+ * ({@link CONTENT_OVERFLOW_JS}).
150
+ *
151
+ * @param cdp - Session connection
152
+ * @param ref - The element
153
+ * @param bounds - Border box
154
+ * @returns The area
155
+ */
156
+ async function paintedArea(cdp, ref, bounds) {
157
+ try {
158
+ const overflow = await measureOverflow(cdp, ref);
159
+ if (!overflow)
160
+ return bounds;
161
+ const [left, top, right, bottom] = ['left', 'top', 'right', 'bottom'].map((side) => Math.max(0, overflow[side] ?? 0));
162
+ if (Math.max(left, top, right, bottom) <= OVERFLOW_SLACK)
163
+ return bounds;
164
+ return {
165
+ x: bounds.x - left,
166
+ y: bounds.y - top,
167
+ width: bounds.width + left + right,
168
+ height: bounds.height + top + bottom,
169
+ };
170
+ }
171
+ catch (error) {
172
+ log.debug(`Could not measure overflowing content: ${getErrorMessage(error)}`);
173
+ return bounds;
174
+ }
175
+ }
176
+ /**
177
+ * Run {@link CONTENT_OVERFLOW_JS} on the element in bdg's world.
178
+ *
179
+ * @param cdp - Session connection
180
+ * @param ref - The element
181
+ * @returns Overflow per side, or undefined when the page did not answer
182
+ */
183
+ async function measureOverflow(cdp, ref) {
184
+ const objectGroup = `bdg-screenshot-${++measurements}`;
185
+ try {
186
+ const resolved = await resolveNodeInBdgWorld(cdp, { ...ref, objectGroup });
187
+ const objectId = resolved.object.objectId;
188
+ if (!objectId)
189
+ return undefined;
190
+ const response = await new TypedCDPConnection(cdp).send('Runtime.callFunctionOn', {
191
+ objectId,
192
+ functionDeclaration: CONTENT_OVERFLOW_JS,
193
+ returnByValue: true,
194
+ });
195
+ return response.result.value;
196
+ }
197
+ finally {
198
+ await cdp
199
+ .send('Runtime.releaseObjectGroup', { objectGroup })
200
+ .catch((error) => log.debug(`Object group not released: ${getErrorMessage(error)}`));
201
+ }
202
+ }
203
+ //# sourceMappingURL=captureArea.js.map
@@ -70,16 +70,25 @@ export declare const FLAT_TEXT_JS = "(el, limit, fields) => {\n const composed
70
70
  export declare const ELEMENT_TEXT_JS = "(el, full) => {\n const withoutDecorations = (el, text) => {\n if (!text || typeof el.querySelectorAll !== 'function') return text;\n const glyph = /^\\s*[\u00D7\u2715\u2716\u2717\u2A2F]\\s*$/;\n const textOf = (node) => (typeof node.innerText === 'string' ? node.innerText : node.textContent || '').trim();\n const closer = '.close, [aria-label=\"close\" i], [aria-label=\"dismiss\" i]';\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const found = Array.from(el.querySelectorAll(closer + ', [aria-hidden=\"true\"]'))\n .filter((node) => rendered(node) && (node.matches(closer) || !/[\\p{L}\\p{N}]/u.test(textOf(node))));\n if (/[\u00D7\u2715\u2716\u2717\u2A2F]/.test(text)) {\n found.push(...Array.from(el.querySelectorAll('button, a, [role=\"button\"]')).filter((node) => glyph.test(node.textContent || '')));\n }\n const unique = Array.from(new Set(found));\n const outermost = unique.filter((node) => !unique.some((other) => other !== node && other.contains(node)));\n let result = text;\n for (const node of outermost) {\n const part = textOf(node);\n const at = part ? result.lastIndexOf(part) : -1;\n if (at >= 0) result = result.slice(0, at) + result.slice(at + part.length);\n }\n return result;\n};\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const flatText = (el, limit, fields) => {\n const composed = (node) => {\n const composes = (n) => Boolean(n.shadowRoot) || n.localName === 'slot';\n if (composes(node)) return true;\n const walker = node.ownerDocument.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);\n while (walker.nextNode()) if (composes(walker.currentNode)) return true;\n return false;\n};\n const view = el.ownerDocument.defaultView;\n let text = '';\n const nodesOf = (node) =>\n node.localName === 'slot' ? node.assignedNodes({ flatten: true }) : Array.from((node.shadowRoot || node).childNodes);\n const read = (node, visible) => {\n if (text.length >= limit) return;\n if (node.nodeType === 3 && visible) text += node.data.replace(/\\s+/g, ' ');\n if (node.nodeType !== 1) return;\n if (/^(input|textarea)$/.test(node.localName)) return;\n if (!fields && (node.localName === 'select' || node.isContentEditable)) return;\n const style = view.getComputedStyle(node);\n if (node.checkVisibility && !node.checkVisibility() && style.display !== 'contents') return;\n if (node.localName === 'br') text += '\\n';\n const apart = style.display.startsWith('inline') || style.display === 'contents' ? '' : '\\n';\n text += apart;\n const own = node.textContent || '';\n if (composed(node)) nodesOf(node).forEach((child) => read(child, style.visibility === 'visible'));\n else if (own.length > limit - text.length) text += own.slice(0, limit - text.length);\n else text += typeof node.innerText === 'string' ? node.innerText : own;\n text += apart;\n };\n const visible = view.getComputedStyle(el).visibility === 'visible';\n nodesOf(el).forEach((child) => read(child, visible));\n return text.slice(0, limit);\n};\n const all = el.textContent || '';\n if (el.tagName === 'OPTION') return el.label;\n if (typeof el.innerText !== 'string') return full ? all : all.slice(0, 2000);\n const rendered = (node) => !node.checkVisibility || node.checkVisibility();\n const shown = rendered(el);\n const boxless = !shown && el.ownerDocument.defaultView.getComputedStyle(el).display === 'contents';\n if (!shown && !boxless) return '';\n if (composed(el)) return withoutDecorations(el, flatText(el, full ? Infinity : 2000));\n if (boxless) return withoutDecorations(el, full ? all : all.slice(0, 2000));\n if (full || all.length <= 2000) return withoutDecorations(el, el.innerText);\n const walker = el.ownerDocument.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n let start = '';\n while (start.length < 1000 && walker.nextNode()) {\n const parent = walker.currentNode.parentElement;\n if (!parent || rendered(parent)) start += walker.currentNode.data;\n }\n return withoutDecorations(el, start);\n}";
71
71
  /** Shown instead of a secret field value (the same for every length) */
72
72
  export declare const MASKED_VALUE = "\u2022\u2022\u2022\u2022";
73
+ /**
74
+ * Regular expression source (case-insensitive) matching names of fields that
75
+ * hold a password, one-time code, PIN, social security number or card data:
76
+ * `password`, `passwd`, `pwd`, `passcode`, `passphrase`, `otp`, `pin`, `ssn`
77
+ * (these three as whole words), `cvv`, `cvc`, `card_number`, `cardNo` (whole
78
+ * words). Shared
79
+ * by the page-side {@link SENSITIVE_FIELD_JS} and HAR sanitization.
80
+ */
81
+ export declare const SENSITIVE_NAME_SOURCE = "passw|passwd|pwd|passcode|passphrase|(^|[^a-z])(otp|pin|ssn)([^a-z]|$)|cvv|cvc|card[-_]?(num(ber)?|no)([^a-z]|$)";
73
82
  /**
74
83
  * Page-side check whether a form control holds a secret whose value must
75
84
  * never leave the page: a password field (live type or type attribute), a
76
85
  * field shown masked by CSS (`-webkit-text-security` other than `none`),
77
86
  * one whose `autocomplete` names a password, a payment card (`cc-*`) or a
78
- * one-time code, or a field named like a password, one-time code or card code
79
- * (`password`, `passwd`, `pwd`, `passcode`, `otp`, `cvv`, `cvc`), which
87
+ * one-time code, or a field named like a password, one-time code, PIN, SSN
88
+ * or card data ({@link SENSITIVE_NAME_SOURCE}), which
80
89
  * covers a password field switched to text by a "show password" button.
81
90
  */
82
- export declare const SENSITIVE_FIELD_JS = "(el) => {\n const autocomplete = el.getAttribute('autocomplete') || '';\n if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;\n if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;\n const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');\n if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;\n try {\n const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');\n return Boolean(security) && security !== 'none';\n } catch (e) {\n return false;\n }\n}";
91
+ export declare const SENSITIVE_FIELD_JS = "(el) => {\n const autocomplete = el.getAttribute('autocomplete') || '';\n if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;\n if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;\n const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');\n if (/passw|passwd|pwd|passcode|passphrase|(^|[^a-z])(otp|pin|ssn)([^a-z]|$)|cvv|cvc|card[-_]?(num(ber)?|no)([^a-z]|$)/i.test(names)) return true;\n try {\n const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');\n return Boolean(security) && security !== 'none';\n } catch (e) {\n return false;\n }\n}";
83
92
  /**
84
93
  * Page-side live state of a form control, which its attributes do not show:
85
94
  * the type and current value of an `<input>` (for checkboxes and radios
@@ -93,7 +102,7 @@ export declare const SENSITIVE_FIELD_JS = "(el) => {\n const autocomplete = el.
93
102
  * is replaced by {@link MASKED_VALUE}, whatever its length, with
94
103
  * `sensitive: true`.
95
104
  */
96
- export declare const ELEMENT_STATE_JS = "(el) => {\n const isSensitive = (el) => {\n const autocomplete = el.getAttribute('autocomplete') || '';\n if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;\n if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;\n const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');\n if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;\n try {\n const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');\n return Boolean(security) && security !== 'none';\n } catch (e) {\n return false;\n }\n};\n const mask = (value) => (value ? '\u2022\u2022\u2022\u2022' : '');\n const guarded = (state) => {\n if (!isSensitive(el)) return state;\n const result = { ...state, sensitive: true };\n if ('value' in result) result.value = mask(result.value);\n if ('selected' in result) result.selected = mask(result.selected);\n return result;\n };\n switch (el.localName) {\n case 'input':\n if (el.type === 'hidden') return { type: 'hidden' };\n return guarded(\n /^(checkbox|radio)$/.test(el.type)\n ? { type: el.type, checked: el.checked, value: el.getAttribute('value') || '' }\n : { type: el.type, value: el.value }\n );\n case 'textarea':\n return guarded({ value: el.value });\n case 'select':\n return guarded({ selected: Array.from(el.selectedOptions || [], (option) => option.label).join(', ') });\n case 'button':\n return el.form ? { type: el.type } : {};\n default:\n return {};\n }\n}";
105
+ export declare const ELEMENT_STATE_JS = "(el) => {\n const isSensitive = (el) => {\n const autocomplete = el.getAttribute('autocomplete') || '';\n if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;\n if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;\n const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');\n if (/passw|passwd|pwd|passcode|passphrase|(^|[^a-z])(otp|pin|ssn)([^a-z]|$)|cvv|cvc|card[-_]?(num(ber)?|no)([^a-z]|$)/i.test(names)) return true;\n try {\n const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');\n return Boolean(security) && security !== 'none';\n } catch (e) {\n return false;\n }\n};\n const mask = (value) => (value ? '\u2022\u2022\u2022\u2022' : '');\n const guarded = (state) => {\n if (!isSensitive(el)) return state;\n const result = { ...state, sensitive: true };\n if ('value' in result) result.value = mask(result.value);\n if ('selected' in result) result.selected = mask(result.selected);\n return result;\n };\n switch (el.localName) {\n case 'input':\n if (el.type === 'hidden') return { type: 'hidden' };\n return guarded(\n /^(checkbox|radio)$/.test(el.type)\n ? { type: el.type, checked: el.checked, value: el.getAttribute('value') || '' }\n : { type: el.type, value: el.value }\n );\n case 'textarea':\n return guarded({ value: el.value });\n case 'select':\n return guarded({ selected: Array.from(el.selectedOptions || [], (option) => option.label).join(', ') });\n case 'button':\n return el.form ? { type: el.type } : {};\n default:\n return {};\n }\n}";
97
106
  /**
98
107
  * Page-side short description of an element: tag, id and up to two classes
99
108
  * ({@link LABEL_CLASSES_JS}), e.g. `button#save.primary.large`.
@@ -157,13 +157,22 @@ export const ELEMENT_TEXT_JS = `(el, full) => {
157
157
  }`;
158
158
  /** Shown instead of a secret field value (the same for every length) */
159
159
  export const MASKED_VALUE = '••••';
160
+ /**
161
+ * Regular expression source (case-insensitive) matching names of fields that
162
+ * hold a password, one-time code, PIN, social security number or card data:
163
+ * `password`, `passwd`, `pwd`, `passcode`, `passphrase`, `otp`, `pin`, `ssn`
164
+ * (these three as whole words), `cvv`, `cvc`, `card_number`, `cardNo` (whole
165
+ * words). Shared
166
+ * by the page-side {@link SENSITIVE_FIELD_JS} and HAR sanitization.
167
+ */
168
+ export const SENSITIVE_NAME_SOURCE = 'passw|passwd|pwd|passcode|passphrase|(^|[^a-z])(otp|pin|ssn)([^a-z]|$)|cvv|cvc|card[-_]?(num(ber)?|no)([^a-z]|$)';
160
169
  /**
161
170
  * Page-side check whether a form control holds a secret whose value must
162
171
  * never leave the page: a password field (live type or type attribute), a
163
172
  * field shown masked by CSS (`-webkit-text-security` other than `none`),
164
173
  * one whose `autocomplete` names a password, a payment card (`cc-*`) or a
165
- * one-time code, or a field named like a password, one-time code or card code
166
- * (`password`, `passwd`, `pwd`, `passcode`, `otp`, `cvv`, `cvc`), which
174
+ * one-time code, or a field named like a password, one-time code, PIN, SSN
175
+ * or card data ({@link SENSITIVE_NAME_SOURCE}), which
167
176
  * covers a password field switched to text by a "show password" button.
168
177
  */
169
178
  export const SENSITIVE_FIELD_JS = `(el) => {
@@ -171,7 +180,7 @@ export const SENSITIVE_FIELD_JS = `(el) => {
171
180
  if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;
172
181
  if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;
173
182
  const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');
174
- if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;
183
+ if (/${SENSITIVE_NAME_SOURCE}/i.test(names)) return true;
175
184
  try {
176
185
  const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');
177
186
  return Boolean(security) && security !== 'none';
@@ -26,6 +26,8 @@ export interface EvalResult {
26
26
  type: string;
27
27
  /** Object subtype (`node`, `date`, `map`, `array`, ...) */
28
28
  subtype?: string;
29
+ /** Elements of an array result in the page (its copy holds at most 1000 unless `full`) */
30
+ length?: number;
29
31
  /** Set when the page replaced built-ins bdg's copy of the result uses, so the browser copied it */
30
32
  warning?: string;
31
33
  }
@@ -36,11 +38,27 @@ export interface EvalResult {
36
38
  * dates ISO strings, maps and sets entries, errors their message, BigInts
37
39
  * `12n`, functions `function name()`, and cycles `[Circular]` (an object
38
40
  * shared by two properties is copied twice). Works for objects of iframes
39
- * (other realms); lists and objects are cut after 1000 entries, and a
40
- * throwing getter becomes `[Error: …]`. It uses the page's built-ins
41
- * ({@link COPY_BUILTINS}), so it only runs when the page left them alone.
41
+ * (other realms); lists and objects are cut after 1000 entries and levels
42
+ * below the 20th become `[…]` (with `full`: no entries are cut, levels
43
+ * below the 100th), and a throwing getter becomes `[Error: …]`. It uses the
44
+ * page's built-ins ({@link COPY_BUILTINS}), so it only runs when the page
45
+ * left them alone.
46
+ *
47
+ * @param full - `--full`: copy every entry
48
+ * @returns Function declaration for Runtime.callFunctionOn
49
+ */
50
+ export declare function jsonSafeCopyFunction(full?: boolean): string;
51
+ /**
52
+ * Elements of an array result (also a node list or typed array), read from
53
+ * its description (`Array(20000)`, `NodeList(5)`), since its copy holds
54
+ * at most 1000 unless `--full`.
55
+ *
56
+ * @param remote - Remote object returned by Runtime.evaluate
57
+ * @returns `length`, when the result is an array
42
58
  */
43
- export declare const JSON_SAFE_COPY_FUNCTION = "function () {\n const MAX_ITEMS = 1000;\n const ancestors = new Set();\n const kind = (value) => Object.prototype.toString.call(value).slice(8, -1);\n const isNode = (value) => typeof value.nodeType === 'number' && typeof value.nodeName === 'string';\n const describeNode = (node) => {\n if (node.nodeType !== 1) return node.nodeName.toLowerCase();\n return node.tagName.toLowerCase() + (node.id ? '#' + node.id : '') +\n (node.classList && node.classList.length ? '.' + Array.from(node.classList).join('.') : '');\n };\n const items = (list, next) => {\n const result = [];\n if (typeof list.length === 'number') {\n for (let i = 0; i < list.length; i++) {\n if (i === MAX_ITEMS) { result.push('\u2026'); break; }\n result.push(next(list[i]));\n }\n return result;\n }\n let count = 0;\n for (const item of list) {\n if (count++ === MAX_ITEMS) { result.push('\u2026'); break; }\n result.push(next(item));\n }\n return result;\n };\n const leaf = (value) => {\n if (value === undefined) return null;\n if (typeof value === 'number') {\n if (Number.isNaN(value) || !Number.isFinite(value)) return String(value);\n return Object.is(value, -0) ? '-0' : value;\n }\n if (typeof value === 'bigint') return value + 'n';\n if (typeof value === 'symbol') return value.toString();\n if (typeof value === 'function') return 'function ' + (value.name || '(anonymous)') + '()';\n return value;\n };\n const copy = (value, depth) => {\n if (value === null || typeof value !== 'object') return leaf(value);\n if (ancestors.has(value)) return '[Circular]';\n if (depth > 20) return '[\u2026]';\n const next = (item) => copy(item, depth + 1);\n const type = kind(value);\n if (isNode(value)) return describeNode(value);\n if (value.window === value) return 'Window';\n if (type === 'Date') return isNaN(value) ? 'Invalid Date' : value.toISOString();\n if (type === 'RegExp') return String(value);\n if (type === 'Error' || value instanceof Error) return value.name + ': ' + value.message;\n ancestors.add(value);\n try {\n if (type === 'Map') return items(value, ([k, v]) => [next(k), next(v)]);\n if (type === 'Set' || type === 'NodeList' || type === 'HTMLCollection') return items(value, next);\n if (ArrayBuffer.isView(value) && type !== 'DataView') return items(value, next);\n if (Array.isArray(value)) return items(value, next);\n const result = {};\n const keys = Object.keys(value);\n for (const key of keys.slice(0, MAX_ITEMS)) {\n try { result[key] = next(value[key]); } catch (e) { result[key] = '[Error: ' + (e && e.message) + ']'; }\n }\n if (keys.length > MAX_ITEMS) result['\u2026'] = (keys.length - MAX_ITEMS) + ' more keys';\n return result;\n } finally {\n ancestors.delete(value);\n }\n };\n return copy(this, 0);\n}";
59
+ export declare function arrayLength(remote: Protocol.Runtime.RemoteObject): {
60
+ length?: number;
61
+ };
44
62
  /** What a busy target is called in messages: the page, or an iframe (`--frame`) */
45
63
  export type BusyScope = 'page' | 'frame';
46
64
  /** When {@link withBusyPageRecovery} checks the target, and what it calls it */
@@ -100,6 +118,8 @@ export interface EvalTarget {
100
118
  recovery?: CDPSender;
101
119
  /** Execution context of a frame (`ExecutionContextDescription.uniqueId`) */
102
120
  uniqueContextId?: string;
121
+ /** `--full`: copy the result with every entry */
122
+ full?: boolean;
103
123
  }
104
124
  /**
105
125
  * Whether the page still answers. A page that does not answer in time is
@@ -156,6 +156,10 @@ const DESCRIBED_SUBTYPES = new Set([
156
156
  'iterator',
157
157
  'generator',
158
158
  ]);
159
+ /** Entries per list or object, and levels, of a copied eval result */
160
+ const COPY_LIMITS = { maxItems: 1000, maxDepth: 20 };
161
+ /** Limits of a copied eval result with `--full`: every entry, levels deep enough for real data */
162
+ const FULL_COPY_LIMITS = { maxItems: Infinity, maxDepth: 100 };
159
163
  /** Object subtypes shown by their short description (`button#submit`, `ArrayBuffer(8)`). */
160
164
  const BRIEF_SUBTYPES = new Set(['node', 'arraybuffer', 'dataview']);
161
165
  /**
@@ -165,12 +169,19 @@ const BRIEF_SUBTYPES = new Set(['node', 'arraybuffer', 'dataview']);
165
169
  * dates ISO strings, maps and sets entries, errors their message, BigInts
166
170
  * `12n`, functions `function name()`, and cycles `[Circular]` (an object
167
171
  * shared by two properties is copied twice). Works for objects of iframes
168
- * (other realms); lists and objects are cut after 1000 entries, and a
169
- * throwing getter becomes `[Error: …]`. It uses the page's built-ins
170
- * ({@link COPY_BUILTINS}), so it only runs when the page left them alone.
172
+ * (other realms); lists and objects are cut after 1000 entries and levels
173
+ * below the 20th become `[…]` (with `full`: no entries are cut, levels
174
+ * below the 100th), and a throwing getter becomes `[Error: …]`. It uses the
175
+ * page's built-ins ({@link COPY_BUILTINS}), so it only runs when the page
176
+ * left them alone.
177
+ *
178
+ * @param full - `--full`: copy every entry
179
+ * @returns Function declaration for Runtime.callFunctionOn
171
180
  */
172
- export const JSON_SAFE_COPY_FUNCTION = `function () {
173
- const MAX_ITEMS = 1000;
181
+ export function jsonSafeCopyFunction(full = false) {
182
+ const limits = full ? FULL_COPY_LIMITS : COPY_LIMITS;
183
+ return `function () {
184
+ const MAX_ITEMS = ${limits.maxItems};
174
185
  const ancestors = new Set();
175
186
  const kind = (value) => Object.prototype.toString.call(value).slice(8, -1);
176
187
  const isNode = (value) => typeof value.nodeType === 'number' && typeof value.nodeName === 'string';
@@ -209,7 +220,7 @@ export const JSON_SAFE_COPY_FUNCTION = `function () {
209
220
  const copy = (value, depth) => {
210
221
  if (value === null || typeof value !== 'object') return leaf(value);
211
222
  if (ancestors.has(value)) return '[Circular]';
212
- if (depth > 20) return '[…]';
223
+ if (depth > ${limits.maxDepth}) return '[…]';
213
224
  const next = (item) => copy(item, depth + 1);
214
225
  const type = kind(value);
215
226
  if (isNode(value)) return describeNode(value);
@@ -236,6 +247,7 @@ export const JSON_SAFE_COPY_FUNCTION = `function () {
236
247
  };
237
248
  return copy(this, 0);
238
249
  }`;
250
+ }
239
251
  /** Built-ins {@link JSON_SAFE_COPY_FUNCTION} uses (it runs in the page's world, next to the result) */
240
252
  const COPY_BUILTINS = [
241
253
  'Object.keys',
@@ -284,9 +296,10 @@ function isTerminated(details) {
284
296
  *
285
297
  * @param cdp - CDP connection
286
298
  * @param remote - Remote object returned by Runtime.evaluate
299
+ * @param full - `--full`: copy every entry ({@link jsonSafeCopyFunction})
287
300
  * @returns Value and type
288
301
  */
289
- async function toEvalResult(cdp, remote) {
302
+ async function toEvalResult(cdp, remote, full) {
290
303
  const kind = { type: remote.type, ...(remote.subtype && { subtype: remote.subtype }) };
291
304
  if (remote.unserializableValue !== undefined)
292
305
  return { value: remote.unserializableValue, ...kind };
@@ -301,12 +314,27 @@ async function toEvalResult(cdp, remote) {
301
314
  return { value: formatRemoteObject(remote), ...kind };
302
315
  }
303
316
  const replaced = await findReplacedBuiltins(cdp, COPY_BUILTINS, remote.objectId);
304
- const copy = await copyByValue(cdp, remote.objectId, replaced.length === 0 ? JSON_SAFE_COPY_FUNCTION : SELF_FUNCTION);
317
+ const copy = await copyByValue(cdp, remote.objectId, replaced.length === 0 ? jsonSafeCopyFunction(full) : SELF_FUNCTION);
305
318
  const value = copy ? copy.value : formatRemoteObject(remote);
319
+ const copied = { value, ...kind, ...arrayLength(remote) };
306
320
  if (replaced.length === 0)
307
- return { value, ...kind };
321
+ return copied;
308
322
  const warning = copy ? evalCopiedByBrowserWarning(replaced) : evalPreviewWarning(replaced);
309
- return { value, ...kind, warning };
323
+ return { ...copied, warning };
324
+ }
325
+ /**
326
+ * Elements of an array result (also a node list or typed array), read from
327
+ * its description (`Array(20000)`, `NodeList(5)`), since its copy holds
328
+ * at most 1000 unless `--full`.
329
+ *
330
+ * @param remote - Remote object returned by Runtime.evaluate
331
+ * @returns `length`, when the result is an array
332
+ */
333
+ export function arrayLength(remote) {
334
+ if (remote.subtype !== 'array' && remote.subtype !== 'typedarray')
335
+ return {};
336
+ const match = /\((\d+)\)$/.exec(remote.description ?? '');
337
+ return match ? { length: Number(match[1]) } : {};
310
338
  }
311
339
  /**
312
340
  * Copy a result by value: what a function called on it returns, which the
@@ -314,7 +342,7 @@ async function toEvalResult(cdp, remote) {
314
342
  *
315
343
  * @param cdp - CDP connection
316
344
  * @param objectId - The result
317
- * @param functionDeclaration - {@link JSON_SAFE_COPY_FUNCTION}, or
345
+ * @param functionDeclaration - {@link jsonSafeCopyFunction}'s, or
318
346
  * {@link SELF_FUNCTION} for the browser's plain copy (it fails on cycles
319
347
  * and BigInts)
320
348
  * @returns The copy, or undefined when it failed
@@ -560,7 +588,7 @@ export async function evaluateScript(cdp, script, target = {}) {
560
588
  try {
561
589
  const response = await withDeadline(executeScript(session, script, evaluateOptions(target)), EVAL_TIMEOUT_MS + TERMINATION_GRACE_MS, () => evaluationTimeoutError(target.recovery ?? session, scope));
562
590
  const settled = await withDeadline(settlePromise(session, response.result, script), EVAL_TIMEOUT_MS, promiseTimeout);
563
- return await toEvalResult(session, settled);
591
+ return await toEvalResult(session, settled, target.full ?? false);
564
592
  }
565
593
  catch (error) {
566
594
  throw scope === 'page' && isContextLostError(error)
@@ -19,7 +19,7 @@ import type { RawFormData } from './formTypes.js';
19
19
  * sensitive select's options do not say which is chosen)
20
20
  * - Form relevance scoring for multi-form pages
21
21
  */
22
- export declare const FORM_DISCOVERY_SCRIPT = "\n(function() {\n const result = { forms: [] };\n\n // Rendered and not visibility-hidden, ancestors included (opacity is left\n // out: styled checkboxes and radios are often transparent)\n function isShown(element) {\n if (typeof element.checkVisibility === 'function') {\n return element.checkVisibility({ visibilityProperty: true });\n }\n const style = window.getComputedStyle(element);\n return style.display !== 'none' && style.visibility !== 'hidden';\n }\n\n // Inside an open dialog: <dialog open>, aria-modal or a dialog role, or a\n // modal without them (DocSearch): an overlay ancestor below <body>\n // (position fixed or absolute) that is raised (z-index > 0) or has a\n // backdrop and whose class names a modal or dialog, or a fixed overlay over\n // half the viewport or more that is raised or lies over other page content\n // at the viewport centre. Static wrappers (Drupal's\n // dialog-off-canvas-main-canvas) and fixed app shells holding the whole\n // page are not dialogs.\n function inDialog(element) {\n if (element.closest('dialog[open], [aria-modal=\"true\"], [role=\"dialog\"], [role=\"alertdialog\"]')) return true;\n const viewportArea = window.innerWidth * window.innerHeight;\n for (let node = element.parentElement; node && node !== document.body; node = node.parentElement) {\n const style = window.getComputedStyle(node);\n if (style.position !== 'fixed' && style.position !== 'absolute') continue;\n const raised = parseInt(style.zIndex, 10) > 0;\n const className = typeof node.className === 'string' ? node.className : '';\n if (/(^|[\\s_-])(modal|dialog)($|[\\s_-])/i.test(className) && (raised || hasBackdrop(node))) return true;\n if (style.position !== 'fixed') continue;\n const box = node.getBoundingClientRect();\n if (box.width * box.height >= viewportArea / 2 && (raised || coversContent(node))) return true;\n }\n return false;\n }\n\n // A backdrop element next to or inside an overlay\n function hasBackdrop(node) {\n const backdrop = /backdrop|overlay/i;\n const near = [node.previousElementSibling, node.nextElementSibling, ...Array.from(node.children)];\n return near.some((n) => n && typeof n.className === 'string' && backdrop.test(n.className));\n }\n\n // The overlay is on top at the viewport centre with other page content\n // (not its ancestors) below it\n function coversContent(node) {\n const stack = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight / 2);\n if (!stack.length || !node.contains(stack[0])) return false;\n return stack.some((n) => !n.contains(node) && !node.contains(n));\n }\n\n function isUnique(selector) {\n try {\n return document.querySelectorAll(selector).length === 1;\n } catch (e) {\n return false;\n }\n }\n\n function generateSelector(element) {\n if (element.id && isUnique('#' + CSS.escape(element.id))) {\n return '#' + CSS.escape(element.id);\n }\n if (element.name) {\n const byName = element.tagName.toLowerCase() + '[name=\"' + CSS.escape(element.name) + '\"]';\n if (isUnique(byName)) return byName;\n const byValue = byName + '[value=\"' + CSS.escape(element.getAttribute('value') || '') + '\"]';\n if (element.hasAttribute('value') && isUnique(byValue)) return byValue;\n }\n const tag = element.tagName.toLowerCase();\n const parent = element.parentElement;\n if (!parent) return tag;\n const siblings = Array.from(parent.children).filter(c => c.tagName === element.tagName);\n if (siblings.length === 1) {\n const parentSelector = generateSelector(parent);\n return parentSelector + ' > ' + tag;\n }\n const index = siblings.indexOf(element);\n const parentSelector = generateSelector(parent);\n return parentSelector + ' > ' + tag + ':nth-of-type(' + (index + 1) + ')';\n }\n\n function cleanLabelText(text) {\n if (!text) return text;\n const patterns = [\n /Previous\\s*arrow/gi,\n /Next\\s*arrow/gi,\n /\\barrow\\b/gi,\n /\\bchevron\\b/gi,\n /\u2190|\u2192|\u2191|\u2193|\u25B2|\u25BC|\u25C0|\u25B6/g,\n /\\u25C0|\\u25B6|\\u25B2|\\u25BC/g,\n /^\\s*[<>]\\s*/,\n /\\s*[<>]\\s*$/,\n ];\n let cleaned = text;\n for (const pattern of patterns) {\n cleaned = cleaned.replace(pattern, '');\n }\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n cleaned = cleaned.replace(/^\\*\\s+/, '').replace(/\\s+\\*(\\s*:?)$/, '$1');\n return cleaned || text.trim();\n }\n\n // The element labelling a field: label[for], aria-labelledby, wrapping label\n function labelElement(element) {\n const labelFor = element.id ? document.querySelector('label[for=\"' + CSS.escape(element.id) + '\"]') : null;\n const labelledBy = element.getAttribute('aria-labelledby');\n return labelFor || (labelledBy && document.getElementById(labelledBy)) || element.closest('label');\n }\n\n // A standalone required star in label text: \"* Name\", \"Name *\", \"Name *:\"\n // (not a footnote mark such as \"Terms*\")\n function labelMarksRequired(text) {\n return /^\\*\\s|\\s\\*\\s*:?$/.test(String(text).trim());\n }\n\n // Required by attribute, ARIA, or a label marked with a standalone star,\n // or with an element of its own holding just \"*\" (<span aria-hidden>*</span>)\n function isRequired(element) {\n if (element.required || element.getAttribute('aria-required') === 'true') return true;\n const label = labelElement(element);\n if (!label) return false;\n const clone = label.cloneNode(true);\n clone.querySelectorAll('input, select, textarea, button').forEach(i => i.remove());\n return labelMarksRequired(clone.textContent || '') ||\n Array.from(clone.querySelectorAll('*')).some(n => n.textContent.trim() === '*');\n }\n\n // Name of the radio or checkbox group a field belongs to: its radiogroup's\n // label, its fieldset's legend, or its name\n function groupLabel(element) {\n const type = element.type?.toLowerCase();\n if ((type !== 'radio' && type !== 'checkbox') || !element.name) return undefined;\n const group = element.closest('[role=\"radiogroup\"], fieldset');\n const named = group && (group.getAttribute('aria-label') ||\n (group.querySelector(':scope > legend') || {}).textContent);\n if (named && named.trim()) return cleanLabelText(named);\n return element.name\n .replace(/[_-]/g, ' ')\n .replace(/([a-z])([A-Z])/g, '$1 $2')\n .replace(/^./, s => s.toUpperCase());\n }\n\n function extractLabel(element) {\n if (element.id) {\n const labelFor = document.querySelector('label[for=\"' + CSS.escape(element.id) + '\"]');\n if (labelFor) return cleanLabelText(labelFor.textContent);\n }\n const ariaLabel = element.getAttribute('aria-label');\n if (ariaLabel) return cleanLabelText(ariaLabel);\n const ariaLabelledBy = element.getAttribute('aria-labelledby');\n if (ariaLabelledBy) {\n const labelEl = document.getElementById(ariaLabelledBy);\n if (labelEl) return cleanLabelText(labelEl.textContent);\n }\n const wrappingLabel = element.closest('label');\n if (wrappingLabel) {\n const clone = wrappingLabel.cloneNode(true);\n const inputs = clone.querySelectorAll('input, select, textarea, button');\n inputs.forEach(i => i.remove());\n const text = clone.textContent.trim();\n if (text) return cleanLabelText(text);\n }\n if (element.placeholder) return cleanLabelText(element.placeholder);\n const title = element.getAttribute('title');\n if (title) return cleanLabelText(title);\n if (element.name) {\n return element.name\n .replace(/[_-]/g, ' ')\n .replace(/([a-z])([A-Z])/g, '$1 $2')\n .replace(/^./, s => s.toUpperCase());\n }\n return 'Unlabeled field';\n }\n\n function extractButtonLabel(element) {\n const ariaLabel = element.getAttribute('aria-label');\n if (ariaLabel) return ariaLabel.trim();\n if (element.value && element.type !== 'submit') return element.value;\n const text = element.textContent.trim();\n if (text) return text;\n if (element.value) return element.value;\n const title = element.getAttribute('title');\n if (title) return title;\n return 'Button';\n }\n\n function getFieldValue(element) {\n const tag = element.tagName.toLowerCase();\n const type = element.type?.toLowerCase() || 'text';\n if (type === 'hidden') return '';\n if (isSecret(element)) {\n return (element.isContentEditable ? element.textContent : element.value) ? '\u2022\u2022\u2022\u2022' : '';\n }\n if (tag === 'select') {\n if (element.multiple) {\n return Array.from(element.selectedOptions).map(o => o.value);\n }\n return element.value;\n }\n if (type === 'checkbox' || type === 'radio') {\n return element.checked;\n }\n if (element.getAttribute('role') === 'checkbox' || element.getAttribute('role') === 'switch') {\n return element.getAttribute('aria-checked') === 'true';\n }\n if (element.isContentEditable) {\n return element.textContent || '';\n }\n return element.value || '';\n }\n\n function getFieldState(element, value) {\n const type = element.type?.toLowerCase() || 'text';\n if (type === 'checkbox' || element.getAttribute('role') === 'checkbox' || element.getAttribute('role') === 'switch') {\n return value ? 'checked' : 'unchecked';\n }\n if (type === 'radio') {\n return value ? 'checked' : 'unchecked';\n }\n if (Array.isArray(value)) {\n return value.length > 0 ? 'filled' : 'empty';\n }\n if (typeof value === 'string') {\n return value.length > 0 ? 'filled' : 'empty';\n }\n return 'empty';\n }\n\n function getFieldType(element) {\n const tag = element.tagName.toLowerCase();\n const role = element.getAttribute('role');\n if (role === 'textbox') return 'textbox';\n if (role === 'checkbox') return 'checkbox';\n if (role === 'radio') return 'radio';\n if (role === 'combobox') return 'combobox';\n if (role === 'listbox') return 'listbox';\n if (role === 'switch') return 'switch';\n if (element.isContentEditable) return 'contenteditable';\n if (tag === 'textarea') return 'textarea';\n if (tag === 'select') return 'select';\n if (tag === 'input') {\n const type = element.type?.toLowerCase() || 'text';\n return type;\n }\n return 'unknown';\n }\n\n function isNativeInput(element) {\n const tag = element.tagName.toLowerCase();\n return tag === 'input' || tag === 'textarea' || tag === 'select';\n }\n\n function isSecret(element) {\n const type = element.type?.toLowerCase() || 'text';\n if (type === 'checkbox' || type === 'radio') return false;\n if (element.getAttribute('role') === 'checkbox' || element.getAttribute('role') === 'switch') return false;\n return ((el) => {\n const autocomplete = el.getAttribute('autocomplete') || '';\n if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;\n if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;\n const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');\n if (/passw|passwd|pwd|passcode|(^|[^a-z])otp([^a-z]|$)|cvv|cvc/i.test(names)) return true;\n try {\n const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');\n return Boolean(security) && security !== 'none';\n } catch (e) {\n return false;\n }\n})(element);\n }\n\n function getSelectOptions(element) {\n if (element.tagName.toLowerCase() !== 'select') return undefined;\n const secret = isSecret(element);\n return Array.from(element.options).map(opt => ({\n value: secret ? '' : opt.value,\n label: opt.textContent.trim(),\n selected: secret ? false : opt.selected\n }));\n }\n\n function findSiblingError(element) {\n const next = element.nextElementSibling;\n if (next) {\n const isError = next.classList.contains('error') ||\n next.classList.contains('invalid') ||\n next.classList.contains('field-error') ||\n next.classList.contains('error-message') ||\n next.getAttribute('role') === 'alert';\n if (isError) return next.textContent.trim();\n }\n // aria-errormessage always names the error; aria-describedby does when the\n // field is marked invalid or the description looks like an error\n const invalid = element.getAttribute('aria-invalid') === 'true';\n const errorIds = (element.getAttribute('aria-errormessage') || '').split(/\\s+/).filter(Boolean);\n const describedIds = (element.getAttribute('aria-describedby') || '').split(/\\s+/).filter(Boolean);\n for (const id of [...errorIds, ...describedIds]) {\n const target = document.getElementById(id);\n const looksLikeError = errorIds.includes(id) || invalid ||\n /error|invalid|alert/i.test(target ? target.className + ' ' + (target.getAttribute('role') || '') : '');\n if (target && looksLikeError && target.textContent.trim()) {\n return target.textContent.trim();\n }\n }\n const parent = element.parentElement;\n const controls = parent ? parent.querySelectorAll('input, select, textarea, [contenteditable=\"true\"]') : [];\n if (parent && controls.length === 1) {\n const errorEl = parent.querySelector('.error-message, .field-error, [role=\"alert\"]');\n if (errorEl && errorEl !== element) return errorEl.textContent.trim();\n }\n return undefined;\n }\n\n function calculateRelevance(formEl, fields, buttons) {\n let score = 0;\n const rect = formEl.getBoundingClientRect();\n const viewportHeight = window.innerHeight;\n const viewportWidth = window.innerWidth;\n const centerX = rect.left + rect.width / 2;\n const centerY = rect.top + rect.height / 2;\n if (centerX > viewportWidth * 0.2 && centerX < viewportWidth * 0.8) score += 10;\n if (centerY > 0 && centerY < viewportHeight) score += 5;\n const isInMain = formEl.closest('main, [role=\"main\"], article, .main-content, #main');\n if (isInMain) score += 15;\n const isInHeader = formEl.closest('header, [role=\"banner\"], nav, [role=\"navigation\"]');\n if (isInHeader) score -= 10;\n const isInAside = formEl.closest('aside, [role=\"complementary\"], footer, [role=\"contentinfo\"]');\n if (isInAside) score -= 5;\n const distinctFields = new Set(\n fields.map((f) => (f.type === 'radio' || f.type === 'checkbox') && f.name ? f.type + ':' + f.name : f.index)\n ).size;\n score += Math.min(distinctFields * 3, 30);\n const textTypes = ['text', 'search', 'email', 'password', 'textarea', 'tel', 'url', 'number', 'textbox'];\n if (fields.some((f) => textTypes.includes(f.type))) score += 10;\n const hasSubmit = buttons.some(b => b.type === 'submit' || b.primaryClass);\n if (hasSubmit) score += 10;\n const style = window.getComputedStyle(formEl);\n if (style.display === 'none' || style.visibility === 'hidden') score -= 100;\n return score;\n }\n\n function detectFormStep(formEl) {\n const stepIndicators = formEl.querySelectorAll('[aria-current=\"step\"], .step.active, .wizard-step.current');\n if (stepIndicators.length === 0) {\n const pageSteps = document.querySelectorAll('.step, .wizard-step, [role=\"tablist\"] [role=\"tab\"]');\n if (pageSteps.length > 1) {\n const activeIndex = Array.from(pageSteps).findIndex(s =>\n s.classList.contains('active') ||\n s.classList.contains('current') ||\n s.getAttribute('aria-selected') === 'true'\n );\n if (activeIndex >= 0) {\n return { current: activeIndex + 1, total: pageSteps.length };\n }\n }\n }\n const ariaStep = formEl.querySelector('[aria-current=\"step\"]');\n if (ariaStep) {\n const allSteps = formEl.querySelectorAll('[role=\"listitem\"], .step');\n const currentIdx = Array.from(allSteps).indexOf(ariaStep);\n if (currentIdx >= 0) {\n return { current: currentIdx + 1, total: allSteps.length };\n }\n }\n return null;\n }\n\n function findNearbyHeading(formEl) {\n let sibling = formEl.previousElementSibling;\n let distance = 0;\n while (sibling && distance < 3) {\n if (sibling.matches('h1, h2, h3, h4, h5, h6, [role=\"heading\"]')) {\n return sibling.textContent.trim();\n }\n const heading = sibling.querySelector('h1, h2, h3, h4, h5, h6, [role=\"heading\"]');\n if (heading) return heading.textContent.trim();\n sibling = sibling.previousElementSibling;\n distance++;\n }\n const parent = formEl.parentElement;\n if (parent) {\n const heading = parent.querySelector(':scope > h1, :scope > h2, :scope > h3');\n if (heading && heading.compareDocumentPosition(formEl) & Node.DOCUMENT_POSITION_FOLLOWING) {\n return heading.textContent.trim();\n }\n }\n return null;\n }\n\n function matchesWord(text, word) {\n const pattern = new RegExp('\\\\b' + word + '\\\\b', 'i');\n return pattern.test(text);\n }\n\n function inferFormType(formEl) {\n const inputs = formEl.querySelectorAll('input, textarea, select');\n const types = Array.from(inputs).map(i => i.type?.toLowerCase() || i.tagName.toLowerCase());\n const names = Array.from(inputs).map(i => (i.name || '').toLowerCase());\n const allText = (Array.from(inputs).map(i => i.name + ' ' + i.placeholder + ' ' + i.id).join(' ')).toLowerCase();\n if (types.includes('password')) {\n if (types.filter(t => t === 'password').length >= 2) return 'Change Password';\n if (matchesWord(allText, 'register') || matchesWord(allText, 'signup') || matchesWord(allText, 'create')) return 'Registration';\n return 'Login';\n }\n const hasSearchRole = formEl.closest('[role=\"search\"]') || formEl.getAttribute('role') === 'search';\n const hasSearchInput = formEl.querySelector('[type=\"search\"], [aria-label*=\"search\" i]');\n if (hasSearchRole || hasSearchInput) return 'Search';\n const interactiveTypes = types.filter(t => !['hidden', 'submit', 'button', 'reset', 'image'].includes(t));\n if (interactiveTypes.length <= 2 && matchesWord(allText, 'search')) return 'Search';\n if (matchesWord(allText, 'address') || matchesWord(allText, 'street') || matchesWord(allText, 'postcode') ||\n matchesWord(allText, 'zipcode') || matchesWord(allText, 'city') || matchesWord(allText, 'county')) {\n return 'Address';\n }\n if (matchesWord(allText, 'email') && (matchesWord(allText, 'message') || matchesWord(allText, 'subject'))) {\n return 'Contact';\n }\n if (matchesWord(allText, 'card') || matchesWord(allText, 'cvv') || matchesWord(allText, 'expiry')) {\n return 'Payment';\n }\n if (names.some(n => matchesWord(n, 'subscribe') || matchesWord(n, 'newsletter'))) {\n return 'Newsletter';\n }\n return null;\n }\n\n function extractFormName(formEl) {\n const ariaLabel = formEl.getAttribute('aria-label');\n if (ariaLabel) return ariaLabel;\n const ariaLabelledBy = formEl.getAttribute('aria-labelledby');\n if (ariaLabelledBy) {\n const labelEl = document.getElementById(ariaLabelledBy);\n if (labelEl) return labelEl.textContent.trim();\n }\n const headings = formEl.querySelectorAll('h1, h2, h3, h4, h5, h6, [role=\"heading\"]');\n for (const h of headings) {\n if (!h.closest('[role=\"dialog\"], [role=\"alertdialog\"], [aria-modal=\"true\"]')) {\n return h.textContent.trim();\n }\n }\n const title = formEl.getAttribute('title');\n if (title) return title;\n const name = formEl.getAttribute('name');\n if (name) {\n return name\n .replace(/[_-]/g, ' ')\n .replace(/([a-z])([A-Z])/g, '$1 $2')\n .replace(/^./, s => s.toUpperCase());\n }\n const nearbyHeading = findNearbyHeading(formEl);\n if (nearbyHeading) return nearbyHeading;\n const inferredType = inferFormType(formEl);\n if (inferredType) return inferredType;\n return null;\n }\n\n function discoverFields(container, formIndex, startIndex) {\n const fields = [];\n const nativeInputs = container.querySelectorAll(\n 'input:not([type=\"hidden\"]):not([type=\"submit\"]):not([type=\"button\"]):not([type=\"reset\"]):not([type=\"image\"]), ' +\n 'textarea, ' +\n 'select'\n );\n const customInputs = container.querySelectorAll(\n '[role=\"textbox\"], ' +\n '[role=\"checkbox\"]:not(input), ' +\n '[role=\"radio\"]:not(input), ' +\n '[role=\"combobox\"], ' +\n '[role=\"listbox\"], ' +\n '[role=\"switch\"], ' +\n '[contenteditable=\"true\"]'\n );\n const allInputs = new Set([...nativeInputs, ...customInputs]);\n let idx = startIndex;\n for (const el of allInputs) {\n const isHidden = el.type === 'hidden' || !isShown(el);\n const value = getFieldValue(el);\n fields.push({\n index: idx,\n formIndex: formIndex,\n selector: generateSelector(el),\n type: getFieldType(el),\n inputType: el.type?.toLowerCase(),\n label: extractLabel(el),\n name: el.name || null,\n placeholder: el.placeholder || undefined,\n required: isRequired(el),\n groupLabel: groupLabel(el),\n disabled: el.disabled || el.getAttribute('aria-disabled') === 'true',\n readOnly: el.readOnly || false,\n hidden: isHidden,\n native: isNativeInput(el),\n value: value,\n checked: el.checked,\n validationMessage: el.validationMessage || undefined,\n isValid: el.validity ? el.validity.valid : true,\n valueMissing: el.validity ? el.validity.valueMissing : false,\n ariaInvalid: el.getAttribute('aria-invalid') === 'true',\n hasErrorClass: el.classList.contains('error') || el.classList.contains('invalid'),\n siblingErrorText: findSiblingError(el),\n options: getSelectOptions(el)\n });\n idx++;\n }\n return fields;\n }\n\n function discoverButtons(container, startIndex) {\n const buttons = [];\n const buttonEls = container.querySelectorAll(\n 'button, ' +\n 'input[type=\"submit\"], ' +\n 'input[type=\"button\"], ' +\n 'input[type=\"reset\"], ' +\n '[role=\"button\"]'\n );\n let idx = startIndex;\n for (const el of buttonEls) {\n if (!isShown(el)) continue;\n const type = el.type?.toLowerCase() || 'button';\n const btnType = type === 'submit' ? 'submit' : type === 'reset' ? 'reset' : 'button';\n const explicitSubmit = btnType === 'submit' &&\n (el.tagName === 'INPUT' || (el.getAttribute('type') || '').toLowerCase() === 'submit');\n const formDefault = btnType === 'submit' && !explicitSubmit && Boolean(el.form);\n const primaryClass = Array.from(el.classList).some(c => /^(btn[-_])?(primary|submit)$/i.test(c));\n buttons.push({\n index: idx,\n selector: generateSelector(el),\n label: extractButtonLabel(el),\n type: btnType,\n disabled: el.disabled || el.getAttribute('aria-disabled') === 'true',\n explicitSubmit: explicitSubmit,\n formDefault: formDefault,\n primaryClass: primaryClass\n });\n idx++;\n }\n return buttons;\n }\n\n const forms = document.querySelectorAll('form');\n let globalIndex = 0;\n\n // A page still loading may not have a body yet\n const pageRoot = document.body || document.documentElement;\n if (forms.length === 0 && pageRoot) {\n const bodyFields = discoverFields(pageRoot, 0, 0);\n const bodyButtons = discoverButtons(pageRoot, bodyFields.length);\n if (bodyFields.length > 0) {\n result.forms.push({\n index: 0,\n name: document.title || 'Page',\n action: null,\n method: 'GET',\n step: null,\n relevanceScore: bodyFields.length * 3,\n hidden: false,\n inDialog: false,\n inIframe: false,\n fields: bodyFields,\n buttons: bodyButtons\n });\n }\n } else {\n for (let i = 0; i < forms.length; i++) {\n const formEl = forms[i];\n const fields = discoverFields(formEl, i, globalIndex);\n globalIndex += fields.length;\n const buttons = discoverButtons(formEl, globalIndex);\n globalIndex += buttons.length;\n const inIframe = formEl.ownerDocument !== document;\n // Shown when a field or a button is (buttons list shown ones only), or\n // it has neither (a form with only type=hidden inputs and no button)\n const shown = isShown(formEl) &&\n (fields.some((f) => !f.hidden) || buttons.length > 0 || fields.length === 0);\n result.forms.push({\n index: i,\n name: extractFormName(formEl),\n action: formEl.action || null,\n method: (formEl.method || 'GET').toUpperCase(),\n step: detectFormStep(formEl),\n relevanceScore: calculateRelevance(formEl, fields, buttons),\n hidden: !shown,\n inDialog: shown && inDialog(formEl),\n inIframe: inIframe,\n fields: fields,\n buttons: buttons\n });\n }\n }\n\n // Forms the main document does not contain may be in its same-origin iframes\n if (result.forms.length === 0) {\n result.frameForms = [];\n for (const frame of document.querySelectorAll('iframe, frame')) {\n let frameDocument = null;\n try { frameDocument = frame.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument && frameDocument.querySelector('form, input:not([type=hidden]), select, textarea')) {\n result.frameForms.push({ url: frame.src || 'about:blank' });\n }\n }\n }\n\n result.readyState = document.readyState;\n return result;\n})()\n";
22
+ export declare const FORM_DISCOVERY_SCRIPT = "\n(function() {\n const result = { forms: [] };\n\n // Rendered and not visibility-hidden, ancestors included (opacity is left\n // out: styled checkboxes and radios are often transparent)\n function isShown(element) {\n if (typeof element.checkVisibility === 'function') {\n return element.checkVisibility({ visibilityProperty: true });\n }\n const style = window.getComputedStyle(element);\n return style.display !== 'none' && style.visibility !== 'hidden';\n }\n\n // Inside an open dialog: <dialog open>, aria-modal or a dialog role, or a\n // modal without them (DocSearch): an overlay ancestor below <body>\n // (position fixed or absolute) that is raised (z-index > 0) or has a\n // backdrop and whose class names a modal or dialog, or a fixed overlay over\n // half the viewport or more that is raised or lies over other page content\n // at the viewport centre. Static wrappers (Drupal's\n // dialog-off-canvas-main-canvas) and fixed app shells holding the whole\n // page are not dialogs.\n function inDialog(element) {\n if (element.closest('dialog[open], [aria-modal=\"true\"], [role=\"dialog\"], [role=\"alertdialog\"]')) return true;\n const viewportArea = window.innerWidth * window.innerHeight;\n for (let node = element.parentElement; node && node !== document.body; node = node.parentElement) {\n const style = window.getComputedStyle(node);\n if (style.position !== 'fixed' && style.position !== 'absolute') continue;\n const raised = parseInt(style.zIndex, 10) > 0;\n const className = typeof node.className === 'string' ? node.className : '';\n if (/(^|[\\s_-])(modal|dialog)($|[\\s_-])/i.test(className) && (raised || hasBackdrop(node))) return true;\n if (style.position !== 'fixed') continue;\n const box = node.getBoundingClientRect();\n if (box.width * box.height >= viewportArea / 2 && (raised || coversContent(node))) return true;\n }\n return false;\n }\n\n // A backdrop element next to or inside an overlay\n function hasBackdrop(node) {\n const backdrop = /backdrop|overlay/i;\n const near = [node.previousElementSibling, node.nextElementSibling, ...Array.from(node.children)];\n return near.some((n) => n && typeof n.className === 'string' && backdrop.test(n.className));\n }\n\n // The overlay is on top at the viewport centre with other page content\n // (not its ancestors) below it\n function coversContent(node) {\n const stack = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight / 2);\n if (!stack.length || !node.contains(stack[0])) return false;\n return stack.some((n) => !n.contains(node) && !node.contains(n));\n }\n\n function isUnique(selector) {\n try {\n return document.querySelectorAll(selector).length === 1;\n } catch (e) {\n return false;\n }\n }\n\n function generateSelector(element) {\n if (element.id && isUnique('#' + CSS.escape(element.id))) {\n return '#' + CSS.escape(element.id);\n }\n if (element.name) {\n const byName = element.tagName.toLowerCase() + '[name=\"' + CSS.escape(element.name) + '\"]';\n if (isUnique(byName)) return byName;\n const byValue = byName + '[value=\"' + CSS.escape(element.getAttribute('value') || '') + '\"]';\n if (element.hasAttribute('value') && isUnique(byValue)) return byValue;\n }\n const tag = element.tagName.toLowerCase();\n const parent = element.parentElement;\n if (!parent) return tag;\n const siblings = Array.from(parent.children).filter(c => c.tagName === element.tagName);\n if (siblings.length === 1) {\n const parentSelector = generateSelector(parent);\n return parentSelector + ' > ' + tag;\n }\n const index = siblings.indexOf(element);\n const parentSelector = generateSelector(parent);\n return parentSelector + ' > ' + tag + ':nth-of-type(' + (index + 1) + ')';\n }\n\n function cleanLabelText(text) {\n if (!text) return text;\n const patterns = [\n /Previous\\s*arrow/gi,\n /Next\\s*arrow/gi,\n /\\barrow\\b/gi,\n /\\bchevron\\b/gi,\n /\u2190|\u2192|\u2191|\u2193|\u25B2|\u25BC|\u25C0|\u25B6/g,\n /\\u25C0|\\u25B6|\\u25B2|\\u25BC/g,\n /^\\s*[<>]\\s*/,\n /\\s*[<>]\\s*$/,\n ];\n let cleaned = text;\n for (const pattern of patterns) {\n cleaned = cleaned.replace(pattern, '');\n }\n cleaned = cleaned.replace(/\\s{2,}/g, ' ').trim();\n cleaned = cleaned.replace(/^\\*\\s+/, '').replace(/\\s+\\*(\\s*:?)$/, '$1');\n return cleaned || text.trim();\n }\n\n // The element labelling a field: label[for], aria-labelledby, wrapping label\n function labelElement(element) {\n const labelFor = element.id ? document.querySelector('label[for=\"' + CSS.escape(element.id) + '\"]') : null;\n const labelledBy = element.getAttribute('aria-labelledby');\n return labelFor || (labelledBy && document.getElementById(labelledBy)) || element.closest('label');\n }\n\n // A standalone required star in label text: \"* Name\", \"Name *\", \"Name *:\"\n // (not a footnote mark such as \"Terms*\")\n function labelMarksRequired(text) {\n return /^\\*\\s|\\s\\*\\s*:?$/.test(String(text).trim());\n }\n\n // Required by attribute, ARIA, or a label marked with a standalone star,\n // or with an element of its own holding just \"*\" (<span aria-hidden>*</span>)\n function isRequired(element) {\n if (element.required || element.getAttribute('aria-required') === 'true') return true;\n const label = labelElement(element);\n if (!label) return false;\n const clone = label.cloneNode(true);\n clone.querySelectorAll('input, select, textarea, button').forEach(i => i.remove());\n return labelMarksRequired(clone.textContent || '') ||\n Array.from(clone.querySelectorAll('*')).some(n => n.textContent.trim() === '*');\n }\n\n // Name of the radio or checkbox group a field belongs to: its radiogroup's\n // label, its fieldset's legend, or its name\n function groupLabel(element) {\n const type = element.type?.toLowerCase();\n if ((type !== 'radio' && type !== 'checkbox') || !element.name) return undefined;\n const group = element.closest('[role=\"radiogroup\"], fieldset');\n const named = group && (group.getAttribute('aria-label') ||\n (group.querySelector(':scope > legend') || {}).textContent);\n if (named && named.trim()) return cleanLabelText(named);\n return element.name\n .replace(/[_-]/g, ' ')\n .replace(/([a-z])([A-Z])/g, '$1 $2')\n .replace(/^./, s => s.toUpperCase());\n }\n\n function extractLabel(element) {\n if (element.id) {\n const labelFor = document.querySelector('label[for=\"' + CSS.escape(element.id) + '\"]');\n if (labelFor) return cleanLabelText(labelFor.textContent);\n }\n const ariaLabel = element.getAttribute('aria-label');\n if (ariaLabel) return cleanLabelText(ariaLabel);\n const ariaLabelledBy = element.getAttribute('aria-labelledby');\n if (ariaLabelledBy) {\n const labelEl = document.getElementById(ariaLabelledBy);\n if (labelEl) return cleanLabelText(labelEl.textContent);\n }\n const wrappingLabel = element.closest('label');\n if (wrappingLabel) {\n const clone = wrappingLabel.cloneNode(true);\n const inputs = clone.querySelectorAll('input, select, textarea, button');\n inputs.forEach(i => i.remove());\n const text = clone.textContent.trim();\n if (text) return cleanLabelText(text);\n }\n if (element.placeholder) return cleanLabelText(element.placeholder);\n const title = element.getAttribute('title');\n if (title) return cleanLabelText(title);\n if (element.name) {\n return element.name\n .replace(/[_-]/g, ' ')\n .replace(/([a-z])([A-Z])/g, '$1 $2')\n .replace(/^./, s => s.toUpperCase());\n }\n return 'Unlabeled field';\n }\n\n function extractButtonLabel(element) {\n const ariaLabel = element.getAttribute('aria-label');\n if (ariaLabel) return ariaLabel.trim();\n if (element.value && element.type !== 'submit') return element.value;\n const text = element.textContent.trim();\n if (text) return text;\n if (element.value) return element.value;\n const title = element.getAttribute('title');\n if (title) return title;\n return 'Button';\n }\n\n function getFieldValue(element) {\n const tag = element.tagName.toLowerCase();\n const type = element.type?.toLowerCase() || 'text';\n if (type === 'hidden') return '';\n if (isSecret(element)) {\n return (element.isContentEditable ? element.textContent : element.value) ? '\u2022\u2022\u2022\u2022' : '';\n }\n if (tag === 'select') {\n if (element.multiple) {\n return Array.from(element.selectedOptions).map(o => o.value);\n }\n return element.value;\n }\n if (type === 'checkbox' || type === 'radio') {\n return element.checked;\n }\n if (element.getAttribute('role') === 'checkbox' || element.getAttribute('role') === 'switch') {\n return element.getAttribute('aria-checked') === 'true';\n }\n if (element.isContentEditable) {\n return element.textContent || '';\n }\n return element.value || '';\n }\n\n function getFieldState(element, value) {\n const type = element.type?.toLowerCase() || 'text';\n if (type === 'checkbox' || element.getAttribute('role') === 'checkbox' || element.getAttribute('role') === 'switch') {\n return value ? 'checked' : 'unchecked';\n }\n if (type === 'radio') {\n return value ? 'checked' : 'unchecked';\n }\n if (Array.isArray(value)) {\n return value.length > 0 ? 'filled' : 'empty';\n }\n if (typeof value === 'string') {\n return value.length > 0 ? 'filled' : 'empty';\n }\n return 'empty';\n }\n\n function getFieldType(element) {\n const tag = element.tagName.toLowerCase();\n const role = element.getAttribute('role');\n if (role === 'textbox') return 'textbox';\n if (role === 'checkbox') return 'checkbox';\n if (role === 'radio') return 'radio';\n if (role === 'combobox') return 'combobox';\n if (role === 'listbox') return 'listbox';\n if (role === 'switch') return 'switch';\n if (element.isContentEditable) return 'contenteditable';\n if (tag === 'textarea') return 'textarea';\n if (tag === 'select') return 'select';\n if (tag === 'input') {\n const type = element.type?.toLowerCase() || 'text';\n return type;\n }\n return 'unknown';\n }\n\n function isNativeInput(element) {\n const tag = element.tagName.toLowerCase();\n return tag === 'input' || tag === 'textarea' || tag === 'select';\n }\n\n function isSecret(element) {\n const type = element.type?.toLowerCase() || 'text';\n if (type === 'checkbox' || type === 'radio') return false;\n if (element.getAttribute('role') === 'checkbox' || element.getAttribute('role') === 'switch') return false;\n return ((el) => {\n const autocomplete = el.getAttribute('autocomplete') || '';\n if (/(^|\\s)(cc-[a-z-]+|one-time-code|current-password|new-password)(\\s|$)/i.test(autocomplete)) return true;\n if (el.type === 'password' || /^password$/i.test(el.getAttribute('type') || '')) return true;\n const names = [el.getAttribute('name'), el.id, autocomplete].join(' ');\n if (/passw|passwd|pwd|passcode|passphrase|(^|[^a-z])(otp|pin|ssn)([^a-z]|$)|cvv|cvc|card[-_]?(num(ber)?|no)([^a-z]|$)/i.test(names)) return true;\n try {\n const security = el.ownerDocument.defaultView.getComputedStyle(el).getPropertyValue('-webkit-text-security');\n return Boolean(security) && security !== 'none';\n } catch (e) {\n return false;\n }\n})(element);\n }\n\n function getSelectOptions(element) {\n if (element.tagName.toLowerCase() !== 'select') return undefined;\n const secret = isSecret(element);\n return Array.from(element.options).map(opt => ({\n value: secret ? '' : opt.value,\n label: opt.textContent.trim(),\n selected: secret ? false : opt.selected\n }));\n }\n\n function findSiblingError(element) {\n const next = element.nextElementSibling;\n if (next) {\n const isError = next.classList.contains('error') ||\n next.classList.contains('invalid') ||\n next.classList.contains('field-error') ||\n next.classList.contains('error-message') ||\n next.getAttribute('role') === 'alert';\n if (isError) return next.textContent.trim();\n }\n // aria-errormessage always names the error; aria-describedby does when the\n // field is marked invalid or the description looks like an error\n const invalid = element.getAttribute('aria-invalid') === 'true';\n const errorIds = (element.getAttribute('aria-errormessage') || '').split(/\\s+/).filter(Boolean);\n const describedIds = (element.getAttribute('aria-describedby') || '').split(/\\s+/).filter(Boolean);\n for (const id of [...errorIds, ...describedIds]) {\n const target = document.getElementById(id);\n const looksLikeError = errorIds.includes(id) || invalid ||\n /error|invalid|alert/i.test(target ? target.className + ' ' + (target.getAttribute('role') || '') : '');\n if (target && looksLikeError && target.textContent.trim()) {\n return target.textContent.trim();\n }\n }\n const parent = element.parentElement;\n const controls = parent ? parent.querySelectorAll('input, select, textarea, [contenteditable=\"true\"]') : [];\n if (parent && controls.length === 1) {\n const errorEl = parent.querySelector('.error-message, .field-error, [role=\"alert\"]');\n if (errorEl && errorEl !== element) return errorEl.textContent.trim();\n }\n return undefined;\n }\n\n function calculateRelevance(formEl, fields, buttons) {\n let score = 0;\n const rect = formEl.getBoundingClientRect();\n const viewportHeight = window.innerHeight;\n const viewportWidth = window.innerWidth;\n const centerX = rect.left + rect.width / 2;\n const centerY = rect.top + rect.height / 2;\n if (centerX > viewportWidth * 0.2 && centerX < viewportWidth * 0.8) score += 10;\n if (centerY > 0 && centerY < viewportHeight) score += 5;\n const isInMain = formEl.closest('main, [role=\"main\"], article, .main-content, #main');\n if (isInMain) score += 15;\n const isInHeader = formEl.closest('header, [role=\"banner\"], nav, [role=\"navigation\"]');\n if (isInHeader) score -= 10;\n const isInAside = formEl.closest('aside, [role=\"complementary\"], footer, [role=\"contentinfo\"]');\n if (isInAside) score -= 5;\n const distinctFields = new Set(\n fields.map((f) => (f.type === 'radio' || f.type === 'checkbox') && f.name ? f.type + ':' + f.name : f.index)\n ).size;\n score += Math.min(distinctFields * 3, 30);\n const textTypes = ['text', 'search', 'email', 'password', 'textarea', 'tel', 'url', 'number', 'textbox'];\n if (fields.some((f) => textTypes.includes(f.type))) score += 10;\n const hasSubmit = buttons.some(b => b.type === 'submit' || b.primaryClass);\n if (hasSubmit) score += 10;\n const style = window.getComputedStyle(formEl);\n if (style.display === 'none' || style.visibility === 'hidden') score -= 100;\n return score;\n }\n\n function detectFormStep(formEl) {\n const stepIndicators = formEl.querySelectorAll('[aria-current=\"step\"], .step.active, .wizard-step.current');\n if (stepIndicators.length === 0) {\n const pageSteps = document.querySelectorAll('.step, .wizard-step, [role=\"tablist\"] [role=\"tab\"]');\n if (pageSteps.length > 1) {\n const activeIndex = Array.from(pageSteps).findIndex(s =>\n s.classList.contains('active') ||\n s.classList.contains('current') ||\n s.getAttribute('aria-selected') === 'true'\n );\n if (activeIndex >= 0) {\n return { current: activeIndex + 1, total: pageSteps.length };\n }\n }\n }\n const ariaStep = formEl.querySelector('[aria-current=\"step\"]');\n if (ariaStep) {\n const allSteps = formEl.querySelectorAll('[role=\"listitem\"], .step');\n const currentIdx = Array.from(allSteps).indexOf(ariaStep);\n if (currentIdx >= 0) {\n return { current: currentIdx + 1, total: allSteps.length };\n }\n }\n return null;\n }\n\n function findNearbyHeading(formEl) {\n let sibling = formEl.previousElementSibling;\n let distance = 0;\n while (sibling && distance < 3) {\n if (sibling.matches('h1, h2, h3, h4, h5, h6, [role=\"heading\"]')) {\n return sibling.textContent.trim();\n }\n const heading = sibling.querySelector('h1, h2, h3, h4, h5, h6, [role=\"heading\"]');\n if (heading) return heading.textContent.trim();\n sibling = sibling.previousElementSibling;\n distance++;\n }\n const parent = formEl.parentElement;\n if (parent) {\n const heading = parent.querySelector(':scope > h1, :scope > h2, :scope > h3');\n if (heading && heading.compareDocumentPosition(formEl) & Node.DOCUMENT_POSITION_FOLLOWING) {\n return heading.textContent.trim();\n }\n }\n return null;\n }\n\n function matchesWord(text, word) {\n const pattern = new RegExp('\\\\b' + word + '\\\\b', 'i');\n return pattern.test(text);\n }\n\n function inferFormType(formEl) {\n const inputs = formEl.querySelectorAll('input, textarea, select');\n const types = Array.from(inputs).map(i => i.type?.toLowerCase() || i.tagName.toLowerCase());\n const names = Array.from(inputs).map(i => (i.name || '').toLowerCase());\n const allText = (Array.from(inputs).map(i => i.name + ' ' + i.placeholder + ' ' + i.id).join(' ')).toLowerCase();\n if (types.includes('password')) {\n if (types.filter(t => t === 'password').length >= 2) return 'Change Password';\n if (matchesWord(allText, 'register') || matchesWord(allText, 'signup') || matchesWord(allText, 'create')) return 'Registration';\n return 'Login';\n }\n const hasSearchRole = formEl.closest('[role=\"search\"]') || formEl.getAttribute('role') === 'search';\n const hasSearchInput = formEl.querySelector('[type=\"search\"], [aria-label*=\"search\" i]');\n if (hasSearchRole || hasSearchInput) return 'Search';\n const interactiveTypes = types.filter(t => !['hidden', 'submit', 'button', 'reset', 'image'].includes(t));\n if (interactiveTypes.length <= 2 && matchesWord(allText, 'search')) return 'Search';\n if (matchesWord(allText, 'address') || matchesWord(allText, 'street') || matchesWord(allText, 'postcode') ||\n matchesWord(allText, 'zipcode') || matchesWord(allText, 'city') || matchesWord(allText, 'county')) {\n return 'Address';\n }\n if (matchesWord(allText, 'email') && (matchesWord(allText, 'message') || matchesWord(allText, 'subject'))) {\n return 'Contact';\n }\n if (matchesWord(allText, 'card') || matchesWord(allText, 'cvv') || matchesWord(allText, 'expiry')) {\n return 'Payment';\n }\n if (names.some(n => matchesWord(n, 'subscribe') || matchesWord(n, 'newsletter'))) {\n return 'Newsletter';\n }\n return null;\n }\n\n function extractFormName(formEl) {\n const ariaLabel = formEl.getAttribute('aria-label');\n if (ariaLabel) return ariaLabel;\n const ariaLabelledBy = formEl.getAttribute('aria-labelledby');\n if (ariaLabelledBy) {\n const labelEl = document.getElementById(ariaLabelledBy);\n if (labelEl) return labelEl.textContent.trim();\n }\n const headings = formEl.querySelectorAll('h1, h2, h3, h4, h5, h6, [role=\"heading\"]');\n for (const h of headings) {\n if (!h.closest('[role=\"dialog\"], [role=\"alertdialog\"], [aria-modal=\"true\"]')) {\n return h.textContent.trim();\n }\n }\n const title = formEl.getAttribute('title');\n if (title) return title;\n const name = formEl.getAttribute('name');\n if (name) {\n return name\n .replace(/[_-]/g, ' ')\n .replace(/([a-z])([A-Z])/g, '$1 $2')\n .replace(/^./, s => s.toUpperCase());\n }\n const nearbyHeading = findNearbyHeading(formEl);\n if (nearbyHeading) return nearbyHeading;\n const inferredType = inferFormType(formEl);\n if (inferredType) return inferredType;\n return null;\n }\n\n function discoverFields(container, formIndex, startIndex) {\n const fields = [];\n const nativeInputs = container.querySelectorAll(\n 'input:not([type=\"hidden\"]):not([type=\"submit\"]):not([type=\"button\"]):not([type=\"reset\"]):not([type=\"image\"]), ' +\n 'textarea, ' +\n 'select'\n );\n const customInputs = container.querySelectorAll(\n '[role=\"textbox\"], ' +\n '[role=\"checkbox\"]:not(input), ' +\n '[role=\"radio\"]:not(input), ' +\n '[role=\"combobox\"], ' +\n '[role=\"listbox\"], ' +\n '[role=\"switch\"], ' +\n '[contenteditable=\"true\"]'\n );\n const allInputs = new Set([...nativeInputs, ...customInputs]);\n let idx = startIndex;\n for (const el of allInputs) {\n const isHidden = el.type === 'hidden' || !isShown(el);\n const value = getFieldValue(el);\n fields.push({\n index: idx,\n formIndex: formIndex,\n selector: generateSelector(el),\n type: getFieldType(el),\n inputType: el.type?.toLowerCase(),\n label: extractLabel(el),\n name: el.name || null,\n placeholder: el.placeholder || undefined,\n required: isRequired(el),\n groupLabel: groupLabel(el),\n disabled: el.disabled || el.getAttribute('aria-disabled') === 'true',\n readOnly: el.readOnly || false,\n hidden: isHidden,\n native: isNativeInput(el),\n value: value,\n checked: el.checked,\n validationMessage: el.validationMessage || undefined,\n isValid: el.validity ? el.validity.valid : true,\n valueMissing: el.validity ? el.validity.valueMissing : false,\n ariaInvalid: el.getAttribute('aria-invalid') === 'true',\n hasErrorClass: el.classList.contains('error') || el.classList.contains('invalid'),\n siblingErrorText: findSiblingError(el),\n options: getSelectOptions(el)\n });\n idx++;\n }\n return fields;\n }\n\n function discoverButtons(container, startIndex) {\n const buttons = [];\n const buttonEls = container.querySelectorAll(\n 'button, ' +\n 'input[type=\"submit\"], ' +\n 'input[type=\"button\"], ' +\n 'input[type=\"reset\"], ' +\n '[role=\"button\"]'\n );\n let idx = startIndex;\n for (const el of buttonEls) {\n if (!isShown(el)) continue;\n const type = el.type?.toLowerCase() || 'button';\n const btnType = type === 'submit' ? 'submit' : type === 'reset' ? 'reset' : 'button';\n const explicitSubmit = btnType === 'submit' &&\n (el.tagName === 'INPUT' || (el.getAttribute('type') || '').toLowerCase() === 'submit');\n const formDefault = btnType === 'submit' && !explicitSubmit && Boolean(el.form);\n const primaryClass = Array.from(el.classList).some(c => /^(btn[-_])?(primary|submit)$/i.test(c));\n buttons.push({\n index: idx,\n selector: generateSelector(el),\n label: extractButtonLabel(el),\n type: btnType,\n disabled: el.disabled || el.getAttribute('aria-disabled') === 'true',\n explicitSubmit: explicitSubmit,\n formDefault: formDefault,\n primaryClass: primaryClass\n });\n idx++;\n }\n return buttons;\n }\n\n const forms = document.querySelectorAll('form');\n let globalIndex = 0;\n\n // A page still loading may not have a body yet\n const pageRoot = document.body || document.documentElement;\n if (forms.length === 0 && pageRoot) {\n const bodyFields = discoverFields(pageRoot, 0, 0);\n const bodyButtons = discoverButtons(pageRoot, bodyFields.length);\n if (bodyFields.length > 0) {\n result.forms.push({\n index: 0,\n name: document.title || 'Page',\n action: null,\n method: 'GET',\n step: null,\n relevanceScore: bodyFields.length * 3,\n hidden: false,\n inDialog: false,\n inIframe: false,\n fields: bodyFields,\n buttons: bodyButtons\n });\n }\n } else {\n for (let i = 0; i < forms.length; i++) {\n const formEl = forms[i];\n const fields = discoverFields(formEl, i, globalIndex);\n globalIndex += fields.length;\n const buttons = discoverButtons(formEl, globalIndex);\n globalIndex += buttons.length;\n const inIframe = formEl.ownerDocument !== document;\n // Shown when a field or a button is (buttons list shown ones only), or\n // it has neither (a form with only type=hidden inputs and no button)\n const shown = isShown(formEl) &&\n (fields.some((f) => !f.hidden) || buttons.length > 0 || fields.length === 0);\n result.forms.push({\n index: i,\n name: extractFormName(formEl),\n action: formEl.action || null,\n method: (formEl.method || 'GET').toUpperCase(),\n step: detectFormStep(formEl),\n relevanceScore: calculateRelevance(formEl, fields, buttons),\n hidden: !shown,\n inDialog: shown && inDialog(formEl),\n inIframe: inIframe,\n fields: fields,\n buttons: buttons\n });\n }\n }\n\n // Forms the main document does not contain may be in its same-origin iframes\n if (result.forms.length === 0) {\n result.frameForms = [];\n for (const frame of document.querySelectorAll('iframe, frame')) {\n let frameDocument = null;\n try { frameDocument = frame.contentDocument; } catch (e) { frameDocument = null; }\n if (frameDocument && frameDocument.querySelector('form, input:not([type=hidden]), select, textarea')) {\n result.frameForms.push({ url: frame.src || 'about:blank' });\n }\n }\n }\n\n result.readyState = document.readyState;\n return result;\n})()\n";
23
23
  /**
24
24
  * Type guard for raw form data.
25
25
  *
@@ -84,11 +84,12 @@ export declare function frameContextLostError(conn: Pick<CDPConnection, 'send'>,
84
84
  * @param script - JavaScript expression
85
85
  * @param query - Requested frame (index, name/id attribute, or part of the name, id or URL)
86
86
  * @param listedIds - Frame id behind each index of the last `dom frames` listing, if any
87
+ * @param full - `--full`: copy the result with every entry
87
88
  * @returns Value, type and the frame's URL
88
89
  * @throws CommandError (81/83) when the frame is ambiguous or missing, (87)
89
90
  * when an index names another frame than when it was listed, (83) when it
90
91
  * navigated or was removed while the script ran, else as evaluateScript
91
92
  */
92
- export declare function evaluateInFrame(page: CDPConnection, wsUrl: string, script: string, query: string, listedIds?: string[]): Promise<DomEvalData>;
93
+ export declare function evaluateInFrame(page: CDPConnection, wsUrl: string, script: string, query: string, listedIds?: string[], full?: boolean): Promise<DomEvalData>;
93
94
  export {};
94
95
  //# sourceMappingURL=frames.d.ts.map