From 937b2d95e85b864fc5378125ef40ba77f53029c3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 07:13:21 +0000 Subject: [PATCH 1/5] test(server): preview click timeouts should name the missing target Reproduces #3714: a click on a selector that matches nothing surfaces only 'Preview automation click timed out after 15000ms.' Co-Authored-By: Claude Opus 5.5 --- .../src/mcp/PreviewAutomationBroker.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts index 7ae8bfbefead5..5fbbdd58ed7e1 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts @@ -317,6 +317,41 @@ it.effect.each([ ), ); +it.effect("names the click target that matched nothing when the server browser times out", () => + Effect.scoped( + Effect.gen(function* () { + const broker = yield* makeBroker; + const clientId = SERVER_BROWSER_AUTOMATION_CLIENT_ID; + const requests = requestsFrom(yield* broker.connect(makeHost({ clientId }))); + yield* Stream.runForEach(requests, (request) => + broker.respond({ + clientId, + connectionId: request.connectionId, + requestId: request.requestId, + ok: false, + error: { + _tag: "PreviewAutomationTimeoutError", + message: "locator.click: Timeout 15000ms exceeded.", + }, + }), + ).pipe(Effect.forkScoped); + yield* Effect.yieldNow; + + const error = yield* broker + .invoke({ + scope, + operation: "click", + input: { selector: ".definitely-no-such-t3-target", timeoutMs: 15_000 }, + }) + .pipe(Effect.flip); + + expect(error._tag).toBe("PreviewAutomationTimeoutError"); + expect(error.message).toContain(".definitely-no-such-t3-target"); + expect(error.message).toMatch(/no (element|match)|matched no|not found/i); + }), + ), +); + it.effect("distinguishes malformed remote failures", () => Effect.scoped( Effect.gen(function* () { From 717d99505b72b0d910b21fa0f5bda7284350e050 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 08:17:30 +0000 Subject: [PATCH 2/5] fix(server): preview click timeouts name a target that matched nothing A click on a selector that matched nothing failed with only "Preview automation click timed out after 15000ms.", indistinguishable from a target that exists but never became clickable. After Playwright's own wait times out, the server browser counts the target once (bounded so the reply still beats the broker's grace) and reports either that nothing matched the selector or that it matched but never became clickable. The broker now carries the server browser's timeout message as the error's reason, as it already did for execution errors. Co-Authored-By: Claude Opus 5.5 --- .../src/mcp/PreviewAutomationBroker.test.ts | 3 +- .../server/src/mcp/PreviewAutomationBroker.ts | 13 ++++-- .../src/preview/ServerBrowserPage.test.ts | 18 ++++++++ apps/server/src/preview/ServerBrowserPage.ts | 44 +++++++++++++++++++ packages/contracts/src/previewAutomation.ts | 3 +- 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts index 5fbbdd58ed7e1..ce802e57f3c28 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts @@ -331,7 +331,8 @@ it.effect("names the click target that matched nothing when the server browser t ok: false, error: { _tag: "PreviewAutomationTimeoutError", - message: "locator.click: Timeout 15000ms exceeded.", + message: + 'No element matched ".definitely-no-such-t3-target". Take a fresh snapshot and use its locator.', }, }), ).pipe(Effect.forkScoped); diff --git a/apps/server/src/mcp/PreviewAutomationBroker.ts b/apps/server/src/mcp/PreviewAutomationBroker.ts index c1ae21dfd3b4c..4cad5a4b4bf45 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.ts @@ -207,6 +207,14 @@ function remoteDetailKind(detail: unknown): RemoteDetailKind { const MAX_REASON_CHARS = 500; +const serverBrowserReason = ( + context: PreviewAutomationRequestErrorContext, + error: NonNullable, +) => + context.clientId === SERVER_BROWSER_AUTOMATION_CLIENT_ID + ? { reason: error.message.slice(0, MAX_REASON_CHARS) } + : {}; + const classifyResponseError = ( context: PreviewAutomationRequestErrorContext, error: NonNullable, @@ -257,6 +265,7 @@ const classifyResponseError = ( return new PreviewAutomationTimeoutError({ ...context, ...remoteDiagnostics, + ...serverBrowserReason(context, error), }); case "PreviewAutomationControlInterruptedError": { const reason = decodeControlReason(error.detail); @@ -334,9 +343,7 @@ const classifyResponseError = ( return new PreviewAutomationExecutionError({ ...context, ...remoteDiagnostics, - ...(context.clientId === SERVER_BROWSER_AUTOMATION_CLIENT_ID - ? { reason: error.message.slice(0, MAX_REASON_CHARS) } - : {}), + ...serverBrowserReason(context, error), }); } }; diff --git a/apps/server/src/preview/ServerBrowserPage.test.ts b/apps/server/src/preview/ServerBrowserPage.test.ts index 2491e6ac9a5fc..e616861d22f84 100644 --- a/apps/server/src/preview/ServerBrowserPage.test.ts +++ b/apps/server/src/preview/ServerBrowserPage.test.ts @@ -98,6 +98,24 @@ describe("server browser element refs", () => { expect(await page.locator("li").count()).toBe(5); }); + it("tells a click target that matches nothing from one that never becomes clickable", async () => { + await page.setContent(``); + const failure = (selector: string) => + ServerBrowserPage.click(page, { selector, timeoutMs: 500 }).then( + () => expect.unreachable(), + ServerBrowserPage.toOperationError, + ); + + const absent = await failure(".definitely-no-such-t3-target"); + expect(absent.tag).toBe("PreviewAutomationTimeoutError"); + expect(absent.message).toBe( + 'No element matched ".definitely-no-such-t3-target". Take a fresh snapshot and use its locator.', + ); + const hidden = await failure("button"); + expect(hidden.tag).toBe("PreviewAutomationTimeoutError"); + expect(hidden.message).toBe('"button" matched an element, but it never became clickable.'); + }); + it("does not retarget a removed ref to a replacement node", async () => { await page.setContent(""); const locator = buttonLocator((await takeSnapshot()).accessibilityTree, "original"); diff --git a/apps/server/src/preview/ServerBrowserPage.ts b/apps/server/src/preview/ServerBrowserPage.ts index 02089cca3205c..adec77222b6e3 100644 --- a/apps/server/src/preview/ServerBrowserPage.ts +++ b/apps/server/src/preview/ServerBrowserPage.ts @@ -64,6 +64,7 @@ export const toOperationError = (cause: unknown): ServerBrowserOperationError => }; const DEFAULT_TIMEOUT_MS = 15_000; +const TARGET_COUNT_MS = 250; const pageRefs = new WeakMap }>(); // A compact runtime namespace prevents old refs from aliasing after a server restart. @@ -650,6 +651,49 @@ export const click = async ( ): Promise<{ readonly x: number; readonly y: number }> => { const timeout = input.timeoutMs ?? DEFAULT_TIMEOUT_MS; const locator = targetLocator(page, input); + try { + return await clickTarget(page, locator, input, timeout, pointer); + } catch (cause) { + throw locator === null ? cause : await namedTargetTimeout(locator, input, cause); + } +}; + +/** + * Playwright's timeout names neither the target nor why it waited. Counting only after the + * wait failed keeps auto-wait intact while telling an absent target from one that never + * became actionable. + */ +const namedTargetTimeout = async ( + locator: Locator, + input: { readonly locator?: string | undefined; readonly selector?: string | undefined }, + cause: unknown, +) => { + if (!(cause instanceof Error && cause.name === "TimeoutError")) return cause; + const target = JSON.stringify(input.locator ?? input.selector); + // The host's reply must still beat the broker's short grace after the operation timeout. + let timer: ReturnType | undefined; + const matches = await Promise.race([ + locator.count().catch(() => undefined), + new Promise((resolve) => { + timer = setTimeout(() => resolve(undefined), TARGET_COUNT_MS); + }), + ]).finally(() => clearTimeout(timer)); + if (matches === undefined) return cause; + return new ServerBrowserOperationError( + "PreviewAutomationTimeoutError", + matches === 0 + ? `No element matched ${target}. Take a fresh snapshot and use its locator.` + : `${target} matched an element, but it never became clickable.`, + ); +}; + +const clickTarget = async ( + page: Page, + locator: Locator | null, + input: PreviewAutomationClickInput, + timeout: number, + pointer: PointerReporter, +) => { const point = await targetPoint(page, locator, input, timeout); await pointer(point, "click"); const options = { button: input.button ?? "left", clickCount: input.clickCount ?? 1 } as const; diff --git a/packages/contracts/src/previewAutomation.ts b/packages/contracts/src/previewAutomation.ts index 0e9ff94d6ffe5..f7df75c2d94ef 100644 --- a/packages/contracts/src/previewAutomation.ts +++ b/packages/contracts/src/previewAutomation.ts @@ -946,11 +946,12 @@ export class PreviewAutomationTimeoutError extends Schema.TaggedError Date: Thu, 22 Oct 2026 08:25:00 +0000 Subject: [PATCH 3/5] fix(server): click timeout diagnostics no longer claim the click never happened The target count runs after Playwright's timeout, and Playwright can dispatch a click and then time out waiting for its navigation. Describe what matches after the timeout and warn that the click may have happened. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/mcp/PreviewAutomationBroker.test.ts | 2 +- apps/server/src/preview/ServerBrowserPage.test.ts | 6 ++++-- apps/server/src/preview/ServerBrowserPage.ts | 9 +++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts index ce802e57f3c28..a5e110b8f864a 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts @@ -332,7 +332,7 @@ it.effect("names the click target that matched nothing when the server browser t error: { _tag: "PreviewAutomationTimeoutError", message: - 'No element matched ".definitely-no-such-t3-target". Take a fresh snapshot and use its locator.', + 'No element matches ".definitely-no-such-t3-target" after the timeout. Take a fresh snapshot and use its locator.', }, }), ).pipe(Effect.forkScoped); diff --git a/apps/server/src/preview/ServerBrowserPage.test.ts b/apps/server/src/preview/ServerBrowserPage.test.ts index e616861d22f84..711cfdce44170 100644 --- a/apps/server/src/preview/ServerBrowserPage.test.ts +++ b/apps/server/src/preview/ServerBrowserPage.test.ts @@ -109,11 +109,13 @@ describe("server browser element refs", () => { const absent = await failure(".definitely-no-such-t3-target"); expect(absent.tag).toBe("PreviewAutomationTimeoutError"); expect(absent.message).toBe( - 'No element matched ".definitely-no-such-t3-target". Take a fresh snapshot and use its locator.', + 'No element matches ".definitely-no-such-t3-target" after the timeout. Take a fresh snapshot and use its locator.', ); const hidden = await failure("button"); expect(hidden.tag).toBe("PreviewAutomationTimeoutError"); - expect(hidden.message).toBe('"button" matched an element, but it never became clickable.'); + expect(hidden.message).toBe( + '"button" still matches an element after the timeout. The click may have happened; check the page before retrying.', + ); }); it("does not retarget a removed ref to a replacement node", async () => { diff --git a/apps/server/src/preview/ServerBrowserPage.ts b/apps/server/src/preview/ServerBrowserPage.ts index adec77222b6e3..98ad8fbb890e5 100644 --- a/apps/server/src/preview/ServerBrowserPage.ts +++ b/apps/server/src/preview/ServerBrowserPage.ts @@ -660,8 +660,9 @@ export const click = async ( /** * Playwright's timeout names neither the target nor why it waited. Counting only after the - * wait failed keeps auto-wait intact while telling an absent target from one that never - * became actionable. + * wait failed keeps auto-wait intact, but the count describes the page after the timeout, not + * what the click did: Playwright can dispatch the click and then time out waiting for the + * navigation it started. */ const namedTargetTimeout = async ( locator: Locator, @@ -682,8 +683,8 @@ const namedTargetTimeout = async ( return new ServerBrowserOperationError( "PreviewAutomationTimeoutError", matches === 0 - ? `No element matched ${target}. Take a fresh snapshot and use its locator.` - : `${target} matched an element, but it never became clickable.`, + ? `No element matches ${target} after the timeout. Take a fresh snapshot and use its locator.` + : `${target} still matches an element after the timeout. The click may have happened; check the page before retrying.`, ); }; From b459078b4be677f49a3e2c10d9091c48f5ed7e17 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 08:52:34 +0000 Subject: [PATCH 4/5] fix(server): click timeouts with no remaining match no longer rule out the click A target can vanish because the click removed it or navigated, so the zero-match message now warns to check the page before retrying. The named-target wrapper also keeps Playwright's TimeoutError as its cause. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/mcp/PreviewAutomationBroker.test.ts | 2 +- apps/server/src/preview/ServerBrowserPage.test.ts | 3 ++- apps/server/src/preview/ServerBrowserPage.ts | 8 +++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts index a5e110b8f864a..4183a8bd35b57 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts @@ -332,7 +332,7 @@ it.effect("names the click target that matched nothing when the server browser t error: { _tag: "PreviewAutomationTimeoutError", message: - 'No element matches ".definitely-no-such-t3-target" after the timeout. Take a fresh snapshot and use its locator.', + 'No element matches ".definitely-no-such-t3-target" after the timeout. The click may have happened and removed its target or navigated; check the page before retrying, then take a fresh snapshot and use its locator.', }, }), ).pipe(Effect.forkScoped); diff --git a/apps/server/src/preview/ServerBrowserPage.test.ts b/apps/server/src/preview/ServerBrowserPage.test.ts index 711cfdce44170..8ac5264104fdd 100644 --- a/apps/server/src/preview/ServerBrowserPage.test.ts +++ b/apps/server/src/preview/ServerBrowserPage.test.ts @@ -109,8 +109,9 @@ describe("server browser element refs", () => { const absent = await failure(".definitely-no-such-t3-target"); expect(absent.tag).toBe("PreviewAutomationTimeoutError"); expect(absent.message).toBe( - 'No element matches ".definitely-no-such-t3-target" after the timeout. Take a fresh snapshot and use its locator.', + 'No element matches ".definitely-no-such-t3-target" after the timeout. The click may have happened and removed its target or navigated; check the page before retrying, then take a fresh snapshot and use its locator.', ); + expect(absent.cause).toMatchObject({ name: "TimeoutError" }); const hidden = await failure("button"); expect(hidden.tag).toBe("PreviewAutomationTimeoutError"); expect(hidden.message).toBe( diff --git a/apps/server/src/preview/ServerBrowserPage.ts b/apps/server/src/preview/ServerBrowserPage.ts index 98ad8fbb890e5..c5a80926f1d39 100644 --- a/apps/server/src/preview/ServerBrowserPage.ts +++ b/apps/server/src/preview/ServerBrowserPage.ts @@ -32,8 +32,8 @@ export class ServerBrowserOperationError extends Error { readonly tag: string; readonly detail: unknown; - constructor(tag: string, message: string, detail?: unknown) { - super(message); + constructor(tag: string, message: string, detail?: unknown, options?: ErrorOptions) { + super(message, options); this.tag = tag; this.detail = detail; } @@ -683,8 +683,10 @@ const namedTargetTimeout = async ( return new ServerBrowserOperationError( "PreviewAutomationTimeoutError", matches === 0 - ? `No element matches ${target} after the timeout. Take a fresh snapshot and use its locator.` + ? `No element matches ${target} after the timeout. The click may have happened and removed its target or navigated; check the page before retrying, then take a fresh snapshot and use its locator.` : `${target} still matches an element after the timeout. The click may have happened; check the page before retrying.`, + undefined, + { cause }, ); }; From ae4aa9a96d44876694a79e2f4c9b8433a331bf20 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:20:04 -0800 Subject: [PATCH 5/5] fix(server): only click timeouts carry a reason, and its warning comes first A server browser timeout forwarded Playwright's raw message as its reason, so preview_open repeated it. The click diagnostic now travels in the error detail, leads with the possible-click warning, caps the target, and still names the target when the count is unavailable. Co-Authored-By: Claude Opus 5.5 --- .../src/mcp/PreviewAutomationBroker.test.ts | 9 ++++--- .../server/src/mcp/PreviewAutomationBroker.ts | 25 +++++++++++-------- .../src/preview/ServerBrowserPage.test.ts | 4 +-- apps/server/src/preview/ServerBrowserPage.ts | 19 +++++++++----- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/apps/server/src/mcp/PreviewAutomationBroker.test.ts b/apps/server/src/mcp/PreviewAutomationBroker.test.ts index 427ac1271bf16..1c193096db273 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.test.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.test.ts @@ -349,8 +349,8 @@ it.effect("names the click target that matched nothing when the server browser t ok: false, error: { _tag: "PreviewAutomationTimeoutError", - message: - 'No element matches ".definitely-no-such-t3-target" after the timeout. The click may have happened and removed its target or navigated; check the page before retrying, then take a fresh snapshot and use its locator.', + message: "No element matches the target.", + detail: { reason: 'No element matches ".definitely-no-such-t3-target".' }, }, }), ).pipe(Effect.forkScoped); @@ -365,8 +365,9 @@ it.effect("names the click target that matched nothing when the server browser t .pipe(Effect.flip); expect(error._tag).toBe("PreviewAutomationTimeoutError"); - expect(error.message).toContain(".definitely-no-such-t3-target"); - expect(error.message).toMatch(/no (element|match)|matched no|not found/i); + expect(error.message).toBe( + 'Preview automation click timed out after 15000ms. No element matches ".definitely-no-such-t3-target".', + ); }), ), ); diff --git a/apps/server/src/mcp/PreviewAutomationBroker.ts b/apps/server/src/mcp/PreviewAutomationBroker.ts index 6339a9f64ff58..85646d387160d 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.ts @@ -205,15 +205,15 @@ const readRemoteTimeoutMs = (detail: unknown): number | undefined => { : undefined; }; -const MAX_REASON_CHARS = 500; +const readRemoteReason = (detail: unknown): string | undefined => + typeof detail === "object" && + detail !== null && + "reason" in detail && + typeof detail.reason === "string" + ? detail.reason + : undefined; -const serverBrowserReason = ( - context: PreviewAutomationRequestErrorContext, - error: NonNullable, -) => - context.clientId === SERVER_BROWSER_AUTOMATION_CLIENT_ID - ? { reason: error.message.slice(0, MAX_REASON_CHARS) } - : {}; +const MAX_REASON_CHARS = 500; const classifyResponseError = ( context: PreviewAutomationRequestErrorContext, @@ -263,10 +263,13 @@ const classifyResponseError = ( }); case "PreviewAutomationTimeoutError": { const remoteTimeoutMs = readRemoteTimeoutMs(error.detail); + const remoteReason = readRemoteReason(error.detail); return new PreviewAutomationTimeoutError({ ...context, ...remoteDiagnostics, - ...serverBrowserReason(context, error), + ...(context.clientId === SERVER_BROWSER_AUTOMATION_CLIENT_ID && remoteReason !== undefined + ? { reason: remoteReason.slice(0, MAX_REASON_CHARS) } + : {}), ...(remoteTimeoutMs === undefined ? {} : { timeoutMs: Math.min(context.timeoutMs, remoteTimeoutMs) }), @@ -348,7 +351,9 @@ const classifyResponseError = ( return new PreviewAutomationExecutionError({ ...context, ...remoteDiagnostics, - ...serverBrowserReason(context, error), + ...(context.clientId === SERVER_BROWSER_AUTOMATION_CLIENT_ID + ? { reason: error.message.slice(0, MAX_REASON_CHARS) } + : {}), }); } }; diff --git a/apps/server/src/preview/ServerBrowserPage.test.ts b/apps/server/src/preview/ServerBrowserPage.test.ts index 47eeb12ceec22..2c6d3a01bcf74 100644 --- a/apps/server/src/preview/ServerBrowserPage.test.ts +++ b/apps/server/src/preview/ServerBrowserPage.test.ts @@ -109,13 +109,13 @@ describe("server browser element refs", () => { const absent = await failure(".definitely-no-such-t3-target"); expect(absent.tag).toBe("PreviewAutomationTimeoutError"); expect(absent.message).toBe( - 'No element matches ".definitely-no-such-t3-target" after the timeout. The click may have happened and removed its target or navigated; check the page before retrying, then take a fresh snapshot and use its locator.', + 'The click may have happened and removed its target or navigated; check the page before retrying. No element matches ".definitely-no-such-t3-target" after the timeout; take a fresh snapshot and use its locator.', ); expect(absent.cause).toMatchObject({ name: "TimeoutError" }); const hidden = await failure("button"); expect(hidden.tag).toBe("PreviewAutomationTimeoutError"); expect(hidden.message).toBe( - '"button" still matches an element after the timeout. The click may have happened; check the page before retrying.', + 'The click may have happened; check the page before retrying. "button" still matches an element after the timeout.', ); }); diff --git a/apps/server/src/preview/ServerBrowserPage.ts b/apps/server/src/preview/ServerBrowserPage.ts index 9b6ca78a18a83..f128bf445e603 100644 --- a/apps/server/src/preview/ServerBrowserPage.ts +++ b/apps/server/src/preview/ServerBrowserPage.ts @@ -73,6 +73,7 @@ export const toOperationError = ( const DEFAULT_TIMEOUT_MS = 15_000; const TARGET_COUNT_MS = 250; +const MAX_TARGET_CHARS = 200; const pageRefs = new WeakMap }>(); // A compact runtime namespace prevents old refs from aliasing after a server restart. @@ -654,7 +655,10 @@ const namedTargetTimeout = async ( cause: unknown, ) => { if (!(cause instanceof Error && cause.name === "TimeoutError")) return cause; - const target = JSON.stringify(input.locator ?? input.selector); + const selector = JSON.stringify(input.locator ?? input.selector); + // The warning leads and the target is capped, so a reply cut short still carries the warning. + const target = + selector.length > MAX_TARGET_CHARS ? `${selector.slice(0, MAX_TARGET_CHARS)}...` : selector; // The host's reply must still beat the broker's short grace after the operation timeout. let timer: ReturnType | undefined; const matches = await Promise.race([ @@ -663,13 +667,16 @@ const namedTargetTimeout = async ( timer = setTimeout(() => resolve(undefined), TARGET_COUNT_MS); }), ]).finally(() => clearTimeout(timer)); - if (matches === undefined) return cause; + const reason = + matches === undefined + ? `The click may have happened; check the page before retrying. Whether ${target} still matches an element is unknown.` + : matches === 0 + ? `The click may have happened and removed its target or navigated; check the page before retrying. No element matches ${target} after the timeout; take a fresh snapshot and use its locator.` + : `The click may have happened; check the page before retrying. ${target} still matches an element after the timeout.`; return new ServerBrowserOperationError( "PreviewAutomationTimeoutError", - matches === 0 - ? `No element matches ${target} after the timeout. The click may have happened and removed its target or navigated; check the page before retrying, then take a fresh snapshot and use its locator.` - : `${target} still matches an element after the timeout. The click may have happened; check the page before retrying.`, - undefined, + reason, + { reason }, { cause }, ); };