@volter-ai-dev/supercode-browser-playwright 0.1.0 → 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 +36 -6
- package/dist/executor.d.ts +54 -10
- package/dist/executor.mjs +312 -32
- 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 +310 -32
- 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,8 +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
|
-
|
|
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.
|
|
161
|
+
|
|
162
|
+
A host's `snapshotExclude` is a CSS selector for its own elements in the page
|
|
163
|
+
(an overlay, a launcher): `browser.snapshot` leaves out the nodes of every
|
|
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";
|
|
@@ -22,24 +23,56 @@ export interface PlaywrightOperationExecutorOptions {
|
|
|
22
23
|
/** `browser.version()` of the connected endpoint, reported by `browser.status`. */
|
|
23
24
|
endpoint: string;
|
|
24
25
|
/**
|
|
25
|
-
* The host's policy for one
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* The host's policy for one action, asked before it runs: each locator
|
|
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>;
|
|
36
|
+
/**
|
|
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.
|
|
42
|
+
*/
|
|
43
|
+
snapshotExclude?: string;
|
|
30
44
|
}
|
|
31
|
-
/** One
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
/** One action an operation is about to perform. */
|
|
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;
|
|
35
53
|
value?: unknown;
|
|
36
54
|
}
|
|
55
|
+
export interface PlaywrightScriptAction {
|
|
56
|
+
/** `browser.script`: the author's source, run with the real `page`. */
|
|
57
|
+
action: "script";
|
|
58
|
+
source: string;
|
|
59
|
+
args: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* An AI-mode aria snapshot's YAML without the nodes whose `[ref=…]` is in
|
|
63
|
+
* `refs`, their subtrees, and any ref-less node whose children all went with
|
|
64
|
+
* them. A node is a `- ` line; its subtree is the lines indented under it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function snapshotWithoutRefs(text: string, refs: ReadonlySet<string>): string;
|
|
37
67
|
/** Map a Playwright failure onto the wire's error codes. */
|
|
38
68
|
export declare function classifyPlaywrightError(error: unknown): BrowserOperationErrorCode;
|
|
39
69
|
export declare class PlaywrightOperationExecutor {
|
|
40
70
|
private readonly page;
|
|
41
71
|
private readonly options;
|
|
42
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;
|
|
43
76
|
constructor(page: Page, options: PlaywrightOperationExecutorOptions);
|
|
44
77
|
execute(raw: unknown): Promise<BrowserOperationResult>;
|
|
45
78
|
private locator;
|
|
@@ -55,8 +88,19 @@ export declare class PlaywrightOperationExecutor {
|
|
|
55
88
|
private inspect;
|
|
56
89
|
/** Run a locator action; a failure on a locator that matches nothing is NOT_FOUND. */
|
|
57
90
|
private act;
|
|
58
|
-
/**
|
|
59
|
-
|
|
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;
|
|
102
|
+
/** The aria refs of the elements `snapshotExclude` matches, and of their contents. */
|
|
103
|
+
private excludedRefs;
|
|
60
104
|
private revision;
|
|
61
105
|
private executeCall;
|
|
62
106
|
private script;
|
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,
|
|
@@ -49,6 +187,49 @@ var PLAYWRIGHT_FEATURES = Object.freeze([
|
|
|
49
187
|
"page.screenshot.png",
|
|
50
188
|
"page.evaluate"
|
|
51
189
|
]);
|
|
190
|
+
var SNAPSHOT_EXCLUDE_LIMIT = 16;
|
|
191
|
+
function collectRefs(nodes, into) {
|
|
192
|
+
if (Array.isArray(nodes)) {
|
|
193
|
+
for (const node2 of nodes) collectRefs(node2, into);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (!nodes || typeof nodes !== "object") return;
|
|
197
|
+
const node = nodes;
|
|
198
|
+
if (typeof node.ref === "string") into.add(node.ref);
|
|
199
|
+
collectRefs(node.children, into);
|
|
200
|
+
}
|
|
201
|
+
function snapshotWithoutRefs(text, refs) {
|
|
202
|
+
if (refs.size === 0) return text;
|
|
203
|
+
const root = { text: "", indent: -1, children: [], removed: false };
|
|
204
|
+
const stack = [root];
|
|
205
|
+
const lines = [];
|
|
206
|
+
for (const raw of text.split("\n")) {
|
|
207
|
+
const indent = raw.length - raw.trimStart().length;
|
|
208
|
+
const line = { text: raw, indent, children: [], removed: false };
|
|
209
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
210
|
+
stack[stack.length - 1].children.push(line);
|
|
211
|
+
stack.push(line);
|
|
212
|
+
lines.push(line);
|
|
213
|
+
}
|
|
214
|
+
const refOf = (line) => /\[ref=([^\]\s]+)\]/.exec(line.text)?.[1];
|
|
215
|
+
const remove = (line) => {
|
|
216
|
+
line.removed = true;
|
|
217
|
+
for (const child of line.children) remove(child);
|
|
218
|
+
};
|
|
219
|
+
const visit = (line) => {
|
|
220
|
+
const ref = refOf(line);
|
|
221
|
+
if (ref !== void 0 && refs.has(ref)) {
|
|
222
|
+
remove(line);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const before = line.children.length;
|
|
226
|
+
for (const child of line.children) visit(child);
|
|
227
|
+
if (line !== root && ref === void 0 && before > 0 && line.children.every((child) => child.removed))
|
|
228
|
+
line.removed = true;
|
|
229
|
+
};
|
|
230
|
+
visit(root);
|
|
231
|
+
return lines.filter((line) => !line.removed).map((line) => line.text).join("\n");
|
|
232
|
+
}
|
|
52
233
|
var REVISION_KEY = "__supercodeBrowserRevision";
|
|
53
234
|
function installRevisionCounter(key) {
|
|
54
235
|
const scope = globalThis;
|
|
@@ -182,9 +363,13 @@ var PlaywrightOperationExecutor = class {
|
|
|
182
363
|
page;
|
|
183
364
|
options;
|
|
184
365
|
ready;
|
|
366
|
+
targets;
|
|
367
|
+
/** Where the mouse is: `page.mouse` keeps it, but does not report it. */
|
|
368
|
+
mouse = { x: 0, y: 0 };
|
|
185
369
|
constructor(page, options) {
|
|
186
370
|
this.page = page;
|
|
187
371
|
this.options = options;
|
|
372
|
+
this.targets = new BrowserSideTargets(page, options.snapshotExclude ?? null);
|
|
188
373
|
this.ready = (async () => {
|
|
189
374
|
await page.addInitScript(installRevisionCounter, REVISION_KEY);
|
|
190
375
|
await page.evaluate(installRevisionCounter, REVISION_KEY).catch(() => void 0);
|
|
@@ -258,10 +443,68 @@ var PlaywrightOperationExecutor = class {
|
|
|
258
443
|
throw error;
|
|
259
444
|
}
|
|
260
445
|
}
|
|
261
|
-
/**
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/** The aria refs of the elements `snapshotExclude` matches, and of their contents. */
|
|
497
|
+
async excludedRefs() {
|
|
498
|
+
const refs = /* @__PURE__ */ new Set();
|
|
499
|
+
const selector = this.options.snapshotExclude;
|
|
500
|
+
if (!selector) return refs;
|
|
501
|
+
const matches = this.page.locator(selector);
|
|
502
|
+
const count = Math.min(await matches.count().catch(() => 0), SNAPSHOT_EXCLUDE_LIMIT);
|
|
503
|
+
for (let index = 0; index < count; index += 1) {
|
|
504
|
+
const nodes = await matches.nth(index).ariaSnapshotJSON({ mode: "ai", timeout: ACTION_TIMEOUT_MS }).catch(() => null);
|
|
505
|
+
collectRefs(nodes, refs);
|
|
506
|
+
}
|
|
507
|
+
return refs;
|
|
265
508
|
}
|
|
266
509
|
async revision() {
|
|
267
510
|
const value = await this.page.evaluate((key) => globalThis[key], REVISION_KEY).catch(() => void 0);
|
|
@@ -287,7 +530,8 @@ var PlaywrightOperationExecutor = class {
|
|
|
287
530
|
if (call.operation === "browser.snapshot") {
|
|
288
531
|
await this.refreshRefs(input.locator);
|
|
289
532
|
const scoped = input.locator ? this.locator(input.locator).first() : null;
|
|
290
|
-
const
|
|
533
|
+
const excluded = await this.excludedRefs();
|
|
534
|
+
const text = snapshotWithoutRefs(scoped ? await this.act(scoped, "ariaSnapshot", () => scoped.ariaSnapshot({ mode: "ai", timeout: ACTION_TIMEOUT_MS })) : await page.ariaSnapshot({ mode: "ai", timeout: ACTION_TIMEOUT_MS }), excluded);
|
|
291
535
|
const bounded = text.slice(0, 64e3);
|
|
292
536
|
return this.success(call.operation, { text: bounded, truncated: bounded.length < text.length });
|
|
293
537
|
}
|
|
@@ -323,35 +567,48 @@ var PlaywrightOperationExecutor = class {
|
|
|
323
567
|
if (call.operation === "browser.script") return this.script(call);
|
|
324
568
|
if (call.operation === "browser.mouse") {
|
|
325
569
|
const action = input.action;
|
|
326
|
-
|
|
327
|
-
|
|
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);
|
|
328
575
|
else if (action === "down") await page.mouse.down();
|
|
329
576
|
else await page.mouse.up();
|
|
577
|
+
if (action === "move" || action === "click") this.mouse = { x: at.x, y: at.y };
|
|
330
578
|
return this.success(call.operation, { action, ...typeof input.x === "number" ? { x: input.x, y: input.y } : {} });
|
|
331
579
|
}
|
|
332
580
|
if (call.operation === "browser.wheel") {
|
|
333
581
|
const deltaX = typeof input.deltaX === "number" ? input.deltaX : 0;
|
|
334
582
|
const deltaY = typeof input.deltaY === "number" ? input.deltaY : 0;
|
|
583
|
+
await this.guardPoint("wheel", this.mouse.x, this.mouse.y);
|
|
335
584
|
await page.mouse.wheel(deltaX, deltaY);
|
|
336
585
|
return this.success(call.operation, { deltaX, deltaY });
|
|
337
586
|
}
|
|
338
587
|
if (call.operation === "browser.drag") {
|
|
339
588
|
const resolve = async (endpoint) => {
|
|
340
|
-
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
|
+
}
|
|
341
594
|
await this.refreshRefs(endpoint.locator);
|
|
342
595
|
const locator2 = this.locator(endpoint.locator).first();
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
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
|
+
}
|
|
346
604
|
};
|
|
347
|
-
for (const end of [input.from, input.to])
|
|
348
|
-
if (end.locator !== void 0) await this.guard({ action: "drag", locator: this.locator(end.locator).first() });
|
|
349
605
|
const from = await resolve(input.from);
|
|
350
606
|
const to = await resolve(input.to);
|
|
351
607
|
await page.mouse.move(from.x, from.y);
|
|
352
608
|
await page.mouse.down();
|
|
353
609
|
await page.mouse.move(to.x, to.y, { steps: typeof input.steps === "number" ? input.steps : 8 });
|
|
354
610
|
await page.mouse.up();
|
|
611
|
+
this.mouse = to;
|
|
355
612
|
return this.success(call.operation, { from, to });
|
|
356
613
|
}
|
|
357
614
|
if (call.operation === "browser.back") {
|
|
@@ -375,28 +632,38 @@ var PlaywrightOperationExecutor = class {
|
|
|
375
632
|
return this.success(call.operation, box);
|
|
376
633
|
}
|
|
377
634
|
if (call.operation === "browser.press" && !locator) {
|
|
378
|
-
|
|
379
|
-
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
|
+
}
|
|
380
642
|
return this.success(call.operation, { key: input.key });
|
|
381
643
|
}
|
|
382
644
|
const target = locator;
|
|
383
|
-
const
|
|
384
|
-
await this.
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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 }) };
|
|
389
652
|
const before = await this.inspect(target).catch(() => null);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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);
|
|
400
667
|
}
|
|
401
668
|
const after = await this.inspect(target).catch(() => null);
|
|
402
669
|
const inspection = after ?? before;
|
|
@@ -411,6 +678,7 @@ var PlaywrightOperationExecutor = class {
|
|
|
411
678
|
const timeout = Math.min(requested, SCRIPT_TIMEOUT_CLAMP_MS);
|
|
412
679
|
const AsyncFunction = Object.getPrototypeOf(async function() {
|
|
413
680
|
}).constructor;
|
|
681
|
+
if (this.options.actionGuard) await this.options.actionGuard({ action: "script", source, args });
|
|
414
682
|
let run;
|
|
415
683
|
try {
|
|
416
684
|
run = new AsyncFunction("page", "args", source);
|
|
@@ -463,6 +731,17 @@ function operationName(raw) {
|
|
|
463
731
|
const candidate = raw && typeof raw === "object" && !Array.isArray(raw) ? raw.operation : void 0;
|
|
464
732
|
return typeof candidate === "string" && BROWSER_OPERATION_NAMES.includes(candidate) ? candidate : "browser.status";
|
|
465
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
|
+
}
|
|
466
745
|
export {
|
|
467
746
|
ACTION_TIMEOUT_MS,
|
|
468
747
|
PLAYWRIGHT_FEATURES,
|
|
@@ -470,6 +749,7 @@ export {
|
|
|
470
749
|
PlaywrightOperationExecutor,
|
|
471
750
|
SCRIPT_TIMEOUT_CLAMP_MS,
|
|
472
751
|
WAIT_TIMEOUT_CLAMP_MS,
|
|
473
|
-
classifyPlaywrightError
|
|
752
|
+
classifyPlaywrightError,
|
|
753
|
+
snapshotWithoutRefs
|
|
474
754
|
};
|
|
475
755
|
//# sourceMappingURL=executor.mjs.map
|