@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 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. The provider listens on `127.0.0.1` and writes an owner-only
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 locator action (`click`,
133
- `fill`, `press`, `hover`, `focus`, `check`, `uncheck`, `select`, `drag`) with
134
- the action's `Locator` and value, and before each `browser.script` with
135
- `{action: "script", source, args}`; throwing a `BrowserActionRefusal` answers
136
- the call with its code, such as `APPROVAL_REQUIRED`. A script's own locator
137
- calls are the real `page`'s and are not asked again, so a host that guards
138
- locator actions guards scripts as a whole.
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
- The page is not changed, so assistive technology still reads those elements.
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.
@@ -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 Locator, type Page } from "playwright-core";
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, and each `browser.script` before its source is compiled. Throwing
27
- * a `BrowserActionRefusal` refuses the operation with that code.
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 (Playwright's, which pierces open shadow roots) for the
32
- * host's own elements: `browser.snapshot` leaves out every matching
33
- * element's nodes, and a node left with nothing by that. The page itself is
34
- * not changed, so what assistive technology reads stays the same.
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 = PlaywrightLocatorAction | PlaywrightScriptAction;
40
- /** A locator action. */
41
- export interface PlaywrightLocatorAction {
42
- action: "click" | "fill" | "press" | "hover" | "focus" | "check" | "uncheck" | "select" | "drag";
43
- locator: Locator;
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
- /** The host's policy for an action; an element that is not there is the action's own failure. */
80
- private guard;
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
- /** The host's policy for an action; an element that is not there is the action's own failure. */
305
- async guard(action) {
306
- if (!this.options.actionGuard || await action.locator.count().catch(() => 0) === 0) return;
307
- await this.options.actionGuard(action);
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
- if (action === "move") await page.mouse.move(input.x, input.y);
384
- else if (action === "click") await page.mouse.click(input.x, input.y);
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) return { x: endpoint.x, y: endpoint.y };
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 box = await locator2.count() === 0 ? null : await locator2.boundingBox({ timeout: ACTION_TIMEOUT_MS });
401
- if (!box) throw new BrowserActionRefusal("NOT_FOUND", "The drag locator matched no element.");
402
- return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
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
- await this.guard({ action: "press", locator: page.locator("*:focus").first(), value: input.key });
436
- await page.keyboard.press(input.key);
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 guarded = call.operation === "browser.select" ? "select" : call.operation.slice("browser.".length);
441
- await this.guard({
442
- action: guarded,
443
- locator: target.first(),
444
- ...call.operation === "browser.fill" ? { value: input.value } : call.operation === "browser.press" ? { value: input.key } : call.operation === "browser.select" ? { value: input.values } : {}
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
- if (call.operation === "browser.click") await this.act(target, "click", () => target.click({ timeout }));
448
- else if (call.operation === "browser.fill") await this.act(target, "fill", () => target.fill(input.value, { timeout }));
449
- else if (call.operation === "browser.press") await this.act(target, "press", () => target.press(input.key, { timeout }));
450
- else if (call.operation === "browser.hover") await this.act(target, "hover", () => target.hover({ timeout }));
451
- else if (call.operation === "browser.focus") await this.act(target, "focus", () => target.focus({ timeout }));
452
- else if (call.operation === "browser.check") await this.act(target, "check", () => target.check({ timeout }));
453
- else if (call.operation === "browser.uncheck") await this.act(target, "uncheck", () => target.uncheck({ timeout }));
454
- else if (call.operation === "browser.select") {
455
- const values = await this.act(target, "selectOption", () => target.selectOption(input.values, { timeout }));
456
- return this.success(call.operation, { values });
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,