@testmuai/playwright-bindings 0.1.26 → 0.1.27-beta.1
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/dist/actionEngine.d.ts +37 -0
- package/dist/actionEngine.d.ts.map +1 -0
- package/dist/actionEngine.js +194 -0
- package/dist/actionEngine.js.map +1 -0
- package/dist/actionSpecs.d.ts +35 -0
- package/dist/actionSpecs.d.ts.map +1 -0
- package/dist/actionSpecs.js +354 -0
- package/dist/actionSpecs.js.map +1 -0
- package/dist/capability.d.ts +3 -5
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +66 -6
- package/dist/capability.js.map +1 -1
- package/dist/configure.d.ts +16 -0
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +14 -0
- package/dist/configure.js.map +1 -1
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -1
- package/dist/healCache.d.ts +28 -0
- package/dist/healCache.d.ts.map +1 -0
- package/dist/healCache.js +17 -0
- package/dist/healCache.js.map +1 -0
- package/dist/healPatch.d.ts.map +1 -1
- package/dist/healPatch.js +45 -3
- package/dist/healPatch.js.map +1 -1
- package/dist/helpers/assertion.d.ts.map +1 -1
- package/dist/helpers/assertion.js +18 -0
- package/dist/helpers/assertion.js.map +1 -1
- package/dist/helpers/assertionV3.d.ts +67 -0
- package/dist/helpers/assertionV3.d.ts.map +1 -0
- package/dist/helpers/assertionV3.js +384 -0
- package/dist/helpers/assertionV3.js.map +1 -0
- package/dist/helpers/condition.d.ts +84 -0
- package/dist/helpers/condition.d.ts.map +1 -0
- package/dist/helpers/condition.js +292 -0
- package/dist/helpers/condition.js.map +1 -0
- package/dist/helpers/cookies.d.ts +12 -0
- package/dist/helpers/cookies.d.ts.map +1 -0
- package/dist/helpers/cookies.js +66 -0
- package/dist/helpers/cookies.js.map +1 -0
- package/dist/helpers/coordinateResolver.d.ts +29 -0
- package/dist/helpers/coordinateResolver.d.ts.map +1 -0
- package/dist/helpers/coordinateResolver.js +118 -0
- package/dist/helpers/coordinateResolver.js.map +1 -0
- package/dist/helpers/coordinateResolverRead.d.ts +36 -0
- package/dist/helpers/coordinateResolverRead.d.ts.map +1 -0
- package/dist/helpers/coordinateResolverRead.js +107 -0
- package/dist/helpers/coordinateResolverRead.js.map +1 -0
- package/dist/helpers/dialog.d.ts +29 -0
- package/dist/helpers/dialog.d.ts.map +1 -0
- package/dist/helpers/dialog.js +35 -0
- package/dist/helpers/dialog.js.map +1 -0
- package/dist/helpers/drag.d.ts +36 -1
- package/dist/helpers/drag.d.ts.map +1 -1
- package/dist/helpers/drag.js +267 -0
- package/dist/helpers/drag.js.map +1 -1
- package/dist/helpers/executeApi.d.ts +2 -0
- package/dist/helpers/executeApi.d.ts.map +1 -1
- package/dist/helpers/executeApi.js +30 -10
- package/dist/helpers/executeApi.js.map +1 -1
- package/dist/helpers/executeJs.d.ts.map +1 -1
- package/dist/helpers/executeJs.js +46 -3
- package/dist/helpers/executeJs.js.map +1 -1
- package/dist/helpers/gesture.d.ts +45 -0
- package/dist/helpers/gesture.d.ts.map +1 -0
- package/dist/helpers/gesture.js +131 -0
- package/dist/helpers/gesture.js.map +1 -0
- package/dist/helpers/healCascade.d.ts +57 -0
- package/dist/helpers/healCascade.d.ts.map +1 -0
- package/dist/helpers/healCascade.js +363 -0
- package/dist/helpers/healCascade.js.map +1 -0
- package/dist/helpers/png.d.ts +3 -0
- package/dist/helpers/png.d.ts.map +1 -0
- package/dist/helpers/png.js +18 -0
- package/dist/helpers/png.js.map +1 -0
- package/dist/helpers/screenshot.d.ts +27 -0
- package/dist/helpers/screenshot.d.ts.map +1 -1
- package/dist/helpers/screenshot.js +56 -0
- package/dist/helpers/screenshot.js.map +1 -1
- package/dist/helpers/scrollUntil.d.ts +13 -0
- package/dist/helpers/scrollUntil.d.ts.map +1 -0
- package/dist/helpers/scrollUntil.js +88 -0
- package/dist/helpers/scrollUntil.js.map +1 -0
- package/dist/helpers/tabs.d.ts +42 -1
- package/dist/helpers/tabs.d.ts.map +1 -1
- package/dist/helpers/tabs.js +259 -5
- package/dist/helpers/tabs.js.map +1 -1
- package/dist/helpers/text.d.ts +15 -0
- package/dist/helpers/text.d.ts.map +1 -0
- package/dist/helpers/text.js +24 -0
- package/dist/helpers/text.js.map +1 -0
- package/dist/helpers/textNormalize.d.ts +18 -0
- package/dist/helpers/textNormalize.d.ts.map +1 -0
- package/dist/helpers/textNormalize.js +39 -0
- package/dist/helpers/textNormalize.js.map +1 -0
- package/dist/helpers/textualRead.d.ts +37 -0
- package/dist/helpers/textualRead.d.ts.map +1 -0
- package/dist/helpers/textualRead.js +211 -0
- package/dist/helpers/textualRead.js.map +1 -0
- package/dist/helpers/vision.d.ts +2 -0
- package/dist/helpers/vision.d.ts.map +1 -1
- package/dist/helpers/vision.js +11 -0
- package/dist/helpers/vision.js.map +1 -1
- package/dist/helpers/visionV3.d.ts +38 -0
- package/dist/helpers/visionV3.d.ts.map +1 -0
- package/dist/helpers/visionV3.js +291 -0
- package/dist/helpers/visionV3.js.map +1 -0
- package/dist/helpers/wait.d.ts +14 -1
- package/dist/helpers/wait.d.ts.map +1 -1
- package/dist/helpers/wait.js +25 -0
- package/dist/helpers/wait.js.map +1 -1
- package/dist/index.d.ts +29 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +4 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +8 -3
- package/dist/log.js.map +1 -1
- package/dist/rankedLocator.d.ts +27 -0
- package/dist/rankedLocator.d.ts.map +1 -0
- package/dist/rankedLocator.js +85 -0
- package/dist/rankedLocator.js.map +1 -0
- package/dist/reporters/index.d.ts +2 -0
- package/dist/reporters/index.d.ts.map +1 -1
- package/dist/reporters/index.js.map +1 -1
- package/dist/reporters/lt.d.ts.map +1 -1
- package/dist/reporters/lt.js +7 -1
- package/dist/reporters/lt.js.map +1 -1
- package/dist/routeFailure.d.ts +9 -0
- package/dist/routeFailure.d.ts.map +1 -0
- package/dist/routeFailure.js +34 -0
- package/dist/routeFailure.js.map +1 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +21 -0
- package/dist/session.js.map +1 -1
- package/dist/step.d.ts +10 -0
- package/dist/step.d.ts.map +1 -1
- package/dist/step.js +14 -1
- package/dist/step.js.map +1 -1
- package/dist/testConfig.d.ts.map +1 -1
- package/dist/testConfig.js +8 -1
- package/dist/testConfig.js.map +1 -1
- package/dist/vars.d.ts +10 -0
- package/dist/vars.d.ts.map +1 -1
- package/dist/vars.js +30 -3
- package/dist/vars.js.map +1 -1
- package/dist/visionLocator.d.ts +53 -0
- package/dist/visionLocator.d.ts.map +1 -0
- package/dist/visionLocator.js +117 -0
- package/dist/visionLocator.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find → act → heal loop for Playwright verbs — port of PY _action_engine.py.
|
|
3
|
+
*
|
|
4
|
+
* Entry: runVerb(page, locator, spec, args, opts). Callers:
|
|
5
|
+
* - VisionLocator methods (empty-selector vision-agent ops; locator=null)
|
|
6
|
+
* - the Locator heal-patch V3 delegation (locator = the original)
|
|
7
|
+
*
|
|
8
|
+
* Per attempt: (1) try the runner on the locator; recoverable failure falls
|
|
9
|
+
* to (2) the heal cascade (per-verb tiers); (3) selectorXpath → rebuild
|
|
10
|
+
* locator and retry, keeping coordinates as the in-round fallback; (4)
|
|
11
|
+
* coordinates-only → coordRunner. The final attempt propagates its own
|
|
12
|
+
* failure (PY :245); a full-cascade miss propagates AutohealExhausted
|
|
13
|
+
* (honouring codegen fallback_coordinates first, spec §3.4).
|
|
14
|
+
*/
|
|
15
|
+
import type { Locator, Page } from 'playwright';
|
|
16
|
+
import { type VerbSpec } from './actionSpecs.js';
|
|
17
|
+
export declare function inEngine(): boolean;
|
|
18
|
+
/** Pin the right Playwright engine for a heal-derived selector: an absolute
|
|
19
|
+
* xpath ('/html[1]/...') or union '(//a|//b)[1]' parses as CSS and throws
|
|
20
|
+
* unless prefixed; engine-pinned and genuine CSS pass through. */
|
|
21
|
+
export declare function engineSelector(selector: string): string;
|
|
22
|
+
export interface RunVerbOptions {
|
|
23
|
+
description: string;
|
|
24
|
+
verbName: string;
|
|
25
|
+
autoheal?: boolean;
|
|
26
|
+
maxAttempts?: number;
|
|
27
|
+
retryDelayMs?: number;
|
|
28
|
+
/** Authoring-time coords persisted by codegen for selectorless ops —
|
|
29
|
+
* consumed ONLY when the cascade exhausts (spec §3.4). */
|
|
30
|
+
fallbackCoordinates?: [number, number] | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Execute the verb spec on the locator (or via heal when locator is null),
|
|
34
|
+
* retrying with the heal cascade on recoverable failures.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runVerb(page: Page, locator: Locator | null, spec: VerbSpec, args: unknown[], opts: RunVerbOptions): Promise<unknown>;
|
|
37
|
+
//# sourceMappingURL=actionEngine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actionEngine.d.ts","sourceRoot":"","sources":["../src/actionEngine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EAA6B,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAU5E,wBAAgB,QAAQ,IAAI,OAAO,CAElC;AA8CD;;kEAEkE;AAClE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;8DAC0D;IAC1D,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC/C;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,OAAO,GAAG,IAAI,EACvB,IAAI,EAAE,QAAQ,EACd,IAAI,EAAE,OAAO,EAAE,EACf,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,OAAO,CAAC,CAQlB"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { AutohealExhausted } from './errors.js';
|
|
2
|
+
import { healCascade } from './helpers/healCascade.js';
|
|
3
|
+
import { DEFAULT_RECOVERABLE_NAMES } from './actionSpecs.js';
|
|
4
|
+
import { log } from './log.js';
|
|
5
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
6
|
+
// Re-entry guard: set while the engine drives Locator methods so the heal
|
|
7
|
+
// patch's wrapper passes calls straight through instead of re-entering the
|
|
8
|
+
// cascade. Module-level is correct — session.run() executes tests
|
|
9
|
+
// sequentially (see the contract's runtime-conventions block).
|
|
10
|
+
let _inEngine = false;
|
|
11
|
+
export function inEngine() {
|
|
12
|
+
return _inEngine;
|
|
13
|
+
}
|
|
14
|
+
/** Recoverable = heal-triggering. Match by `.name` (contract rule — a nested
|
|
15
|
+
* playwright install makes instanceof silently false) plus Playwright's
|
|
16
|
+
* strict-mode violation, which is a plain Error with a message signature. */
|
|
17
|
+
function isRecoverable(e, names) {
|
|
18
|
+
const err = e;
|
|
19
|
+
if (!err)
|
|
20
|
+
return false;
|
|
21
|
+
if (names.includes(err.name))
|
|
22
|
+
return true;
|
|
23
|
+
return typeof err.message === 'string' && err.message.includes('strict mode violation');
|
|
24
|
+
}
|
|
25
|
+
/** Recover the owning Page when codegen hands a frame/shadow scope where a
|
|
26
|
+
* Page is required (screenshots, page.mouse, isClosed). Locator → .page();
|
|
27
|
+
* FrameLocator → .owner().page(). Fall back to the original so the caller's
|
|
28
|
+
* error surfaces unchanged. */
|
|
29
|
+
function coerceToPage(page) {
|
|
30
|
+
const p = page;
|
|
31
|
+
if (typeof p['isClosed'] === 'function')
|
|
32
|
+
return page;
|
|
33
|
+
try {
|
|
34
|
+
if (typeof p['page'] === 'function') {
|
|
35
|
+
const rec = p['page']();
|
|
36
|
+
if (rec && typeof rec['isClosed'] === 'function')
|
|
37
|
+
return rec;
|
|
38
|
+
}
|
|
39
|
+
if (typeof p['owner'] === 'function') {
|
|
40
|
+
const owner = p['owner']();
|
|
41
|
+
if (owner && typeof owner['page'] === 'function') {
|
|
42
|
+
const rec = owner['page']();
|
|
43
|
+
if (rec && typeof rec['isClosed'] === 'function')
|
|
44
|
+
return rec;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { /* fall through */ }
|
|
49
|
+
return page;
|
|
50
|
+
}
|
|
51
|
+
/** A Playwright action error: TimeoutError, or a plain Error whose message
|
|
52
|
+
* carries the locator-method prefix / strict-mode signature. Matched by shape
|
|
53
|
+
* (never instanceof — nested playwright installs). */
|
|
54
|
+
function isPlaywrightActionError(e) {
|
|
55
|
+
const err = e;
|
|
56
|
+
if (!err || typeof err.message !== 'string')
|
|
57
|
+
return false;
|
|
58
|
+
if (err.name === 'TimeoutError')
|
|
59
|
+
return true;
|
|
60
|
+
if (err.name !== 'Error')
|
|
61
|
+
return false;
|
|
62
|
+
return /^\s*locator\.\w+/i.test(err.message) || err.message.includes('strict mode violation');
|
|
63
|
+
}
|
|
64
|
+
/** Pin the right Playwright engine for a heal-derived selector: an absolute
|
|
65
|
+
* xpath ('/html[1]/...') or union '(//a|//b)[1]' parses as CSS and throws
|
|
66
|
+
* unless prefixed; engine-pinned and genuine CSS pass through. */
|
|
67
|
+
export function engineSelector(selector) {
|
|
68
|
+
if (selector.startsWith('xpath=') || selector.startsWith('css='))
|
|
69
|
+
return selector;
|
|
70
|
+
if (selector.startsWith('/') || selector.startsWith('('))
|
|
71
|
+
return `xpath=${selector}`;
|
|
72
|
+
return selector;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Execute the verb spec on the locator (or via heal when locator is null),
|
|
76
|
+
* retrying with the heal cascade on recoverable failures.
|
|
77
|
+
*/
|
|
78
|
+
export async function runVerb(page, locator, spec, args, opts) {
|
|
79
|
+
const prevGuard = _inEngine;
|
|
80
|
+
_inEngine = true;
|
|
81
|
+
try {
|
|
82
|
+
return await runVerbInner(page, locator, spec, args, opts);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
_inEngine = prevGuard;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function runVerbInner(pageIn, locatorIn, spec, args, opts) {
|
|
89
|
+
const { description, verbName, autoheal = true, maxAttempts = 4, retryDelayMs = 500, fallbackCoordinates = null, } = opts;
|
|
90
|
+
const recoverable = spec.recoverableNames ?? DEFAULT_RECOVERABLE_NAMES;
|
|
91
|
+
let page = coerceToPage(pageIn);
|
|
92
|
+
let locator = locatorIn;
|
|
93
|
+
let firstExc = null;
|
|
94
|
+
// Stash for a heal round that returned BOTH selector and coordinates: when
|
|
95
|
+
// the xpath attempt fails, fall back to the same round's coordinates
|
|
96
|
+
// without a second cascade call (spec §3.3).
|
|
97
|
+
let pendingCoordFallback = null;
|
|
98
|
+
// Dead-page guard: re-point to the most-recent surviving page (Selenium
|
|
99
|
+
// dead-window heal parity) and null the stale locator.
|
|
100
|
+
if (page.isClosed()) {
|
|
101
|
+
const ctxPages = page.context().pages();
|
|
102
|
+
if (!ctxPages.length) {
|
|
103
|
+
throw new Error(`all pages are closed; cannot perform '${verbName}' on '${description}'`);
|
|
104
|
+
}
|
|
105
|
+
page = ctxPages[ctxPages.length - 1];
|
|
106
|
+
await page.bringToFront();
|
|
107
|
+
locator = null;
|
|
108
|
+
}
|
|
109
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
110
|
+
// Step 1 — try the verb on the current locator.
|
|
111
|
+
if (locator !== null) {
|
|
112
|
+
try {
|
|
113
|
+
return await spec.runner(locator, ...args);
|
|
114
|
+
}
|
|
115
|
+
catch (exc) {
|
|
116
|
+
// A Playwright ACTION error on a heal-derived selector (e.g. the
|
|
117
|
+
// resolver up-walked to a label -> 'Element is not an <input>') is a
|
|
118
|
+
// wrong-element symptom, not a fatal: the same round's coordinates
|
|
119
|
+
// still carry the verb (keyboard input works on any focusable).
|
|
120
|
+
if (pendingCoordFallback !== null && spec.coordRunner &&
|
|
121
|
+
isPlaywrightActionError(exc)) {
|
|
122
|
+
const [fx, fy] = pendingCoordFallback;
|
|
123
|
+
log.helper(`[engine] healed-selector action failed (${exc?.message?.slice(0, 60)}); coordinate fallback (${fx}, ${fy}) for ${verbName}`);
|
|
124
|
+
return await spec.coordRunner(page, fx, fy, ...args);
|
|
125
|
+
}
|
|
126
|
+
// Heal-triggering: the verb's recoverable set (timeouts/strict-mode)
|
|
127
|
+
// PLUS any Playwright ACTION error — an authored selector resolving to
|
|
128
|
+
// the WRONG element (e.g. the search <form> instead of its input)
|
|
129
|
+
// raises 'Element is not an <input>...' on the FIRST attempt, and the
|
|
130
|
+
// cascade can re-find the true target by description (grid-proven:
|
|
131
|
+
// bing sb_form fill). Non-Playwright errors still propagate untouched.
|
|
132
|
+
if (!isRecoverable(exc, recoverable) && !isPlaywrightActionError(exc))
|
|
133
|
+
throw exc;
|
|
134
|
+
if (firstExc === null)
|
|
135
|
+
firstExc = exc;
|
|
136
|
+
if (!autoheal)
|
|
137
|
+
throw exc;
|
|
138
|
+
// In-round coordinate fallback — checked BEFORE the final-attempt
|
|
139
|
+
// raise (Selenium parity: the stash fires even on the last attempt).
|
|
140
|
+
if (pendingCoordFallback !== null && spec.coordRunner) {
|
|
141
|
+
const [x, y] = pendingCoordFallback;
|
|
142
|
+
log.helper(`[engine] derived-xpath attempt failed (${exc?.name}); coordinate fallback (${x}, ${y}) for ${verbName}`);
|
|
143
|
+
return await spec.coordRunner(page, x, y, ...args);
|
|
144
|
+
}
|
|
145
|
+
if (attempt === maxAttempts - 1)
|
|
146
|
+
throw exc;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (!autoheal) {
|
|
150
|
+
throw new AutohealExhausted(`no locator and autoheal disabled for '${description}' (${verbName})`);
|
|
151
|
+
}
|
|
152
|
+
// Step 2 — heal cascade (fallback_coordinates honoured on exhaustion).
|
|
153
|
+
let heal;
|
|
154
|
+
try {
|
|
155
|
+
heal = await healCascade({
|
|
156
|
+
page, description, verbName,
|
|
157
|
+
...(spec.tiers ? { tiers: spec.tiers } : {}),
|
|
158
|
+
lastException: firstExc,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (healExc) {
|
|
162
|
+
if (healExc?.name === 'AutohealExhausted' &&
|
|
163
|
+
fallbackCoordinates !== null && spec.coordRunner) {
|
|
164
|
+
const [x, y] = fallbackCoordinates;
|
|
165
|
+
log.helper(`[engine] cascade exhausted; recorded fallback_coordinates (${x}, ${y}) for ${verbName}`);
|
|
166
|
+
return await spec.coordRunner(page, x, y, ...args);
|
|
167
|
+
}
|
|
168
|
+
throw healExc;
|
|
169
|
+
}
|
|
170
|
+
// Step 3 — selector-first dispatch.
|
|
171
|
+
if (heal.selectorXpath != null) {
|
|
172
|
+
locator = page.locator(engineSelector(heal.selectorXpath));
|
|
173
|
+
pendingCoordFallback = heal.coordinates ?? null; // fresh supersedes stale
|
|
174
|
+
log.helper(`[engine] heal via ${heal.tierUsed} -> selector ${JSON.stringify(heal.selectorXpath)}, retrying ${verbName}`);
|
|
175
|
+
if (retryDelayMs > 0)
|
|
176
|
+
await sleep(retryDelayMs);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (heal.coordinates != null) {
|
|
180
|
+
pendingCoordFallback = null;
|
|
181
|
+
if (!spec.coordRunner) {
|
|
182
|
+
throw new Error(`heal cascade resolved to coordinates ${JSON.stringify(heal.coordinates)} via tier '${heal.tierUsed}', but verb '${verbName}' has no coordRunner. Provide one on the VerbSpec or drop the coordinate tier.`);
|
|
183
|
+
}
|
|
184
|
+
const [x, y] = heal.coordinates;
|
|
185
|
+
log.helper(`[engine] heal via ${heal.tierUsed} -> coordRunner at (${x}, ${y}) for ${verbName}`);
|
|
186
|
+
return await spec.coordRunner(page, x, y, ...args);
|
|
187
|
+
}
|
|
188
|
+
throw new AutohealExhausted(`heal tier=${heal.tierUsed} returned no selector or coordinates for '${description}' (${verbName})`);
|
|
189
|
+
}
|
|
190
|
+
if (firstExc !== null)
|
|
191
|
+
throw firstExc;
|
|
192
|
+
throw new AutohealExhausted(`max_attempts=${maxAttempts} exhausted for '${description}' (${verbName})`);
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=actionEngine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actionEngine.js","sourceRoot":"","sources":["../src/actionEngine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-verb runner/coord_runner specs for the V3 find→act→heal loop.
|
|
3
|
+
* Port of PY _action_specs.py. Coord runners click at the healed coordinate
|
|
4
|
+
* first to focus the field, then drive the keyboard (Selenium ActionBuilder
|
|
5
|
+
* dance adapted to Playwright's mouse + keyboard APIs).
|
|
6
|
+
*/
|
|
7
|
+
import type { ElementHandle, Locator, Page } from 'playwright';
|
|
8
|
+
export type VerbKwargs = Record<string, unknown>;
|
|
9
|
+
export type Runner = (locator: Locator, ...args: unknown[]) => Promise<unknown>;
|
|
10
|
+
export type CoordRunner = (page: Page, x: number, y: number, ...args: unknown[]) => Promise<unknown>;
|
|
11
|
+
/** Per-verb behaviour for the find→act→heal loop. `recoverableNames` are the
|
|
12
|
+
* error `.name`s that trigger heal (the contract's .name rule — instanceof is
|
|
13
|
+
* unsafe across nested playwright installs). `tiers` overrides the cascade. */
|
|
14
|
+
export interface VerbSpec {
|
|
15
|
+
runner: Runner;
|
|
16
|
+
coordRunner?: CoordRunner;
|
|
17
|
+
recoverableNames?: readonly string[];
|
|
18
|
+
tiers?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
export declare const DEFAULT_RECOVERABLE_NAMES: readonly string[];
|
|
21
|
+
export declare const CLICK_SPEC: VerbSpec;
|
|
22
|
+
export declare const DBLCLICK_SPEC: VerbSpec;
|
|
23
|
+
export declare const HOVER_SPEC: VerbSpec;
|
|
24
|
+
export declare const LONG_PRESS_SPEC: VerbSpec;
|
|
25
|
+
export declare const MULTI_CLICK_SPEC: VerbSpec;
|
|
26
|
+
export declare const CLEAR_SPEC: VerbSpec;
|
|
27
|
+
export declare const FILL_SPEC: VerbSpec;
|
|
28
|
+
export declare const TYPE_SPEC: VerbSpec;
|
|
29
|
+
export declare const PRESS_SPEC: VerbSpec;
|
|
30
|
+
/** First <input type=file> via pure DOM (light + open shadow). Never raises. */
|
|
31
|
+
export declare function resolveFileInputDom(page: Page): Promise<ElementHandle | null>;
|
|
32
|
+
export declare const SET_INPUT_FILES_SPEC: VerbSpec;
|
|
33
|
+
export declare const SCROLL_INTO_VIEW_SPEC: VerbSpec;
|
|
34
|
+
export declare function getVerbSpec(verbName: string): VerbSpec | null;
|
|
35
|
+
//# sourceMappingURL=actionSpecs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actionSpecs.d.ts","sourceRoot":"","sources":["../src/actionSpecs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAM/D,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACjD,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAChF,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAErG;;+EAE+E;AAC/E,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AAED,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAAqB,CAAC;AA0D7E,eAAO,MAAM,UAAU,EAAE,QAkBxB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,QAc3B,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,QAUxB,CAAC;AAKF,eAAO,MAAM,eAAe,EAAE,QAgB7B,CAAC;AAIF,eAAO,MAAM,gBAAgB,EAAE,QAmB9B,CAAC;AAIF,eAAO,MAAM,UAAU,EAAE,QAOxB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,QAUvB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,QASvB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,QASxB,CAAC;AAoBF,gFAAgF;AAChF,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAQnF;AAsDD,eAAO,MAAM,oBAAoB,EAAE,QAiBlC,CAAC;AAIF,eAAO,MAAM,qBAAqB,EAAE,QAenC,CAAC;AAsBF,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAE7D"}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { asText } from './helpers/text.js';
|
|
2
|
+
import { log } from './log.js';
|
|
3
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4
|
+
export const DEFAULT_RECOVERABLE_NAMES = ['TimeoutError'];
|
|
5
|
+
function kw(args) {
|
|
6
|
+
// Trailing options object convention: the last arg when it's a plain object.
|
|
7
|
+
const last = args[args.length - 1];
|
|
8
|
+
if (last !== null && typeof last === 'object' && !Array.isArray(last)) {
|
|
9
|
+
return last;
|
|
10
|
+
}
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
/** Press modifier keys; returns a releaser that ups them in reverse. */
|
|
14
|
+
async function holdModifiers(page, modifiers) {
|
|
15
|
+
const mods = Array.isArray(modifiers) ? modifiers : [];
|
|
16
|
+
if (!mods.length)
|
|
17
|
+
return async () => { };
|
|
18
|
+
for (const m of mods)
|
|
19
|
+
await page.keyboard.down(m);
|
|
20
|
+
return async () => {
|
|
21
|
+
for (const m of [...mods].reverse())
|
|
22
|
+
await page.keyboard.up(m);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Selenium-parity bridge: rewrite `<option>.click()` into
|
|
27
|
+
* `<select>.selectOption({label})` on the ancestor. A native option inside a
|
|
28
|
+
* closed select is not clickable in modern browsers; without the bridge the
|
|
29
|
+
* verb sinks into the cascade, whose tiers can't see closed-dropdown text.
|
|
30
|
+
* Returns true when the bridge fired (caller skips the real click).
|
|
31
|
+
*/
|
|
32
|
+
async function tryOptionBridge(locator) {
|
|
33
|
+
let tag;
|
|
34
|
+
try {
|
|
35
|
+
// Real function, NOT a string — the grid CDP relay returns undefined
|
|
36
|
+
// for string page-functions.
|
|
37
|
+
tag = await locator.evaluate((el) => el && el.tagName);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (typeof tag !== 'string' || tag.toUpperCase() !== 'OPTION')
|
|
43
|
+
return false;
|
|
44
|
+
const parent = locator.locator('xpath=ancestor::select[1]');
|
|
45
|
+
try {
|
|
46
|
+
if ((await parent.count()) === 0)
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
let label;
|
|
53
|
+
try {
|
|
54
|
+
label = await locator.evaluate((el) => el.textContent || '');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const text = String(label ?? '').trim();
|
|
60
|
+
if (!text)
|
|
61
|
+
return false;
|
|
62
|
+
await parent.selectOption({ label: text });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// ── click / dblclick / hover ──────────────────────────────────────────────────
|
|
66
|
+
export const CLICK_SPEC = {
|
|
67
|
+
runner: async (locator, ...args) => {
|
|
68
|
+
if (await tryOptionBridge(locator))
|
|
69
|
+
return;
|
|
70
|
+
await locator.click(kw(args));
|
|
71
|
+
},
|
|
72
|
+
coordRunner: async (page, x, y, ...args) => {
|
|
73
|
+
const o = kw(args);
|
|
74
|
+
const release = await holdModifiers(page, o['modifiers']);
|
|
75
|
+
try {
|
|
76
|
+
await page.mouse.click(x, y, {
|
|
77
|
+
button: o['button'] ?? 'left',
|
|
78
|
+
clickCount: o['clickCount'] ?? 1,
|
|
79
|
+
delay: o['delay'] ?? 0,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
await release();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
export const DBLCLICK_SPEC = {
|
|
88
|
+
runner: async (locator, ...args) => { await locator.dblclick(kw(args)); },
|
|
89
|
+
coordRunner: async (page, x, y, ...args) => {
|
|
90
|
+
const o = kw(args);
|
|
91
|
+
const release = await holdModifiers(page, o['modifiers']);
|
|
92
|
+
try {
|
|
93
|
+
await page.mouse.dblclick(x, y, {
|
|
94
|
+
button: o['button'] ?? 'left',
|
|
95
|
+
delay: o['delay'] ?? 0,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await release();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
export const HOVER_SPEC = {
|
|
104
|
+
runner: async (locator, ...args) => { await locator.hover(kw(args)); },
|
|
105
|
+
coordRunner: async (page, x, y, ...args) => {
|
|
106
|
+
const release = await holdModifiers(page, kw(args)['modifiers']);
|
|
107
|
+
try {
|
|
108
|
+
await page.mouse.move(x, y);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await release();
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
// ── long_press / multi_click (V3 gestures) ────────────────────────────────────
|
|
116
|
+
// hover → down → hold (duration + 0.1s buffer) → up (V2 _do_long_press parity).
|
|
117
|
+
export const LONG_PRESS_SPEC = {
|
|
118
|
+
runner: async (locator, ...args) => {
|
|
119
|
+
const duration = Number(kw(args)['duration'] ?? 0) || 0;
|
|
120
|
+
const page = locator.page();
|
|
121
|
+
await locator.hover();
|
|
122
|
+
await page.mouse.down();
|
|
123
|
+
await sleep((duration + 0.1) * 1000);
|
|
124
|
+
await page.mouse.up();
|
|
125
|
+
},
|
|
126
|
+
coordRunner: async (page, x, y, ...args) => {
|
|
127
|
+
const duration = Number(kw(args)['duration'] ?? 0) || 0;
|
|
128
|
+
await page.mouse.move(x, y);
|
|
129
|
+
await page.mouse.down();
|
|
130
|
+
await sleep((duration + 0.1) * 1000);
|
|
131
|
+
await page.mouse.up();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
// N discrete clicks on ONE resolved target — the cascade resolves once, the
|
|
135
|
+
// runner clicks N times (V2 _do_multi_click parity). freq<=2 never reach here.
|
|
136
|
+
export const MULTI_CLICK_SPEC = {
|
|
137
|
+
runner: async (locator, ...args) => {
|
|
138
|
+
const o = kw(args);
|
|
139
|
+
const frequency = Math.trunc(Number(o['frequency'] ?? 0));
|
|
140
|
+
const gap = Number(o['gap'] ?? 0.1);
|
|
141
|
+
for (let i = 0; i < frequency; i++) {
|
|
142
|
+
await locator.click();
|
|
143
|
+
if (i < frequency - 1)
|
|
144
|
+
await sleep(gap * 1000);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
coordRunner: async (page, x, y, ...args) => {
|
|
148
|
+
const o = kw(args);
|
|
149
|
+
const frequency = Math.trunc(Number(o['frequency'] ?? 0));
|
|
150
|
+
const gap = Number(o['gap'] ?? 0.1);
|
|
151
|
+
for (let i = 0; i < frequency; i++) {
|
|
152
|
+
await page.mouse.click(x, y);
|
|
153
|
+
if (i < frequency - 1)
|
|
154
|
+
await sleep(gap * 1000);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
// ── clear / fill / type / press (text-input verbs) ────────────────────────────
|
|
159
|
+
export const CLEAR_SPEC = {
|
|
160
|
+
runner: async (locator, ...args) => { await locator.clear(kw(args)); },
|
|
161
|
+
coordRunner: async (page, x, y) => {
|
|
162
|
+
await page.mouse.click(x, y);
|
|
163
|
+
await page.keyboard.press('Control+a');
|
|
164
|
+
await page.keyboard.press('Delete');
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
export const FILL_SPEC = {
|
|
168
|
+
runner: async (locator, value, ...args) => {
|
|
169
|
+
await locator.fill(asText(value), kw(args));
|
|
170
|
+
},
|
|
171
|
+
coordRunner: async (page, x, y, value) => {
|
|
172
|
+
// Click + Ctrl+A + type — Playwright fill semantics (replace existing).
|
|
173
|
+
await page.mouse.click(x, y);
|
|
174
|
+
await page.keyboard.press('Control+a');
|
|
175
|
+
await page.keyboard.type(asText(value));
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
export const TYPE_SPEC = {
|
|
179
|
+
runner: async (locator, text, ...args) => {
|
|
180
|
+
await locator.pressSequentially(asText(text), kw(args));
|
|
181
|
+
},
|
|
182
|
+
coordRunner: async (page, x, y, text, ...args) => {
|
|
183
|
+
const delay = Number(kw(args)['delay'] ?? 0);
|
|
184
|
+
await page.mouse.click(x, y);
|
|
185
|
+
await page.keyboard.type(asText(text), { delay });
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
export const PRESS_SPEC = {
|
|
189
|
+
runner: async (locator, key, ...args) => {
|
|
190
|
+
await locator.press(String(key), kw(args));
|
|
191
|
+
},
|
|
192
|
+
coordRunner: async (page, x, y, key, ...args) => {
|
|
193
|
+
const delay = Number(kw(args)['delay'] ?? 0);
|
|
194
|
+
await page.mouse.click(x, y);
|
|
195
|
+
await page.keyboard.press(String(key), { delay });
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
// ── set_input_files ───────────────────────────────────────────────────────────
|
|
199
|
+
// Real function, NOT a string — the grid CDP relay returns undefined for
|
|
200
|
+
// string page-functions.
|
|
201
|
+
const FIND_FIRST_FILE_INPUT_FN = () => {
|
|
202
|
+
const found = [];
|
|
203
|
+
const collect = (root) => {
|
|
204
|
+
let direct = [];
|
|
205
|
+
try {
|
|
206
|
+
direct = root.querySelectorAll('input[type=file]');
|
|
207
|
+
}
|
|
208
|
+
catch { /* skip */ }
|
|
209
|
+
for (const inp of direct)
|
|
210
|
+
found.push(inp);
|
|
211
|
+
let all = [];
|
|
212
|
+
try {
|
|
213
|
+
all = root.querySelectorAll('*');
|
|
214
|
+
}
|
|
215
|
+
catch { /* skip */ }
|
|
216
|
+
for (const host of all) {
|
|
217
|
+
if (host.shadowRoot)
|
|
218
|
+
collect(host.shadowRoot);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
collect(document);
|
|
222
|
+
return found.length ? found[0] : null;
|
|
223
|
+
};
|
|
224
|
+
/** First <input type=file> via pure DOM (light + open shadow). Never raises. */
|
|
225
|
+
export async function resolveFileInputDom(page) {
|
|
226
|
+
try {
|
|
227
|
+
const handle = await page.evaluateHandle(FIND_FIRST_FILE_INPUT_FN);
|
|
228
|
+
return handle.asElement();
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
log.helper(' [upload] DOM file-input probe failed');
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Real function, NOT a string — the grid CDP relay returns undefined for
|
|
236
|
+
// string page-functions.
|
|
237
|
+
const RESOLVE_FILE_INPUT_FN = ([x, y]) => {
|
|
238
|
+
const isFile = (n) => !!n && n.tagName === 'INPUT' && (n.type || '').toLowerCase() === 'file';
|
|
239
|
+
const el = document.elementFromPoint(x, y);
|
|
240
|
+
if (isFile(el))
|
|
241
|
+
return el;
|
|
242
|
+
if (el && el.closest) {
|
|
243
|
+
const lbl = el.closest('label');
|
|
244
|
+
if (lbl && isFile(lbl.control))
|
|
245
|
+
return lbl.control;
|
|
246
|
+
}
|
|
247
|
+
let node = el;
|
|
248
|
+
while (node) {
|
|
249
|
+
if (isFile(node))
|
|
250
|
+
return node;
|
|
251
|
+
if (node.shadowRoot) {
|
|
252
|
+
const inner = node.shadowRoot.querySelector('input[type=file]');
|
|
253
|
+
if (inner)
|
|
254
|
+
return inner;
|
|
255
|
+
}
|
|
256
|
+
if (node.querySelector) {
|
|
257
|
+
const inner = node.querySelector('input[type=file]');
|
|
258
|
+
if (inner)
|
|
259
|
+
return inner;
|
|
260
|
+
}
|
|
261
|
+
const root = node.getRootNode && node.getRootNode();
|
|
262
|
+
node = node.parentElement || (root instanceof ShadowRoot ? root.host : null);
|
|
263
|
+
}
|
|
264
|
+
const found = [];
|
|
265
|
+
const collect = (root) => {
|
|
266
|
+
let direct = [];
|
|
267
|
+
try {
|
|
268
|
+
direct = root.querySelectorAll('input[type=file]');
|
|
269
|
+
}
|
|
270
|
+
catch { /* skip */ }
|
|
271
|
+
for (const inp of direct)
|
|
272
|
+
found.push(inp);
|
|
273
|
+
let all = [];
|
|
274
|
+
try {
|
|
275
|
+
all = root.querySelectorAll('*');
|
|
276
|
+
}
|
|
277
|
+
catch { /* skip */ }
|
|
278
|
+
for (const host of all) {
|
|
279
|
+
if (host.shadowRoot)
|
|
280
|
+
collect(host.shadowRoot);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
collect(document);
|
|
284
|
+
if (found.length === 1)
|
|
285
|
+
return found[0];
|
|
286
|
+
if (found.length > 1) {
|
|
287
|
+
let best = null;
|
|
288
|
+
let bestDist = Infinity;
|
|
289
|
+
for (const inp of found) {
|
|
290
|
+
const r = inp.getBoundingClientRect();
|
|
291
|
+
const cx = r.left + r.width / 2, cy = r.top + r.height / 2;
|
|
292
|
+
const d = (cx - x) * (cx - x) + (cy - y) * (cy - y);
|
|
293
|
+
if (d < bestDist) {
|
|
294
|
+
bestDist = d;
|
|
295
|
+
best = inp;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return best;
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
};
|
|
302
|
+
export const SET_INPUT_FILES_SPEC = {
|
|
303
|
+
runner: async (locator, files) => {
|
|
304
|
+
await locator.setInputFiles(files);
|
|
305
|
+
},
|
|
306
|
+
coordRunner: async (page, x, y, files) => {
|
|
307
|
+
const handle = await page.evaluateHandle(RESOLVE_FILE_INPUT_FN, [x, y]);
|
|
308
|
+
const element = handle.asElement();
|
|
309
|
+
if (element === null) {
|
|
310
|
+
throw new Error(`set_input_files: no <input type='file'> at/near healed coordinate (${x}, ${y})`);
|
|
311
|
+
}
|
|
312
|
+
await element.setInputFiles(files);
|
|
313
|
+
},
|
|
314
|
+
// DESKTOP_LOCATE primary (file inputs live in shadow roots / off-viewport);
|
|
315
|
+
// VISION_QUERY retained as the selector fallback.
|
|
316
|
+
tiers: ['DESKTOP_LOCATE', 'VISION_QUERY'],
|
|
317
|
+
};
|
|
318
|
+
// ── scroll_into_view ──────────────────────────────────────────────────────────
|
|
319
|
+
export const SCROLL_INTO_VIEW_SPEC = {
|
|
320
|
+
runner: async (locator, ...args) => {
|
|
321
|
+
await locator.scrollIntoViewIfNeeded(kw(args));
|
|
322
|
+
},
|
|
323
|
+
coordRunner: async (page, x, y) => {
|
|
324
|
+
// Idempotent fine-adjustment after the full-page tier scrolled (spec §9.3).
|
|
325
|
+
await page.evaluate(([px, py]) => {
|
|
326
|
+
const el = document.elementFromPoint(px, py);
|
|
327
|
+
if (el)
|
|
328
|
+
el.scrollIntoView({ block: 'center' });
|
|
329
|
+
}, [x, y]);
|
|
330
|
+
},
|
|
331
|
+
tiers: ['DESKTOP_LOCATE_FULL_PAGE', 'VISION_QUERY'],
|
|
332
|
+
};
|
|
333
|
+
// ── Verb registry ─────────────────────────────────────────────────────────────
|
|
334
|
+
const VERB_SPECS = {
|
|
335
|
+
click: CLICK_SPEC,
|
|
336
|
+
dblclick: DBLCLICK_SPEC,
|
|
337
|
+
long_press: LONG_PRESS_SPEC,
|
|
338
|
+
multi_click: MULTI_CLICK_SPEC,
|
|
339
|
+
hover: HOVER_SPEC,
|
|
340
|
+
clear: CLEAR_SPEC,
|
|
341
|
+
fill: FILL_SPEC,
|
|
342
|
+
type: TYPE_SPEC,
|
|
343
|
+
press: PRESS_SPEC,
|
|
344
|
+
set_input_files: SET_INPUT_FILES_SPEC,
|
|
345
|
+
scroll_into_view_if_needed: SCROLL_INTO_VIEW_SPEC,
|
|
346
|
+
// Playwright-JS method-name aliases (the heal patch passes the method name).
|
|
347
|
+
pressSequentially: TYPE_SPEC,
|
|
348
|
+
setInputFiles: SET_INPUT_FILES_SPEC,
|
|
349
|
+
scrollIntoViewIfNeeded: SCROLL_INTO_VIEW_SPEC,
|
|
350
|
+
};
|
|
351
|
+
export function getVerbSpec(verbName) {
|
|
352
|
+
return VERB_SPECS[verbName] ?? null;
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=actionSpecs.js.map
|