diff --git a/extensions/browser/src/browser/cdp.ts b/extensions/browser/src/browser/cdp.ts index d7079719c1a..230e4a6c313 100644 --- a/extensions/browser/src/browser/cdp.ts +++ b/extensions/browser/src/browser/cdp.ts @@ -296,6 +296,9 @@ export type AriaSnapshotNode = { depth: number; }; +export const AX_REF_PREFIX = "ax"; +export const AX_REF_PATTERN = new RegExp(`^${AX_REF_PREFIX}\\d+$`); + export type RawAXNode = { nodeId?: string; role?: { value?: string }; @@ -362,7 +365,7 @@ export function formatAriaSnapshot(nodes: RawAXNode[], limit: number): AriaSnaps const name = axValue(n.name); const value = axValue(n.value); const description = axValue(n.description); - const ref = `ax${out.length + 1}`; + const ref = `${AX_REF_PREFIX}${out.length + 1}`; out.push({ ref, role: role || "unknown", diff --git a/extensions/browser/src/browser/pw-session.test.ts b/extensions/browser/src/browser/pw-session.test.ts index a472cda6fcf..ef6f2749c7c 100644 --- a/extensions/browser/src/browser/pw-session.test.ts +++ b/extensions/browser/src/browser/pw-session.test.ts @@ -71,6 +71,13 @@ describe("pw-session refLocator", () => { expect(mocks.locator).toHaveBeenCalledWith("aria-ref=e1"); }); + + it("rejects axN refs from format=aria snapshots instead of timing out", () => { + const { page, mocks } = fakePage(); + + expect(() => refLocator(page, "ax12")).toThrow(/format=aria snapshot/); + expect(mocks.locator).not.toHaveBeenCalled(); + }); }); describe("pw-session role refs cache", () => { diff --git a/extensions/browser/src/browser/pw-session.ts b/extensions/browser/src/browser/pw-session.ts index 21dc079097e..212f3dc3a08 100644 --- a/extensions/browser/src/browser/pw-session.ts +++ b/extensions/browser/src/browser/pw-session.ts @@ -20,7 +20,7 @@ import { normalizeCdpHttpBaseForJsonEndpoints, withCdpSocket, } from "./cdp.helpers.js"; -import { normalizeCdpWsUrl } from "./cdp.js"; +import { AX_REF_PATTERN, normalizeCdpWsUrl } from "./cdp.js"; import { getChromeWebSocketUrl } from "./chrome.js"; import { BrowserTabNotFoundError } from "./errors.js"; import { @@ -884,6 +884,13 @@ export function refLocator(page: Page, ref: string) { return info.nth !== undefined ? locator.nth(info.nth) : locator; } + if (AX_REF_PATTERN.test(normalized)) { + throw new Error( + `Ref "${normalized}" comes from a format=aria snapshot and cannot be used with act. ` + + `Re-snapshot with format=ai and use the eN refs from that snapshot.`, + ); + } + return page.locator(`aria-ref=${normalized}`); } diff --git a/extensions/browser/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts b/extensions/browser/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts index 73c20bd5668..82ad1bb762c 100644 --- a/extensions/browser/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts +++ b/extensions/browser/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts @@ -60,6 +60,12 @@ describe("pw-tools-core", () => { errorMessage: 'Timeout 5000ms exceeded. waiting for locator("aria-ref=1") to be visible', expectedMessage: /not found or not visible/i, }, + { + name: "bare locator timeouts into snapshot hints", + errorMessage: + "locator.click: Timeout 30000ms exceeded.\nCall log:\n - waiting for locator('aria-ref=ax13')", + expectedMessage: /not found or not visible/i, + }, ])("rewrites $name", async ({ errorMessage, expectedMessage }) => { const click = vi.fn(async () => { throw new Error(errorMessage); diff --git a/extensions/browser/src/browser/pw-tools-core.shared.ts b/extensions/browser/src/browser/pw-tools-core.shared.ts index 37e6f5f1197..f51503ba785 100644 --- a/extensions/browser/src/browser/pw-tools-core.shared.ts +++ b/extensions/browser/src/browser/pw-tools-core.shared.ts @@ -64,7 +64,9 @@ export function toAIFriendlyError(error: unknown, selector: string): Error { if ( (message.includes("Timeout") || message.includes("waiting for")) && - (message.includes("to be visible") || message.includes("not visible")) + (message.includes("to be visible") || + message.includes("not visible") || + message.includes("waiting for locator(")) ) { return new Error( `Element "${selector}" not found or not visible. ` +