| 1 | import { describe, expect, it, vi } from "vite-plus/test"; |
| 2 | |
| 3 | import { createCdpRelayConnection, type CdpRelayTarget } from "./CdpRelay.ts"; |
| 4 | |
| 5 | const 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. |
| 18 | const settle = () => new Promise<void>((resolve) => setImmediate(resolve)); |
| 19 | |
| 20 | describe("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 | |
| 119 | describe("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 | |
| 160 | describe("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 | }); |