1import { describe, expect, it, vi } from "vite-plus/test";
2
3import { createCdpRelayConnection, type CdpRelayTarget } from "./CdpRelay.ts";
4
5const makeTarget = (
6 send: CdpRelayTarget["send"],
7 setDownloadDirectory: CdpRelayTarget["setDownloadDirectory"] = () => {},
8): CdpRelayTarget => ({
9 send,
10 targetId: async () => "GUEST-TARGET",
11 url: () => "http://localhost:4719/",
12 title: () => "Fixture",
13 userAgent: () => "Electron",
14 setDownloadDirectory,
15});
16
17// Each relay reply resolves after a few microtasks; one macrotask lets them all land.
18const settle = () => new Promise<void>((resolve) => setImmediate(resolve));
19
20describe("CDP relay", () => {
21 it("announces only the tab's page, under its real target id", async () => {
22 const written: Array<Record<string, unknown>> = [];
23 const relay = createCdpRelayConnection(makeTarget(vi.fn()), (raw) =>
24 written.push(JSON.parse(raw)),
25 );
26 relay.receive(JSON.stringify({ id: 1, method: "Target.setAutoAttach", params: {} }));
27 relay.receive(JSON.stringify({ id: 2, method: "Target.createTarget", params: {} }));
28 await settle();
29 const attach = written.findIndex((message) => message["id"] === 1);
30 // The page is announced right after the auto-attach reply, never before it.
31 expect(written.slice(attach, attach + 2)).toEqual([
32 { id: 1, result: {} },
33 {
34 method: "Target.attachedToTarget",
35 params: expect.objectContaining({
36 sessionId: "t3-preview-page",
37 targetInfo: expect.objectContaining({ targetId: "GUEST-TARGET", type: "page" }),
38 }),
39 },
40 ]);
41 expect(written).toContainEqual({
42 id: 2,
43 error: expect.objectContaining({ message: expect.stringContaining("createTarget") }),
44 });
45 });
46
47 it("forwards page commands to the tab and lets a slow one finish last", async () => {
48 const pending: Array<() => void> = [];
49 const send = vi.fn(
50 (method: string) =>
51 new Promise<unknown>((resolve) => pending.push(() => resolve({ method }))),
52 );
53 const written: Array<Record<string, unknown>> = [];
54 const relay = createCdpRelayConnection(makeTarget(send), (raw) =>
55 written.push(JSON.parse(raw)),
56 );
57 relay.receive(
58 JSON.stringify({ id: 5, method: "Page.captureScreenshot", sessionId: "t3-preview-page" }),
59 );
60 relay.receive(JSON.stringify({ id: 6, method: "DOM.enable", sessionId: "iframe-session" }));
61 // A slow screenshot must not hold up the reply behind it.
62 pending[1]!();
63 await settle();
64 expect(written).toEqual([
65 { id: 6, result: { method: "DOM.enable" }, sessionId: "iframe-session" },
66 ]);
67 pending[0]!();
68 await settle();
69 expect(send).toHaveBeenNthCalledWith(1, "Page.captureScreenshot", {}, undefined);
70 expect(send).toHaveBeenNthCalledWith(2, "DOM.enable", {}, "iframe-session");
71 expect(written.at(-1)).toEqual({
72 id: 5,
73 result: { method: "Page.captureScreenshot" },
74 sessionId: "t3-preview-page",
75 });
76 });
77
78 it("resumes paused child frames, so cross-site iframes run", async () => {
79 const send = vi.fn(async () => ({}));
80 const relay = createCdpRelayConnection(makeTarget(send), () => {});
81 // The page's own session is already running; only its children wait.
82 relay.receive(
83 JSON.stringify({
84 id: 1,
85 method: "Runtime.runIfWaitingForDebugger",
86 sessionId: "t3-preview-page",
87 }),
88 );
89 relay.receive(
90 JSON.stringify({
91 id: 2,
92 method: "Runtime.runIfWaitingForDebugger",
93 sessionId: "iframe-session",
94 }),
95 );
96 await settle();
97 expect(send.mock.calls).toEqual([["Runtime.runIfWaitingForDebugger", {}, "iframe-session"]]);
98 });
99
100 it("routes tab events to the page session once the page is announced", async () => {
101 const written: Array<Record<string, unknown>> = [];
102 const relay = createCdpRelayConnection(makeTarget(vi.fn()), (raw) =>
103 written.push(JSON.parse(raw)),
104 );
105 relay.event("Page.loadEventFired", {}, undefined);
106 expect(written).toEqual([]);
107 relay.receive(JSON.stringify({ id: 1, method: "Target.setAutoAttach", params: {} }));
108 await settle();
109 // Electron passes an empty session id for the page's own events.
110 relay.event("Page.loadEventFired", { timestamp: 1 }, "");
111 relay.event("Runtime.consoleAPICalled", {}, "iframe-session");
112 expect(written.slice(-2)).toEqual([
113 { method: "Page.loadEventFired", params: { timestamp: 1 }, sessionId: "t3-preview-page" },
114 { method: "Runtime.consoleAPICalled", params: {}, sessionId: "iframe-session" },
115 ]);
116 });
117});
118
119describe("CDP relay extra sessions", () => {
120 it("gives a second page session the page's commands and events", async () => {
121 const send = vi.fn(async () => ({}));
122 const written: Array<Record<string, unknown>> = [];
123 const relay = createCdpRelayConnection(makeTarget(send), (raw) =>
124 written.push(JSON.parse(raw)),
125 );
126 relay.receive(JSON.stringify({ id: 1, method: "Target.setAutoAttach", params: {} }));
127 relay.receive(JSON.stringify({ id: 2, method: "Target.attachToBrowserTarget" }));
128 await settle();
129 const browserSession = (written.find((m) => m["id"] === 2)!["result"] as { sessionId: string })
130 .sessionId;
131 relay.receive(
132 JSON.stringify({
133 id: 3,
134 method: "Target.attachToTarget",
135 params: { targetId: "GUEST-TARGET", flatten: true },
136 sessionId: browserSession,
137 }),
138 );
139 relay.receive(
140 JSON.stringify({
141 id: 4,
142 method: "Target.attachToTarget",
143 params: { targetId: "SOMETHING-ELSE" },
144 sessionId: browserSession,
145 }),
146 );
147 await settle();
148 const extra = (written.find((m) => m["id"] === 3)!["result"] as { sessionId: string })
149 .sessionId;
150 expect(written.find((m) => m["id"] === 4)).toHaveProperty("error");
151 relay.receive(JSON.stringify({ id: 5, method: "Page.startScreencast", sessionId: extra }));
152 await settle();
153 expect(send).toHaveBeenLastCalledWith("Page.startScreencast", {}, undefined);
154 written.length = 0;
155 relay.event("Page.screencastFrame", { data: "x" }, undefined);
156 expect(written.map((m) => m["sessionId"])).toEqual(["t3-preview-page", extra]);
157 });
158});
159
160describe("CDP relay downloads", () => {
161 it("applies the server's download directory and reports downloads on the root session", async () => {
162 const send = vi.fn(async () => ({}));
163 const directories: Array<string | null> = [];
164 const written: Array<Record<string, unknown>> = [];
165 const relay = createCdpRelayConnection(
166 makeTarget(send, (directory) => directories.push(directory)),
167 (raw) => written.push(JSON.parse(raw)),
168 );
169 relay.receive(JSON.stringify({ id: 1, method: "Target.setAutoAttach", params: {} }));
170 relay.receive(
171 JSON.stringify({
172 id: 2,
173 method: "Browser.setDownloadBehavior",
174 params: {
175 behavior: "allowAndName",
176 browserContextId: "t3-preview",
177 downloadPath: "/srv/artifacts",
178 eventsEnabled: true,
179 },
180 }),
181 );
182 await settle();
183 expect(directories).toEqual(["/srv/artifacts"]);
184 // The tab's own debugger has no such context, so the id stays here.
185 expect(send).toHaveBeenCalledWith(
186 "Browser.setDownloadBehavior",
187 { behavior: "allowAndName", downloadPath: "/srv/artifacts", eventsEnabled: true },
188 undefined,
189 );
190 written.length = 0;
191 relay.event("Browser.downloadWillBegin", { guid: "g1", suggestedFilename: "r.csv" }, "");
192 expect(written).toEqual([
193 { method: "Browser.downloadWillBegin", params: { guid: "g1", suggestedFilename: "r.csv" } },
194 ]);
195 });
196});