169 lines
5.1 KiB
TypeScript
169 lines
5.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { CdpConnection } from "../../../src/browser/CdpConnection.js";
|
|
import {
|
|
startFakeCdpBrowser,
|
|
type FakeCdpBrowser,
|
|
} from "../../fixtures/fakeCdpBrowser.js";
|
|
|
|
const INVALID_REPLIES = [
|
|
["missing result and error", (id: number) => ({ id })],
|
|
[
|
|
"both result and error",
|
|
(id: number) => ({
|
|
id,
|
|
result: {},
|
|
error: { code: -32_602, message: "Invalid params" },
|
|
}),
|
|
],
|
|
[
|
|
"malformed error",
|
|
(id: number) => ({
|
|
id,
|
|
error: { code: "bad", message: "Invalid params" },
|
|
}),
|
|
],
|
|
["invalid result", (id: number) => ({ id, result: [] })],
|
|
["invalid session id", (id: number) => ({ id, result: {}, sessionId: 7 })],
|
|
[
|
|
"foreign session id",
|
|
(id: number) => ({ id, result: {}, sessionId: "foreign-session" }),
|
|
],
|
|
] as const;
|
|
|
|
describe("CDP connection", () => {
|
|
const browsers: FakeCdpBrowser[] = [];
|
|
|
|
afterEach(async () => {
|
|
vi.useRealTimers();
|
|
await Promise.all(
|
|
browsers.splice(0).map(async (browser) => browser.close()),
|
|
);
|
|
});
|
|
|
|
it("correlates concurrent command responses over a real WebSocket", async () => {
|
|
const browser = await startFakeCdpBrowser();
|
|
browsers.push(browser);
|
|
const connection = await CdpConnection.connect(
|
|
browser.browserWebSocketUrl,
|
|
"inspect_web_page",
|
|
);
|
|
try {
|
|
const [attached, frames] = await Promise.all([
|
|
connection.send("Target.attachToTarget"),
|
|
connection.send("Page.getFrameTree"),
|
|
]);
|
|
expect(attached).toMatchObject({ sessionId: "session-1" });
|
|
expect(frames).toMatchObject({
|
|
frameTree: { frame: { id: "frame-main" } },
|
|
});
|
|
browser.emitRawMessage(JSON.stringify({ id: 999 }));
|
|
await expect(connection.send("Runtime.enable")).resolves.toEqual({});
|
|
} finally {
|
|
await connection.close();
|
|
}
|
|
});
|
|
|
|
it("waits for an unresponsive command until caller cancellation", async () => {
|
|
const browser = await startFakeCdpBrowser({ hangOnMethod: "Page.enable" });
|
|
browsers.push(browser);
|
|
const connection = await CdpConnection.connect(
|
|
browser.browserWebSocketUrl,
|
|
"observe_web_session",
|
|
);
|
|
try {
|
|
const controller = new AbortController();
|
|
const pending = connection.send(
|
|
"Page.enable",
|
|
{},
|
|
undefined,
|
|
controller.signal,
|
|
);
|
|
const assertion = expect(pending).rejects.toMatchObject({
|
|
_tag: "AnalysisCancelledError",
|
|
operation: "observe_web_session",
|
|
});
|
|
controller.abort();
|
|
await assertion;
|
|
} finally {
|
|
await connection.close();
|
|
}
|
|
});
|
|
|
|
it("surfaces an unmodeled fake command as a CDP method rejection", async () => {
|
|
const browser = await startFakeCdpBrowser();
|
|
browsers.push(browser);
|
|
const connection = await CdpConnection.connect(
|
|
browser.browserWebSocketUrl,
|
|
"observe_web_session",
|
|
);
|
|
try {
|
|
await expect(
|
|
connection.send("Fixture.unmodeledMethod"),
|
|
).rejects.toMatchObject({
|
|
_tag: "BrowserObservationError",
|
|
command: "Fixture.unmodeledMethod",
|
|
code: -32_601,
|
|
reportedMessage: "Method not found: Fixture.unmodeledMethod",
|
|
});
|
|
} finally {
|
|
await connection.close();
|
|
}
|
|
});
|
|
|
|
it.each(INVALID_REPLIES)(
|
|
"fails the connection for a correlated reply with %s",
|
|
async (_case, reply) => {
|
|
const browser = await startFakeCdpBrowser({
|
|
hangOnMethod: "Page.enable",
|
|
});
|
|
browsers.push(browser);
|
|
const connection = await CdpConnection.connect(
|
|
browser.browserWebSocketUrl,
|
|
"observe_web_session",
|
|
);
|
|
try {
|
|
const pending = connection.send("Page.enable", {}, "selected-session");
|
|
const rejection = expect(pending).rejects.toMatchObject({
|
|
_tag: "BrowserObservationError",
|
|
reason: "protocol_error",
|
|
});
|
|
await vi.waitFor(() => expect(browser.commands).toHaveLength(1));
|
|
const command = browser.commands[0];
|
|
if (command === undefined)
|
|
throw new Error("Fake browser received no command");
|
|
browser.emitRawMessage(JSON.stringify(reply(command.id)));
|
|
await rejection;
|
|
await expect(connection.send("Runtime.enable")).rejects.toMatchObject({
|
|
reason: "protocol_error",
|
|
});
|
|
} finally {
|
|
await connection.close();
|
|
}
|
|
},
|
|
);
|
|
|
|
it("preserves the selected payload limit reason for pending and subsequent commands", async () => {
|
|
const browser = await startFakeCdpBrowser({
|
|
commandResult: () => ({ oversized: "x".repeat(2_048) }),
|
|
});
|
|
browsers.push(browser);
|
|
const connection = await CdpConnection.connect(
|
|
browser.browserWebSocketUrl,
|
|
"observe_web_execution",
|
|
undefined,
|
|
{ maxPayloadBytes: 512 },
|
|
);
|
|
try {
|
|
for (const method of ["Runtime.enable", "Debugger.enable"]) {
|
|
await expect(connection.send(method)).rejects.toMatchObject({
|
|
reason: "payload_limit",
|
|
userMessage: expect.stringContaining("512 byte protocol budget"),
|
|
});
|
|
}
|
|
} finally {
|
|
await connection.close();
|
|
}
|
|
});
|
|
});
|