@volter-ai-dev/supercode-browser-playwright 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/dist/executor.d.ts +35 -14
- package/dist/executor.mjs +251 -30
- package/dist/executor.mjs.map +4 -4
- package/dist/guard.d.ts +63 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +249 -30
- package/dist/index.mjs.map +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,8 @@ Both carry the same envelope, `{protocol, id, token, call}` in and
|
|
|
58
58
|
|
|
59
59
|
- **TCP** is the wire the harness speaks (`crates/harness/src/browser.rs`): one
|
|
60
60
|
newline-terminated JSON request per connection, one JSON response, then the
|
|
61
|
-
provider closes.
|
|
61
|
+
provider closes. A provider waiting for the person may first write
|
|
62
|
+
`{protocol, id, pending}` lines; each extends the harness's wait to 120 s. The provider listens on `127.0.0.1` and writes an owner-only
|
|
62
63
|
discovery record (file `0600`, directory `0700`) to
|
|
63
64
|
`<providers dir>/browser/playwright.cdp.<port>.json`, scoped to the
|
|
64
65
|
canonical workspace, and removes it on stop.
|
|
@@ -129,15 +130,37 @@ Two entry points use no Node API:
|
|
|
129
130
|
`@volter/almostcdp/playwright`, and answers the operations against an
|
|
130
131
|
AlmostCDP surface with no Node process.
|
|
131
132
|
|
|
132
|
-
A host's `actionGuard(action)` is asked before each
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
`
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
A host's `actionGuard(action)` is asked before each element action and before
|
|
134
|
+
each `browser.script` (`{action: "script", source, args}`); throwing a
|
|
135
|
+
`BrowserActionRefusal` answers the call with its code, such as
|
|
136
|
+
`APPROVAL_REQUIRED`. A script's own locator calls are the real `page`'s and are
|
|
137
|
+
not asked again, so a host that guards element actions guards scripts as a
|
|
138
|
+
whole.
|
|
139
|
+
|
|
140
|
+
An element action (`click`, `fill`, `press`, `hover`, `focus`, `check`,
|
|
141
|
+
`uncheck`, `select`, `drag`, and the coordinate actions `mousedown`, `mouseup`,
|
|
142
|
+
`wheel`) carries a `target`, described from the browser side:
|
|
143
|
+
|
|
144
|
+
- A locator is resolved once, waiting up to the action budget (5 s), so an
|
|
145
|
+
element that renders late is guarded, not skipped. The guard sees that
|
|
146
|
+
element and the action then runs on the same element handle.
|
|
147
|
+
- A coordinate action (`browser.mouse` click/down/up/move, `browser.drag` from
|
|
148
|
+
or to x/y, `browser.wheel` at the mouse's position) is guarded on the element
|
|
149
|
+
at that point.
|
|
150
|
+
- The elements are found in an isolated world the page's scripts cannot reach
|
|
151
|
+
(the element's exact box, open shadow roots included; for a point,
|
|
152
|
+
`elementFromPoint`), then described over CDP from each backend node:
|
|
153
|
+
`DOM.describeNode` for tag and attributes, `Accessibility.getPartialAXTree`
|
|
154
|
+
for role, name and accessibility ancestors. A page cannot change what the
|
|
155
|
+
guard reads by overriding DOM functions. Over AlmostCDP the CDP endpoint
|
|
156
|
+
itself runs in the page, so there the description is only as trustworthy as
|
|
157
|
+
the page's world.
|
|
158
|
+
- A target that cannot be resolved or described (inside an embedded frame or a
|
|
159
|
+
closed shadow root, without a box, or a CDP failure) refuses the action with
|
|
160
|
+
`UNSUPPORTED`; the guard is never skipped.
|
|
139
161
|
|
|
140
162
|
A host's `snapshotExclude` is a CSS selector for its own elements in the page
|
|
141
163
|
(an overlay, a launcher): `browser.snapshot` leaves out the nodes of every
|
|
142
|
-
element it matches, found by their aria refs, and a node left empty by that
|
|
143
|
-
|
|
164
|
+
element it matches, found by their aria refs, and a node left empty by that,
|
|
165
|
+
and a coordinate action that lands on one is refused with `UNSUPPORTED`. The
|
|
166
|
+
page is not changed, so assistive technology still reads those elements.
|
package/dist/executor.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* over CDP. No Node API is used, so the executor also runs in a browser realm
|
|
5
5
|
* with a browser build of playwright-core (`@volter/almostcdp/playwright`).
|
|
6
6
|
*/
|
|
7
|
-
import { type
|
|
7
|
+
import { type Page } from "playwright-core";
|
|
8
|
+
import { type GuardedTarget } from "./guard.js";
|
|
8
9
|
import { type BrowserOperationErrorCode, type BrowserOperationResult } from "./protocol.js";
|
|
9
10
|
/** The implementation this provider reports; the pinned playwright-core. */
|
|
10
11
|
export declare const PLAYWRIGHT_IMPLEMENTATION = "playwright-core 1.63.0";
|
|
@@ -23,24 +24,32 @@ export interface PlaywrightOperationExecutorOptions {
|
|
|
23
24
|
endpoint: string;
|
|
24
25
|
/**
|
|
25
26
|
* The host's policy for one action, asked before it runs: each locator
|
|
26
|
-
* action,
|
|
27
|
-
*
|
|
27
|
+
* action, each coordinate action (`browser.mouse`, `browser.drag`,
|
|
28
|
+
* `browser.wheel`), and each `browser.script` before its source is
|
|
29
|
+
* compiled. An element action's target is resolved once, described from the
|
|
30
|
+
* browser side (see `GuardedTarget`), and the action then runs on that same
|
|
31
|
+
* element. A target that cannot be resolved or described refuses the
|
|
32
|
+
* operation without asking. Throwing a `BrowserActionRefusal` refuses the
|
|
33
|
+
* operation with that code.
|
|
28
34
|
*/
|
|
29
35
|
actionGuard?: (action: PlaywrightAction) => void | Promise<void>;
|
|
30
36
|
/**
|
|
31
|
-
* A CSS selector
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* not changed, so what assistive technology reads stays the
|
|
37
|
+
* A CSS selector for the host's own elements: `browser.snapshot` leaves out
|
|
38
|
+
* every matching element's nodes (Playwright's selector, which pierces open
|
|
39
|
+
* shadow roots), and a coordinate action that lands on one is refused. The
|
|
40
|
+
* page itself is not changed, so what assistive technology reads stays the
|
|
41
|
+
* same.
|
|
35
42
|
*/
|
|
36
43
|
snapshotExclude?: string;
|
|
37
44
|
}
|
|
38
45
|
/** One action an operation is about to perform. */
|
|
39
|
-
export type PlaywrightAction =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
export type PlaywrightAction = PlaywrightElementAction | PlaywrightScriptAction;
|
|
47
|
+
export type { GuardedNode, GuardedTarget } from "./guard.js";
|
|
48
|
+
/** An action that lands on an element: by locator, or at a viewport point. */
|
|
49
|
+
export interface PlaywrightElementAction {
|
|
50
|
+
action: "click" | "fill" | "press" | "hover" | "focus" | "check" | "uncheck" | "select" | "drag" | "mousedown" | "mouseup" | "wheel";
|
|
51
|
+
/** What the action lands on, described from the browser side. */
|
|
52
|
+
target: GuardedTarget;
|
|
44
53
|
value?: unknown;
|
|
45
54
|
}
|
|
46
55
|
export interface PlaywrightScriptAction {
|
|
@@ -61,6 +70,9 @@ export declare class PlaywrightOperationExecutor {
|
|
|
61
70
|
private readonly page;
|
|
62
71
|
private readonly options;
|
|
63
72
|
private readonly ready;
|
|
73
|
+
private readonly targets;
|
|
74
|
+
/** Where the mouse is: `page.mouse` keeps it, but does not report it. */
|
|
75
|
+
private mouse;
|
|
64
76
|
constructor(page: Page, options: PlaywrightOperationExecutorOptions);
|
|
65
77
|
execute(raw: unknown): Promise<BrowserOperationResult>;
|
|
66
78
|
private locator;
|
|
@@ -76,8 +88,17 @@ export declare class PlaywrightOperationExecutor {
|
|
|
76
88
|
private inspect;
|
|
77
89
|
/** Run a locator action; a failure on a locator that matches nothing is NOT_FOUND. */
|
|
78
90
|
private act;
|
|
79
|
-
/**
|
|
80
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Resolves the locator's element once (waiting up to the action budget),
|
|
93
|
+
* asks the host's policy about it, and returns the handle the action must
|
|
94
|
+
* use, so the element guarded is the element acted on. Without a policy the
|
|
95
|
+
* action runs on the locator. Resolution or description failing refuses.
|
|
96
|
+
*/
|
|
97
|
+
private guarded;
|
|
98
|
+
/** Asks the host's policy about the element at a viewport point. */
|
|
99
|
+
private guardPoint;
|
|
100
|
+
/** A browser-side description, or the refusal that replaces the action. */
|
|
101
|
+
private describe;
|
|
81
102
|
/** The aria refs of the elements `snapshotExclude` matches, and of their contents. */
|
|
82
103
|
private excludedRefs;
|
|
83
104
|
private revision;
|
package/dist/executor.mjs
CHANGED
|
@@ -1,5 +1,143 @@
|
|
|
1
1
|
// src/executor.ts
|
|
2
2
|
import { errors } from "playwright-core";
|
|
3
|
+
|
|
4
|
+
// src/guard.ts
|
|
5
|
+
var UnidentifiedTarget = class extends Error {
|
|
6
|
+
};
|
|
7
|
+
var HostElementTarget = class extends Error {
|
|
8
|
+
};
|
|
9
|
+
var WORLD = "supercode-guard";
|
|
10
|
+
var GROUP = "supercode-guard";
|
|
11
|
+
var MAX_NODES = 8;
|
|
12
|
+
var MAX_ANCESTORS = 6;
|
|
13
|
+
function findInWorld(query) {
|
|
14
|
+
const onHost = (element) => {
|
|
15
|
+
if (!query.host) return false;
|
|
16
|
+
for (let node = element; node; ) {
|
|
17
|
+
if (node.matches(query.host)) return true;
|
|
18
|
+
const parent = node.parentElement;
|
|
19
|
+
node = parent ?? (node.getRootNode().host ?? null);
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
};
|
|
23
|
+
if (query.kind === "point") {
|
|
24
|
+
let element = document.elementFromPoint(query.x, query.y);
|
|
25
|
+
if (!element) return "Nothing is at that point.";
|
|
26
|
+
if (onHost(element)) return "host";
|
|
27
|
+
while (element.shadowRoot) {
|
|
28
|
+
const inner = element.shadowRoot.elementFromPoint(query.x, query.y);
|
|
29
|
+
if (!inner || inner === element) break;
|
|
30
|
+
element = inner;
|
|
31
|
+
if (onHost(element)) return "host";
|
|
32
|
+
}
|
|
33
|
+
if (/^(IFRAME|FRAME|OBJECT|EMBED)$/.test(element.tagName))
|
|
34
|
+
return "The point is inside an embedded frame, which the guard cannot inspect.";
|
|
35
|
+
return [element];
|
|
36
|
+
}
|
|
37
|
+
const { box } = query;
|
|
38
|
+
const found = [];
|
|
39
|
+
const near = (a, b) => Math.abs(a - b) <= 1;
|
|
40
|
+
const visit = (root) => {
|
|
41
|
+
for (const element of Array.from(root.querySelectorAll("*"))) {
|
|
42
|
+
if (found.length >= query.max) return;
|
|
43
|
+
const rect = element.getBoundingClientRect();
|
|
44
|
+
if (near(rect.x, box.x) && near(rect.y, box.y) && near(rect.width, box.width) && near(rect.height, box.height))
|
|
45
|
+
found.push(element);
|
|
46
|
+
if (element.shadowRoot) visit(element.shadowRoot);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
visit(document);
|
|
50
|
+
return found.length ? found : "The element could not be found in the page's top document (it may be inside an embedded frame or a closed shadow root).";
|
|
51
|
+
}
|
|
52
|
+
var BrowserSideTargets = class {
|
|
53
|
+
constructor(page, host) {
|
|
54
|
+
this.page = page;
|
|
55
|
+
this.host = host;
|
|
56
|
+
}
|
|
57
|
+
session = null;
|
|
58
|
+
/** The element a resolved handle is, scrolled into view. */
|
|
59
|
+
async ofHandle(handle, timeout) {
|
|
60
|
+
await handle.scrollIntoViewIfNeeded({ timeout });
|
|
61
|
+
const box = await handle.boundingBox();
|
|
62
|
+
if (!box || box.width === 0 || box.height === 0)
|
|
63
|
+
throw new UnidentifiedTarget("The element has no box on the page.");
|
|
64
|
+
return await this.resolve({ kind: "box", box, host: this.host, max: MAX_NODES });
|
|
65
|
+
}
|
|
66
|
+
/** The element at a viewport point. */
|
|
67
|
+
async atPoint(x, y) {
|
|
68
|
+
return { ...await this.resolve({ kind: "point", x, y, host: this.host }), point: { x, y } };
|
|
69
|
+
}
|
|
70
|
+
cdp() {
|
|
71
|
+
this.session ??= this.page.context().newCDPSession(this.page).catch((error) => {
|
|
72
|
+
this.session = null;
|
|
73
|
+
throw error;
|
|
74
|
+
});
|
|
75
|
+
return this.session;
|
|
76
|
+
}
|
|
77
|
+
async resolve(query) {
|
|
78
|
+
const cdp = await this.cdp();
|
|
79
|
+
const tree = await cdp.send("Page.getFrameTree");
|
|
80
|
+
const world = await cdp.send("Page.createIsolatedWorld", { frameId: tree.frameTree.frame.id, worldName: WORLD });
|
|
81
|
+
try {
|
|
82
|
+
const evaluated = await cdp.send("Runtime.evaluate", {
|
|
83
|
+
expression: `(${findInWorld.toString()})(${JSON.stringify(query)})`,
|
|
84
|
+
contextId: world.executionContextId,
|
|
85
|
+
objectGroup: GROUP,
|
|
86
|
+
returnByValue: false
|
|
87
|
+
});
|
|
88
|
+
if (evaluated.exceptionDetails)
|
|
89
|
+
throw new UnidentifiedTarget(`The target could not be resolved: ${evaluated.exceptionDetails.text ?? "exception"}.`);
|
|
90
|
+
if (evaluated.result.type === "string") {
|
|
91
|
+
if (evaluated.result.value === "host") throw new HostElementTarget("The point is on the host's own interface, which agents cannot operate.");
|
|
92
|
+
throw new UnidentifiedTarget(String(evaluated.result.value));
|
|
93
|
+
}
|
|
94
|
+
if (!evaluated.result.objectId) throw new UnidentifiedTarget("The target could not be resolved.");
|
|
95
|
+
const properties = await cdp.send("Runtime.getProperties", { objectId: evaluated.result.objectId, ownProperties: true });
|
|
96
|
+
const objects = properties.result.filter((property) => /^\d+$/.test(property.name) && property.value?.objectId).map((property) => property.value.objectId);
|
|
97
|
+
if (!objects.length) throw new UnidentifiedTarget("The target could not be resolved.");
|
|
98
|
+
const nodes = [];
|
|
99
|
+
const ancestors = [];
|
|
100
|
+
const seen = /* @__PURE__ */ new Set();
|
|
101
|
+
for (const objectId of objects) {
|
|
102
|
+
const described = await cdp.send("DOM.describeNode", { objectId });
|
|
103
|
+
const attributes = {};
|
|
104
|
+
const list = described.node.attributes ?? [];
|
|
105
|
+
for (let index = 0; index + 1 < list.length; index += 2) attributes[list[index].toLowerCase()] = list[index + 1];
|
|
106
|
+
const ax = await this.accessibility(cdp, described.node.backendNodeId);
|
|
107
|
+
nodes.push({
|
|
108
|
+
backendNodeId: described.node.backendNodeId,
|
|
109
|
+
tag: described.node.nodeName.toLowerCase(),
|
|
110
|
+
attributes,
|
|
111
|
+
role: ax.self?.role ?? "",
|
|
112
|
+
name: ax.self?.name ?? ""
|
|
113
|
+
});
|
|
114
|
+
seen.add(described.node.backendNodeId);
|
|
115
|
+
for (const ancestor of ax.ancestors) {
|
|
116
|
+
if (ancestors.length >= MAX_ANCESTORS || seen.has(ancestor.backendNodeId)) continue;
|
|
117
|
+
seen.add(ancestor.backendNodeId);
|
|
118
|
+
ancestors.push(ancestor);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { url: this.page.url(), nodes, ancestors };
|
|
122
|
+
} finally {
|
|
123
|
+
await cdp.send("Runtime.releaseObjectGroup", { objectGroup: GROUP }).catch(() => void 0);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async accessibility(cdp, backendNodeId) {
|
|
127
|
+
const tree = await cdp.send("Accessibility.getPartialAXTree", { backendNodeId, fetchRelatives: true });
|
|
128
|
+
const byId = new Map(tree.nodes.map((node) => [node.nodeId, node]));
|
|
129
|
+
const text = (value) => typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
130
|
+
const self = tree.nodes.find((node) => node.backendDOMNodeId === backendNodeId) ?? null;
|
|
131
|
+
const ancestors = [];
|
|
132
|
+
for (let node = self?.parentId ? byId.get(self.parentId) : void 0; node && ancestors.length < MAX_ANCESTORS; node = node.parentId ? byId.get(node.parentId) : void 0) {
|
|
133
|
+
if (node.ignored || typeof node.backendDOMNodeId !== "number") continue;
|
|
134
|
+
ancestors.push({ backendNodeId: node.backendDOMNodeId, tag: "", attributes: {}, role: text(node.role?.value), name: text(node.name?.value) });
|
|
135
|
+
}
|
|
136
|
+
return { self: self ? { role: text(self.role?.value), name: text(self.name?.value) } : null, ancestors };
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/executor.ts
|
|
3
141
|
import {
|
|
4
142
|
BROWSER_OPERATION_NAMES,
|
|
5
143
|
BrowserActionRefusal,
|
|
@@ -225,9 +363,13 @@ var PlaywrightOperationExecutor = class {
|
|
|
225
363
|
page;
|
|
226
364
|
options;
|
|
227
365
|
ready;
|
|
366
|
+
targets;
|
|
367
|
+
/** Where the mouse is: `page.mouse` keeps it, but does not report it. */
|
|
368
|
+
mouse = { x: 0, y: 0 };
|
|
228
369
|
constructor(page, options) {
|
|
229
370
|
this.page = page;
|
|
230
371
|
this.options = options;
|
|
372
|
+
this.targets = new BrowserSideTargets(page, options.snapshotExclude ?? null);
|
|
231
373
|
this.ready = (async () => {
|
|
232
374
|
await page.addInitScript(installRevisionCounter, REVISION_KEY);
|
|
233
375
|
await page.evaluate(installRevisionCounter, REVISION_KEY).catch(() => void 0);
|
|
@@ -301,10 +443,55 @@ var PlaywrightOperationExecutor = class {
|
|
|
301
443
|
throw error;
|
|
302
444
|
}
|
|
303
445
|
}
|
|
304
|
-
/**
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
446
|
+
/**
|
|
447
|
+
* Resolves the locator's element once (waiting up to the action budget),
|
|
448
|
+
* asks the host's policy about it, and returns the handle the action must
|
|
449
|
+
* use, so the element guarded is the element acted on. Without a policy the
|
|
450
|
+
* action runs on the locator. Resolution or description failing refuses.
|
|
451
|
+
*/
|
|
452
|
+
async guarded(locator, action, value) {
|
|
453
|
+
const guard = this.options.actionGuard;
|
|
454
|
+
if (!guard) return null;
|
|
455
|
+
let handle;
|
|
456
|
+
try {
|
|
457
|
+
handle = await locator.elementHandle({ timeout: ACTION_TIMEOUT_MS });
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (error instanceof errors.TimeoutError)
|
|
460
|
+
throw new BrowserActionRefusal("NOT_FOUND", `${action} found no element for the locator in ${ACTION_TIMEOUT_MS}ms.`);
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
if (!handle) throw new BrowserActionRefusal("NOT_FOUND", `${action} found no element for the locator.`);
|
|
464
|
+
try {
|
|
465
|
+
await guard({
|
|
466
|
+
action,
|
|
467
|
+
target: await this.describe(() => this.targets.ofHandle(handle, ACTION_TIMEOUT_MS)),
|
|
468
|
+
...value !== void 0 ? { value } : {}
|
|
469
|
+
});
|
|
470
|
+
} catch (error) {
|
|
471
|
+
await handle.dispose().catch(() => void 0);
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
return handle;
|
|
475
|
+
}
|
|
476
|
+
/** Asks the host's policy about the element at a viewport point. */
|
|
477
|
+
async guardPoint(action, x, y) {
|
|
478
|
+
const guard = this.options.actionGuard;
|
|
479
|
+
if (!guard) return;
|
|
480
|
+
await guard({ action, target: await this.describe(() => this.targets.atPoint(x, y)) });
|
|
481
|
+
}
|
|
482
|
+
/** A browser-side description, or the refusal that replaces the action. */
|
|
483
|
+
async describe(run) {
|
|
484
|
+
try {
|
|
485
|
+
return await run();
|
|
486
|
+
} catch (error) {
|
|
487
|
+
if (error instanceof HostElementTarget) throw new BrowserActionRefusal("UNSUPPORTED", error.message);
|
|
488
|
+
if (error instanceof UnidentifiedTarget)
|
|
489
|
+
throw new BrowserActionRefusal("UNSUPPORTED", `${error.message} The action was refused because its target could not be checked.`);
|
|
490
|
+
throw new BrowserActionRefusal(
|
|
491
|
+
"UNSUPPORTED",
|
|
492
|
+
`The action was refused because its target could not be checked: ${errorMessage(error)}`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
308
495
|
}
|
|
309
496
|
/** The aria refs of the elements `snapshotExclude` matches, and of their contents. */
|
|
310
497
|
async excludedRefs() {
|
|
@@ -380,35 +567,48 @@ var PlaywrightOperationExecutor = class {
|
|
|
380
567
|
if (call.operation === "browser.script") return this.script(call);
|
|
381
568
|
if (call.operation === "browser.mouse") {
|
|
382
569
|
const action = input.action;
|
|
383
|
-
|
|
384
|
-
|
|
570
|
+
const at = typeof input.x === "number" && typeof input.y === "number" ? { x: input.x, y: input.y } : this.mouse;
|
|
571
|
+
if (action === "move") await this.guardPoint("hover", at.x, at.y);
|
|
572
|
+
else await this.guardPoint(action === "click" ? "click" : action === "down" ? "mousedown" : "mouseup", at.x, at.y);
|
|
573
|
+
if (action === "move") await page.mouse.move(at.x, at.y);
|
|
574
|
+
else if (action === "click") await page.mouse.click(at.x, at.y);
|
|
385
575
|
else if (action === "down") await page.mouse.down();
|
|
386
576
|
else await page.mouse.up();
|
|
577
|
+
if (action === "move" || action === "click") this.mouse = { x: at.x, y: at.y };
|
|
387
578
|
return this.success(call.operation, { action, ...typeof input.x === "number" ? { x: input.x, y: input.y } : {} });
|
|
388
579
|
}
|
|
389
580
|
if (call.operation === "browser.wheel") {
|
|
390
581
|
const deltaX = typeof input.deltaX === "number" ? input.deltaX : 0;
|
|
391
582
|
const deltaY = typeof input.deltaY === "number" ? input.deltaY : 0;
|
|
583
|
+
await this.guardPoint("wheel", this.mouse.x, this.mouse.y);
|
|
392
584
|
await page.mouse.wheel(deltaX, deltaY);
|
|
393
585
|
return this.success(call.operation, { deltaX, deltaY });
|
|
394
586
|
}
|
|
395
587
|
if (call.operation === "browser.drag") {
|
|
396
588
|
const resolve = async (endpoint) => {
|
|
397
|
-
if (endpoint.locator === void 0)
|
|
589
|
+
if (endpoint.locator === void 0) {
|
|
590
|
+
const point = { x: endpoint.x, y: endpoint.y };
|
|
591
|
+
await this.guardPoint("drag", point.x, point.y);
|
|
592
|
+
return point;
|
|
593
|
+
}
|
|
398
594
|
await this.refreshRefs(endpoint.locator);
|
|
399
595
|
const locator2 = this.locator(endpoint.locator).first();
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
596
|
+
const handle2 = await this.guarded(locator2, "drag");
|
|
597
|
+
try {
|
|
598
|
+
const box = handle2 ? await handle2.boundingBox() : await locator2.count() === 0 ? null : await locator2.boundingBox({ timeout: ACTION_TIMEOUT_MS });
|
|
599
|
+
if (!box) throw new BrowserActionRefusal("NOT_FOUND", "The drag locator matched no element.");
|
|
600
|
+
return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
601
|
+
} finally {
|
|
602
|
+
await handle2?.dispose().catch(() => void 0);
|
|
603
|
+
}
|
|
403
604
|
};
|
|
404
|
-
for (const end of [input.from, input.to])
|
|
405
|
-
if (end.locator !== void 0) await this.guard({ action: "drag", locator: this.locator(end.locator).first() });
|
|
406
605
|
const from = await resolve(input.from);
|
|
407
606
|
const to = await resolve(input.to);
|
|
408
607
|
await page.mouse.move(from.x, from.y);
|
|
409
608
|
await page.mouse.down();
|
|
410
609
|
await page.mouse.move(to.x, to.y, { steps: typeof input.steps === "number" ? input.steps : 8 });
|
|
411
610
|
await page.mouse.up();
|
|
611
|
+
this.mouse = to;
|
|
412
612
|
return this.success(call.operation, { from, to });
|
|
413
613
|
}
|
|
414
614
|
if (call.operation === "browser.back") {
|
|
@@ -432,28 +632,38 @@ var PlaywrightOperationExecutor = class {
|
|
|
432
632
|
return this.success(call.operation, box);
|
|
433
633
|
}
|
|
434
634
|
if (call.operation === "browser.press" && !locator) {
|
|
435
|
-
|
|
436
|
-
await page.
|
|
635
|
+
const focused = page.locator("*:focus").first();
|
|
636
|
+
const handle2 = await this.guarded(await focused.count() ? focused : page.locator("body"), "press", input.key);
|
|
637
|
+
try {
|
|
638
|
+
await page.keyboard.press(input.key);
|
|
639
|
+
} finally {
|
|
640
|
+
await handle2?.dispose().catch(() => void 0);
|
|
641
|
+
}
|
|
437
642
|
return this.success(call.operation, { key: input.key });
|
|
438
643
|
}
|
|
439
644
|
const target = locator;
|
|
440
|
-
const
|
|
441
|
-
await this.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
645
|
+
const guardedAction = call.operation === "browser.select" ? "select" : call.operation.slice("browser.".length);
|
|
646
|
+
const handle = await this.guarded(
|
|
647
|
+
target.first(),
|
|
648
|
+
guardedAction,
|
|
649
|
+
call.operation === "browser.fill" ? input.value : call.operation === "browser.press" ? input.key : call.operation === "browser.select" ? input.values : void 0
|
|
650
|
+
);
|
|
651
|
+
const element = handle ?? { ...bind(target), focus: () => target.focus({ timeout }) };
|
|
446
652
|
const before = await this.inspect(target).catch(() => null);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
653
|
+
try {
|
|
654
|
+
if (call.operation === "browser.click") await this.act(target, "click", () => element.click({ timeout }));
|
|
655
|
+
else if (call.operation === "browser.fill") await this.act(target, "fill", () => element.fill(input.value, { timeout }));
|
|
656
|
+
else if (call.operation === "browser.press") await this.act(target, "press", () => element.press(input.key, { timeout }));
|
|
657
|
+
else if (call.operation === "browser.hover") await this.act(target, "hover", () => element.hover({ timeout }));
|
|
658
|
+
else if (call.operation === "browser.focus") await this.act(target, "focus", () => element.focus());
|
|
659
|
+
else if (call.operation === "browser.check") await this.act(target, "check", () => element.check({ timeout }));
|
|
660
|
+
else if (call.operation === "browser.uncheck") await this.act(target, "uncheck", () => element.uncheck({ timeout }));
|
|
661
|
+
else if (call.operation === "browser.select") {
|
|
662
|
+
const values = await this.act(target, "selectOption", () => element.selectOption(input.values, { timeout }));
|
|
663
|
+
return this.success(call.operation, { values });
|
|
664
|
+
}
|
|
665
|
+
} finally {
|
|
666
|
+
await handle?.dispose().catch(() => void 0);
|
|
457
667
|
}
|
|
458
668
|
const after = await this.inspect(target).catch(() => null);
|
|
459
669
|
const inspection = after ?? before;
|
|
@@ -521,6 +731,17 @@ function operationName(raw) {
|
|
|
521
731
|
const candidate = raw && typeof raw === "object" && !Array.isArray(raw) ? raw.operation : void 0;
|
|
522
732
|
return typeof candidate === "string" && BROWSER_OPERATION_NAMES.includes(candidate) ? candidate : "browser.status";
|
|
523
733
|
}
|
|
734
|
+
function bind(locator) {
|
|
735
|
+
return {
|
|
736
|
+
click: locator.click.bind(locator),
|
|
737
|
+
fill: locator.fill.bind(locator),
|
|
738
|
+
press: locator.press.bind(locator),
|
|
739
|
+
hover: locator.hover.bind(locator),
|
|
740
|
+
check: locator.check.bind(locator),
|
|
741
|
+
uncheck: locator.uncheck.bind(locator),
|
|
742
|
+
selectOption: locator.selectOption.bind(locator)
|
|
743
|
+
};
|
|
744
|
+
}
|
|
524
745
|
export {
|
|
525
746
|
ACTION_TIMEOUT_MS,
|
|
526
747
|
PLAYWRIGHT_FEATURES,
|