diff --git a/apps/server/src/preview/ServerBrowserPage.test.ts b/apps/server/src/preview/ServerBrowserPage.test.ts index 36b133cf185d8..97aaf9c91f2fe 100644 --- a/apps/server/src/preview/ServerBrowserPage.test.ts +++ b/apps/server/src/preview/ServerBrowserPage.test.ts @@ -765,6 +765,52 @@ describe("server browser drag", () => { }); }); +describe("server browser action timeout", () => { + type StepOptions = { readonly timeout?: number }; + it.each([ + { + action: "hover", + act: (page: Page) => ServerBrowserPage.hover(page, { selector: "#a", timeoutMs: 9_000 }), + last: ["#a.hover 1"], + }, + { + action: "drag", + act: (page: Page) => + ServerBrowserPage.drag(page, { source: "#a", target: "#b", timeoutMs: 9_000 }), + last: ["#a.dragTo 1", "#b.boundingBox 1"], + }, + ])("gives the $action's steps one shared timeout", async ({ act, last }) => { + let now = 0; + using _now = vi.spyOn(Date, "now").mockImplementation(() => now); + const steps: Array = []; + const box = { x: 0, y: 0, width: 20, height: 20 }; + const page = { + locator: (selector: string) => { + const record = (step: string, options: StepOptions) => + steps.push(`${selector}.${step} ${options.timeout}`); + return { + // Each step that finds the target takes 5 of the 9 seconds. + scrollIntoViewIfNeeded: async (options: StepOptions) => { + record("scrollIntoViewIfNeeded", options); + now += 5_000; + }, + boundingBox: async (options: StepOptions) => { + record("boundingBox", options); + now += 5_000; + return box; + }, + hover: async (options: StepOptions) => record("hover", options), + dragTo: async (_target: unknown, options: StepOptions) => record("dragTo", options), + }; + }, + } as unknown as Page; + + await act(page); + + expect(steps).toEqual(["#a.scrollIntoViewIfNeeded 9000", "#a.boundingBox 4000", ...last]); + }); +}); + describe("server browser click", () => { it("clicks a target the page replaced while it was being scrolled into view", async () => { let scrolls = 0; diff --git a/apps/server/src/preview/ServerBrowserPage.ts b/apps/server/src/preview/ServerBrowserPage.ts index bcfbc296a2cb0..657bed9b98822 100644 --- a/apps/server/src/preview/ServerBrowserPage.ts +++ b/apps/server/src/preview/ServerBrowserPage.ts @@ -917,14 +917,15 @@ export const hover = async ( input: PreviewAutomationHoverInput, pointer: PointerReporter = noPointer, ) => { - const timeout = input.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const deadline = Date.now() + (input.timeoutMs ?? DEFAULT_TIMEOUT_MS); + const remaining = () => Math.max(1, deadline - Date.now()); const locator = targetLocator(page, input); - await pointer(await targetPoint(page, locator, input, () => timeout), "move"); + await pointer(await targetPoint(page, locator, input, remaining), "move"); if (locator === null) { await page.mouse.move(input.x ?? 0, input.y ?? 0); return; } - await locator.hover({ timeout }); + await locator.hover({ timeout: remaining() }); }; export const select = async ( @@ -967,17 +968,18 @@ export const drag = async ( input: PreviewAutomationDragInput, pointer: PointerReporter = noPointer, ) => { - const timeout = input.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const deadline = Date.now() + (input.timeoutMs ?? DEFAULT_TIMEOUT_MS); + const remaining = () => Math.max(1, deadline - Date.now()); const source = targetLocator(page, { locator: input.source })!; const target = targetLocator(page, { locator: input.target })!; - await pointer(await targetPoint(page, source, {}, () => timeout), "move"); + await pointer(await targetPoint(page, source, {}, remaining), "move"); // Observe drag rejection immediately; an unhandled rejection during cursor movement exits the server. // Chromium delivers a single move before dragstart; steps give the page mousemoves during the drag. - const dragFailure = source.dragTo(target, { timeout, steps: 5 }).then( + const dragFailure = source.dragTo(target, { timeout: remaining(), steps: 5 }).then( () => null, (error: unknown) => ({ error }), ); - const end = await target.boundingBox({ timeout }).catch(() => null); + const end = await target.boundingBox({ timeout: remaining() }).catch(() => null); if (end) await pointer({ x: end.x + end.width / 2, y: end.y + end.height / 2 }, "move"); const failure = await dragFailure; if (failure) throw failure.error;