From ec90c738c087d805eca977f98f87ffe5a2a81c80 Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Wed, 23 Sep 2026 13:30:33 -0700 Subject: [PATCH 1/3] wire progress into locator action delegates --- packages/extension/understudy/deepLocator.ts | 69 +++++------ packages/extension/understudy/frameLocator.ts | 47 ++++---- packages/extension/understudy/locator.ts | 72 ++++++------ .../understudy/locatorActions.test.ts | 108 ++++++++++++++++++ .../extension/understudy/selectorResolver.ts | 20 ++-- .../extensionassets/stagehand-extension.zip | Bin 445871 -> 445921 bytes 6 files changed, 220 insertions(+), 96 deletions(-) create mode 100644 packages/extension/understudy/locatorActions.test.ts diff --git a/packages/extension/understudy/deepLocator.ts b/packages/extension/understudy/deepLocator.ts index 7476794b7..f09be1b1e 100644 --- a/packages/extension/understudy/deepLocator.ts +++ b/packages/extension/understudy/deepLocator.ts @@ -129,14 +129,14 @@ export class DeepLocatorDelegate { } // Locator API delegates - async click(options?: { button?: MouseButton; clickCount?: number }) { - return (await this.real()).click(options); + async click(options?: { button?: MouseButton; clickCount?: number }, progress?: Progress) { + return (await this.real(progress)).click(options, progress); } - async count() { - return (await this.real()).count(); + async count(progress?: Progress) { + return (await this.real(progress)).count(progress); } - async hover() { - return (await this.real()).hover(); + async hover(progress?: Progress) { + return (await this.real(progress)).hover(progress); } async fill(value: string) { return (await this.real()).fill(value); @@ -144,35 +144,35 @@ export class DeepLocatorDelegate { async type(text: string, options?: { delay?: number }) { return (await this.real()).type(text, options); } - async selectOption(values: string | string[]) { - return (await this.real()).selectOption(values); + async selectOption(values: string | string[], progress?: Progress) { + return (await this.real(progress)).selectOption(values, progress); } - async scrollTo(percent: number | string) { - return (await this.real()).scrollTo(percent); + async scrollTo(percent: number | string, progress?: Progress) { + return (await this.real(progress)).scrollTo(percent, progress); } - async isVisible() { - return (await this.real()).isVisible(); + async isVisible(progress?: Progress) { + return (await this.real(progress)).isVisible(progress); } - async isChecked() { - return (await this.real()).isChecked(); + async isChecked(progress?: Progress) { + return (await this.real(progress)).isChecked(progress); } - async inputValue() { - return (await this.real()).inputValue(); + async inputValue(progress?: Progress) { + return (await this.real(progress)).inputValue(progress); } - async textContent() { - return (await this.real()).textContent(); + async textContent(progress?: Progress) { + return (await this.real(progress)).textContent(progress); } - async innerHtml() { - return (await this.real()).innerHtml(); + async innerHtml(progress?: Progress) { + return (await this.real(progress)).innerHtml(progress); } - async innerText() { - return (await this.real()).innerText(); + async innerText(progress?: Progress) { + return (await this.real(progress)).innerText(progress); } - async centroid() { - return (await this.real()).centroid(); + async centroid(progress?: Progress) { + return (await this.real(progress)).centroid(progress); } - async backendNodeId() { - return (await this.real()).backendNodeId(); + async backendNodeId(progress?: Progress) { + return (await this.real(progress)).backendNodeId(progress); } async highlight(options?: { durationMs?: number; @@ -181,13 +181,16 @@ export class DeepLocatorDelegate { }) { return (await this.real()).highlight(options); } - async sendClickEvent(options?: { - bubbles?: boolean; - cancelable?: boolean; - composed?: boolean; - detail?: number; - }) { - return (await this.real()).sendClickEvent(options); + async sendClickEvent( + options?: { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + detail?: number; + }, + progress?: Progress, + ) { + return (await this.real(progress)).sendClickEvent(options, progress); } async setInputFiles(files: SetInputFilesArgument) { return (await this.real()).setInputFiles(files); diff --git a/packages/extension/understudy/frameLocator.ts b/packages/extension/understudy/frameLocator.ts index 99431d69d..e3dc40ca5 100644 --- a/packages/extension/understudy/frameLocator.ts +++ b/packages/extension/understudy/frameLocator.ts @@ -123,11 +123,14 @@ class LocatorDelegate { } // Locator API delegates - async click(options?: { button?: "left" | "right" | "middle"; clickCount?: number }) { - return (await this.real()).click(options); + async click( + options?: { button?: "left" | "right" | "middle"; clickCount?: number }, + progress?: Progress, + ) { + return (await this.real(progress)).click(options, progress); } - async hover() { - return (await this.real()).hover(); + async hover(progress?: Progress) { + return (await this.real(progress)).hover(progress); } async fill(value: string) { return (await this.real()).fill(value); @@ -135,32 +138,32 @@ class LocatorDelegate { async type(text: string, options?: { delay?: number }) { return (await this.real()).type(text, options); } - async selectOption(values: string | string[]) { - return (await this.real()).selectOption(values); + async selectOption(values: string | string[], progress?: Progress) { + return (await this.real(progress)).selectOption(values, progress); } - async scrollTo(percent: number | string) { - return (await this.real()).scrollTo(percent); + async scrollTo(percent: number | string, progress?: Progress) { + return (await this.real(progress)).scrollTo(percent, progress); } - async isVisible() { - return (await this.real()).isVisible(); + async isVisible(progress?: Progress) { + return (await this.real(progress)).isVisible(progress); } - async isChecked() { - return (await this.real()).isChecked(); + async isChecked(progress?: Progress) { + return (await this.real(progress)).isChecked(progress); } - async inputValue() { - return (await this.real()).inputValue(); + async inputValue(progress?: Progress) { + return (await this.real(progress)).inputValue(progress); } - async textContent() { - return (await this.real()).textContent(); + async textContent(progress?: Progress) { + return (await this.real(progress)).textContent(progress); } - async innerHtml() { - return (await this.real()).innerHtml(); + async innerHtml(progress?: Progress) { + return (await this.real(progress)).innerHtml(progress); } - async innerText() { - return (await this.real()).innerText(); + async innerText(progress?: Progress) { + return (await this.real(progress)).innerText(progress); } - async count() { - return (await this.real()).count(); + async count(progress?: Progress) { + return (await this.real(progress)).count(progress); } first(): LocatorDelegate { return this.nth(0); diff --git a/packages/extension/understudy/locator.ts b/packages/extension/understudy/locator.ts index 88fede0e0..ec9cc69f0 100644 --- a/packages/extension/understudy/locator.ts +++ b/packages/extension/understudy/locator.ts @@ -153,9 +153,9 @@ export class Locator { * Return the DOM backendNodeId for this locator's target element. * Useful for identity comparisons without needing element handles. */ - async backendNodeId(): Promise { + async backendNodeId(progress?: Progress): Promise { const session = this.frame.session; - const { objectId } = await this.resolveNode(); + const { objectId } = await this.resolveNode(progress); try { await session.send("DOM.enable").catch(() => {}); const { node } = await session.send<{ node: Protocol.DOM.Node }>("DOM.describeNode", { @@ -168,20 +168,20 @@ export class Locator { } /** Return how many nodes the current selector resolves to. */ - public async count(): Promise { + public async count(progress?: Progress): Promise { const session = this.frame.session; await session.send("Runtime.enable"); await session.send("DOM.enable"); - return this.selectorResolver.count(this.selectorQuery); + return this.selectorResolver.count(this.selectorQuery, progress); } /** * Return the center of the element's bounding box in the owning frame's viewport * (CSS pixels), rounded to integers. Scrolls into view best-effort. */ - public async centroid(): Promise<{ x: number; y: number }> { + public async centroid(progress?: Progress): Promise<{ x: number; y: number }> { const session = this.frame.session; - const { objectId } = await this.resolveNode(); + const { objectId } = await this.resolveNode(progress); try { await session.send("DOM.scrollIntoViewIfNeeded", { objectId }).catch(() => {}); const box = await session.send("DOM.getBoxModel", { @@ -271,9 +271,9 @@ export class Locator { * Move the mouse cursor to the element's visual center without clicking. * - Scrolls into view best-effort, resolves geometry, then dispatches a mouse move. */ - async hover(): Promise { + async hover(progress?: Progress): Promise { const session = this.frame.session; - const { objectId } = await this.resolveNode(); + const { objectId } = await this.resolveNode(progress); try { await session.send("DOM.scrollIntoViewIfNeeded", { objectId }).catch(() => {}); @@ -302,9 +302,12 @@ export class Locator { * 3) Read geometry via `DOM.getBoxModel({ objectId })` → compute a center point. * 4) Synthesize mouse press + release via `Input.dispatchMouseEvent`. */ - async click(options?: { button?: MouseButton; clickCount?: number }): Promise { + async click( + options?: { button?: MouseButton; clickCount?: number }, + progress?: Progress, + ): Promise { const session = this.frame.session; - const { objectId } = await this.resolveNode(); + const { objectId } = await this.resolveNode(progress); const button = options?.button ?? "left"; const clickCount = options?.clickCount ?? 1; @@ -369,14 +372,17 @@ export class Locator { * - Does not synthesize real pointer input; directly dispatches an event. * - Useful for elements that rely on click handlers without needing hit-testing. */ - async sendClickEvent(options?: { - bubbles?: boolean; - cancelable?: boolean; - composed?: boolean; - detail?: number; - }): Promise { + async sendClickEvent( + options?: { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + detail?: number; + }, + progress?: Progress, + ): Promise { const session = this.frame.session; - const { objectId } = await this.resolveNode(); + const { objectId } = await this.resolveNode(progress); const bubbles = options?.bubbles ?? true; const cancelable = options?.cancelable ?? true; const composed = options?.composed ?? true; @@ -403,9 +409,9 @@ export class Locator { * - If the element is or , scrolls the window/document. * - Otherwise, scrolls the element itself via element.scrollTo. */ - async scrollTo(percent: number | string): Promise { + async scrollTo(percent: number | string, progress?: Progress): Promise { const session = this.frame.session; - const { objectId } = await this.resolveNode(); + const { objectId } = await this.resolveNode(progress); try { await session.send("Runtime.callFunctionOn", { objectId, @@ -582,10 +588,10 @@ export class Locator { * Select one or more options on a `