Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5409cb3
test(desktop): reproduce #15579, relay forces focus emulation onto hi…
juliusmarminge Oct 25, 2026
dd53cbd
fix(desktop): hidden preview tabs throttle again
juliusmarminge Oct 25, 2026
0d96515
test(desktop): reproduce #15579, focus emulation expires under a pend…
juliusmarminge Oct 25, 2026
0881cac
fix(desktop): a preview guest stays focused until its pending command…
juliusmarminge Oct 25, 2026
3d9f3a4
fix(desktop): an agent's preview tab stays focused for its whole turn
juliusmarminge Oct 25, 2026
726ceeb
fix(desktop): preview focus holds keep their owners through navigatio…
juliusmarminge Oct 25, 2026
15d6e0d
fix(desktop): preview focus holds follow the agent's own use and the …
juliusmarminge Oct 25, 2026
78c9213
fix(desktop): a hidden preview guest paints while the agent captures it
juliusmarminge Oct 25, 2026
c8e04b8
fix(desktop): a navigated preview tab keeps the server tab it answers…
juliusmarminge Oct 25, 2026
d13a9f7
fix(desktop): a detached viewer's queued screenshot never starts
t3dotgg Oct 26, 2026
c379f99
fix(desktop): preview focus holds end with the agent's own session an…
t3dotgg Oct 26, 2026
6600bc1
fix(desktop): a queued screenshot paints only while its relay and ses…
t3dotgg Oct 26, 2026
23b7079
fix(desktop): turn end releases queued screenshots, and a refused scr…
t3dotgg Oct 26, 2026
ac85fbd
fix(desktop): a person's input through a viewer is not agent use
t3dotgg Oct 26, 2026
0ff7016
fix(desktop): a screenshot queued past its turn's end goes unheld
t3dotgg Oct 26, 2026
05f0e36
fix(server): a child handed to another run still ends preview activity
t3dotgg Oct 26, 2026
184ffb4
fix(desktop): a refused child screencast leaves the page's own running
t3dotgg Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions apps/desktop/src/preview/CdpRelay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ describe("CDP relay", () => {
]);
pending[0]!();
await settle();
expect(send).toHaveBeenNthCalledWith(1, "Page.captureScreenshot", {}, undefined);
expect(send).toHaveBeenNthCalledWith(2, "DOM.enable", {}, "iframe-session");
expect(send).toHaveBeenNthCalledWith(1, "Page.captureScreenshot", {}, undefined, undefined);
expect(send).toHaveBeenNthCalledWith(2, "DOM.enable", {}, "iframe-session", undefined);
expect(written.at(-1)).toEqual({
id: 5,
result: { method: "Page.captureScreenshot" },
Expand All @@ -92,7 +92,30 @@ describe("CDP relay", () => {
}),
);
await settle();
expect(send.mock.calls).toEqual([["Runtime.runIfWaitingForDebugger", {}, "iframe-session"]]);
expect(send.mock.calls).toEqual([
["Runtime.runIfWaitingForDebugger", {}, "iframe-session", undefined],
]);
});

// Playwright enables focus emulation on every page it connects to, and Chromium then
// counts the guest as captured: a preview the panel hides keeps its timers unthrottled.
it("acknowledges Playwright's focus emulation without forcing it onto the guest", async () => {
const send = vi.fn(async () => ({}));
const written: Array<Record<string, unknown>> = [];
const relay = createCdpRelayConnection(makeTarget(send), (raw) =>
written.push(JSON.parse(raw)),
);
relay.receive(
JSON.stringify({
id: 1,
method: "Emulation.setFocusEmulationEnabled",
params: { enabled: true },
sessionId: "t3-preview-page",
}),
);
await settle();
expect(written).toEqual([{ id: 1, result: {}, sessionId: "t3-preview-page" }]);
expect(send).not.toHaveBeenCalled();
});

it("routes tab events to the page session once the page is announced", async () => {
Expand Down Expand Up @@ -147,7 +170,7 @@ describe("CDP relay extra sessions", () => {
expect(written.find((m) => m["id"] === 4)).toHaveProperty("error");
relay.receive(JSON.stringify({ id: 5, method: "Page.startScreencast", sessionId: extra }));
await settle();
expect(send).toHaveBeenLastCalledWith("Page.startScreencast", {}, undefined);
expect(send).toHaveBeenLastCalledWith("Page.startScreencast", {}, undefined, extra);
written.length = 0;
relay.event("Page.screencastFrame", { data: "x" }, undefined);
expect(written.map((m) => m["sessionId"])).toEqual(["t3-preview-page", extra]);
Expand Down
13 changes: 11 additions & 2 deletions apps/desktop/src/preview/CdpRelay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ export interface CdpRelayTarget {
method: string,
params: Record<string, unknown>,
sessionId: string | undefined,
/** The extra session that sent it, such as a viewer's; the main connection has none. */
owner?: string,
) => Promise<unknown>;
readonly sessionDetached?: (owner: string) => void;
/** The page's own CDP target id; Playwright keys the main frame by it. */
readonly targetId: () => Promise<string>;
readonly url: () => string;
Expand Down Expand Up @@ -111,8 +114,10 @@ export function createCdpRelayConnection(
}
case "Target.detachFromTarget": {
const sessionId = command.params?.["sessionId"];
if (typeof sessionId === "string" && sessionId !== PAGE_SESSION_ID)
if (typeof sessionId === "string" && sessionId !== PAGE_SESSION_ID) {
sessions.delete(sessionId);
target.sessionDetached?.(sessionId);
}
return {};
}
case "Target.setAutoAttach": {
Expand All @@ -133,11 +138,15 @@ export function createCdpRelayConnection(

const pageCommand = (command: CdpCommand): Promise<unknown> => {
const child = sessions.get(command.sessionId!) === "page" ? undefined : command.sessionId;
const owner =
child === undefined && command.sessionId !== PAGE_SESSION_ID ? command.sessionId : undefined;
// Child frames/workers pause on auto-attach until resumed; only the main page can skip this command.
if (command.method === "Runtime.runIfWaitingForDebugger" && child === undefined) {
return Promise.resolve({});
}
return target.send(command.method, command.params ?? {}, child);
// Playwright asks for this on every page; the host emulates focus only while it is in use.
if (command.method === "Emulation.setFocusEmulationEnabled") return Promise.resolve({});
return target.send(command.method, command.params ?? {}, child, owner);
};

return {
Expand Down
Loading
Loading