@mercuryo-ai/agentbrowse 0.2.50
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/CHANGELOG.md +15 -0
- package/README.md +335 -0
- package/dist/assistive-runtime.d.ts +110 -0
- package/dist/assistive-runtime.d.ts.map +1 -0
- package/dist/assistive-runtime.js +79 -0
- package/dist/assistive-runtime.test-support.d.ts +7 -0
- package/dist/assistive-runtime.test-support.d.ts.map +1 -0
- package/dist/assistive-runtime.test-support.js +106 -0
- package/dist/assistive-stagehand.d.ts +12 -0
- package/dist/assistive-stagehand.d.ts.map +1 -0
- package/dist/assistive-stagehand.js +10 -0
- package/dist/browser-session-state.d.ts +95 -0
- package/dist/browser-session-state.d.ts.map +1 -0
- package/dist/browser-session-state.js +279 -0
- package/dist/client-bindings.d.ts +10 -0
- package/dist/client-bindings.d.ts.map +1 -0
- package/dist/client-bindings.js +18 -0
- package/dist/client.d.ts +49 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +63 -0
- package/dist/command-api-tracing.d.ts +20 -0
- package/dist/command-api-tracing.d.ts.map +1 -0
- package/dist/command-api-tracing.js +149 -0
- package/dist/command-name.d.ts +3 -0
- package/dist/command-name.d.ts.map +1 -0
- package/dist/command-name.js +11 -0
- package/dist/commands/act.d.ts +43 -0
- package/dist/commands/act.d.ts.map +1 -0
- package/dist/commands/act.js +1107 -0
- package/dist/commands/action-acceptance.d.ts +93 -0
- package/dist/commands/action-acceptance.d.ts.map +1 -0
- package/dist/commands/action-acceptance.js +1938 -0
- package/dist/commands/action-artifacts.d.ts +33 -0
- package/dist/commands/action-artifacts.d.ts.map +1 -0
- package/dist/commands/action-artifacts.js +104 -0
- package/dist/commands/action-execution-guards.d.ts +5 -0
- package/dist/commands/action-execution-guards.d.ts.map +1 -0
- package/dist/commands/action-execution-guards.js +3 -0
- package/dist/commands/action-executor-helpers.d.ts +21 -0
- package/dist/commands/action-executor-helpers.d.ts.map +1 -0
- package/dist/commands/action-executor-helpers.js +265 -0
- package/dist/commands/action-executor.d.ts +14 -0
- package/dist/commands/action-executor.d.ts.map +1 -0
- package/dist/commands/action-executor.js +46 -0
- package/dist/commands/action-fallbacks.d.ts +6 -0
- package/dist/commands/action-fallbacks.d.ts.map +1 -0
- package/dist/commands/action-fallbacks.js +43 -0
- package/dist/commands/action-result-resolution.d.ts +17 -0
- package/dist/commands/action-result-resolution.d.ts.map +1 -0
- package/dist/commands/action-result-resolution.js +132 -0
- package/dist/commands/action-value-projection.d.ts +32 -0
- package/dist/commands/action-value-projection.d.ts.map +1 -0
- package/dist/commands/action-value-projection.js +151 -0
- package/dist/commands/attach.d.ts +41 -0
- package/dist/commands/attach.d.ts.map +1 -0
- package/dist/commands/attach.js +103 -0
- package/dist/commands/browse-actions.d.ts +4 -0
- package/dist/commands/browse-actions.d.ts.map +1 -0
- package/dist/commands/browse-actions.js +4 -0
- package/dist/commands/browser-status.d.ts +57 -0
- package/dist/commands/browser-status.d.ts.map +1 -0
- package/dist/commands/browser-status.js +243 -0
- package/dist/commands/click-action-executor.d.ts +12 -0
- package/dist/commands/click-action-executor.d.ts.map +1 -0
- package/dist/commands/click-action-executor.js +111 -0
- package/dist/commands/click-activation-policy.d.ts +5 -0
- package/dist/commands/click-activation-policy.d.ts.map +1 -0
- package/dist/commands/click-activation-policy.js +13 -0
- package/dist/commands/close.d.ts +26 -0
- package/dist/commands/close.d.ts.map +1 -0
- package/dist/commands/close.js +124 -0
- package/dist/commands/datepicker-action-executor.d.ts +12 -0
- package/dist/commands/datepicker-action-executor.d.ts.map +1 -0
- package/dist/commands/datepicker-action-executor.js +218 -0
- package/dist/commands/descriptor-validation.d.ts +27 -0
- package/dist/commands/descriptor-validation.d.ts.map +1 -0
- package/dist/commands/descriptor-validation.js +192 -0
- package/dist/commands/extract-scope-resolution.d.ts +20 -0
- package/dist/commands/extract-scope-resolution.d.ts.map +1 -0
- package/dist/commands/extract-scope-resolution.js +109 -0
- package/dist/commands/extract-scoped-dialog-text.d.ts +3 -0
- package/dist/commands/extract-scoped-dialog-text.d.ts.map +1 -0
- package/dist/commands/extract-scoped-dialog-text.js +210 -0
- package/dist/commands/extract-snapshot-sanitizer.d.ts +5 -0
- package/dist/commands/extract-snapshot-sanitizer.d.ts.map +1 -0
- package/dist/commands/extract-snapshot-sanitizer.js +98 -0
- package/dist/commands/extract-stagehand-executor.d.ts +17 -0
- package/dist/commands/extract-stagehand-executor.d.ts.map +1 -0
- package/dist/commands/extract-stagehand-executor.js +112 -0
- package/dist/commands/extract.d.ts +57 -0
- package/dist/commands/extract.d.ts.map +1 -0
- package/dist/commands/extract.js +668 -0
- package/dist/commands/interaction-kernel.d.ts +46 -0
- package/dist/commands/interaction-kernel.d.ts.map +1 -0
- package/dist/commands/interaction-kernel.js +215 -0
- package/dist/commands/launch.d.ts +41 -0
- package/dist/commands/launch.d.ts.map +1 -0
- package/dist/commands/launch.js +182 -0
- package/dist/commands/navigate.d.ts +31 -0
- package/dist/commands/navigate.d.ts.map +1 -0
- package/dist/commands/navigate.js +202 -0
- package/dist/commands/observe-accessibility.d.ts +22 -0
- package/dist/commands/observe-accessibility.d.ts.map +1 -0
- package/dist/commands/observe-accessibility.js +566 -0
- package/dist/commands/observe-display-label.d.ts +4 -0
- package/dist/commands/observe-display-label.d.ts.map +1 -0
- package/dist/commands/observe-display-label.js +26 -0
- package/dist/commands/observe-dom-label-contract.d.ts +2 -0
- package/dist/commands/observe-dom-label-contract.d.ts.map +1 -0
- package/dist/commands/observe-dom-label-contract.js +564 -0
- package/dist/commands/observe-fallback-semantics.d.ts +6 -0
- package/dist/commands/observe-fallback-semantics.d.ts.map +1 -0
- package/dist/commands/observe-fallback-semantics.js +86 -0
- package/dist/commands/observe-inventory.d.ts +149 -0
- package/dist/commands/observe-inventory.d.ts.map +1 -0
- package/dist/commands/observe-inventory.js +3545 -0
- package/dist/commands/observe-label-policy.d.ts +8 -0
- package/dist/commands/observe-label-policy.d.ts.map +1 -0
- package/dist/commands/observe-label-policy.js +21 -0
- package/dist/commands/observe-page-state.d.ts +11 -0
- package/dist/commands/observe-page-state.d.ts.map +1 -0
- package/dist/commands/observe-page-state.js +89 -0
- package/dist/commands/observe-persistence.d.ts +15 -0
- package/dist/commands/observe-persistence.d.ts.map +1 -0
- package/dist/commands/observe-persistence.js +238 -0
- package/dist/commands/observe-projection.d.ts +119 -0
- package/dist/commands/observe-projection.d.ts.map +1 -0
- package/dist/commands/observe-projection.js +726 -0
- package/dist/commands/observe-protected.d.ts +6 -0
- package/dist/commands/observe-protected.d.ts.map +1 -0
- package/dist/commands/observe-protected.js +31 -0
- package/dist/commands/observe-semantics.d.ts +10 -0
- package/dist/commands/observe-semantics.d.ts.map +1 -0
- package/dist/commands/observe-semantics.js +535 -0
- package/dist/commands/observe-signals.d.ts +48 -0
- package/dist/commands/observe-signals.d.ts.map +1 -0
- package/dist/commands/observe-signals.js +461 -0
- package/dist/commands/observe-stagehand.d.ts +49 -0
- package/dist/commands/observe-stagehand.d.ts.map +1 -0
- package/dist/commands/observe-stagehand.js +94 -0
- package/dist/commands/observe-surfaces.d.ts +11 -0
- package/dist/commands/observe-surfaces.d.ts.map +1 -0
- package/dist/commands/observe-surfaces.js +290 -0
- package/dist/commands/observe.d.ts +113 -0
- package/dist/commands/observe.d.ts.map +1 -0
- package/dist/commands/observe.js +556 -0
- package/dist/commands/screenshot.d.ts +37 -0
- package/dist/commands/screenshot.d.ts.map +1 -0
- package/dist/commands/screenshot.js +269 -0
- package/dist/commands/select-action-executor.d.ts +10 -0
- package/dist/commands/select-action-executor.d.ts.map +1 -0
- package/dist/commands/select-action-executor.js +156 -0
- package/dist/commands/semantic-observe-lexical.d.ts +31 -0
- package/dist/commands/semantic-observe-lexical.d.ts.map +1 -0
- package/dist/commands/semantic-observe-lexical.js +186 -0
- package/dist/commands/semantic-observe.d.ts +37 -0
- package/dist/commands/semantic-observe.d.ts.map +1 -0
- package/dist/commands/semantic-observe.js +1339 -0
- package/dist/commands/structured-grid-action-executor.d.ts +3 -0
- package/dist/commands/structured-grid-action-executor.d.ts.map +1 -0
- package/dist/commands/structured-grid-action-executor.js +4 -0
- package/dist/commands/target-resolution.d.ts +4 -0
- package/dist/commands/target-resolution.d.ts.map +1 -0
- package/dist/commands/target-resolution.js +33 -0
- package/dist/commands/text-input-action-executor.d.ts +5 -0
- package/dist/commands/text-input-action-executor.d.ts.map +1 -0
- package/dist/commands/text-input-action-executor.js +116 -0
- package/dist/commands/user-actionable.d.ts +4 -0
- package/dist/commands/user-actionable.d.ts.map +1 -0
- package/dist/commands/user-actionable.js +216 -0
- package/dist/control-semantics.d.ts +30 -0
- package/dist/control-semantics.d.ts.map +1 -0
- package/dist/control-semantics.js +419 -0
- package/dist/diagnostics.d.ts +132 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +120 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +350 -0
- package/dist/library.d.ts +17 -0
- package/dist/library.d.ts.map +1 -0
- package/dist/library.js +14 -0
- package/dist/output.d.ts +32 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +33 -0
- package/dist/owned-browser.d.ts +12 -0
- package/dist/owned-browser.d.ts.map +1 -0
- package/dist/owned-browser.js +69 -0
- package/dist/owned-process.d.ts +19 -0
- package/dist/owned-process.d.ts.map +1 -0
- package/dist/owned-process.js +145 -0
- package/dist/playwright-runtime.d.ts +43 -0
- package/dist/playwright-runtime.d.ts.map +1 -0
- package/dist/playwright-runtime.js +339 -0
- package/dist/protected-fill-browser.d.ts +22 -0
- package/dist/protected-fill-browser.d.ts.map +1 -0
- package/dist/protected-fill-browser.js +52 -0
- package/dist/protected-fill.d.ts +82 -0
- package/dist/protected-fill.d.ts.map +1 -0
- package/dist/protected-fill.js +20 -0
- package/dist/runtime-metrics.d.ts +27 -0
- package/dist/runtime-metrics.d.ts.map +1 -0
- package/dist/runtime-metrics.js +66 -0
- package/dist/runtime-page-state.d.ts +11 -0
- package/dist/runtime-page-state.d.ts.map +1 -0
- package/dist/runtime-page-state.js +62 -0
- package/dist/runtime-protected-state.d.ts +14 -0
- package/dist/runtime-protected-state.d.ts.map +1 -0
- package/dist/runtime-protected-state.js +148 -0
- package/dist/runtime-resolution.d.ts +9 -0
- package/dist/runtime-resolution.d.ts.map +1 -0
- package/dist/runtime-resolution.js +19 -0
- package/dist/runtime-state.d.ts +251 -0
- package/dist/runtime-state.d.ts.map +1 -0
- package/dist/runtime-state.js +599 -0
- package/dist/secrets/catalog-applicability.d.ts +5 -0
- package/dist/secrets/catalog-applicability.d.ts.map +1 -0
- package/dist/secrets/catalog-applicability.js +59 -0
- package/dist/secrets/field-policy.d.ts +3 -0
- package/dist/secrets/field-policy.d.ts.map +1 -0
- package/dist/secrets/field-policy.js +3 -0
- package/dist/secrets/fill-ordering.d.ts +10 -0
- package/dist/secrets/fill-ordering.d.ts.map +1 -0
- package/dist/secrets/fill-ordering.js +41 -0
- package/dist/secrets/form-matcher.d.ts +60 -0
- package/dist/secrets/form-matcher.d.ts.map +1 -0
- package/dist/secrets/form-matcher.js +948 -0
- package/dist/secrets/protected-artifact-guard.d.ts +39 -0
- package/dist/secrets/protected-artifact-guard.d.ts.map +1 -0
- package/dist/secrets/protected-artifact-guard.js +44 -0
- package/dist/secrets/protected-bindings.d.ts +14 -0
- package/dist/secrets/protected-bindings.d.ts.map +1 -0
- package/dist/secrets/protected-bindings.js +29 -0
- package/dist/secrets/protected-exact-value-redaction.d.ts +14 -0
- package/dist/secrets/protected-exact-value-redaction.d.ts.map +1 -0
- package/dist/secrets/protected-exact-value-redaction.js +360 -0
- package/dist/secrets/protected-field-semantics.d.ts +9 -0
- package/dist/secrets/protected-field-semantics.d.ts.map +1 -0
- package/dist/secrets/protected-field-semantics.js +154 -0
- package/dist/secrets/protected-field-values.d.ts +15 -0
- package/dist/secrets/protected-field-values.d.ts.map +1 -0
- package/dist/secrets/protected-field-values.js +131 -0
- package/dist/secrets/protected-fill.d.ts +47 -0
- package/dist/secrets/protected-fill.d.ts.map +1 -0
- package/dist/secrets/protected-fill.js +446 -0
- package/dist/secrets/protected-value-adapters.d.ts +4 -0
- package/dist/secrets/protected-value-adapters.d.ts.map +1 -0
- package/dist/secrets/protected-value-adapters.js +118 -0
- package/dist/secrets/types.d.ts +70 -0
- package/dist/secrets/types.d.ts.map +1 -0
- package/dist/secrets/types.js +30 -0
- package/dist/session.d.ts +19 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +120 -0
- package/dist/solver/browser-launcher.d.ts +14 -0
- package/dist/solver/browser-launcher.d.ts.map +1 -0
- package/dist/solver/browser-launcher.js +799 -0
- package/dist/solver/config.d.ts +18 -0
- package/dist/solver/config.d.ts.map +1 -0
- package/dist/solver/config.js +67 -0
- package/dist/solver/fingerprint.d.ts +9 -0
- package/dist/solver/fingerprint.d.ts.map +1 -0
- package/dist/solver/fingerprint.js +96 -0
- package/dist/solver/profile-manager.d.ts +8 -0
- package/dist/solver/profile-manager.d.ts.map +1 -0
- package/dist/solver/profile-manager.js +74 -0
- package/dist/solver/turnstile-challenge.d.ts +3 -0
- package/dist/solver/turnstile-challenge.d.ts.map +1 -0
- package/dist/solver/turnstile-challenge.js +173 -0
- package/dist/solver/types.d.ts +67 -0
- package/dist/solver/types.d.ts.map +1 -0
- package/dist/solver/types.js +1 -0
- package/dist/stagehand-runtime.d.ts +4 -0
- package/dist/stagehand-runtime.d.ts.map +1 -0
- package/dist/stagehand-runtime.js +10 -0
- package/dist/stagehand.d.ts +15 -0
- package/dist/stagehand.d.ts.map +1 -0
- package/dist/stagehand.js +19 -0
- package/dist/testing.d.ts +5 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +4 -0
- package/dist/update-check.d.ts +14 -0
- package/dist/update-check.d.ts.map +1 -0
- package/dist/update-check.js +182 -0
- package/dist/workflow-session-state.d.ts +30 -0
- package/dist/workflow-session-state.d.ts.map +1 -0
- package/dist/workflow-session-state.js +74 -0
- package/docs/README.md +25 -0
- package/docs/api-reference.md +242 -0
- package/docs/assistive-runtime.md +148 -0
- package/docs/configuration.md +287 -0
- package/docs/getting-started.md +237 -0
- package/docs/integration-checklist.md +36 -0
- package/docs/protected-fill.md +112 -0
- package/docs/testing.md +50 -0
- package/docs/troubleshooting.md +71 -0
- package/examples/README.md +18 -0
- package/examples/attach.ts +27 -0
- package/examples/basic.ts +36 -0
- package/examples/extract.ts +50 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1938 @@
|
|
|
1
|
+
import { getSurface, getTarget } from '../runtime-state.js';
|
|
2
|
+
import { inferComparableValueTypeFromFacts, } from '../control-semantics.js';
|
|
3
|
+
import { buildLocator, resolveLocatorRoot } from './action-fallbacks.js';
|
|
4
|
+
import { OBSERVE_DOM_LABEL_CONTRACT_HELPER_SCRIPT } from './observe-dom-label-contract.js';
|
|
5
|
+
import { resolveSurfaceScopeRoot } from './target-resolution.js';
|
|
6
|
+
import { isLocatorUserActionable } from './user-actionable.js';
|
|
7
|
+
const ACTION_CANDIDATE_PRIORITY = {
|
|
8
|
+
click: {
|
|
9
|
+
role: 0,
|
|
10
|
+
testId: 1,
|
|
11
|
+
label: 2,
|
|
12
|
+
text: 3,
|
|
13
|
+
title: 4,
|
|
14
|
+
css: 5,
|
|
15
|
+
xpath: 6,
|
|
16
|
+
},
|
|
17
|
+
fill: {
|
|
18
|
+
css: 0,
|
|
19
|
+
xpath: 1,
|
|
20
|
+
testId: 2,
|
|
21
|
+
label: 3,
|
|
22
|
+
placeholder: 4,
|
|
23
|
+
role: 5,
|
|
24
|
+
text: 6,
|
|
25
|
+
title: 7,
|
|
26
|
+
},
|
|
27
|
+
type: {
|
|
28
|
+
css: 0,
|
|
29
|
+
xpath: 1,
|
|
30
|
+
testId: 2,
|
|
31
|
+
label: 3,
|
|
32
|
+
placeholder: 4,
|
|
33
|
+
role: 5,
|
|
34
|
+
text: 6,
|
|
35
|
+
title: 7,
|
|
36
|
+
},
|
|
37
|
+
select: {
|
|
38
|
+
css: 0,
|
|
39
|
+
xpath: 1,
|
|
40
|
+
testId: 2,
|
|
41
|
+
label: 3,
|
|
42
|
+
role: 4,
|
|
43
|
+
text: 5,
|
|
44
|
+
title: 6,
|
|
45
|
+
placeholder: 7,
|
|
46
|
+
},
|
|
47
|
+
press: {
|
|
48
|
+
css: 0,
|
|
49
|
+
xpath: 1,
|
|
50
|
+
testId: 2,
|
|
51
|
+
label: 3,
|
|
52
|
+
placeholder: 4,
|
|
53
|
+
role: 5,
|
|
54
|
+
text: 6,
|
|
55
|
+
title: 7,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const ACCEPTANCE_POLL_INTERVAL_MS = 100;
|
|
59
|
+
const ACCEPTANCE_POLL_TIMEOUT_MS = 2_500;
|
|
60
|
+
const CHILD_FRAME_SIGNAL_LIMIT = 20;
|
|
61
|
+
const NO_PROGRESS_SIGNAL_LIMIT = 6;
|
|
62
|
+
const NO_PROGRESS_OVERLAY_LIMIT = 4;
|
|
63
|
+
const ASSOCIATED_CHOICE_STATE_HELPER_SCRIPT = String.raw `
|
|
64
|
+
const ownerWindowOf = (node) => node?.ownerDocument?.defaultView ?? window;
|
|
65
|
+
const isHTMLElementNode = (value) => {
|
|
66
|
+
const view = ownerWindowOf(value);
|
|
67
|
+
return Boolean(view && value instanceof view.HTMLElement);
|
|
68
|
+
};
|
|
69
|
+
const isHTMLInputNode = (value) => {
|
|
70
|
+
const view = ownerWindowOf(value);
|
|
71
|
+
return Boolean(view && value instanceof view.HTMLInputElement);
|
|
72
|
+
};
|
|
73
|
+
const isHTMLLabelNode = (value) => {
|
|
74
|
+
const view = ownerWindowOf(value);
|
|
75
|
+
return Boolean(view && value instanceof view.HTMLLabelElement);
|
|
76
|
+
};
|
|
77
|
+
const isChoiceInput = (candidate) => {
|
|
78
|
+
if (!isHTMLInputNode(candidate)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const type = (candidate.type || '').toLowerCase();
|
|
82
|
+
return type === 'radio' || type === 'checkbox';
|
|
83
|
+
};
|
|
84
|
+
const associatedLabelControlOf = (candidate) => {
|
|
85
|
+
if (!isHTMLLabelNode(candidate)) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const directControl = candidate.control;
|
|
90
|
+
if (isChoiceInput(directControl)) {
|
|
91
|
+
return directControl;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const nestedControl = candidate.querySelector?.('input');
|
|
95
|
+
return isChoiceInput(nestedControl) ? nestedControl : undefined;
|
|
96
|
+
};
|
|
97
|
+
const hiddenChoiceSiblingControlOf = (candidate) => {
|
|
98
|
+
if (!isHTMLElementNode(candidate)) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parent = candidate.parentElement;
|
|
103
|
+
if (!isHTMLElementNode(parent)) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const hiddenChoiceSiblings = Array.from(parent.children).filter((sibling) => {
|
|
108
|
+
if (sibling === candidate || !isChoiceInput(sibling)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const style = ownerWindowOf(sibling).getComputedStyle(sibling);
|
|
113
|
+
const rect = sibling.getBoundingClientRect();
|
|
114
|
+
return (
|
|
115
|
+
style.display === 'none' ||
|
|
116
|
+
style.visibility === 'hidden' ||
|
|
117
|
+
style.pointerEvents === 'none' ||
|
|
118
|
+
Number(style.opacity || '1') < 0.05 ||
|
|
119
|
+
rect.width < 4 ||
|
|
120
|
+
rect.height < 4
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (hiddenChoiceSiblings.length !== 1) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const visibleNonChoiceSiblings = Array.from(parent.children).filter((sibling) => {
|
|
129
|
+
if (sibling === hiddenChoiceSiblings[0] || !isHTMLElementNode(sibling)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const style = ownerWindowOf(sibling).getComputedStyle(sibling);
|
|
134
|
+
const rect = sibling.getBoundingClientRect();
|
|
135
|
+
return (
|
|
136
|
+
style.display !== 'none' &&
|
|
137
|
+
style.visibility !== 'hidden' &&
|
|
138
|
+
style.visibility !== 'collapse' &&
|
|
139
|
+
rect.width > 0 &&
|
|
140
|
+
rect.height > 0
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (visibleNonChoiceSiblings.length !== 1 || visibleNonChoiceSiblings[0] !== candidate) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return hiddenChoiceSiblings[0];
|
|
149
|
+
};
|
|
150
|
+
const associatedChoiceControl =
|
|
151
|
+
(isChoiceInput(element) ? element : undefined) ||
|
|
152
|
+
associatedLabelControlOf(element) ||
|
|
153
|
+
hiddenChoiceSiblingControlOf(element);
|
|
154
|
+
if (!associatedChoiceControl) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
checked: associatedChoiceControl.indeterminate ? 'mixed' : associatedChoiceControl.checked,
|
|
160
|
+
};
|
|
161
|
+
`;
|
|
162
|
+
const PAGE_OBSERVATION_SCRIPT = String.raw `(() => {
|
|
163
|
+
const normalizeText = (value) => (value ?? '').replace(/\s+/g, ' ').trim();
|
|
164
|
+
const sampleText = (value, limit) => {
|
|
165
|
+
if (value.length <= limit) {
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const edge = Math.floor(limit / 3);
|
|
170
|
+
const middle = Math.max(1, limit - edge * 2);
|
|
171
|
+
const middleStart = Math.max(0, Math.floor((value.length - middle) / 2));
|
|
172
|
+
return [
|
|
173
|
+
value.slice(0, edge),
|
|
174
|
+
value.slice(middleStart, middleStart + middle),
|
|
175
|
+
value.slice(-edge),
|
|
176
|
+
].join('\n');
|
|
177
|
+
};
|
|
178
|
+
const isVisible = (element) => {
|
|
179
|
+
if (!(element instanceof HTMLElement)) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const style = element.ownerDocument?.defaultView?.getComputedStyle(element);
|
|
184
|
+
if (!style || style.display === 'none' || style.visibility !== 'visible') {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const rect = element.getBoundingClientRect();
|
|
189
|
+
return rect.width > 0 && rect.height > 0;
|
|
190
|
+
};
|
|
191
|
+
const interactiveSummary = () => {
|
|
192
|
+
const selectors = [
|
|
193
|
+
'button',
|
|
194
|
+
'a',
|
|
195
|
+
'input',
|
|
196
|
+
'textarea',
|
|
197
|
+
'select',
|
|
198
|
+
'[role="button"]',
|
|
199
|
+
'[role="link"]',
|
|
200
|
+
'[role="option"]',
|
|
201
|
+
'[role="combobox"]',
|
|
202
|
+
];
|
|
203
|
+
const items = [];
|
|
204
|
+
|
|
205
|
+
for (const element of Array.from(document.querySelectorAll(selectors.join(', ')))) {
|
|
206
|
+
if (!isVisible(element)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const role = element.getAttribute('role') || element.tagName.toLowerCase();
|
|
211
|
+
const label =
|
|
212
|
+
normalizeText(element.getAttribute('aria-label')) ||
|
|
213
|
+
normalizeText(
|
|
214
|
+
element instanceof HTMLInputElement ||
|
|
215
|
+
element instanceof HTMLTextAreaElement ||
|
|
216
|
+
element instanceof HTMLSelectElement
|
|
217
|
+
? element.value
|
|
218
|
+
: element.textContent
|
|
219
|
+
) ||
|
|
220
|
+
normalizeText(element.getAttribute('title')) ||
|
|
221
|
+
normalizeText(element.getAttribute('placeholder'));
|
|
222
|
+
if (!label) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
items.push(
|
|
227
|
+
[
|
|
228
|
+
role,
|
|
229
|
+
label.slice(0, 80),
|
|
230
|
+
element.getAttribute('aria-expanded') || '',
|
|
231
|
+
element.getAttribute('aria-selected') || '',
|
|
232
|
+
element.getAttribute('aria-pressed') || '',
|
|
233
|
+
].join(':')
|
|
234
|
+
);
|
|
235
|
+
if (items.length >= 60) {
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return items.join('|');
|
|
241
|
+
};
|
|
242
|
+
const stableControlSummary = () => {
|
|
243
|
+
const selectors = [
|
|
244
|
+
'button',
|
|
245
|
+
'a[href]',
|
|
246
|
+
'input',
|
|
247
|
+
'textarea',
|
|
248
|
+
'select',
|
|
249
|
+
'[role="button"]',
|
|
250
|
+
'[role="link"]',
|
|
251
|
+
'[role="option"]',
|
|
252
|
+
'[role="combobox"]',
|
|
253
|
+
'[role="textbox"]',
|
|
254
|
+
];
|
|
255
|
+
const items = [];
|
|
256
|
+
|
|
257
|
+
for (const element of Array.from(document.querySelectorAll(selectors.join(', ')))) {
|
|
258
|
+
if (!isVisible(element)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const role = element.getAttribute('role') || element.tagName.toLowerCase();
|
|
263
|
+
const descriptor =
|
|
264
|
+
normalizeText(element.getAttribute('aria-label')) ||
|
|
265
|
+
normalizeText(element.getAttribute('name')) ||
|
|
266
|
+
normalizeText(element.getAttribute('placeholder')) ||
|
|
267
|
+
normalizeText(element.getAttribute('title')) ||
|
|
268
|
+
normalizeText(element.id) ||
|
|
269
|
+
normalizeText(element.getAttribute('data-testid')) ||
|
|
270
|
+
normalizeText(element.getAttribute('data-test-id')) ||
|
|
271
|
+
'';
|
|
272
|
+
const inputType =
|
|
273
|
+
element instanceof HTMLInputElement
|
|
274
|
+
? normalizeText(element.type || 'text')
|
|
275
|
+
: element instanceof HTMLSelectElement
|
|
276
|
+
? 'select'
|
|
277
|
+
: element instanceof HTMLTextAreaElement
|
|
278
|
+
? 'textarea'
|
|
279
|
+
: '';
|
|
280
|
+
const disabled =
|
|
281
|
+
element instanceof HTMLButtonElement ||
|
|
282
|
+
element instanceof HTMLInputElement ||
|
|
283
|
+
element instanceof HTMLSelectElement ||
|
|
284
|
+
element instanceof HTMLTextAreaElement
|
|
285
|
+
? element.disabled
|
|
286
|
+
: element.getAttribute('aria-disabled') === 'true';
|
|
287
|
+
const valueState =
|
|
288
|
+
element instanceof HTMLInputElement ||
|
|
289
|
+
element instanceof HTMLTextAreaElement ||
|
|
290
|
+
element instanceof HTMLSelectElement
|
|
291
|
+
? normalizeText(element.value)
|
|
292
|
+
? 'value'
|
|
293
|
+
: 'empty'
|
|
294
|
+
: '';
|
|
295
|
+
|
|
296
|
+
items.push(
|
|
297
|
+
[
|
|
298
|
+
role,
|
|
299
|
+
inputType,
|
|
300
|
+
descriptor || 'unlabeled',
|
|
301
|
+
disabled ? 'disabled' : '',
|
|
302
|
+
valueState,
|
|
303
|
+
element.getAttribute('aria-expanded') || '',
|
|
304
|
+
element.getAttribute('aria-selected') || '',
|
|
305
|
+
element.getAttribute('aria-pressed') || '',
|
|
306
|
+
].join(':')
|
|
307
|
+
);
|
|
308
|
+
if (items.length >= 80) {
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return items.join('|');
|
|
314
|
+
};
|
|
315
|
+
const dialogSummary = () => {
|
|
316
|
+
const items = [];
|
|
317
|
+
|
|
318
|
+
for (const element of Array.from(
|
|
319
|
+
document.querySelectorAll('dialog, [role="dialog"], [aria-modal="true"]')
|
|
320
|
+
)) {
|
|
321
|
+
if (!isVisible(element)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const label =
|
|
326
|
+
normalizeText(element.getAttribute('aria-label')) ||
|
|
327
|
+
normalizeText(element.getAttribute('data-testid')) ||
|
|
328
|
+
normalizeText(element.id) ||
|
|
329
|
+
sampleText(normalizeText(element.textContent || ''), 120);
|
|
330
|
+
items.push('dialog:' + (label || 'visible'));
|
|
331
|
+
if (items.length >= 20) {
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return items.join('|');
|
|
337
|
+
};
|
|
338
|
+
const frameSummary = () => {
|
|
339
|
+
const items = [];
|
|
340
|
+
|
|
341
|
+
for (const element of Array.from(document.querySelectorAll('iframe, frame'))) {
|
|
342
|
+
if (!isVisible(element)) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const title =
|
|
347
|
+
normalizeText(element.getAttribute('title')) ||
|
|
348
|
+
normalizeText(element.getAttribute('name'));
|
|
349
|
+
const rawSrc = element.getAttribute('src') || '';
|
|
350
|
+
let normalizedSrc = normalizeText(rawSrc);
|
|
351
|
+
if (rawSrc) {
|
|
352
|
+
try {
|
|
353
|
+
const url = new URL(rawSrc, document.baseURI);
|
|
354
|
+
normalizedSrc = (url.origin + url.pathname).toLowerCase();
|
|
355
|
+
} catch {
|
|
356
|
+
normalizedSrc = normalizeText(rawSrc);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
items.push(
|
|
361
|
+
[
|
|
362
|
+
'frame',
|
|
363
|
+
title || 'untitled',
|
|
364
|
+
normalizedSrc.slice(0, 160),
|
|
365
|
+
element.getAttribute('aria-hidden') || '',
|
|
366
|
+
].join(':')
|
|
367
|
+
);
|
|
368
|
+
if (items.length >= 20) {
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return items.join('|');
|
|
374
|
+
};
|
|
375
|
+
const headingSummary = () => {
|
|
376
|
+
const items = [];
|
|
377
|
+
|
|
378
|
+
for (const element of Array.from(
|
|
379
|
+
document.querySelectorAll('h1, h2, h3, legend, [role="heading"]')
|
|
380
|
+
)) {
|
|
381
|
+
if (!isVisible(element)) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const label = sampleText(normalizeText(element.textContent || ''), 120);
|
|
386
|
+
if (!label) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
items.push(label);
|
|
391
|
+
if (items.length >= 20) {
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return items.join('|');
|
|
397
|
+
};
|
|
398
|
+
const processingSummary = () => {
|
|
399
|
+
const selectors = [
|
|
400
|
+
'[aria-busy="true"]',
|
|
401
|
+
'[role="progressbar"]',
|
|
402
|
+
'[role="status"]',
|
|
403
|
+
'[data-loading]',
|
|
404
|
+
'[data-busy]',
|
|
405
|
+
'button[disabled]',
|
|
406
|
+
'input[disabled]',
|
|
407
|
+
'select[disabled]',
|
|
408
|
+
'textarea[disabled]',
|
|
409
|
+
];
|
|
410
|
+
const items = [];
|
|
411
|
+
|
|
412
|
+
for (const element of Array.from(document.querySelectorAll(selectors.join(', ')))) {
|
|
413
|
+
if (!isVisible(element)) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const role = element.getAttribute('role') || element.tagName.toLowerCase();
|
|
418
|
+
const label =
|
|
419
|
+
normalizeText(element.getAttribute('aria-label')) ||
|
|
420
|
+
normalizeText(element.getAttribute('name')) ||
|
|
421
|
+
normalizeText(element.getAttribute('title')) ||
|
|
422
|
+
normalizeText(element.id) ||
|
|
423
|
+
sampleText(normalizeText(element.textContent || ''), 80);
|
|
424
|
+
items.push(
|
|
425
|
+
[
|
|
426
|
+
role,
|
|
427
|
+
label || 'processing',
|
|
428
|
+
element.getAttribute('aria-busy') || '',
|
|
429
|
+
element.getAttribute('data-loading') || '',
|
|
430
|
+
element.getAttribute('data-busy') || '',
|
|
431
|
+
].join(':')
|
|
432
|
+
);
|
|
433
|
+
if (items.length >= 20) {
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return items.join('|');
|
|
439
|
+
};
|
|
440
|
+
const validationSummary = () => {
|
|
441
|
+
const fieldSelectors =
|
|
442
|
+
'input, textarea, select, [role="textbox"], [contenteditable="true"], [aria-invalid="true"]';
|
|
443
|
+
const validationMessageSelectors = [
|
|
444
|
+
'[role="alert"]',
|
|
445
|
+
'[aria-live="assertive"]',
|
|
446
|
+
'[aria-live="polite"]',
|
|
447
|
+
'.error',
|
|
448
|
+
'.errors',
|
|
449
|
+
'.invalid-feedback',
|
|
450
|
+
'.form-error',
|
|
451
|
+
'.warning',
|
|
452
|
+
'[data-testid*="error"]',
|
|
453
|
+
'[data-testid*="warning"]',
|
|
454
|
+
];
|
|
455
|
+
const validationTextRe =
|
|
456
|
+
/(?:required|invalid|incorrect|too\s+(?:short|long)|must|error|format|please\s+(?:enter|select|choose|fill)|невер|ошиб|обязател|заполн|введите|укажите|выберите|долж|нужно|формат|цифр|символ)/i;
|
|
457
|
+
const items = [];
|
|
458
|
+
|
|
459
|
+
for (const element of Array.from(document.querySelectorAll(fieldSelectors))) {
|
|
460
|
+
if (!(element instanceof HTMLElement) || !isVisible(element)) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const ariaInvalid = element.getAttribute('aria-invalid') === 'true';
|
|
465
|
+
const htmlInvalid =
|
|
466
|
+
element instanceof HTMLInputElement ||
|
|
467
|
+
element instanceof HTMLTextAreaElement ||
|
|
468
|
+
element instanceof HTMLSelectElement
|
|
469
|
+
? !element.checkValidity()
|
|
470
|
+
: false;
|
|
471
|
+
if (!ariaInvalid && !htmlInvalid) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const label =
|
|
476
|
+
normalizeText(element.getAttribute('aria-label')) ||
|
|
477
|
+
normalizeText(element.getAttribute('name')) ||
|
|
478
|
+
normalizeText(element.getAttribute('placeholder')) ||
|
|
479
|
+
normalizeText(element.id);
|
|
480
|
+
items.push('field:' + (label || 'invalid'));
|
|
481
|
+
if (items.length >= 12) {
|
|
482
|
+
return items.join('|');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
for (const element of Array.from(document.querySelectorAll(validationMessageSelectors.join(', ')))) {
|
|
487
|
+
if (!(element instanceof HTMLElement) || !isVisible(element)) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const text = normalizeText(element.textContent || '');
|
|
492
|
+
if (!text || !validationTextRe.test(text)) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
items.push('message:' + text.slice(0, 160));
|
|
497
|
+
if (items.length >= 12) {
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return items.join('|');
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const body = document.body;
|
|
506
|
+
if (!body) {
|
|
507
|
+
return {
|
|
508
|
+
content: '',
|
|
509
|
+
structure: '',
|
|
510
|
+
submitSignals: '',
|
|
511
|
+
resultSignals: '',
|
|
512
|
+
validationBlockerCount: 0,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const attrs = body
|
|
517
|
+
.getAttributeNames()
|
|
518
|
+
.sort()
|
|
519
|
+
.map((name) => name + '=' + (body.getAttribute(name) ?? ''))
|
|
520
|
+
.join('|');
|
|
521
|
+
const text = sampleText(normalizeText(body.innerText || ''), 6000);
|
|
522
|
+
const controls = interactiveSummary();
|
|
523
|
+
const stableControls = stableControlSummary();
|
|
524
|
+
const dialogs = dialogSummary();
|
|
525
|
+
const frames = frameSummary();
|
|
526
|
+
const headings = headingSummary();
|
|
527
|
+
const processing = processingSummary();
|
|
528
|
+
const resultSignals = (() => {
|
|
529
|
+
const selectors = [
|
|
530
|
+
'[role="alert"]',
|
|
531
|
+
'[aria-live="assertive"]',
|
|
532
|
+
'[role="dialog"]',
|
|
533
|
+
'[aria-modal="true"]',
|
|
534
|
+
];
|
|
535
|
+
const items = [];
|
|
536
|
+
|
|
537
|
+
for (const element of Array.from(document.querySelectorAll(selectors.join(', ')))) {
|
|
538
|
+
if (!(element instanceof HTMLElement) || !isVisible(element)) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (element.matches('[aria-busy="true"], [role="progressbar"]')) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const role = element.getAttribute('role') || '';
|
|
547
|
+
const label =
|
|
548
|
+
normalizeText(element.getAttribute('aria-label')) ||
|
|
549
|
+
normalizeText(element.id) ||
|
|
550
|
+
sampleText(normalizeText(element.textContent || ''), 160);
|
|
551
|
+
if (!label) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
items.push([role || 'live', label].join(':'));
|
|
556
|
+
if (items.length >= 20) {
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return items.join('|');
|
|
562
|
+
})();
|
|
563
|
+
const validation = validationSummary();
|
|
564
|
+
return {
|
|
565
|
+
content: [attrs, text, controls, dialogs, frames].join('\n'),
|
|
566
|
+
structure: [attrs, stableControls, dialogs, frames, headings].join('\n'),
|
|
567
|
+
submitSignals: [processing, dialogs, frames, headings, stableControls].join('\n'),
|
|
568
|
+
resultSignals,
|
|
569
|
+
validationBlockerCount: validation ? validation.split('|').length : 0,
|
|
570
|
+
};
|
|
571
|
+
})()`;
|
|
572
|
+
const NO_PROGRESS_PAGE_SIGNALS_SCRIPT = String.raw `(() => {
|
|
573
|
+
const normalizeText = (value) => (value ?? '').replace(/\s+/g, ' ').trim();
|
|
574
|
+
const isVisible = (element) => {
|
|
575
|
+
if (!(element instanceof HTMLElement)) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const style = element.ownerDocument?.defaultView?.getComputedStyle(element);
|
|
580
|
+
if (!style || style.display === 'none' || style.visibility !== 'visible') {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const rect = element.getBoundingClientRect();
|
|
585
|
+
return rect.width > 0 && rect.height > 0;
|
|
586
|
+
};
|
|
587
|
+
const uniqueTexts = (elements, limit) => {
|
|
588
|
+
const values = [];
|
|
589
|
+
for (const element of elements) {
|
|
590
|
+
if (!isVisible(element)) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const text = normalizeText(element.textContent);
|
|
594
|
+
if (!text || values.includes(text)) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
values.push(text.slice(0, 240));
|
|
598
|
+
if (values.length >= limit) {
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return values;
|
|
603
|
+
};
|
|
604
|
+
const labelForField = (element) => {
|
|
605
|
+
if (!(element instanceof HTMLElement)) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const ariaLabel = normalizeText(element.getAttribute('aria-label'));
|
|
610
|
+
if (ariaLabel) {
|
|
611
|
+
return ariaLabel;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (
|
|
615
|
+
element instanceof HTMLInputElement ||
|
|
616
|
+
element instanceof HTMLTextAreaElement ||
|
|
617
|
+
element instanceof HTMLSelectElement
|
|
618
|
+
) {
|
|
619
|
+
if (element.labels?.length) {
|
|
620
|
+
const labels = Array.from(element.labels)
|
|
621
|
+
.map((label) => normalizeText(label.textContent))
|
|
622
|
+
.filter(Boolean);
|
|
623
|
+
if (labels.length > 0) {
|
|
624
|
+
return labels.join(' / ');
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const placeholder = normalizeText(element.getAttribute('placeholder'));
|
|
630
|
+
if (placeholder) {
|
|
631
|
+
return placeholder;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const name = normalizeText(element.getAttribute('name'));
|
|
635
|
+
if (name) {
|
|
636
|
+
return name;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const id = normalizeText(element.id);
|
|
640
|
+
if (id) {
|
|
641
|
+
return id;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return normalizeText(element.textContent) || null;
|
|
645
|
+
};
|
|
646
|
+
const pushUniqueText = (values, nextValue, limit) => {
|
|
647
|
+
const text = normalizeText(nextValue);
|
|
648
|
+
if (!text || values.includes(text)) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
values.push(text.slice(0, 240));
|
|
653
|
+
if (values.length > limit) {
|
|
654
|
+
values.length = limit;
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
const VALIDATION_TEXT_RE =
|
|
658
|
+
/(?:required|invalid|incorrect|too\s+(?:short|long)|must|error|format|please\s+(?:enter|select|choose|fill)|невер|ошиб|обязател|заполн|введите|укажите|выберите|долж|нужно|формат|цифр|символ)/i;
|
|
659
|
+
|
|
660
|
+
const fieldSelectors =
|
|
661
|
+
'input, textarea, select, [role="textbox"], [contenteditable="true"], [aria-invalid="true"]';
|
|
662
|
+
const relatedHelperTexts = (element) => {
|
|
663
|
+
if (!(element instanceof HTMLElement)) {
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const values = [];
|
|
668
|
+
const describedBy = normalizeText(element.getAttribute('aria-describedby'));
|
|
669
|
+
if (describedBy) {
|
|
670
|
+
for (const id of describedBy.split(/\s+/)) {
|
|
671
|
+
const helper = document.getElementById(id);
|
|
672
|
+
if (helper && isVisible(helper)) {
|
|
673
|
+
const helperText = normalizeText(helper.textContent);
|
|
674
|
+
if (VALIDATION_TEXT_RE.test(helperText)) {
|
|
675
|
+
pushUniqueText(values, helperText, 4);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
let anchor = element.parentElement;
|
|
682
|
+
for (let depth = 0; anchor && depth < 4 && values.length < 4; depth += 1, anchor = anchor.parentElement) {
|
|
683
|
+
const anchorFieldCount = anchor.querySelectorAll(fieldSelectors).length;
|
|
684
|
+
if (anchorFieldCount === 0 || anchorFieldCount > 3) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
for (const candidate of Array.from(anchor.children)) {
|
|
689
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (candidate === element || candidate.contains(element) || element.contains(candidate)) {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (!isVisible(candidate) || candidate.matches('label, legend')) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (candidate.querySelector(fieldSelectors)) {
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const helperText = normalizeText(candidate.textContent);
|
|
703
|
+
if (!VALIDATION_TEXT_RE.test(helperText)) {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
pushUniqueText(values, helperText, 4);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return values;
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const invalidFields = [];
|
|
714
|
+
const messages = [];
|
|
715
|
+
for (const element of Array.from(document.querySelectorAll(fieldSelectors))) {
|
|
716
|
+
if (!isVisible(element)) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const ariaInvalid = element.getAttribute('aria-invalid') === 'true';
|
|
721
|
+
const htmlInvalid =
|
|
722
|
+
element instanceof HTMLInputElement ||
|
|
723
|
+
element instanceof HTMLTextAreaElement ||
|
|
724
|
+
element instanceof HTMLSelectElement
|
|
725
|
+
? !element.checkValidity()
|
|
726
|
+
: false;
|
|
727
|
+
const helperMessages = relatedHelperTexts(element);
|
|
728
|
+
|
|
729
|
+
if (!ariaInvalid && !htmlInvalid && helperMessages.length === 0) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const label = labelForField(element);
|
|
734
|
+
if (!label || invalidFields.includes(label)) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
invalidFields.push(label.slice(0, 140));
|
|
738
|
+
for (const helperMessage of helperMessages) {
|
|
739
|
+
pushUniqueText(messages, helperMessage, 6);
|
|
740
|
+
}
|
|
741
|
+
if (invalidFields.length >= 6) {
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const messageSelectors = [
|
|
747
|
+
'[role="alert"]',
|
|
748
|
+
'[aria-live="assertive"]',
|
|
749
|
+
'[aria-live="polite"]',
|
|
750
|
+
'.error',
|
|
751
|
+
'.errors',
|
|
752
|
+
'.invalid-feedback',
|
|
753
|
+
'.form-error',
|
|
754
|
+
'.warning',
|
|
755
|
+
'[data-testid*="error"]',
|
|
756
|
+
'[data-testid*="warning"]',
|
|
757
|
+
];
|
|
758
|
+
for (const message of uniqueTexts(Array.from(document.querySelectorAll(messageSelectors.join(', '))), 6)) {
|
|
759
|
+
pushUniqueText(messages, message, 6);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const overlaySelectors = [
|
|
763
|
+
'[role="alertdialog"]',
|
|
764
|
+
'[role="dialog"][aria-modal="true"]',
|
|
765
|
+
'[role="dialog"][data-state="open"]',
|
|
766
|
+
'[data-state="open"][role="dialog"]',
|
|
767
|
+
];
|
|
768
|
+
const blockingOverlays = uniqueTexts(
|
|
769
|
+
Array.from(document.querySelectorAll(overlaySelectors.join(', '))),
|
|
770
|
+
4
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
messages,
|
|
775
|
+
invalidFields,
|
|
776
|
+
blockingOverlays,
|
|
777
|
+
};
|
|
778
|
+
})()`;
|
|
779
|
+
const SUBMIT_FOLLOW_UP_SURFACE_SCRIPT = String.raw `(() => {
|
|
780
|
+
const normalizeText = (value) => (value ?? '').replace(/\s+/g, ' ').trim();
|
|
781
|
+
const sampleText = (value, limit = 120) => {
|
|
782
|
+
const normalized = normalizeText(value);
|
|
783
|
+
if (!normalized) {
|
|
784
|
+
return '';
|
|
785
|
+
}
|
|
786
|
+
return normalized.length <= limit ? normalized : normalized.slice(0, limit - 1).trimEnd() + '…';
|
|
787
|
+
};
|
|
788
|
+
const isVisible = (element) => {
|
|
789
|
+
if (!(element instanceof HTMLElement)) {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
const style = element.ownerDocument?.defaultView?.getComputedStyle(element);
|
|
793
|
+
if (!style || style.display === 'none' || style.visibility === 'hidden') {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
const rect = element.getBoundingClientRect();
|
|
797
|
+
return rect.width > 0 && rect.height > 0;
|
|
798
|
+
};
|
|
799
|
+
const interactiveDescendantCountOf = (element) =>
|
|
800
|
+
element.querySelectorAll(
|
|
801
|
+
'button, [role="button"], input:not([type="hidden"]), textarea, select, a[href], [tabindex]:not([tabindex="-1"])'
|
|
802
|
+
).length;
|
|
803
|
+
const overlayKeywordRe = /(modal|drawer|sheet|tray|popover|overlay|dialog)/i;
|
|
804
|
+
const overlayHintOf = (element) =>
|
|
805
|
+
[
|
|
806
|
+
element.getAttribute('role') || '',
|
|
807
|
+
element.getAttribute('aria-label') || '',
|
|
808
|
+
element.getAttribute('data-testid') || '',
|
|
809
|
+
element.id || '',
|
|
810
|
+
element.getAttribute('class') || '',
|
|
811
|
+
].join(' ');
|
|
812
|
+
const positionedOverlayLike = (element) => {
|
|
813
|
+
if (!(element instanceof HTMLElement)) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
const style = element.ownerDocument?.defaultView?.getComputedStyle(element);
|
|
817
|
+
if (!style) {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
const position = (style.position || '').toLowerCase();
|
|
821
|
+
const zIndex = Number(style.zIndex || '0');
|
|
822
|
+
const rect = element.getBoundingClientRect();
|
|
823
|
+
const viewportArea = Math.max(window.innerWidth * window.innerHeight, 1);
|
|
824
|
+
const coverage = (rect.width * rect.height) / viewportArea;
|
|
825
|
+
return (
|
|
826
|
+
interactiveDescendantCountOf(element) >= 1 &&
|
|
827
|
+
rect.width >= 160 &&
|
|
828
|
+
rect.height >= 72 &&
|
|
829
|
+
coverage >= 0.02 &&
|
|
830
|
+
coverage <= 0.85 &&
|
|
831
|
+
(position === 'fixed' ||
|
|
832
|
+
position === 'sticky' ||
|
|
833
|
+
(position === 'absolute' && Number.isFinite(zIndex) && zIndex > 0))
|
|
834
|
+
);
|
|
835
|
+
};
|
|
836
|
+
const labelOf = (element) => {
|
|
837
|
+
const heading = element.querySelector?.('h1, h2, h3, [role="heading"]');
|
|
838
|
+
return (
|
|
839
|
+
sampleText(element.getAttribute('aria-label')) ||
|
|
840
|
+
sampleText(element.getAttribute('data-testid')) ||
|
|
841
|
+
sampleText(element.id) ||
|
|
842
|
+
sampleText(heading?.textContent || '') ||
|
|
843
|
+
sampleText(element.textContent || '', 80)
|
|
844
|
+
);
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
const items = [];
|
|
848
|
+
const seen = new Set();
|
|
849
|
+
const selector = [
|
|
850
|
+
'dialog',
|
|
851
|
+
'[role="dialog"]',
|
|
852
|
+
'[aria-modal="true"]',
|
|
853
|
+
'[data-testid]',
|
|
854
|
+
'[id]',
|
|
855
|
+
'[class*="modal"]',
|
|
856
|
+
'[class*="drawer"]',
|
|
857
|
+
'[class*="sheet"]',
|
|
858
|
+
'[class*="tray"]',
|
|
859
|
+
'[class*="popover"]',
|
|
860
|
+
].join(', ');
|
|
861
|
+
|
|
862
|
+
for (const element of Array.from(document.querySelectorAll(selector))) {
|
|
863
|
+
if (!(element instanceof HTMLElement) || !isVisible(element)) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const overlayHint = overlayHintOf(element);
|
|
868
|
+
const semanticOverlay =
|
|
869
|
+
element.matches('dialog, [role="dialog"], [aria-modal="true"]') ||
|
|
870
|
+
overlayKeywordRe.test(overlayHint);
|
|
871
|
+
if (!semanticOverlay && !positionedOverlayLike(element)) {
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const label = labelOf(element);
|
|
876
|
+
const summary = [
|
|
877
|
+
sampleText(element.getAttribute('role') || element.tagName.toLowerCase(), 24) || 'surface',
|
|
878
|
+
label || 'visible',
|
|
879
|
+
sampleText(element.getAttribute('data-testid') || '', 48),
|
|
880
|
+
]
|
|
881
|
+
.filter(Boolean)
|
|
882
|
+
.join(':');
|
|
883
|
+
if (!summary) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const key = summary.toLowerCase();
|
|
888
|
+
if (seen.has(key)) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
seen.add(key);
|
|
892
|
+
items.push(summary);
|
|
893
|
+
if (items.length >= 12) {
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return items.join('|');
|
|
899
|
+
})()`;
|
|
900
|
+
function hashText(value) {
|
|
901
|
+
let hash = 0;
|
|
902
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
903
|
+
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
|
|
904
|
+
}
|
|
905
|
+
return String(hash);
|
|
906
|
+
}
|
|
907
|
+
export async function captureSubmitFollowUpSurfaceHash(page) {
|
|
908
|
+
const summary = await page
|
|
909
|
+
.evaluate(SUBMIT_FOLLOW_UP_SURFACE_SCRIPT)
|
|
910
|
+
.catch(() => '');
|
|
911
|
+
const normalized = typeof summary === 'string' ? summary.trim() : '';
|
|
912
|
+
return normalized ? hashText(normalized) : null;
|
|
913
|
+
}
|
|
914
|
+
function sleep(ms) {
|
|
915
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
916
|
+
}
|
|
917
|
+
export function rankLocatorCandidates(candidates, action) {
|
|
918
|
+
const priority = ACTION_CANDIDATE_PRIORITY[action];
|
|
919
|
+
if (!priority) {
|
|
920
|
+
return [...candidates];
|
|
921
|
+
}
|
|
922
|
+
return [...candidates].sort((left, right) => {
|
|
923
|
+
const leftPriority = priority[left.strategy] ?? 99;
|
|
924
|
+
const rightPriority = priority[right.strategy] ?? 99;
|
|
925
|
+
return leftPriority - rightPriority;
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
export function shouldVerifyObservableProgress(target, action) {
|
|
929
|
+
if (action === 'fill' || action === 'type') {
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
if (action === 'press' && isEditableLikeTarget(target)) {
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
return action === 'click' || action === 'press' || action === 'select';
|
|
936
|
+
}
|
|
937
|
+
function isEditableLikeTarget(target) {
|
|
938
|
+
if (target.controlFamily === 'text-input' ||
|
|
939
|
+
target.controlFamily === 'select' ||
|
|
940
|
+
target.controlFamily === 'datepicker') {
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
const kind = (target.kind ?? '').toLowerCase();
|
|
944
|
+
const role = (target.semantics?.role ?? '').toLowerCase();
|
|
945
|
+
return (['input', 'textarea', 'select', 'combobox'].includes(kind) ||
|
|
946
|
+
['textbox', 'combobox'].includes(role) ||
|
|
947
|
+
target.allowedActions.includes('fill') ||
|
|
948
|
+
target.allowedActions.includes('type') ||
|
|
949
|
+
target.allowedActions.includes('select'));
|
|
950
|
+
}
|
|
951
|
+
function isSelectableChoiceTarget(target) {
|
|
952
|
+
const kind = (target.kind ?? '').toLowerCase();
|
|
953
|
+
const role = (target.semantics?.role ?? '').toLowerCase();
|
|
954
|
+
return (kind === 'option' ||
|
|
955
|
+
role === 'option' ||
|
|
956
|
+
role === 'menuitem' ||
|
|
957
|
+
role === 'gridcell' ||
|
|
958
|
+
target.structure?.family === 'structured-grid');
|
|
959
|
+
}
|
|
960
|
+
function acceptancePolicyForAction(target, action) {
|
|
961
|
+
if (action === 'press' && isEditableLikeTarget(target)) {
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
if ((action === 'fill' || action === 'type') &&
|
|
965
|
+
target.controlFamily === 'select' &&
|
|
966
|
+
target.allowedActions.includes('fill') &&
|
|
967
|
+
target.allowedActions.includes('select') &&
|
|
968
|
+
(target.kind ?? '').toLowerCase() !== 'select' &&
|
|
969
|
+
(target.semantics?.role ?? '').toLowerCase() === 'combobox') {
|
|
970
|
+
return 'selection';
|
|
971
|
+
}
|
|
972
|
+
if (action === 'select') {
|
|
973
|
+
if (target.controlFamily === 'datepicker') {
|
|
974
|
+
return 'date-selection';
|
|
975
|
+
}
|
|
976
|
+
if (target.controlFamily === 'select') {
|
|
977
|
+
return 'selection';
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (action === 'click' || action === 'press') {
|
|
981
|
+
if (target.controlFamily === 'datepicker') {
|
|
982
|
+
return isSelectableChoiceTarget(target) ? 'date-selection' : 'disclosure';
|
|
983
|
+
}
|
|
984
|
+
if (target.controlFamily === 'select') {
|
|
985
|
+
return isSelectableChoiceTarget(target) ? 'selection' : 'disclosure';
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const policy = target.acceptancePolicy;
|
|
989
|
+
if (!policy) {
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
if ((action === 'click' || action === 'press') &&
|
|
993
|
+
policy === 'value-change' &&
|
|
994
|
+
isEditableLikeTarget(target)) {
|
|
995
|
+
return 'generic-click';
|
|
996
|
+
}
|
|
997
|
+
return policy;
|
|
998
|
+
}
|
|
999
|
+
function trackedStateKeys(target, action, policy) {
|
|
1000
|
+
const states = target.semantics?.states;
|
|
1001
|
+
const keys = new Set();
|
|
1002
|
+
if (states) {
|
|
1003
|
+
for (const key of ['selected', 'checked', 'expanded', 'pressed', 'current']) {
|
|
1004
|
+
if (Object.prototype.hasOwnProperty.call(states, key)) {
|
|
1005
|
+
keys.add(key);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (policy === 'disclosure' &&
|
|
1010
|
+
(action === 'click' || action === 'press') &&
|
|
1011
|
+
(target.controlFamily === 'select' || target.controlFamily === 'datepicker')) {
|
|
1012
|
+
keys.add('expanded');
|
|
1013
|
+
}
|
|
1014
|
+
return [...keys];
|
|
1015
|
+
}
|
|
1016
|
+
export async function capturePageObservation(page) {
|
|
1017
|
+
const snapshot = typeof page.evaluate === 'function'
|
|
1018
|
+
? await Promise.resolve(page.evaluate(PAGE_OBSERVATION_SCRIPT)).catch(() => '')
|
|
1019
|
+
: '';
|
|
1020
|
+
const contentSnapshot = typeof snapshot === 'string' ? snapshot : (snapshot?.content ?? '');
|
|
1021
|
+
const structureSnapshot = typeof snapshot === 'string' ? '' : (snapshot?.structure ?? '');
|
|
1022
|
+
const submitSignalSnapshot = typeof snapshot === 'string' ? '' : (snapshot?.submitSignals ?? '');
|
|
1023
|
+
const resultSignalSnapshot = typeof snapshot === 'string' ? '' : (snapshot?.resultSignals ?? '');
|
|
1024
|
+
const validationBlockerCount = typeof snapshot === 'string'
|
|
1025
|
+
? 0
|
|
1026
|
+
: typeof snapshot?.validationBlockerCount === 'number'
|
|
1027
|
+
? snapshot.validationBlockerCount
|
|
1028
|
+
: 0;
|
|
1029
|
+
return {
|
|
1030
|
+
url: page.url(),
|
|
1031
|
+
title: await page.title().catch(() => ''),
|
|
1032
|
+
contentHash: contentSnapshot ? hashText(contentSnapshot) : null,
|
|
1033
|
+
structureHash: structureSnapshot ? hashText(structureSnapshot) : null,
|
|
1034
|
+
submitSignalHash: submitSignalSnapshot ? hashText(submitSignalSnapshot) : null,
|
|
1035
|
+
resultSignalHash: resultSignalSnapshot ? hashText(resultSignalSnapshot) : null,
|
|
1036
|
+
validationBlockerCount,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
async function readLocatorText(locator) {
|
|
1040
|
+
const count = await locator.count().catch(() => 0);
|
|
1041
|
+
if (count === 0) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
const normalizedInnerText = typeof locator.innerText === 'function'
|
|
1045
|
+
? await locator
|
|
1046
|
+
.innerText()
|
|
1047
|
+
.then((value) => value.replace(/\s+/g, ' ').trim())
|
|
1048
|
+
.catch(() => '')
|
|
1049
|
+
: '';
|
|
1050
|
+
if (normalizedInnerText) {
|
|
1051
|
+
return normalizedInnerText;
|
|
1052
|
+
}
|
|
1053
|
+
const normalizedTextContent = typeof locator.textContent === 'function'
|
|
1054
|
+
? await locator
|
|
1055
|
+
.textContent()
|
|
1056
|
+
.then((value) => (value ?? '').replace(/\s+/g, ' ').trim())
|
|
1057
|
+
.catch(() => '')
|
|
1058
|
+
: '';
|
|
1059
|
+
return normalizedTextContent || null;
|
|
1060
|
+
}
|
|
1061
|
+
export async function captureLocatorContextHash(locator) {
|
|
1062
|
+
const candidates = [];
|
|
1063
|
+
if (typeof locator.locator === 'function') {
|
|
1064
|
+
const ancestorContext = locator.locator('xpath=ancestor-or-self::*[' +
|
|
1065
|
+
'@role="option" or @role="row" or @role="gridcell" or @role="listitem" or ' +
|
|
1066
|
+
'@role="tabpanel" or @role="dialog" or @role="listbox" or @role="menu" or @role="grid" or ' +
|
|
1067
|
+
'self::article or self::li or self::tr or self::td or self::section or self::form' +
|
|
1068
|
+
'][1]');
|
|
1069
|
+
const ancestorCount = await ancestorContext.count().catch(() => 0);
|
|
1070
|
+
if (ancestorCount > 0) {
|
|
1071
|
+
candidates.push(typeof ancestorContext.first === 'function' ? ancestorContext.first() : ancestorContext);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
candidates.push(typeof locator.first === 'function' ? locator.first() : locator);
|
|
1075
|
+
for (const candidate of candidates) {
|
|
1076
|
+
const text = await readLocatorText(candidate);
|
|
1077
|
+
if (text) {
|
|
1078
|
+
return hashText(text);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
async function shouldProbePopupCurrentValue(locator) {
|
|
1084
|
+
const readAttribute = async (name) => {
|
|
1085
|
+
if (typeof locator.getAttribute !== 'function') {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
return locator.getAttribute(name).catch(() => null);
|
|
1089
|
+
};
|
|
1090
|
+
const [role, ariaHasPopup, ariaControls, ariaExpanded] = await Promise.all([
|
|
1091
|
+
readAttribute('role'),
|
|
1092
|
+
readAttribute('aria-haspopup'),
|
|
1093
|
+
readAttribute('aria-controls'),
|
|
1094
|
+
readAttribute('aria-expanded'),
|
|
1095
|
+
]);
|
|
1096
|
+
return (Boolean((ariaControls || '').trim()) ||
|
|
1097
|
+
['listbox', 'menu'].includes((ariaHasPopup || '').toLowerCase()) ||
|
|
1098
|
+
(role || '').toLowerCase() === 'combobox' ||
|
|
1099
|
+
ariaExpanded !== null);
|
|
1100
|
+
}
|
|
1101
|
+
async function captureLocatorValue(locator) {
|
|
1102
|
+
const count = await locator.count().catch(() => 0);
|
|
1103
|
+
if (count === 0) {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
const inputValue = typeof locator.inputValue === 'function' ? await locator.inputValue().catch(() => '') : '';
|
|
1107
|
+
if (inputValue) {
|
|
1108
|
+
return inputValue;
|
|
1109
|
+
}
|
|
1110
|
+
if (!(await shouldProbePopupCurrentValue(locator))) {
|
|
1111
|
+
return readLocatorText(locator);
|
|
1112
|
+
}
|
|
1113
|
+
const popupCurrentValue = await locator
|
|
1114
|
+
.evaluate((element, source) => {
|
|
1115
|
+
const read = Function('element', `${source}
|
|
1116
|
+
if (!(element instanceof HTMLElement)) {
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
return observedPopupCurrentValueOf(element) || null;`);
|
|
1120
|
+
return read(element);
|
|
1121
|
+
}, OBSERVE_DOM_LABEL_CONTRACT_HELPER_SCRIPT)
|
|
1122
|
+
.catch(() => null);
|
|
1123
|
+
if (popupCurrentValue) {
|
|
1124
|
+
return popupCurrentValue;
|
|
1125
|
+
}
|
|
1126
|
+
return readLocatorText(locator);
|
|
1127
|
+
}
|
|
1128
|
+
async function captureLocatorValueFromCandidates(locators) {
|
|
1129
|
+
let fallbackValue = null;
|
|
1130
|
+
for (const locator of locators) {
|
|
1131
|
+
const value = await captureLocatorValue(locator).catch(() => null);
|
|
1132
|
+
if (value) {
|
|
1133
|
+
return value;
|
|
1134
|
+
}
|
|
1135
|
+
if (value !== null && fallbackValue === null) {
|
|
1136
|
+
fallbackValue = value;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return fallbackValue;
|
|
1140
|
+
}
|
|
1141
|
+
async function captureSelectedOptionText(locator) {
|
|
1142
|
+
return locator
|
|
1143
|
+
.evaluate((element) => {
|
|
1144
|
+
if (!(element instanceof HTMLSelectElement)) {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
const selected = element.options[element.selectedIndex] ?? null;
|
|
1148
|
+
const text = (selected?.textContent ?? '').replace(/\s+/g, ' ').trim();
|
|
1149
|
+
return text || null;
|
|
1150
|
+
})
|
|
1151
|
+
.catch(() => null);
|
|
1152
|
+
}
|
|
1153
|
+
function pushComparableValue(values, nextValue) {
|
|
1154
|
+
const raw = (nextValue ?? '').replace(/\s+/g, ' ').trim();
|
|
1155
|
+
const normalized = normalizeComparableValue(raw);
|
|
1156
|
+
if (!normalized) {
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
if (values.some((existing) => normalizeComparableValue(existing) === normalized)) {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
values.push(raw);
|
|
1163
|
+
}
|
|
1164
|
+
async function captureLocatorComparableValues(locator) {
|
|
1165
|
+
const count = await locator.count().catch(() => 0);
|
|
1166
|
+
if (count === 0) {
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
const values = [];
|
|
1170
|
+
pushComparableValue(values, await captureLocatorValue(locator));
|
|
1171
|
+
pushComparableValue(values, await captureSelectedOptionText(locator));
|
|
1172
|
+
return values;
|
|
1173
|
+
}
|
|
1174
|
+
async function captureLocatorComparableValuesFromCandidates(locators) {
|
|
1175
|
+
const values = [];
|
|
1176
|
+
for (const locator of locators) {
|
|
1177
|
+
const nextValues = await captureLocatorComparableValues(locator).catch(() => []);
|
|
1178
|
+
for (const nextValue of nextValues) {
|
|
1179
|
+
pushComparableValue(values, nextValue);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return values;
|
|
1183
|
+
}
|
|
1184
|
+
async function captureLocatorStateFromCandidates(locators, keys) {
|
|
1185
|
+
for (const locator of locators) {
|
|
1186
|
+
const state = await captureLocatorState(locator, keys).catch(() => null);
|
|
1187
|
+
if (state && Object.keys(state).length > 0) {
|
|
1188
|
+
return state;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
async function captureLocatorContextHashFromCandidates(locators) {
|
|
1194
|
+
for (const locator of locators) {
|
|
1195
|
+
const hash = await captureLocatorContextHash(locator).catch(() => null);
|
|
1196
|
+
if (hash) {
|
|
1197
|
+
return hash;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
function comparableValueTypeForTarget(target) {
|
|
1203
|
+
return inferComparableValueTypeFromFacts({
|
|
1204
|
+
kind: target.kind,
|
|
1205
|
+
role: target.semantics?.role,
|
|
1206
|
+
label: target.label,
|
|
1207
|
+
displayLabel: target.displayLabel,
|
|
1208
|
+
placeholder: target.placeholder,
|
|
1209
|
+
inputName: target.inputName,
|
|
1210
|
+
inputType: target.inputType,
|
|
1211
|
+
autocomplete: target.autocomplete,
|
|
1212
|
+
states: target.semantics?.states,
|
|
1213
|
+
structure: target.structure,
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
function normalizeComparableValue(value, comparableValueType) {
|
|
1217
|
+
const raw = (value ?? '').replace(/\s+/g, ' ').trim();
|
|
1218
|
+
if (!raw) {
|
|
1219
|
+
return '';
|
|
1220
|
+
}
|
|
1221
|
+
if (comparableValueType === 'card-number' ||
|
|
1222
|
+
comparableValueType === 'expiry' ||
|
|
1223
|
+
comparableValueType === 'cvc') {
|
|
1224
|
+
return raw.replace(/\D/g, '');
|
|
1225
|
+
}
|
|
1226
|
+
if (comparableValueType === 'date') {
|
|
1227
|
+
const isoMatch = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
1228
|
+
if (isoMatch) {
|
|
1229
|
+
return `${isoMatch[1]}${isoMatch[2]}${isoMatch[3]}`;
|
|
1230
|
+
}
|
|
1231
|
+
const localizedMatch = raw.match(/^(\d{1,2})[./-](\d{1,2})[./-](\d{2,4})$/);
|
|
1232
|
+
if (localizedMatch) {
|
|
1233
|
+
const [, day = '', month = '', year = ''] = localizedMatch;
|
|
1234
|
+
if (!day || !month || !year) {
|
|
1235
|
+
return raw.replace(/\D/g, '');
|
|
1236
|
+
}
|
|
1237
|
+
const normalizedYear = year.length === 2 ? `20${year}` : year.padStart(4, '0');
|
|
1238
|
+
return `${normalizedYear}${month.padStart(2, '0')}${day.padStart(2, '0')}`;
|
|
1239
|
+
}
|
|
1240
|
+
return raw.replace(/\D/g, '');
|
|
1241
|
+
}
|
|
1242
|
+
if (comparableValueType === 'phone' ||
|
|
1243
|
+
(!comparableValueType && /^\+?[\d\s().-]{5,}$/.test(raw))) {
|
|
1244
|
+
const normalized = raw.replace(/[^\d+]/g, '');
|
|
1245
|
+
return normalized.startsWith('+') ? `+${normalized.slice(1).replace(/\+/g, '')}` : normalized;
|
|
1246
|
+
}
|
|
1247
|
+
return raw.toLowerCase();
|
|
1248
|
+
}
|
|
1249
|
+
function valuesMatchExpected(expected, actual, comparableValueType, options) {
|
|
1250
|
+
const normalizedExpected = normalizeComparableValue(expected, comparableValueType);
|
|
1251
|
+
const normalizedActual = normalizeComparableValue(actual, comparableValueType);
|
|
1252
|
+
if (!normalizedExpected || !normalizedActual) {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
if (normalizedActual === normalizedExpected || normalizedActual.includes(normalizedExpected)) {
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
if (options?.allowCompactActualSequence !== true) {
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
const expectedTokens = normalizedExpected.split(/\s+/).filter(Boolean);
|
|
1262
|
+
const actualTokens = normalizedActual.split(/\s+/).filter(Boolean);
|
|
1263
|
+
if (actualTokens.length === 0 || actualTokens.length > expectedTokens.length) {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
const compactActualLooksCodeLike = actualTokens.every((token) => token.length <= 3 || /[\d+]/.test(token));
|
|
1267
|
+
if (!compactActualLooksCodeLike) {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
for (let index = 0; index <= expectedTokens.length - actualTokens.length; index += 1) {
|
|
1271
|
+
const matches = actualTokens.every((token, tokenIndex) => expectedTokens[index + tokenIndex] === token);
|
|
1272
|
+
if (matches) {
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
function valuesMatchAnyExpected(expected, actualValues, comparableValueType, options) {
|
|
1279
|
+
return actualValues.some((actualValue) => valuesMatchExpected(expected, actualValue, comparableValueType, options));
|
|
1280
|
+
}
|
|
1281
|
+
function expectedValueForAcceptance(action, actionValue) {
|
|
1282
|
+
if (action === 'fill' || action === 'type' || action === 'select') {
|
|
1283
|
+
return actionValue ?? null;
|
|
1284
|
+
}
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
function recoveryDescendantSelector(target, action) {
|
|
1288
|
+
if (action === 'fill' || action === 'type') {
|
|
1289
|
+
return 'input:not([type="hidden"]), textarea, select, [contenteditable="true"]';
|
|
1290
|
+
}
|
|
1291
|
+
if ((action === 'click' || action === 'press') &&
|
|
1292
|
+
(target.controlFamily === 'text-input' ||
|
|
1293
|
+
target.controlFamily === 'select' ||
|
|
1294
|
+
target.controlFamily === 'datepicker')) {
|
|
1295
|
+
return 'input:not([type="hidden"]), textarea, select, [contenteditable="true"], [role="textbox"], [role="combobox"]';
|
|
1296
|
+
}
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
async function prepareReadLocator(locator, target, action) {
|
|
1300
|
+
const visible = await isLocatorUserActionable(locator);
|
|
1301
|
+
if (!visible) {
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
const descendantSelector = recoveryDescendantSelector(target, action);
|
|
1305
|
+
if (!descendantSelector) {
|
|
1306
|
+
return locator;
|
|
1307
|
+
}
|
|
1308
|
+
const requiresEditableDescendant = action === 'fill' || action === 'type';
|
|
1309
|
+
if (requiresEditableDescendant) {
|
|
1310
|
+
const editable = await locator.isEditable().catch(() => false);
|
|
1311
|
+
if (editable) {
|
|
1312
|
+
return locator;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const descendants = locator.locator(descendantSelector);
|
|
1316
|
+
const count = await descendants.count().catch(() => 0);
|
|
1317
|
+
const visibleDescendants = [];
|
|
1318
|
+
for (let index = 0; index < count; index += 1) {
|
|
1319
|
+
const descendant = descendants.nth(index);
|
|
1320
|
+
const descendantVisible = await isLocatorUserActionable(descendant);
|
|
1321
|
+
if (!descendantVisible) {
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
if (requiresEditableDescendant) {
|
|
1325
|
+
const editable = await descendant.isEditable().catch(() => false);
|
|
1326
|
+
if (!editable) {
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
visibleDescendants.push(descendant);
|
|
1331
|
+
}
|
|
1332
|
+
if (visibleDescendants.length === 1) {
|
|
1333
|
+
return visibleDescendants[0] ?? null;
|
|
1334
|
+
}
|
|
1335
|
+
return requiresEditableDescendant ? null : locator;
|
|
1336
|
+
}
|
|
1337
|
+
function valueMeaningfullyChanged(before, after) {
|
|
1338
|
+
const normalizedBefore = normalizeComparableValue(before);
|
|
1339
|
+
const normalizedAfter = normalizeComparableValue(after);
|
|
1340
|
+
if (!normalizedAfter) {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
return normalizedBefore !== normalizedAfter;
|
|
1344
|
+
}
|
|
1345
|
+
export const __testComparableValues = {
|
|
1346
|
+
comparableValueTypeForTarget,
|
|
1347
|
+
normalizeComparableValue,
|
|
1348
|
+
valuesMatchExpected,
|
|
1349
|
+
};
|
|
1350
|
+
export async function captureLocatorState(locator, keys) {
|
|
1351
|
+
if (keys.length === 0) {
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
const count = await locator.count().catch(() => 0);
|
|
1355
|
+
if (count === 0) {
|
|
1356
|
+
return { current: 'missing' };
|
|
1357
|
+
}
|
|
1358
|
+
const state = {};
|
|
1359
|
+
const readAttribute = async (name) => {
|
|
1360
|
+
if (typeof locator.getAttribute !== 'function') {
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
return locator.getAttribute(name).catch(() => null);
|
|
1364
|
+
};
|
|
1365
|
+
const heuristicFlags = await locator
|
|
1366
|
+
.evaluate((element) => {
|
|
1367
|
+
if (!(element instanceof HTMLElement)) {
|
|
1368
|
+
return { selected: null, current: null, pressed: null };
|
|
1369
|
+
}
|
|
1370
|
+
const blob = (element.getAttribute('class') || '').toLowerCase() +
|
|
1371
|
+
' ' +
|
|
1372
|
+
(element.getAttribute('data-state') || '').toLowerCase() +
|
|
1373
|
+
' ' +
|
|
1374
|
+
(element.getAttribute('data-status') || '').toLowerCase() +
|
|
1375
|
+
' ' +
|
|
1376
|
+
Object.values(element.dataset || {})
|
|
1377
|
+
.join(' ')
|
|
1378
|
+
.toLowerCase();
|
|
1379
|
+
const active = /(?:selected|active|current)\b/.test(blob);
|
|
1380
|
+
const pressed = /(?:pressed|active)\b/.test(blob);
|
|
1381
|
+
return {
|
|
1382
|
+
selected: active,
|
|
1383
|
+
current: active,
|
|
1384
|
+
pressed,
|
|
1385
|
+
};
|
|
1386
|
+
})
|
|
1387
|
+
.catch(() => ({ selected: null, current: null, pressed: null }));
|
|
1388
|
+
const associatedChoiceState = keys.includes('checked')
|
|
1389
|
+
? await locator
|
|
1390
|
+
.evaluate((element, source) => Function('element', source)(element), ASSOCIATED_CHOICE_STATE_HELPER_SCRIPT)
|
|
1391
|
+
.catch(() => null)
|
|
1392
|
+
: null;
|
|
1393
|
+
for (const key of keys) {
|
|
1394
|
+
switch (key) {
|
|
1395
|
+
case 'checked': {
|
|
1396
|
+
if (typeof locator.isChecked === 'function') {
|
|
1397
|
+
const checked = await locator.isChecked().catch(() => undefined);
|
|
1398
|
+
if (typeof checked === 'boolean') {
|
|
1399
|
+
state.checked = checked;
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const ariaChecked = await readAttribute('aria-checked');
|
|
1404
|
+
if (ariaChecked === 'true')
|
|
1405
|
+
state.checked = true;
|
|
1406
|
+
else if (ariaChecked === 'false')
|
|
1407
|
+
state.checked = false;
|
|
1408
|
+
else if (ariaChecked === 'mixed')
|
|
1409
|
+
state.checked = 'mixed';
|
|
1410
|
+
else if (associatedChoiceState && typeof associatedChoiceState === 'object') {
|
|
1411
|
+
state.checked = associatedChoiceState.checked;
|
|
1412
|
+
}
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
case 'selected': {
|
|
1416
|
+
const value = await readAttribute('aria-selected');
|
|
1417
|
+
if (value === 'true')
|
|
1418
|
+
state.selected = true;
|
|
1419
|
+
else if (value === 'false')
|
|
1420
|
+
state.selected = false;
|
|
1421
|
+
else if (typeof heuristicFlags.selected === 'boolean')
|
|
1422
|
+
state.selected = heuristicFlags.selected;
|
|
1423
|
+
break;
|
|
1424
|
+
}
|
|
1425
|
+
case 'expanded': {
|
|
1426
|
+
const value = await readAttribute('aria-expanded');
|
|
1427
|
+
if (value === 'true')
|
|
1428
|
+
state.expanded = true;
|
|
1429
|
+
else if (value === 'false')
|
|
1430
|
+
state.expanded = false;
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
case 'pressed': {
|
|
1434
|
+
const value = await readAttribute('aria-pressed');
|
|
1435
|
+
if (value === 'true')
|
|
1436
|
+
state.pressed = true;
|
|
1437
|
+
else if (value === 'false')
|
|
1438
|
+
state.pressed = false;
|
|
1439
|
+
else if (typeof heuristicFlags.pressed === 'boolean')
|
|
1440
|
+
state.pressed = heuristicFlags.pressed;
|
|
1441
|
+
break;
|
|
1442
|
+
}
|
|
1443
|
+
case 'current': {
|
|
1444
|
+
const value = await readAttribute('aria-current');
|
|
1445
|
+
if (value === 'true')
|
|
1446
|
+
state.current = true;
|
|
1447
|
+
else if (typeof value === 'string' && value.length > 0)
|
|
1448
|
+
state.current = value;
|
|
1449
|
+
else if (typeof heuristicFlags.current === 'boolean')
|
|
1450
|
+
state.current = heuristicFlags.current;
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1453
|
+
case 'focused': {
|
|
1454
|
+
const focused = await locator
|
|
1455
|
+
.evaluate((element) => {
|
|
1456
|
+
if (!(element instanceof HTMLElement)) {
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
return (element.matches?.(':focus') === true ||
|
|
1460
|
+
element.ownerDocument?.activeElement === element);
|
|
1461
|
+
})
|
|
1462
|
+
.catch(() => false);
|
|
1463
|
+
state.focused = focused;
|
|
1464
|
+
break;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return Object.keys(state).length > 0 ? state : null;
|
|
1469
|
+
}
|
|
1470
|
+
export function locatorStateChanged(before, after) {
|
|
1471
|
+
if (!before && !after) {
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
if (!before) {
|
|
1475
|
+
return Boolean(after && Object.keys(after).length > 0);
|
|
1476
|
+
}
|
|
1477
|
+
if (!after) {
|
|
1478
|
+
return Object.keys(before).length > 0;
|
|
1479
|
+
}
|
|
1480
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
1481
|
+
for (const key of keys) {
|
|
1482
|
+
if (before[key] !== after[key]) {
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
export function pageObservationChanged(before, after) {
|
|
1489
|
+
if (!before || !after) {
|
|
1490
|
+
return false;
|
|
1491
|
+
}
|
|
1492
|
+
if (before.url !== after.url || before.title !== after.title) {
|
|
1493
|
+
return true;
|
|
1494
|
+
}
|
|
1495
|
+
if (before.contentHash && after.contentHash && before.contentHash !== after.contentHash) {
|
|
1496
|
+
return true;
|
|
1497
|
+
}
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
export function genericClickObservationChanged(before, after) {
|
|
1501
|
+
if (!before || !after) {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
if (before.url !== after.url || before.title !== after.title) {
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
if ((before.structureHash || after.structureHash) &&
|
|
1508
|
+
before.structureHash !== after.structureHash) {
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
if ((before.submitSignalHash || after.submitSignalHash) &&
|
|
1512
|
+
before.submitSignalHash !== after.submitSignalHash) {
|
|
1513
|
+
return true;
|
|
1514
|
+
}
|
|
1515
|
+
if ((before.resultSignalHash || after.resultSignalHash) &&
|
|
1516
|
+
before.resultSignalHash !== after.resultSignalHash) {
|
|
1517
|
+
return true;
|
|
1518
|
+
}
|
|
1519
|
+
if (before.validationBlockerCount > after.validationBlockerCount) {
|
|
1520
|
+
return true;
|
|
1521
|
+
}
|
|
1522
|
+
return false;
|
|
1523
|
+
}
|
|
1524
|
+
export function submitObservationChanged(before, after) {
|
|
1525
|
+
if (!before || !after) {
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1528
|
+
if (before.url !== after.url || before.title !== after.title) {
|
|
1529
|
+
return true;
|
|
1530
|
+
}
|
|
1531
|
+
if (before.structureHash && after.structureHash && before.structureHash !== after.structureHash) {
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
if (before.validationBlockerCount > 0 && after.validationBlockerCount === 0) {
|
|
1535
|
+
return true;
|
|
1536
|
+
}
|
|
1537
|
+
return false;
|
|
1538
|
+
}
|
|
1539
|
+
function normalizeObservationText(value) {
|
|
1540
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
1541
|
+
}
|
|
1542
|
+
function mergeUniqueObservationTexts(target, values, limit) {
|
|
1543
|
+
for (const value of values) {
|
|
1544
|
+
const normalized = normalizeObservationText(value);
|
|
1545
|
+
if (!normalized || target.includes(normalized)) {
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
target.push(normalized);
|
|
1549
|
+
if (target.length >= limit) {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
async function walkChildFrames(page, visit) {
|
|
1555
|
+
if (typeof page.mainFrame !== 'function') {
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
const mainFrame = page.mainFrame();
|
|
1559
|
+
if (!mainFrame || typeof mainFrame.childFrames !== 'function') {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
let visitedFrames = 0;
|
|
1563
|
+
const isFrameHostVisible = async (frame) => {
|
|
1564
|
+
if (typeof frame.frameElement !== 'function') {
|
|
1565
|
+
return true;
|
|
1566
|
+
}
|
|
1567
|
+
const frameElement = await frame.frameElement().catch(() => null);
|
|
1568
|
+
if (!frameElement) {
|
|
1569
|
+
return false;
|
|
1570
|
+
}
|
|
1571
|
+
try {
|
|
1572
|
+
return await frameElement.evaluate((element) => {
|
|
1573
|
+
if (!(element instanceof HTMLElement)) {
|
|
1574
|
+
return false;
|
|
1575
|
+
}
|
|
1576
|
+
const style = element.ownerDocument?.defaultView?.getComputedStyle(element);
|
|
1577
|
+
if (!style ||
|
|
1578
|
+
style.display === 'none' ||
|
|
1579
|
+
style.visibility === 'hidden' ||
|
|
1580
|
+
style.visibility === 'collapse') {
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
const rect = element.getBoundingClientRect();
|
|
1584
|
+
return rect.width > 0 && rect.height > 0;
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
catch {
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
finally {
|
|
1591
|
+
await frameElement.dispose().catch(() => undefined);
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
const walk = async (frame) => {
|
|
1595
|
+
const childFrames = frame.childFrames().slice(0, CHILD_FRAME_SIGNAL_LIMIT);
|
|
1596
|
+
for (const childFrame of childFrames) {
|
|
1597
|
+
if (visitedFrames >= CHILD_FRAME_SIGNAL_LIMIT) {
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
visitedFrames += 1;
|
|
1601
|
+
if (!(await isFrameHostVisible(childFrame))) {
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
await visit(childFrame).catch(() => undefined);
|
|
1605
|
+
await walk(childFrame);
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
await walk(mainFrame);
|
|
1609
|
+
}
|
|
1610
|
+
async function readNoProgressPageSignals(documentRoot) {
|
|
1611
|
+
const pageSignals = await Promise.resolve(documentRoot.evaluate(NO_PROGRESS_PAGE_SIGNALS_SCRIPT)).catch(() => null);
|
|
1612
|
+
if (!pageSignals || typeof pageSignals !== 'object' || Array.isArray(pageSignals)) {
|
|
1613
|
+
return {
|
|
1614
|
+
messages: [],
|
|
1615
|
+
invalidFields: [],
|
|
1616
|
+
blockingOverlays: [],
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
return {
|
|
1620
|
+
messages: Array.isArray(pageSignals.messages)
|
|
1621
|
+
? pageSignals.messages.filter((value) => typeof value === 'string')
|
|
1622
|
+
: [],
|
|
1623
|
+
invalidFields: Array.isArray(pageSignals.invalidFields)
|
|
1624
|
+
? pageSignals.invalidFields.filter((value) => typeof value === 'string')
|
|
1625
|
+
: [],
|
|
1626
|
+
blockingOverlays: Array.isArray(pageSignals.blockingOverlays)
|
|
1627
|
+
? pageSignals.blockingOverlays.filter((value) => typeof value === 'string')
|
|
1628
|
+
: [],
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
export async function diagnoseNoObservableProgress(page, locator) {
|
|
1632
|
+
const targetState = await locator
|
|
1633
|
+
.evaluate((element) => {
|
|
1634
|
+
if (!(element instanceof HTMLElement)) {
|
|
1635
|
+
return {};
|
|
1636
|
+
}
|
|
1637
|
+
const readonly = element.hasAttribute('readonly') ||
|
|
1638
|
+
element.getAttribute('aria-readonly') === 'true' ||
|
|
1639
|
+
('readOnly' in element &&
|
|
1640
|
+
typeof element.readOnly === 'boolean' &&
|
|
1641
|
+
element.readOnly);
|
|
1642
|
+
return {
|
|
1643
|
+
disabled: Boolean(element.disabled),
|
|
1644
|
+
ariaDisabled: element.getAttribute('aria-disabled') === 'true',
|
|
1645
|
+
readonly,
|
|
1646
|
+
centerHitSelf: (() => {
|
|
1647
|
+
const rect = element.getBoundingClientRect();
|
|
1648
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
const centerX = rect.left + rect.width / 2;
|
|
1652
|
+
const centerY = rect.top + rect.height / 2;
|
|
1653
|
+
const hit = element.ownerDocument?.elementFromPoint(centerX, centerY);
|
|
1654
|
+
return Boolean(hit &&
|
|
1655
|
+
(hit === element ||
|
|
1656
|
+
element.contains(hit) ||
|
|
1657
|
+
(hit instanceof HTMLElement && hit.shadowRoot?.contains(element))));
|
|
1658
|
+
})(),
|
|
1659
|
+
};
|
|
1660
|
+
})
|
|
1661
|
+
.catch(() => ({}));
|
|
1662
|
+
const aggregatedSignals = await readNoProgressPageSignals(page);
|
|
1663
|
+
await walkChildFrames(page, async (frame) => {
|
|
1664
|
+
const frameSignals = await readNoProgressPageSignals(frame);
|
|
1665
|
+
mergeUniqueObservationTexts(aggregatedSignals.messages, frameSignals.messages, NO_PROGRESS_SIGNAL_LIMIT);
|
|
1666
|
+
mergeUniqueObservationTexts(aggregatedSignals.invalidFields, frameSignals.invalidFields, NO_PROGRESS_SIGNAL_LIMIT);
|
|
1667
|
+
mergeUniqueObservationTexts(aggregatedSignals.blockingOverlays, frameSignals.blockingOverlays, NO_PROGRESS_OVERLAY_LIMIT);
|
|
1668
|
+
});
|
|
1669
|
+
const messages = aggregatedSignals.messages;
|
|
1670
|
+
const invalidFields = aggregatedSignals.invalidFields;
|
|
1671
|
+
const rawTargetState = targetState && typeof targetState === 'object' && !Array.isArray(targetState)
|
|
1672
|
+
? targetState
|
|
1673
|
+
: undefined;
|
|
1674
|
+
const disabled = rawTargetState && (Boolean(rawTargetState.disabled) || Boolean(rawTargetState.ariaDisabled));
|
|
1675
|
+
const readonly = rawTargetState ? Boolean(rawTargetState.readonly) : false;
|
|
1676
|
+
const normalizedTargetState = disabled || readonly
|
|
1677
|
+
? {
|
|
1678
|
+
...(disabled ? { disabled: true } : {}),
|
|
1679
|
+
...(readonly ? { readonly: true } : {}),
|
|
1680
|
+
}
|
|
1681
|
+
: undefined;
|
|
1682
|
+
return {
|
|
1683
|
+
visibleMessages: messages,
|
|
1684
|
+
invalidFields,
|
|
1685
|
+
...(normalizedTargetState ? { targetState: normalizedTargetState } : {}),
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
async function resolveTargetLocatorForRead(page, target, surface, action = 'click') {
|
|
1689
|
+
const surfaceRoot = await resolveSurfaceScopeRoot(page, surface);
|
|
1690
|
+
const baseRoot = resolveLocatorRoot(page, target.framePath ?? surface?.framePath);
|
|
1691
|
+
const defaultRoot = surfaceRoot ?? baseRoot;
|
|
1692
|
+
for (const candidate of rankLocatorCandidates(target.locatorCandidates, action)) {
|
|
1693
|
+
const locatorRoot = candidate.scope === 'root'
|
|
1694
|
+
? baseRoot
|
|
1695
|
+
: candidate.scope === 'surface'
|
|
1696
|
+
? surfaceRoot
|
|
1697
|
+
: defaultRoot;
|
|
1698
|
+
if (!locatorRoot) {
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
const locator = buildLocator(locatorRoot, candidate);
|
|
1702
|
+
if (!locator)
|
|
1703
|
+
continue;
|
|
1704
|
+
const count = await locator.count().catch(() => 0);
|
|
1705
|
+
if (count === 0)
|
|
1706
|
+
continue;
|
|
1707
|
+
const first = locator.first();
|
|
1708
|
+
const prepared = await prepareReadLocator(first, target, action);
|
|
1709
|
+
if (!prepared)
|
|
1710
|
+
continue;
|
|
1711
|
+
return prepared;
|
|
1712
|
+
}
|
|
1713
|
+
const descendantSelector = recoveryDescendantSelector(target, action);
|
|
1714
|
+
if (!surfaceRoot || !descendantSelector) {
|
|
1715
|
+
return null;
|
|
1716
|
+
}
|
|
1717
|
+
const descendants = surfaceRoot.locator(descendantSelector);
|
|
1718
|
+
const descendantCount = await descendants.count().catch(() => 0);
|
|
1719
|
+
const visibleDescendants = [];
|
|
1720
|
+
for (let index = 0; index < descendantCount; index += 1) {
|
|
1721
|
+
const descendant = descendants.nth(index);
|
|
1722
|
+
const visible = await isLocatorUserActionable(descendant);
|
|
1723
|
+
if (!visible) {
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
if (action === 'fill' || action === 'type') {
|
|
1727
|
+
const editable = await descendant.isEditable().catch(() => false);
|
|
1728
|
+
if (!editable) {
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
visibleDescendants.push(descendant);
|
|
1733
|
+
}
|
|
1734
|
+
return visibleDescendants.length === 1 ? (visibleDescendants[0] ?? null) : null;
|
|
1735
|
+
}
|
|
1736
|
+
export async function createAcceptanceProbe(args) {
|
|
1737
|
+
const { session, page, target, action, actionValue, locator, beforePageObservation } = args;
|
|
1738
|
+
const policy = acceptancePolicyForAction(target, action);
|
|
1739
|
+
if (!policy) {
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
const trackedStates = shouldVerifyObservableProgress(target, action)
|
|
1743
|
+
? trackedStateKeys(target, action, policy)
|
|
1744
|
+
: [];
|
|
1745
|
+
if ((action === 'click' || action === 'press') &&
|
|
1746
|
+
policy === 'generic-click' &&
|
|
1747
|
+
isEditableLikeTarget(target) &&
|
|
1748
|
+
!trackedStates.includes('focused')) {
|
|
1749
|
+
trackedStates.push('focused');
|
|
1750
|
+
}
|
|
1751
|
+
const beforeLocatorObservation = trackedStates.length > 0 ? await captureLocatorState(locator, trackedStates) : null;
|
|
1752
|
+
const beforeContextHash = policy === 'value-change' || policy === 'submit'
|
|
1753
|
+
? null
|
|
1754
|
+
: await captureLocatorContextHash(locator);
|
|
1755
|
+
const surface = target.surfaceRef ? getSurface(session, target.surfaceRef) : null;
|
|
1756
|
+
const surfaceLocator = surface ? await resolveSurfaceScopeRoot(page, surface) : null;
|
|
1757
|
+
const pageReadLocator = await resolveTargetLocatorForRead(page, target, surface, action);
|
|
1758
|
+
const readLocator = pageReadLocator ?? locator;
|
|
1759
|
+
const readLocators = pageReadLocator && pageReadLocator !== locator ? [locator, pageReadLocator] : [locator];
|
|
1760
|
+
const beforeReadLocatorObservation = trackedStates.length > 0
|
|
1761
|
+
? await captureLocatorStateFromCandidates([readLocator], trackedStates)
|
|
1762
|
+
: null;
|
|
1763
|
+
const beforeReadContextHash = policy === 'value-change' || policy === 'submit'
|
|
1764
|
+
? null
|
|
1765
|
+
: await captureLocatorContextHashFromCandidates([readLocator]);
|
|
1766
|
+
const beforeValue = await captureLocatorValueFromCandidates(readLocators);
|
|
1767
|
+
const ownerTarget = target.ownerRef ? getTarget(session, target.ownerRef) : null;
|
|
1768
|
+
const ownerSurface = ownerTarget?.surfaceRef ? getSurface(session, ownerTarget.surfaceRef) : null;
|
|
1769
|
+
const ownerLocator = ownerTarget
|
|
1770
|
+
? await resolveTargetLocatorForRead(page, ownerTarget, ownerSurface, action)
|
|
1771
|
+
: null;
|
|
1772
|
+
const beforeOwnerValue = ownerLocator && (policy === 'selection' || policy === 'date-selection')
|
|
1773
|
+
? await captureLocatorValue(ownerLocator)
|
|
1774
|
+
: null;
|
|
1775
|
+
const beforeSurfaceContextHash = surfaceLocator &&
|
|
1776
|
+
((target.structure?.family === 'structured-grid' &&
|
|
1777
|
+
(policy === 'selection' || policy === 'date-selection')) ||
|
|
1778
|
+
policy === 'submit')
|
|
1779
|
+
? await captureLocatorContextHash(surfaceLocator)
|
|
1780
|
+
: null;
|
|
1781
|
+
const beforeFollowUpSurfaceHash = policy === 'submit' ? await captureSubmitFollowUpSurfaceHash(page) : null;
|
|
1782
|
+
const comparableValueType = comparableValueTypeForTarget(target);
|
|
1783
|
+
return {
|
|
1784
|
+
policy,
|
|
1785
|
+
page,
|
|
1786
|
+
target,
|
|
1787
|
+
action,
|
|
1788
|
+
surface,
|
|
1789
|
+
ownerTarget,
|
|
1790
|
+
ownerSurface,
|
|
1791
|
+
beforePage: beforePageObservation,
|
|
1792
|
+
beforeLocator: beforeLocatorObservation,
|
|
1793
|
+
beforeContextHash,
|
|
1794
|
+
beforeReadLocator: beforeReadLocatorObservation,
|
|
1795
|
+
beforeReadContextHash,
|
|
1796
|
+
trackedStateKeys: trackedStates,
|
|
1797
|
+
locator,
|
|
1798
|
+
readLocator,
|
|
1799
|
+
readLocators,
|
|
1800
|
+
surfaceLocator,
|
|
1801
|
+
expectedValue: expectedValueForAcceptance(action, actionValue),
|
|
1802
|
+
beforeValue,
|
|
1803
|
+
comparableValueType,
|
|
1804
|
+
ownerLocator,
|
|
1805
|
+
beforeOwnerValue,
|
|
1806
|
+
beforeSurfaceContextHash,
|
|
1807
|
+
beforeFollowUpSurfaceHash,
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
export async function evaluateAcceptanceProbe(probe, afterPageObservation) {
|
|
1811
|
+
const liveReadLocator = (await resolveTargetLocatorForRead(probe.page, probe.target, probe.surface, probe.action).catch(() => null)) ?? probe.readLocator;
|
|
1812
|
+
const liveReadLocators = liveReadLocator && liveReadLocator !== probe.locator
|
|
1813
|
+
? [probe.locator, liveReadLocator]
|
|
1814
|
+
: [probe.locator];
|
|
1815
|
+
const liveOwnerLocator = probe.ownerTarget
|
|
1816
|
+
? ((await resolveTargetLocatorForRead(probe.page, probe.ownerTarget, probe.ownerSurface, probe.action).catch(() => null)) ?? probe.ownerLocator)
|
|
1817
|
+
: null;
|
|
1818
|
+
const liveSurfaceLocator = probe.target.structure?.family === 'structured-grid' && probe.surface
|
|
1819
|
+
? ((await resolveSurfaceScopeRoot(probe.page, probe.surface).catch(() => null)) ??
|
|
1820
|
+
probe.surfaceLocator)
|
|
1821
|
+
: probe.surfaceLocator;
|
|
1822
|
+
const afterLocatorObservation = probe.trackedStateKeys.length > 0
|
|
1823
|
+
? await captureLocatorState(probe.locator, probe.trackedStateKeys)
|
|
1824
|
+
: null;
|
|
1825
|
+
const afterContextHash = probe.policy === 'value-change' || probe.policy === 'submit'
|
|
1826
|
+
? probe.beforeContextHash
|
|
1827
|
+
: await captureLocatorContextHash(probe.locator);
|
|
1828
|
+
const afterReadLocatorObservation = probe.trackedStateKeys.length > 0
|
|
1829
|
+
? await captureLocatorStateFromCandidates([liveReadLocator], probe.trackedStateKeys)
|
|
1830
|
+
: null;
|
|
1831
|
+
const afterReadContextHash = probe.policy === 'value-change' || probe.policy === 'submit'
|
|
1832
|
+
? probe.beforeReadContextHash
|
|
1833
|
+
: await captureLocatorContextHashFromCandidates([liveReadLocator]);
|
|
1834
|
+
const afterValue = await captureLocatorValueFromCandidates(liveReadLocators);
|
|
1835
|
+
const afterOwnerValue = liveOwnerLocator ? await captureLocatorValue(liveOwnerLocator) : null;
|
|
1836
|
+
const afterSurfaceContextHash = liveSurfaceLocator &&
|
|
1837
|
+
probe.target.structure?.family === 'structured-grid' &&
|
|
1838
|
+
(probe.policy === 'selection' || probe.policy === 'date-selection')
|
|
1839
|
+
? await captureLocatorContextHash(liveSurfaceLocator)
|
|
1840
|
+
: probe.beforeSurfaceContextHash;
|
|
1841
|
+
const targetValueChanged = probe.expectedValue === null && valueMeaningfullyChanged(probe.beforeValue, afterValue);
|
|
1842
|
+
const ownerValueChanged = probe.expectedValue === null &&
|
|
1843
|
+
valueMeaningfullyChanged(probe.beforeOwnerValue, afterOwnerValue);
|
|
1844
|
+
const surfaceContextChanged = probe.expectedValue === null &&
|
|
1845
|
+
probe.beforeSurfaceContextHash !== null &&
|
|
1846
|
+
afterSurfaceContextHash !== null &&
|
|
1847
|
+
probe.beforeSurfaceContextHash !== afterSurfaceContextHash;
|
|
1848
|
+
switch (probe.policy) {
|
|
1849
|
+
case 'value-change':
|
|
1850
|
+
return valuesMatchExpected(probe.expectedValue, afterValue, probe.comparableValueType);
|
|
1851
|
+
case 'selection': {
|
|
1852
|
+
const afterComparableValues = await captureLocatorComparableValuesFromCandidates(liveReadLocators);
|
|
1853
|
+
const afterOwnerComparableValues = liveOwnerLocator
|
|
1854
|
+
? await captureLocatorComparableValuesFromCandidates([liveOwnerLocator])
|
|
1855
|
+
: [];
|
|
1856
|
+
if (probe.expectedValue !== null) {
|
|
1857
|
+
return (valuesMatchAnyExpected(probe.expectedValue, afterComparableValues, probe.comparableValueType, { allowCompactActualSequence: true }) ||
|
|
1858
|
+
valuesMatchAnyExpected(probe.expectedValue, afterOwnerComparableValues, probe.comparableValueType, { allowCompactActualSequence: true }));
|
|
1859
|
+
}
|
|
1860
|
+
return (targetValueChanged ||
|
|
1861
|
+
ownerValueChanged ||
|
|
1862
|
+
surfaceContextChanged ||
|
|
1863
|
+
locatorStateChanged(probe.beforeLocator, afterLocatorObservation) ||
|
|
1864
|
+
locatorStateChanged(probe.beforeReadLocator, afterReadLocatorObservation) ||
|
|
1865
|
+
probe.beforeContextHash !== afterContextHash ||
|
|
1866
|
+
probe.beforeReadContextHash !== afterReadContextHash);
|
|
1867
|
+
}
|
|
1868
|
+
case 'toggle':
|
|
1869
|
+
return (locatorStateChanged(probe.beforeLocator, afterLocatorObservation) ||
|
|
1870
|
+
locatorStateChanged(probe.beforeReadLocator, afterReadLocatorObservation));
|
|
1871
|
+
case 'disclosure':
|
|
1872
|
+
return (targetValueChanged ||
|
|
1873
|
+
locatorStateChanged(probe.beforeLocator, afterLocatorObservation) ||
|
|
1874
|
+
locatorStateChanged(probe.beforeReadLocator, afterReadLocatorObservation) ||
|
|
1875
|
+
probe.beforeContextHash !== afterContextHash ||
|
|
1876
|
+
probe.beforeReadContextHash !== afterReadContextHash);
|
|
1877
|
+
case 'date-selection': {
|
|
1878
|
+
const afterDateComparableValues = await captureLocatorComparableValuesFromCandidates(liveReadLocators);
|
|
1879
|
+
const afterDateOwnerComparableValues = liveOwnerLocator
|
|
1880
|
+
? await captureLocatorComparableValuesFromCandidates([liveOwnerLocator])
|
|
1881
|
+
: [];
|
|
1882
|
+
const explicitDateMatched = probe.expectedValue !== null &&
|
|
1883
|
+
(valuesMatchAnyExpected(probe.expectedValue, afterDateComparableValues, probe.comparableValueType) ||
|
|
1884
|
+
valuesMatchAnyExpected(probe.expectedValue, afterDateOwnerComparableValues, probe.comparableValueType));
|
|
1885
|
+
return (explicitDateMatched ||
|
|
1886
|
+
locatorStateChanged(probe.beforeLocator, afterLocatorObservation) ||
|
|
1887
|
+
locatorStateChanged(probe.beforeReadLocator, afterReadLocatorObservation) ||
|
|
1888
|
+
targetValueChanged ||
|
|
1889
|
+
ownerValueChanged ||
|
|
1890
|
+
surfaceContextChanged ||
|
|
1891
|
+
(probe.expectedValue === null &&
|
|
1892
|
+
(probe.beforeContextHash !== afterContextHash ||
|
|
1893
|
+
probe.beforeReadContextHash !== afterReadContextHash)));
|
|
1894
|
+
}
|
|
1895
|
+
case 'submit':
|
|
1896
|
+
return submitObservationChanged(probe.beforePage, afterPageObservation);
|
|
1897
|
+
case 'navigation':
|
|
1898
|
+
return (targetValueChanged ||
|
|
1899
|
+
pageObservationChanged(probe.beforePage, afterPageObservation) ||
|
|
1900
|
+
probe.beforeContextHash !== afterContextHash ||
|
|
1901
|
+
probe.beforeReadContextHash !== afterReadContextHash);
|
|
1902
|
+
case 'generic-click':
|
|
1903
|
+
return (targetValueChanged ||
|
|
1904
|
+
locatorStateChanged(probe.beforeLocator, afterLocatorObservation) ||
|
|
1905
|
+
locatorStateChanged(probe.beforeReadLocator, afterReadLocatorObservation) ||
|
|
1906
|
+
probe.beforeContextHash !== afterContextHash ||
|
|
1907
|
+
probe.beforeReadContextHash !== afterReadContextHash ||
|
|
1908
|
+
genericClickObservationChanged(probe.beforePage, afterPageObservation));
|
|
1909
|
+
}
|
|
1910
|
+
return false;
|
|
1911
|
+
}
|
|
1912
|
+
export async function waitForAcceptanceProbe(probe, options) {
|
|
1913
|
+
const timeoutMs = options?.timeoutMs ?? ACCEPTANCE_POLL_TIMEOUT_MS;
|
|
1914
|
+
const intervalMs = options?.intervalMs ?? ACCEPTANCE_POLL_INTERVAL_MS;
|
|
1915
|
+
const startedAt = Date.now();
|
|
1916
|
+
let polls = 0;
|
|
1917
|
+
let afterPageObservation = probe.beforePage ? await capturePageObservation(probe.page) : null;
|
|
1918
|
+
while (true) {
|
|
1919
|
+
polls += 1;
|
|
1920
|
+
const accepted = await evaluateAcceptanceProbe(probe, afterPageObservation);
|
|
1921
|
+
if (accepted) {
|
|
1922
|
+
return {
|
|
1923
|
+
accepted: true,
|
|
1924
|
+
afterPageObservation,
|
|
1925
|
+
polls,
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
1929
|
+
return {
|
|
1930
|
+
accepted: false,
|
|
1931
|
+
afterPageObservation,
|
|
1932
|
+
polls,
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
await sleep(intervalMs);
|
|
1936
|
+
afterPageObservation = probe.beforePage ? await capturePageObservation(probe.page) : null;
|
|
1937
|
+
}
|
|
1938
|
+
}
|