browser-debugger-cli 0.12.0 → 0.14.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.
- package/.claude/skills/bdg/SKILL.md +100 -186
- package/README.md +5 -4
- package/dist/commands/cdp.d.ts +22 -1
- package/dist/commands/cdp.js +100 -43
- package/dist/commands/console.d.ts +12 -0
- package/dist/commands/console.js +67 -13
- package/dist/commands/dom/DomElementResolver.d.ts +3 -1
- package/dist/commands/dom/DomElementResolver.js +10 -3
- package/dist/commands/dom/a11y.d.ts +1 -1
- package/dist/commands/dom/a11y.js +23 -22
- package/dist/commands/dom/eval.d.ts +4 -2
- package/dist/commands/dom/eval.js +31 -7
- package/dist/commands/dom/form.js +10 -9
- package/dist/commands/dom/formInteraction.js +9 -8
- package/dist/commands/dom/get.js +32 -14
- package/dist/commands/dom/helpers/index.d.ts +1 -1
- package/dist/commands/dom/helpers/index.js +1 -1
- package/dist/commands/dom/helpers/query.d.ts +27 -3
- package/dist/commands/dom/helpers/query.js +152 -64
- package/dist/commands/dom/helpers/screenshot.js +13 -13
- package/dist/commands/dom/index.js +10 -3
- package/dist/commands/dom/query.d.ts +20 -2
- package/dist/commands/dom/query.js +39 -6
- package/dist/commands/dom/screenshot.js +3 -1
- package/dist/commands/dom/semanticUtils.d.ts +3 -2
- package/dist/commands/dom/semanticUtils.js +40 -9
- package/dist/commands/helpJson.d.ts +82 -19
- package/dist/commands/helpJson.js +112 -41
- package/dist/commands/helpTopic.d.ts +16 -1
- package/dist/commands/helpTopic.js +59 -1
- package/dist/commands/installSkill.d.ts +15 -5
- package/dist/commands/installSkill.js +86 -16
- package/dist/commands/network/list.js +65 -12
- package/dist/commands/optionBehaviors.d.ts +25 -2
- package/dist/commands/optionBehaviors.js +81 -46
- package/dist/commands/peek.js +3 -0
- package/dist/commands/shared/CommandRunner.js +13 -13
- package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
- package/dist/commands/shared/daemonErrorHandler.js +21 -10
- package/dist/commands/shared/dataFetcher.d.ts +14 -4
- package/dist/commands/shared/dataFetcher.js +20 -4
- package/dist/commands/shared/followMode.d.ts +9 -1
- package/dist/commands/shared/followMode.js +22 -4
- package/dist/commands/shared/handleValidationError.js +3 -3
- package/dist/commands/shared/optionTypes.d.ts +17 -3
- package/dist/commands/shared/outputFile.js +6 -1
- package/dist/commands/shared/startHelpers.js +3 -3
- package/dist/commands/start.d.ts +7 -5
- package/dist/commands/start.js +65 -21
- package/dist/commands/stop.d.ts +11 -0
- package/dist/commands/stop.js +24 -1
- package/dist/commands.js +1 -1
- package/dist/connection/cdp.d.ts +7 -0
- package/dist/connection/cdp.js +9 -0
- package/dist/connection/chromeIdentity.d.ts +8 -2
- package/dist/connection/chromeIdentity.js +85 -13
- package/dist/connection/launcher.js +3 -2
- package/dist/constants.d.ts +29 -1
- package/dist/constants.js +35 -1
- package/dist/daemon/SessionController.js +8 -1
- package/dist/daemon/launcher.d.ts +3 -2
- package/dist/daemon/launcher.js +47 -3
- package/dist/daemon/session/Session.d.ts +5 -1
- package/dist/daemon/session/Session.js +42 -3
- package/dist/daemon/session/TelemetryStore.d.ts +15 -1
- package/dist/daemon/session/TelemetryStore.js +19 -1
- package/dist/daemon/session/commandRegistry.js +52 -18
- package/dist/daemon/session/interactions.d.ts +2 -1
- package/dist/daemon/session/interactions.js +13 -1
- package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
- package/dist/daemon/session/matchedStylesReset.js +46 -0
- package/dist/daemon/session/plugins.js +17 -2
- package/dist/daemon/session/teardown.js +1 -1
- package/dist/daemon/session/triggeredRequests.d.ts +0 -5
- package/dist/daemon/session/triggeredRequests.js +13 -7
- package/dist/daemon.js +2385 -1229
- package/dist/errors/messages.d.ts +62 -11
- package/dist/errors/messages.js +119 -22
- package/dist/index.js +14995 -9866
- package/dist/ipc/client.d.ts +18 -2
- package/dist/ipc/client.js +26 -5
- package/dist/ipc/protocol/auditTypes.d.ts +8 -2
- package/dist/ipc/protocol/commands.d.ts +16 -0
- package/dist/ipc/protocol/domTypes.d.ts +12 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
- package/dist/ipc/session/types.d.ts +7 -1
- package/dist/program.d.ts +14 -0
- package/dist/program.js +53 -0
- package/dist/runtime/dom/actionEffects.d.ts +5 -1
- package/dist/runtime/dom/actionEffects.js +26 -14
- package/dist/runtime/dom/audit.js +3 -2
- package/dist/runtime/dom/auditModel.js +6 -1
- package/dist/runtime/dom/auditScripts.d.ts +9 -3
- package/dist/runtime/dom/auditScripts.js +41 -5
- package/dist/runtime/dom/elementGeometry.d.ts +33 -3
- package/dist/runtime/dom/elementGeometry.js +44 -19
- package/dist/runtime/dom/elementInfo.d.ts +76 -18
- package/dist/runtime/dom/elementInfo.js +190 -40
- package/dist/runtime/dom/evalHelpers.d.ts +12 -2
- package/dist/runtime/dom/evalHelpers.js +67 -7
- package/dist/runtime/dom/formDiscovery.d.ts +6 -2
- package/dist/runtime/dom/formDiscovery.js +20 -3
- package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
- package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
- package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
- package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
- package/dist/runtime/dom/formSubmitHelpers.js +4 -3
- package/dist/runtime/dom/frameLayout.js +1 -0
- package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
- package/dist/runtime/dom/frameScopedConnection.js +2 -2
- package/dist/runtime/dom/inspect.d.ts +17 -3
- package/dist/runtime/dom/inspect.js +45 -32
- package/dist/runtime/dom/inspectAllStyles.js +1 -0
- package/dist/runtime/dom/inspectHints.d.ts +1 -1
- package/dist/runtime/dom/inspectModel.d.ts +5 -4
- package/dist/runtime/dom/inspectModel.js +7 -3
- package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
- package/dist/runtime/dom/inspectPaintModel.js +3 -1
- package/dist/runtime/dom/inspectRules.d.ts +29 -3
- package/dist/runtime/dom/inspectRules.js +205 -11
- package/dist/runtime/dom/inspectScripts.d.ts +29 -2
- package/dist/runtime/dom/inspectScripts.js +49 -10
- package/dist/runtime/dom/layout.d.ts +0 -2
- package/dist/runtime/dom/layout.js +10 -9
- package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
- package/dist/runtime/dom/reactEventHelpers.js +71 -28
- package/dist/runtime/dom/targetNode.d.ts +27 -10
- package/dist/runtime/dom/targetNode.js +283 -16
- package/dist/runtime/dom/wait.js +2 -1
- package/dist/runtime/page/bdgWorld.d.ts +57 -0
- package/dist/runtime/page/bdgWorld.js +180 -0
- package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
- package/dist/runtime/page/replacedBuiltins.js +136 -0
- package/dist/session/QueryCacheManager.d.ts +4 -1
- package/dist/session/QueryCacheManager.js +5 -2
- package/dist/session/chrome.d.ts +4 -1
- package/dist/session/chrome.js +7 -1
- package/dist/session/cleanup/staleSession.d.ts +21 -4
- package/dist/session/cleanup/staleSession.js +79 -9
- package/dist/session/cleanup/userCommands.d.ts +4 -1
- package/dist/session/cleanup/userCommands.js +10 -5
- package/dist/session/daemonSocket.d.ts +10 -0
- package/dist/session/daemonSocket.js +22 -0
- package/dist/session/lastSession.d.ts +6 -3
- package/dist/session/lastSession.js +11 -5
- package/dist/session/paths.d.ts +3 -1
- package/dist/session/paths.js +5 -5
- package/dist/session/portClaims.js +4 -3
- package/dist/session/sessionList.d.ts +13 -5
- package/dist/session/sessionList.js +31 -7
- package/dist/telemetry/a11y.d.ts +15 -1
- package/dist/telemetry/a11y.js +85 -2
- package/dist/telemetry/console.d.ts +2 -1
- package/dist/telemetry/console.js +30 -21
- package/dist/telemetry/har/builder.js +1 -1
- package/dist/telemetry/network.d.ts +13 -16
- package/dist/telemetry/network.js +30 -52
- package/dist/telemetry/networkRetention.d.ts +83 -0
- package/dist/telemetry/networkRetention.js +117 -0
- package/dist/telemetry/pageCrash.d.ts +26 -0
- package/dist/telemetry/pageCrash.js +53 -0
- package/dist/types.d.ts +42 -0
- package/dist/ui/OutputBuilder.d.ts +10 -0
- package/dist/ui/OutputBuilder.js +12 -0
- package/dist/ui/formatters/a11y.d.ts +5 -7
- package/dist/ui/formatters/a11y.js +7 -61
- package/dist/ui/formatters/audit.js +14 -5
- package/dist/ui/formatters/cdp.d.ts +138 -0
- package/dist/ui/formatters/cdp.js +131 -0
- package/dist/ui/formatters/console/chronological.js +7 -5
- package/dist/ui/formatters/console/follow.d.ts +5 -2
- package/dist/ui/formatters/console/follow.js +7 -4
- package/dist/ui/formatters/console/json.d.ts +4 -7
- package/dist/ui/formatters/console/json.js +16 -14
- package/dist/ui/formatters/console/shared.d.ts +47 -2
- package/dist/ui/formatters/console/shared.js +33 -0
- package/dist/ui/formatters/console/summarize.d.ts +9 -2
- package/dist/ui/formatters/console/summarize.js +57 -11
- package/dist/ui/formatters/console.d.ts +3 -2
- package/dist/ui/formatters/console.js +8 -10
- package/dist/ui/formatters/details.js +4 -2
- package/dist/ui/formatters/dom.d.ts +14 -5
- package/dist/ui/formatters/dom.js +30 -13
- package/dist/ui/formatters/helpFormatters.js +1 -1
- package/dist/ui/formatters/inspect.js +9 -3
- package/dist/ui/formatters/installSkill.d.ts +9 -1
- package/dist/ui/formatters/installSkill.js +32 -6
- package/dist/ui/formatters/layout.js +4 -2
- package/dist/ui/formatters/longValues.d.ts +14 -0
- package/dist/ui/formatters/longValues.js +23 -0
- package/dist/ui/formatters/networkList.d.ts +8 -2
- package/dist/ui/formatters/networkList.js +11 -3
- package/dist/ui/formatters/preview.d.ts +6 -1
- package/dist/ui/formatters/preview.js +67 -15
- package/dist/ui/formatters/sessions.d.ts +2 -2
- package/dist/ui/formatters/sessions.js +9 -2
- package/dist/ui/formatters/status.js +7 -0
- package/dist/ui/formatters/triggeredRequests.js +2 -1
- package/dist/ui/logging/logger.d.ts +1 -1
- package/dist/ui/messages/chrome.d.ts +20 -1
- package/dist/ui/messages/chrome.js +29 -3
- package/dist/ui/messages/commands.d.ts +153 -12
- package/dist/ui/messages/commands.js +198 -15
- package/dist/ui/messages/consoleMessages.d.ts +24 -0
- package/dist/ui/messages/consoleMessages.js +32 -0
- package/dist/ui/messages/networkMessages.d.ts +24 -0
- package/dist/ui/messages/networkMessages.js +45 -0
- package/dist/ui/messages/preview.d.ts +6 -0
- package/dist/ui/messages/preview.js +9 -1
- package/dist/ui/messages/session.d.ts +13 -2
- package/dist/ui/messages/session.js +22 -3
- package/dist/utils/directories.d.ts +34 -0
- package/dist/utils/directories.js +88 -0
- package/dist/utils/display.d.ts +16 -0
- package/dist/utils/display.js +42 -0
- package/dist/utils/exitCodes.d.ts +1 -0
- package/dist/utils/exitCodes.js +6 -0
- package/dist/utils/http.d.ts +9 -2
- package/dist/utils/http.js +4 -3
- package/dist/utils/process.d.ts +12 -0
- package/dist/utils/process.js +25 -0
- package/dist/utils/strings.d.ts +19 -0
- package/dist/utils/strings.js +16 -0
- package/package.json +2 -2
|
@@ -12,10 +12,14 @@ import type { RawFormData } from './formTypes.js';
|
|
|
12
12
|
* - Native form elements and inputs
|
|
13
13
|
* - Custom components with ARIA roles
|
|
14
14
|
* - Labels via priority chain (label[for], aria-label, placeholder, etc.)
|
|
15
|
-
* - Current values and validation state
|
|
15
|
+
* - Current values and validation state (never a secret: a hidden input's
|
|
16
|
+
* value is left out, a sensitive text field's or select's
|
|
17
|
+
* ({@link SENSITIVE_FIELD_JS}; checkboxes and radios named like one keep
|
|
18
|
+
* their checked state) is {@link MASKED_VALUE} when filled, and a
|
|
19
|
+
* sensitive select's options do not say which is chosen)
|
|
16
20
|
* - Form relevance scoring for multi-form pages
|
|
17
21
|
*/
|
|
18
|
-
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 (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 getSelectOptions(element) {\n if (element.tagName.toLowerCase() !== 'select') return undefined;\n return Array.from(element.options).map(opt => ({\n value: opt.value,\n label: opt.textContent.trim(),\n selected: 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|(^|[^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";
|
|
19
23
|
/**
|
|
20
24
|
* Type guard for raw form data.
|
|
21
25
|
*
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Discovers forms, extracts semantic labels, detects state and validation,
|
|
5
5
|
* and returns structured data for agent consumption.
|
|
6
6
|
*/
|
|
7
|
+
import { MASKED_VALUE, SENSITIVE_FIELD_JS } from './elementInfo.js';
|
|
7
8
|
/**
|
|
8
9
|
* Page-context script for form discovery.
|
|
9
10
|
*
|
|
@@ -11,7 +12,11 @@
|
|
|
11
12
|
* - Native form elements and inputs
|
|
12
13
|
* - Custom components with ARIA roles
|
|
13
14
|
* - Labels via priority chain (label[for], aria-label, placeholder, etc.)
|
|
14
|
-
* - Current values and validation state
|
|
15
|
+
* - Current values and validation state (never a secret: a hidden input's
|
|
16
|
+
* value is left out, a sensitive text field's or select's
|
|
17
|
+
* ({@link SENSITIVE_FIELD_JS}; checkboxes and radios named like one keep
|
|
18
|
+
* their checked state) is {@link MASKED_VALUE} when filled, and a
|
|
19
|
+
* sensitive select's options do not say which is chosen)
|
|
15
20
|
* - Form relevance scoring for multi-form pages
|
|
16
21
|
*/
|
|
17
22
|
export const FORM_DISCOVERY_SCRIPT = `
|
|
@@ -206,6 +211,10 @@ export const FORM_DISCOVERY_SCRIPT = `
|
|
|
206
211
|
function getFieldValue(element) {
|
|
207
212
|
const tag = element.tagName.toLowerCase();
|
|
208
213
|
const type = element.type?.toLowerCase() || 'text';
|
|
214
|
+
if (type === 'hidden') return '';
|
|
215
|
+
if (isSecret(element)) {
|
|
216
|
+
return (element.isContentEditable ? element.textContent : element.value) ? '${MASKED_VALUE}' : '';
|
|
217
|
+
}
|
|
209
218
|
if (tag === 'select') {
|
|
210
219
|
if (element.multiple) {
|
|
211
220
|
return Array.from(element.selectedOptions).map(o => o.value);
|
|
@@ -265,12 +274,20 @@ export const FORM_DISCOVERY_SCRIPT = `
|
|
|
265
274
|
return tag === 'input' || tag === 'textarea' || tag === 'select';
|
|
266
275
|
}
|
|
267
276
|
|
|
277
|
+
function isSecret(element) {
|
|
278
|
+
const type = element.type?.toLowerCase() || 'text';
|
|
279
|
+
if (type === 'checkbox' || type === 'radio') return false;
|
|
280
|
+
if (element.getAttribute('role') === 'checkbox' || element.getAttribute('role') === 'switch') return false;
|
|
281
|
+
return (${SENSITIVE_FIELD_JS})(element);
|
|
282
|
+
}
|
|
283
|
+
|
|
268
284
|
function getSelectOptions(element) {
|
|
269
285
|
if (element.tagName.toLowerCase() !== 'select') return undefined;
|
|
286
|
+
const secret = isSecret(element);
|
|
270
287
|
return Array.from(element.options).map(opt => ({
|
|
271
|
-
value: opt.value,
|
|
288
|
+
value: secret ? '' : opt.value,
|
|
272
289
|
label: opt.textContent.trim(),
|
|
273
|
-
selected: opt.selected
|
|
290
|
+
selected: secret ? false : opt.selected
|
|
274
291
|
}));
|
|
275
292
|
}
|
|
276
293
|
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import * as fs from 'node:fs';
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import { CommandError } from '../../../errors/index.js';
|
|
8
|
-
import { fileNotFoundError, uploadDirectoryError, singleFileInputError, fillableElementNotFoundError,
|
|
9
|
-
import { escapeValueForJS,
|
|
10
|
-
import { FILL_READ_BACK_SCRIPT, REACT_FILL_SCRIPT, CLICK_ELEMENT_SCRIPT, SHADOW_FIELD_JS, isFillResult, isClickResult, } from '../reactEventHelpers.js';
|
|
8
|
+
import { fileNotFoundError, uploadDirectoryError, singleFileInputError, fillableElementNotFoundError, clickTargetDetachedError, unexpectedResponseFormatError, operationFailedError, pressNotReceivedError, unreachableElementError, } from '../../../errors/messages.js';
|
|
9
|
+
import { ActionScriptError, escapeValueForJS, exceptionSummary, throwIfInvalidSelector, withMultipleMatchesWarning, withValueMismatchWarning, } from './shared.js';
|
|
10
|
+
import { FILL_READ_BACK_SCRIPT, FIRE_EVENT_JS, REACT_FILL_SCRIPT, CLICK_ELEMENT_SCRIPT, SHADOW_FIELD_JS, isFillResult, isClickResult, } from '../reactEventHelpers.js';
|
|
11
11
|
import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS, selectorArgsJS } from '../targetNode.js';
|
|
12
12
|
import { createLogger } from '../../../ui/logging/index.js';
|
|
13
13
|
import { CLICK_NOT_RECEIVED_WARNING, POINTER_ACTION_DONE, POINTER_ACTION_NOUN, domClickFallbackWarning, } from '../../../ui/messages/commands.js';
|
|
@@ -32,9 +32,7 @@ export async function fillElement(cdp, selector, value, options = {}) {
|
|
|
32
32
|
const cdpResponse = response;
|
|
33
33
|
if (cdpResponse.exceptionDetails) {
|
|
34
34
|
throwIfInvalidSelector(cdpResponse.exceptionDetails, selector);
|
|
35
|
-
|
|
36
|
-
const err = fillableElementNotFoundError(selector);
|
|
37
|
-
throw new CommandError(errorMessage, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
|
|
35
|
+
throw new ActionScriptError('fill', exceptionSummary(cdpResponse.exceptionDetails), selector);
|
|
38
36
|
}
|
|
39
37
|
if (cdpResponse.result?.value && isFillResult(cdpResponse.result.value)) {
|
|
40
38
|
const result = cdpResponse.result.value;
|
|
@@ -106,8 +104,8 @@ function isValueMismatch(value) {
|
|
|
106
104
|
/** Empties a file input as a user removing the selection would (with events). */
|
|
107
105
|
const CLEAR_FILE_INPUT_FUNCTION = `function () {
|
|
108
106
|
this.value = '';
|
|
109
|
-
this
|
|
110
|
-
this
|
|
107
|
+
(${FIRE_EVENT_JS})(this, 'input');
|
|
108
|
+
(${FIRE_EVENT_JS})(this, 'change');
|
|
111
109
|
}`;
|
|
112
110
|
/** Object group for remote objects created while selecting files. */
|
|
113
111
|
const UPLOAD_OBJECT_GROUP = 'bdg-upload';
|
|
@@ -441,9 +439,7 @@ export async function clickElement(cdp, selector, options = {}) {
|
|
|
441
439
|
const cdpResponse = response;
|
|
442
440
|
if (cdpResponse.exceptionDetails) {
|
|
443
441
|
throwIfInvalidSelector(cdpResponse.exceptionDetails, selector);
|
|
444
|
-
|
|
445
|
-
const err = clickableElementNotFoundError(selector);
|
|
446
|
-
throw new CommandError(errorMessage, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
|
|
442
|
+
throw new ActionScriptError(POINTER_ACTION_NOUN[action], exceptionSummary(cdpResponse.exceptionDetails), selector);
|
|
447
443
|
}
|
|
448
444
|
if (cdpResponse.result?.value && isClickResult(cdpResponse.result.value)) {
|
|
449
445
|
return await performClick(cdp, cdpResponse.result.value, action, options.strict === true);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { CommandError } from '../../../errors/index.js';
|
|
7
7
|
import { VIA_LABEL_SUFFIX, keyPressFailedError, operationFailedError, unknownKeyError, } from '../../../errors/messages.js';
|
|
8
|
-
import { ELEMENT_IDENTITY_JS } from '../elementInfo.js';
|
|
8
|
+
import { DISABLED_CAUSE_JS, ELEMENT_IDENTITY_JS } from '../elementInfo.js';
|
|
9
9
|
import { throwIfInvalidSelector, withMultipleMatchesWarning, } from './shared.js';
|
|
10
10
|
import { describeModifiers, getKeyDefinition, impliesShift, MODIFIER_FLAGS, parseModifiers, similarKeyNames, shortcutCommands, } from '../keyMapping.js';
|
|
11
11
|
import { FIND_ELEMENTS_JS, LABEL_CONTROL_JS, selectorArgsJS } from '../targetNode.js';
|
|
@@ -60,7 +60,7 @@ const FOCUS_ELEMENT_SCRIPT = `
|
|
|
60
60
|
return {
|
|
61
61
|
success: false,
|
|
62
62
|
reason: 'not-focusable',
|
|
63
|
-
error: 'Element <' + el.tagName.toLowerCase() + '> cannot receive keyboard focus' + (
|
|
63
|
+
error: 'Element <' + el.tagName.toLowerCase() + '> cannot receive keyboard focus' + ((${DISABLED_CAUSE_JS})(el) !== null ? ' (it is disabled)' : '')
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared internal helpers for form-fill operations: script
|
|
3
|
-
*
|
|
2
|
+
* Shared internal helpers for form-fill operations: script errors and JS
|
|
3
|
+
* string escaping.
|
|
4
4
|
*/
|
|
5
5
|
import type { Protocol } from '../../../connection/typed-cdp.js';
|
|
6
|
+
import { CommandError } from '../../../errors/index.js';
|
|
6
7
|
import type { FillResult } from '../../../ipc/protocol/domTypes.js';
|
|
7
|
-
/**
|
|
8
|
-
* Escape a CSS selector for embedding in a single-quoted JS string.
|
|
9
|
-
*/
|
|
10
|
-
export declare function escapeSelectorForJS(selector: string): string;
|
|
11
8
|
/**
|
|
12
9
|
* Add a warning when the selector matched several elements and no index was
|
|
13
10
|
* given, so the caller knows which one was used. Keeps an existing warning.
|
|
@@ -34,11 +31,20 @@ export declare function withValueMismatchWarning(result: FillResult): FillResult
|
|
|
34
31
|
*/
|
|
35
32
|
export declare function escapeValueForJS(value: string): string;
|
|
36
33
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
34
|
+
* An action's page script threw. The action that ran it reports what was
|
|
35
|
+
* thrown; when the page replaced built-ins the script uses,
|
|
36
|
+
* `onScriptTarget` makes that the error instead.
|
|
40
37
|
*/
|
|
41
|
-
export declare
|
|
38
|
+
export declare class ActionScriptError extends CommandError {
|
|
39
|
+
readonly action: string;
|
|
40
|
+
readonly exception: string;
|
|
41
|
+
/**
|
|
42
|
+
* @param action - The action, e.g. `fill`
|
|
43
|
+
* @param exception - What the script threw, in one line
|
|
44
|
+
* @param selector - Selector the action was for
|
|
45
|
+
*/
|
|
46
|
+
constructor(action: string, exception: string, selector: string);
|
|
47
|
+
}
|
|
42
48
|
/**
|
|
43
49
|
* What a page script threw, in one line (`TypeError: Cannot read …` rather
|
|
44
50
|
* than CDP's bare "Uncaught").
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared internal helpers for form-fill operations: script
|
|
3
|
-
*
|
|
2
|
+
* Shared internal helpers for form-fill operations: script errors and JS
|
|
3
|
+
* string escaping.
|
|
4
4
|
*/
|
|
5
5
|
import { CommandError } from '../../../errors/index.js';
|
|
6
|
-
import { invalidSelectorError } from '../../../errors/messages.js';
|
|
6
|
+
import { actionScriptFailedError, invalidSelectorError } from '../../../errors/messages.js';
|
|
7
7
|
import { multipleMatchesWarning, valueMismatchWarning } from '../../../ui/messages/commands.js';
|
|
8
8
|
import { EXIT_CODES } from '../../../utils/exitCodes.js';
|
|
9
|
-
import { detectSelectorQuoteDamage } from '../../../utils/shellDetection.js';
|
|
10
|
-
/**
|
|
11
|
-
* Escape a CSS selector for embedding in a single-quoted JS string.
|
|
12
|
-
*/
|
|
13
|
-
export function escapeSelectorForJS(selector) {
|
|
14
|
-
return JSON.stringify(selector).slice(1, -1).replace(/'/g, "\\'");
|
|
15
|
-
}
|
|
16
9
|
/**
|
|
17
10
|
* Add a warning when the selector matched several elements and no index was
|
|
18
11
|
* given, so the caller knows which one was used. Keeps an existing warning.
|
|
@@ -49,50 +42,24 @@ export function escapeValueForJS(value) {
|
|
|
49
42
|
return JSON.stringify(value).slice(1, -1).replace(/'/g, "\\'");
|
|
50
43
|
}
|
|
51
44
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
45
|
+
* An action's page script threw. The action that ran it reports what was
|
|
46
|
+
* thrown; when the page replaced built-ins the script uses,
|
|
47
|
+
* `onScriptTarget` makes that the error instead.
|
|
55
48
|
*/
|
|
56
|
-
export
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
lines.push('');
|
|
70
|
-
lines.push('Shell quote damage detected in selector:');
|
|
71
|
-
if (selectorCheck.details) {
|
|
72
|
-
lines.push(` ${selectorCheck.details}`);
|
|
73
|
-
}
|
|
74
|
-
lines.push('');
|
|
75
|
-
lines.push('Try using the two-step pattern:');
|
|
76
|
-
lines.push(` 1. bdg dom query '${selector}'`);
|
|
77
|
-
lines.push(` 2. bdg dom ${operationType} 0${operationType === 'fill' ? ' "value"' : ''}`);
|
|
78
|
-
return lines.join('\n');
|
|
79
|
-
}
|
|
49
|
+
export class ActionScriptError extends CommandError {
|
|
50
|
+
action;
|
|
51
|
+
exception;
|
|
52
|
+
/**
|
|
53
|
+
* @param action - The action, e.g. `fill`
|
|
54
|
+
* @param exception - What the script threw, in one line
|
|
55
|
+
* @param selector - Selector the action was for
|
|
56
|
+
*/
|
|
57
|
+
constructor(action, exception, selector) {
|
|
58
|
+
const err = actionScriptFailedError(action, exception, selector);
|
|
59
|
+
super(err.message, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR);
|
|
60
|
+
this.action = action;
|
|
61
|
+
this.exception = exception;
|
|
80
62
|
}
|
|
81
|
-
const troubleshootingSteps = operationType === 'fill'
|
|
82
|
-
? [
|
|
83
|
-
`1. Verify element exists: bdg dom query "${selector}"`,
|
|
84
|
-
'2. Check element is visible and not disabled',
|
|
85
|
-
`3. Try direct eval: bdg dom eval "document.querySelector('${escapeSelectorForJS(selector)}').value = 'your-value'"`,
|
|
86
|
-
]
|
|
87
|
-
: [
|
|
88
|
-
`1. Verify element exists: bdg dom query "${selector}"`,
|
|
89
|
-
'2. Check element is visible and clickable',
|
|
90
|
-
`3. Try direct eval: bdg dom eval "document.querySelector('${escapeSelectorForJS(selector)}').click()"`,
|
|
91
|
-
];
|
|
92
|
-
lines.push('');
|
|
93
|
-
lines.push('Troubleshooting:');
|
|
94
|
-
lines.push(` ${troubleshootingSteps.join('\n ')}`);
|
|
95
|
-
return lines.join('\n');
|
|
96
63
|
}
|
|
97
64
|
/**
|
|
98
65
|
* What a page script threw, in one line (`TypeError: Cannot read …` rather
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { CDPTimeoutError } from '../../connection/errors.js';
|
|
5
5
|
import { trackInFlightRequests } from '../../connection/inFlightRequests.js';
|
|
6
6
|
import { submitNetworkBusyWarning, submitTimeoutError } from '../../errors/messages.js';
|
|
7
|
-
import { ELEMENT_IDENTITY_JS } from './elementInfo.js';
|
|
7
|
+
import { DISABLED_CAUSE_JS, ELEMENT_IDENTITY_JS } from './elementInfo.js';
|
|
8
8
|
import { throwIfInvalidSelector } from './formFillHelpers/shared.js';
|
|
9
9
|
import { FIND_ELEMENTS_JS, selectorArgsJS } from './targetNode.js';
|
|
10
10
|
import { summarizePendingRequests } from '../page/loadingState.js';
|
|
@@ -49,7 +49,8 @@ const PREPARE_SUBMIT_SCRIPT = `
|
|
|
49
49
|
f.matches('button:not([type]), button[type=submit], input[type=submit], input[type=image]')
|
|
50
50
|
)
|
|
51
51
|
: [];
|
|
52
|
-
|
|
52
|
+
const disabled = (f) => (${DISABLED_CAUSE_JS})(f) !== null;
|
|
53
|
+
if (submitters.length > 0 && submitters.every(disabled)) {
|
|
53
54
|
// A user could not submit: Enter does nothing while the default button is disabled
|
|
54
55
|
return { action: 'fail', reason: 'disabled', error: 'The form\\'s submit button is disabled' };
|
|
55
56
|
}
|
|
@@ -62,7 +63,7 @@ const PREPARE_SUBMIT_SCRIPT = `
|
|
|
62
63
|
if (isForm) {
|
|
63
64
|
// Like pressing Enter: the form's default button is the submitter, so its
|
|
64
65
|
// name=value is sent too
|
|
65
|
-
const submitter = submitters.find((f) => !f
|
|
66
|
+
const submitter = submitters.find((f) => !disabled(f));
|
|
66
67
|
const element = (${ELEMENT_IDENTITY_JS})(submitter || el);
|
|
67
68
|
if (submitter) el.requestSubmit(submitter);
|
|
68
69
|
else el.requestSubmit();
|
|
@@ -127,6 +127,7 @@ export function placeInOwnerFrame(raw, owner, mapping) {
|
|
|
127
127
|
clipper: inner.clipper ?? (inside(rect, frameView) ? outer.clipper : frame.element),
|
|
128
128
|
hidden: inner.hidden ?? (outer.hidden ? 'inside a hidden iframe' : null),
|
|
129
129
|
invisible: inner.invisible ?? outer.invisible,
|
|
130
|
+
masked: inner.invisible ? null : (inner.masked ?? outer.masked ?? null),
|
|
130
131
|
fixed: outer.fixed,
|
|
131
132
|
sticky: outer.sticky ?? false,
|
|
132
133
|
pageScroll: outer.pageScroll,
|
|
@@ -66,6 +66,13 @@ export declare function mapPoint(mapping: FrameMapping, point: LayoutPoint): Lay
|
|
|
66
66
|
* @returns Box in the top-level viewport
|
|
67
67
|
*/
|
|
68
68
|
export declare function mapBox(mapping: FrameMapping, box: LayoutBox): LayoutBox;
|
|
69
|
+
/**
|
|
70
|
+
* Page-side choice of the box to measure: the element's, or its document's
|
|
71
|
+
* root element when the element has no size (hidden, collapsed).
|
|
72
|
+
*/
|
|
73
|
+
export declare const REFERENCE_NODE_FUNCTION = "function () {\n const r = this.getBoundingClientRect();\n return r.width > 0 && r.height > 0 ? this : this.ownerDocument.documentElement;\n}";
|
|
74
|
+
/** Page-side `getBoundingClientRect()` as a plain box */
|
|
75
|
+
export declare const CLIENT_RECT_FUNCTION = "function () { const r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }";
|
|
69
76
|
/**
|
|
70
77
|
* How the element's frame maps into the top-level viewport.
|
|
71
78
|
*
|
|
@@ -150,12 +150,12 @@ export function mapBox(mapping, box) {
|
|
|
150
150
|
* Page-side choice of the box to measure: the element's, or its document's
|
|
151
151
|
* root element when the element has no size (hidden, collapsed).
|
|
152
152
|
*/
|
|
153
|
-
const REFERENCE_NODE_FUNCTION = `function () {
|
|
153
|
+
export const REFERENCE_NODE_FUNCTION = `function () {
|
|
154
154
|
const r = this.getBoundingClientRect();
|
|
155
155
|
return r.width > 0 && r.height > 0 ? this : this.ownerDocument.documentElement;
|
|
156
156
|
}`;
|
|
157
157
|
/** Page-side `getBoundingClientRect()` as a plain box */
|
|
158
|
-
const CLIENT_RECT_FUNCTION = 'function () { const r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }';
|
|
158
|
+
export const CLIENT_RECT_FUNCTION = 'function () { const r = this.getBoundingClientRect(); return { x: r.left, y: r.top, width: r.width, height: r.height }; }';
|
|
159
159
|
/**
|
|
160
160
|
* Measure a box of the element's frame both in the frame and through CDP.
|
|
161
161
|
*
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* tree), `dom layout`'s measurement (page position, hidden, covered,
|
|
9
9
|
* offscreen) and, once the nodes are pushed to CDP, `CSS.getComputedStyleForNode`
|
|
10
10
|
* for the element, its layout parent and its `::before`/`::after`,
|
|
11
|
-
* `CSS.getPlatformFontsForNode` for its text and `DOM.getBoxModel`.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* `CSS.getPlatformFontsForNode` for its text and `DOM.getBoxModel`. After
|
|
12
|
+
* those, for the hints, `--rules` and `--why`, the matched rules
|
|
13
|
+
* (`CSS.getMatchedStylesForNode`, see {@link matchedStyles}). DOM and CSS are
|
|
14
|
+
* enabled on the first inspect and kept on.
|
|
14
15
|
*/
|
|
15
16
|
import type { CDPConnection } from '../../connection/cdp.js';
|
|
16
17
|
import type { DomInspectCommand } from '../../ipc/protocol/commands.js';
|
|
@@ -25,6 +26,19 @@ import type { InspectResult } from '../../ipc/protocol/inspectTypes.js';
|
|
|
25
26
|
* selector or unknown property, (87) the cached element left the page
|
|
26
27
|
*/
|
|
27
28
|
export declare function inspectElement(cdp: CDPConnection, params: DomInspectCommand): Promise<InspectResult>;
|
|
29
|
+
/**
|
|
30
|
+
* How a request reads the element's matched rules, when hints, `--rules` or
|
|
31
|
+
* `--why` need them: within {@link HINTS_BUDGET_MS} for the default hints,
|
|
32
|
+
* not waited for at all on a document marked slow; within
|
|
33
|
+
* {@link RULES_BUDGET_MS} when asked for explicitly.
|
|
34
|
+
*
|
|
35
|
+
* @param params - Request
|
|
36
|
+
* @returns Budget and whether to skip on a slow document, or undefined when not needed
|
|
37
|
+
*/
|
|
38
|
+
export declare function matchedStylesRead(params: DomInspectCommand): {
|
|
39
|
+
budgetMs: number;
|
|
40
|
+
skipWhenSlow: boolean;
|
|
41
|
+
} | undefined;
|
|
28
42
|
/**
|
|
29
43
|
* Enable DOM and CSS once per connection (kept on: CSS.enable replays every
|
|
30
44
|
* stylesheet, which costs up to a few hundred ms on large sites the first time).
|