Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
98 changes: 91 additions & 7 deletions apps/desktop/src/preview/CdpRelay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ describe("CDP relay extra sessions", () => {
});

describe("CDP relay downloads", () => {
it("applies the server's download directory and reports downloads on the root session", async () => {
it("captures preview downloads without changing the default browser context", async () => {
const send = vi.fn(async () => ({}));
const directories: Array<string | null> = [];
const written: Array<Record<string, unknown>> = [];
Expand All @@ -201,15 +201,99 @@ describe("CDP relay downloads", () => {
);
await settle();
expect(directories).toEqual(["/srv/artifacts"]);
expect(send).toHaveBeenCalledWith(
"Browser.setDownloadBehavior",
{ behavior: "allowAndName", downloadPath: "/srv/artifacts", eventsEnabled: true },
undefined,
);
expect(send.mock.calls).toEqual([
["Page.setDownloadBehavior", { behavior: "default" }, undefined],
]);
written.length = 0;
relay.event("Browser.downloadWillBegin", { guid: "g1", suggestedFilename: "r.csv" }, "");
relay.event("Page.downloadWillBegin", { guid: "g1", suggestedFilename: "r.csv" }, "");
relay.event("Page.downloadProgress", { guid: "g1", state: "completed" }, "");
expect(written).toEqual([
{ method: "Browser.downloadWillBegin", params: { guid: "g1", suggestedFilename: "r.csv" } },
{ method: "Browser.downloadProgress", params: { guid: "g1", state: "completed" } },
]);
});

it("keeps denial scoped to the preview and restores Electron's save handling when allowed", async () => {
const send = vi.fn(async () => ({}));
const directories: Array<string | null> = [];
const relay = createCdpRelayConnection(
makeTarget(send, (directory) => directories.push(directory)),
() => {},
);
for (const [id, behavior] of ["deny", "allowAndName"].entries()) {
relay.receive(
JSON.stringify({
id,
method: "Browser.setDownloadBehavior",
params: { behavior, downloadPath: "/srv/artifacts" },
}),
);
await settle();
}
expect(directories).toEqual([null, "/srv/artifacts"]);
expect(send.mock.calls).toEqual([
["Page.setDownloadBehavior", { behavior: "deny" }, undefined],
["Page.setDownloadBehavior", { behavior: "default" }, undefined],
]);
});

it("does not publish download events when the connection has disabled them", async () => {
const written: Array<Record<string, unknown>> = [];
const relay = createCdpRelayConnection(makeTarget(vi.fn(async () => ({}))), (raw) =>
written.push(JSON.parse(raw)),
);
relay.receive(JSON.stringify({ id: 1, method: "Target.setAutoAttach", params: {} }));
relay.receive(
JSON.stringify({
id: 2,
method: "Browser.setDownloadBehavior",
params: { behavior: "allowAndName", downloadPath: "/srv/artifacts", eventsEnabled: false },
}),
);
await settle();
written.length = 0;
relay.event("Page.downloadWillBegin", { guid: "g1" }, "");
relay.event("Page.downloadProgress", { guid: "g1", state: "completed" }, "");
expect(written).toEqual([]);
});

it.each([false, true])(
"keeps the previous download event setting (%s) when configuration fails",
async (enabled) => {
const send = vi
.fn(async () => ({}))
.mockResolvedValueOnce({})
.mockRejectedValueOnce(new Error("Page command rejected"));
const written: Array<Record<string, unknown>> = [];
const relay = createCdpRelayConnection(makeTarget(send), (raw) =>
written.push(JSON.parse(raw)),
);
relay.receive(JSON.stringify({ id: 1, method: "Target.setAutoAttach", params: {} }));
for (const [id, eventsEnabled] of [enabled, !enabled].entries()) {
relay.receive(
JSON.stringify({
id: id + 2,
method: "Browser.setDownloadBehavior",
params: { behavior: "allowAndName", downloadPath: "/srv/artifacts", eventsEnabled },
}),
);
await settle();
}
expect(written).toContainEqual({
id: 3,
error: { code: -32000, message: "Page command rejected" },
});
written.length = 0;
relay.event("Page.downloadWillBegin", { guid: "g1" }, "");
relay.event("Page.downloadProgress", { guid: "g1", state: "completed" }, "");
expect(written).toEqual(
enabled
? [
{ method: "Browser.downloadWillBegin", params: { guid: "g1" } },
{ method: "Browser.downloadProgress", params: { guid: "g1", state: "completed" } },
]
: [],
);
},
);
});
14 changes: 11 additions & 3 deletions apps/desktop/src/preview/CdpRelay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function createCdpRelayConnection(
): CdpRelayConnection {
const send = (message: Record<string, unknown>) => write(JSON.stringify(message));
let attached = false;
let downloadEventsEnabled = false;
/** Relay-created page sessions share one real debugger; child frame sessions retain their own ids. */
const sessions = new Map<string, "browser" | "page">([[PAGE_SESSION_ID, "page"]]);
let sessionSequence = 0;
Expand All @@ -78,8 +79,14 @@ export function createCdpRelayConnection(
const directory = command.params?.["downloadPath"];
const allowed = command.params?.["behavior"] !== "deny" && typeof directory === "string";
target.setDownloadDirectory(allowed ? directory : null);
const { browserContextId: _ignored, ...params } = command.params ?? {};
return target.send("Browser.setDownloadBehavior", params, undefined);
// Browser-scoped behavior targets Electron's app session; preview paths belong to will-download.
const result = await target.send(
"Page.setDownloadBehavior",
{ behavior: command.params?.["behavior"] === "deny" ? "deny" : "default" },
undefined,
);
downloadEventsEnabled = command.params?.["eventsEnabled"] === true;
return result;
}
case "Browser.getVersion":
return {
Expand Down Expand Up @@ -186,9 +193,10 @@ export function createCdpRelayConnection(
},
event: (method, params, sessionId) => {
if (!attached) return;
if (method.startsWith("Page.download")) method = method.replace("Page.", "Browser.");
// Download events are the browser's; Playwright listens on its root session.
if (method.startsWith("Browser.download")) {
send({ method, params });
if (downloadEventsEnabled) send({ method, params });
return;
}
// Electron reports the page's own events with an empty session id.
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/preview/DesktopBrowserHost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -934,9 +934,11 @@ describe("DesktopBrowserHost", () => {
}),
}),
);
debuggee.emit("Browser.downloadWillBegin", { guid: "guid-1", suggestedFilename: "r.csv" });
debuggee.emit("Page.downloadWillBegin", { guid: "guid-1", suggestedFilename: "r.csv" });
expect(host.placeDownload(makeDebuggee().tab.webContents, item)).toBe(false);
expect(host.placeDownload(debuggee.tab.webContents, item)).toBe(true);
expect(paths).toEqual(["/srv/downloads/guid-1"]);
expect(host.placeDownload(debuggee.tab.webContents, item)).toBe(false);
}),
);

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/preview/DesktopBrowserHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ export const make = Effect.gen(function* () {
focusReleasing: false,
screencasts: new Set(),
onMessage: (_event, method, params, sessionId) => {
if (method === "Browser.downloadWillBegin") {
if (method === "Page.downloadWillBegin") {
const guid = (params as { guid?: unknown } | undefined)?.guid;
tab.pendingDownloadGuid = typeof guid === "string" ? guid : null;
}
Expand Down
Loading