Skip to content

Commit b3d6d43

Browse files
ScottN-PVclaude
andcommitted
fix(server): a timed-out browser drag no longer exits the server
drag started dragTo, then awaited the target's position and a cursor move before awaiting the drag. A drag that timed out in that gap rejected with no handler attached, and the unhandled rejection exited the server, whose restart cancelled every running thread. The drag's outcome is now caught as it starts and reported after the cursor arrives. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent d81afa0 commit b3d6d43

2 files changed

Lines changed: 50 additions & 3 deletions

File tree

‎apps/server/src/preview/ServerBrowserPage.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,45 @@ describe("server browser element refs", () => {
246246
expect(await page.evaluate("window.seen")).toEqual(["click", "hover", "drop"]);
247247
});
248248
});
249+
250+
describe("server browser drag", () => {
251+
it("fails the drag, not the server, when it times out while the cursor is still moving", async () => {
252+
const timedOut = new Error("locator.dragTo: Timeout 30000ms exceeded.");
253+
const box = { x: 0, y: 0, width: 20, height: 20 };
254+
const targetBox = Promise.withResolvers<typeof box>();
255+
// Only the calls `drag` makes. The drag fails at once; the target's position arrives only
256+
// when the test hands it over, after the rejection has had a turn to go unobserved.
257+
const page = {
258+
locator: (selector: string) =>
259+
selector === "#card"
260+
? {
261+
scrollIntoViewIfNeeded: async () => {},
262+
boundingBox: async () => box,
263+
dragTo: () => Promise.reject(timedOut),
264+
}
265+
: { boundingBox: () => targetBox.promise },
266+
} as unknown as Page;
267+
const unobserved: Array<unknown> = [];
268+
const onUnhandled = (reason: unknown) => unobserved.push(reason);
269+
process.on("unhandledRejection", onUnhandled);
270+
try {
271+
let finished = false;
272+
const settled = ServerBrowserPage.drag(page, { source: "#card", target: "#lane" }).then(
273+
() => null,
274+
(error: unknown) => error,
275+
);
276+
void settled.then(() => (finished = true));
277+
// Two macrotask turns: Node reports a rejection nobody observed after the first.
278+
await new Promise((resolve) => setImmediate(resolve));
279+
await new Promise((resolve) => setImmediate(resolve));
280+
// The failed drag waits for the cursor to reach the target before it reports.
281+
expect(finished).toBe(false);
282+
targetBox.resolve(box);
283+
284+
expect(await settled).toBe(timedOut);
285+
expect(unobserved).toEqual([]);
286+
} finally {
287+
process.off("unhandledRejection", onUnhandled);
288+
}
289+
});
290+
});

‎apps/server/src/preview/ServerBrowserPage.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,11 +380,16 @@ export const drag = async (
380380
const source = targetLocator(page, { locator: input.source })!;
381381
const target = targetLocator(page, { locator: input.target })!;
382382
await pointer(await targetPoint(page, source, {}, timeout), "move");
383-
// The cursor travels with the drag; the page sees one continuous gesture.
384-
const dropped = source.dragTo(target, { timeout });
383+
// The cursor travels with the drag; the page sees one continuous gesture. Observe the drag's
384+
// outcome as it starts: a rejection nobody handles while the cursor moves exits the server.
385+
const dragFailure = source.dragTo(target, { timeout }).then(
386+
() => null,
387+
(error: unknown) => ({ error }),
388+
);
385389
const end = await target.boundingBox({ timeout }).catch(() => null);
386390
if (end) await pointer({ x: end.x + end.width / 2, y: end.y + end.height / 2 }, "move");
387-
await dropped;
391+
const failure = await dragFailure;
392+
if (failure) throw failure.error;
388393
};
389394

390395
/** Sets files on one file input; false when no locator or selector names one. */

0 commit comments

Comments
 (0)