diff --git a/.changeset/calm-cursors-appear.md b/.changeset/calm-cursors-appear.md new file mode 100644 index 000000000..3cf8523a8 --- /dev/null +++ b/.changeset/calm-cursors-appear.md @@ -0,0 +1,9 @@ +--- +"@browserbasehq/stagehand-protocol": patch +"@browserbasehq/stagehand-python": patch +"@browserbasehq/stagehand-extension": patch +"@browserbasehq/stagehand-go": patch +"@browserbasehq/stagehand": patch +--- + +expose page cursor overlay enablement across the V4 SDKs diff --git a/packages/docs/v4/reference/page.mdx b/packages/docs/v4/reference/page.mdx index fff407414..768f5c5fe 100644 --- a/packages/docs/v4/reference/page.mdx +++ b/packages/docs/v4/reference/page.mdx @@ -389,6 +389,18 @@ await page.addInitScript(() => { Resolves after the operation completes. +## enableCursorOverlay() + +Show the Stagehand cursor overlay used for coordinate-based interactions. + +```typescript +await page.enableCursorOverlay(); +``` + + + Resolves after the overlay is enabled. + + ## setExtraHTTPHeaders() Set additional HTTP headers for this page. @@ -1064,6 +1076,18 @@ await page.add_init_script("window.localStorage.clear()") Resolves after the operation completes. +## enable_cursor_overlay() + +Show the Stagehand cursor overlay used for coordinate-based interactions. + +```python +await page.enable_cursor_overlay() +``` + + + Resolves after the overlay is enabled. + + ## set_extra_http_headers() Set additional HTTP headers for this page. @@ -1778,6 +1802,20 @@ if err := page.AddInitScript(ctx, "window.localStorage.clear()"); err != nil { Returns `nil` after the operation completes. +## EnableCursorOverlay() + +Show the Stagehand cursor overlay used for coordinate-based interactions. + +```go +if err := page.EnableCursorOverlay(ctx); err != nil { + return err +} +``` + + + Returns `nil` after the overlay is enabled. + + ## SetExtraHTTPHeaders() Set additional HTTP headers for this page. diff --git a/packages/extension/controllers/pageController.ts b/packages/extension/controllers/pageController.ts index a3b4b776b..499cebe07 100644 --- a/packages/extension/controllers/pageController.ts +++ b/packages/extension/controllers/pageController.ts @@ -90,6 +90,11 @@ export function createPageController(runtime: StagehandRuntime) { return runtime.pageAddInitScript(params); } + async function enableCursorOverlay(params: PageIdParams, { logger }: HandlerContext) { + logger.debug("page.enable_cursor_overlay", {}); + return runtime.pageEnableCursorOverlay(params); + } + async function setExtraHTTPHeaders( params: PageSetExtraHTTPHeadersParams, { logger }: HandlerContext, @@ -192,6 +197,7 @@ export function createPageController(runtime: StagehandRuntime) { keyPress, evaluate, addInitScript, + enableCursorOverlay, setExtraHTTPHeaders, setViewportSize, waitForLoadState, diff --git a/packages/extension/rpcRouter.ts b/packages/extension/rpcRouter.ts index c796321fc..56218ed48 100644 --- a/packages/extension/rpcRouter.ts +++ b/packages/extension/rpcRouter.ts @@ -323,6 +323,11 @@ export class RPCRouter { parseParams(StagehandMethods.pageAddInitScript, request.params), context, ); + case "page.enable_cursor_overlay": + return this.pageController.enableCursorOverlay( + parseParams(StagehandMethods.pageEnableCursorOverlay, request.params), + context, + ); case "page.on": return this.pageController.on( parseParams(StagehandMethods.pageOn, request.params), diff --git a/packages/extension/runtime.ts b/packages/extension/runtime.ts index 403cd7fd3..c98181b93 100644 --- a/packages/extension/runtime.ts +++ b/packages/extension/runtime.ts @@ -148,6 +148,7 @@ export type UnderstudyRuntimePage = { keyPress(key: string, options?: PageKeyPressParams["options"]): Promise; evaluate(expression: string): Promise; addInitScript(source: string): Promise; + enableCursorOverlay(): Promise; setExtraHTTPHeaders(headers: PageSetExtraHTTPHeadersParams["headers"]): Promise; setViewportSize( width: number, @@ -739,6 +740,11 @@ export class StagehandRuntime { return { closed: true }; } + async pageEnableCursorOverlay(params: PageIdParams): Promise { + await this.resolvePage(params.pageId).enableCursorOverlay(); + return { ok: true }; + } + pageOn(params: PageOnParams): PageVoidResult { if (this.pageEventSubscriptions.has(params.subscriptionId)) { throw new DuplicatePageEventSubscriptionError(); diff --git a/packages/extension/tests/stagehand-clients.test.ts b/packages/extension/tests/stagehand-clients.test.ts index 3c1c78d37..21669a429 100644 --- a/packages/extension/tests/stagehand-clients.test.ts +++ b/packages/extension/tests/stagehand-clients.test.ts @@ -216,6 +216,7 @@ class FakeUnderstudyRuntimePage implements UnderstudyRuntimePage { readonly keyPressCalls: Array<{ key: string; options?: PageKeyPressParams["options"] }> = []; readonly evaluateCalls: string[] = []; readonly addInitScriptCalls: string[] = []; + enableCursorOverlayCalls = 0; readonly setExtraHTTPHeadersCalls: Array = []; readonly setViewportSizeCalls: Array<{ width: number; @@ -349,6 +350,10 @@ class FakeUnderstudyRuntimePage implements UnderstudyRuntimePage { this.addInitScriptCalls.push(source); } + async enableCursorOverlay(): Promise { + this.enableCursorOverlayCalls += 1; + } + async setExtraHTTPHeaders(headers: PageSetExtraHTTPHeadersParams["headers"]): Promise { this.setExtraHTTPHeadersCalls.push(headers); } @@ -1832,6 +1837,22 @@ describe("Stagehand worker clients", () => { expect(page.addInitScriptCalls).toStrictEqual(["globalThis.ready = true"]); }); + it("routes cursor overlay enablement", async () => { + const page = new FakeUnderstudyRuntimePage("page-a", "https://example.test/current"); + const handle = await createConfiguredHandler(new FakeBrowserSession([page])); + + await expect( + handle({ + jsonrpc: "2.0", + id: 240, + method: "page.enable_cursor_overlay", + params: { page_id: "page-a" }, + }), + ).resolves.toStrictEqual({ jsonrpc: "2.0", id: 240, result: { ok: true } }); + + expect(page.enableCursorOverlayCalls).toBe(1); + }); + it("routes page headers and viewport configuration", async () => { const page = new FakeUnderstudyRuntimePage("page-a", "https://example.test/current"); const handle = await createConfiguredHandler(new FakeBrowserSession([page])); diff --git a/packages/protocol/schema-registry.ts b/packages/protocol/schema-registry.ts index 1ebef362e..9040ec7c6 100644 --- a/packages/protocol/schema-registry.ts +++ b/packages/protocol/schema-registry.ts @@ -324,6 +324,11 @@ export const StagehandMethods = { params: PageAddInitScriptParamsSchema, result: PageVoidResultSchema, }, + pageEnableCursorOverlay: { + name: "page.enable_cursor_overlay", + params: PageIdParamsSchema, + result: PageVoidResultSchema, + }, pageOn: { name: "page.on", params: PageOnParamsSchema, diff --git a/packages/protocol/stagehand.v4.json b/packages/protocol/stagehand.v4.json index 35ab62440..08a9403e7 100644 --- a/packages/protocol/stagehand.v4.json +++ b/packages/protocol/stagehand.v4.json @@ -526,6 +526,19 @@ "required": ["params", "result"], "additionalProperties": false }, + "page.enable_cursor_overlay": { + "type": "object", + "properties": { + "params": { + "$ref": "#/$defs/PageIdParams" + }, + "result": { + "$ref": "#/$defs/PageVoidResult" + } + }, + "required": ["params", "result"], + "additionalProperties": false + }, "page.on": { "type": "object", "properties": { @@ -1036,6 +1049,7 @@ "page.key_press", "page.evaluate", "page.add_init_script", + "page.enable_cursor_overlay", "page.on", "page.off", "page.set_extra_http_headers", @@ -6257,6 +6271,33 @@ "required": ["jsonrpc", "id", "method", "params"], "additionalProperties": false }, + { + "type": "object", + "properties": { + "jsonrpc": { + "type": "string", + "const": "2.0" + }, + "id": { + "$ref": "#/$defs/JSONRPCRequestId" + }, + "method": { + "type": "string", + "const": "page.enable_cursor_overlay" + }, + "params": { + "$ref": "#/$defs/PageIdParams" + }, + "traceparent": { + "type": "string" + }, + "tracestate": { + "type": "string" + } + }, + "required": ["jsonrpc", "id", "method", "params"], + "additionalProperties": false + }, { "type": "object", "properties": { diff --git a/packages/protocol/tests/protocol/object-model-protocol.test.ts b/packages/protocol/tests/protocol/object-model-protocol.test.ts index d2c708ffa..241909e59 100644 --- a/packages/protocol/tests/protocol/object-model-protocol.test.ts +++ b/packages/protocol/tests/protocol/object-model-protocol.test.ts @@ -393,6 +393,7 @@ describe("Stagehand object-model protocol", () => { "page.key_press", "page.evaluate", "page.add_init_script", + "page.enable_cursor_overlay", "page.on", "page.off", "page.set_extra_http_headers", diff --git a/packages/sdk-go/internal/extensionassets/stagehand-extension.zip b/packages/sdk-go/internal/extensionassets/stagehand-extension.zip index dbac537ee..001ca5a31 100644 Binary files a/packages/sdk-go/internal/extensionassets/stagehand-extension.zip and b/packages/sdk-go/internal/extensionassets/stagehand-extension.zip differ diff --git a/packages/sdk-go/page.go b/packages/sdk-go/page.go index 6a98cbc7c..36b35b2a8 100644 --- a/packages/sdk-go/page.go +++ b/packages/sdk-go/page.go @@ -243,6 +243,13 @@ func (p *Page) AddInitScript(ctx context.Context, source string) error { return p.rpc.call(ctx, "page.add_init_script", params, &result) } +// EnableCursorOverlay renders the Stagehand cursor for coordinate-based interactions. +func (p *Page) EnableCursorOverlay(ctx context.Context) error { + params := PageIDParams{PageID: p.PageID()} + var result PageVoidResult + return p.rpc.call(ctx, "page.enable_cursor_overlay", params, &result) +} + // On subscribes to console events for this page and its OOPIF sessions. func (p *Page) On( ctx context.Context, diff --git a/packages/sdk-go/page_test.go b/packages/sdk-go/page_test.go index 7d99d1aa9..2f8d78d32 100644 --- a/packages/sdk-go/page_test.go +++ b/packages/sdk-go/page_test.go @@ -103,6 +103,25 @@ func TestPageCoordinateInteractionsReturnOnlyErrors(t *testing.T) { } } +func TestPageEnableCursorOverlay(t *testing.T) { + t.Parallel() + + rpc := &recordingProtocolClient{responses: map[string]any{ + "page.enable_cursor_overlay": PageVoidResult{Ok: true}, + }} + page := &Page{rpc: rpc, ref: PageRef{PageID: "page-1"}} + + if err := page.EnableCursorOverlay(context.Background()); err != nil { + t.Fatalf("EnableCursorOverlay() error = %v", err) + } + if len(rpc.calls) != 1 || rpc.calls[0].method != "page.enable_cursor_overlay" { + t.Fatalf("EnableCursorOverlay() calls = %#v", rpc.calls) + } + if got := rpc.calls[0].params; !reflect.DeepEqual(got, PageIDParams{PageID: "page-1"}) { + t.Fatalf("EnableCursorOverlay() params = %#v", got) + } +} + func TestPageOnDeliversCanonicalConsoleEventsAndUnsubscribes(t *testing.T) { t.Parallel() diff --git a/packages/sdk-python/src/stagehand/page.py b/packages/sdk-python/src/stagehand/page.py index d7df05010..21290c936 100644 --- a/packages/sdk-python/src/stagehand/page.py +++ b/packages/sdk-python/src/stagehand/page.py @@ -385,6 +385,13 @@ async def add_init_script(self, source: str | Path) -> None: PageVoidResult, ) + async def enable_cursor_overlay(self) -> None: + await self._rpc_client.send( + "page.enable_cursor_overlay", + PageIdParams(page_id=self.page_id), + PageVoidResult, + ) + async def on( self, event: PageEventName, diff --git a/packages/sdk-python/tests/test_page.py b/packages/sdk-python/tests/test_page.py index 8abf82dbe..fbff82f47 100644 --- a/packages/sdk-python/tests/test_page.py +++ b/packages/sdk-python/tests/test_page.py @@ -54,6 +54,19 @@ class EvaluationResult(BaseModel): RootResultT = TypeVar("RootResultT") +@pytest.mark.asyncio +async def test_page_enables_cursor_overlay() -> None: + recording = RecordingRPCClient({"page.enable_cursor_overlay": PageVoidResult(ok=True)}) + page = Page(cast(RPCClient, recording), PageRef(page_id="page-1")) + + await page.enable_cursor_overlay() + + method, params, result_model = recording.calls[0] + assert method == "page.enable_cursor_overlay" + assert params == PageIdParams(page_id="page-1") + assert result_model is PageVoidResult + + @pytest.mark.asyncio async def test_page_navigation_uses_generated_wire_models_and_updates_the_page_reference() -> None: recording = RecordingRPCClient({ diff --git a/packages/sdk-ts/src/page.ts b/packages/sdk-ts/src/page.ts index 8a0fc8f76..4362daf23 100644 --- a/packages/sdk-ts/src/page.ts +++ b/packages/sdk-ts/src/page.ts @@ -209,6 +209,12 @@ export class Page { }); } + async enableCursorOverlay(): Promise { + await this.rpcClient.send(StagehandMethods.pageEnableCursorOverlay, { + pageId: this.pageId, + }); + } + async on(event: PageEventName, listener: PageEventListener): Promise { const subscriptionId = crypto.randomUUID(); const removeNotificationListener = this.rpcClient.onNotification((notification) => { diff --git a/packages/sdk-ts/tests/objectWrapper.test.ts b/packages/sdk-ts/tests/objectWrapper.test.ts index 15ac59d4f..d9bd606c3 100644 --- a/packages/sdk-ts/tests/objectWrapper.test.ts +++ b/packages/sdk-ts/tests/objectWrapper.test.ts @@ -866,6 +866,18 @@ describe("Stagehand TS object wrapper", () => { ]); }); + it("enables the page cursor overlay", async () => { + const client = new FakeProtocolClient(); + client.queueResponse(StagehandMethods.pageEnableCursorOverlay, { ok: true }); + const page = new Page(client, { pageId: "page-1" }); + + await page.enableCursorOverlay(); + + expect(client.calls).toStrictEqual([ + requestCall(StagehandMethods.pageEnableCursorOverlay, { pageId: "page-1" }), + ]); + }); + it("routes page headers and viewport configuration", async () => { const client = new FakeProtocolClient(); client.queueResponse(StagehandMethods.pageSetExtraHTTPHeaders, { ok: true });