test/cdp/session-call-test.ts (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { decodeResult } from "../../src/cdp/commands";
import { createCdpSession } from "../../src/cdp/session";
import type { CdpTransport } from "../../src/cdp/types";
import type { CdpEvent } from "../../src/cdp/types";
import { type Result, ok, err } from "../../src/util/result";
import { type CdpError, cdpError } from "../../src/cdp/errors";
type RequestOpts = { readonly sessionId?: string | null; readonly timeoutMs?: number };
type RecordedCall = {
readonly method: string;
readonly params: Record<string, unknown>;
readonly opts: RequestOpts | undefined;
};
const makeStubTransport = (
responses: Record<string, Result<unknown, CdpError>> = {},
): { transport: CdpTransport; calls: RecordedCall[] } => {
const calls: RecordedCall[] = [];
const transport: CdpTransport = {
connect: () => Promise.resolve(ok(undefined)),
close: () => Promise.resolve(),
request: (method, params, opts) => {
calls.push({ method, params, opts });
return Promise.resolve(responses[method] ?? ok({}));
},
// No real events ever arrive, so the session's background consumer awaits forever — with no timers involved, it does not keep the process alive.
events: (): AsyncIterable<CdpEvent> => (async function* (): AsyncGenerator<CdpEvent> {
await new Promise<never>(() => {});
})(),
state: () => "open",
onClose: () => () => {},
};
return { transport, calls };
};
describe("typed call decoding", () => {
test("extra Chrome fields on a result are tolerated", () => {
const r = decodeResult("Target.getTargetInfo", {
targetInfo: { targetId: "t", type: "page", title: "T", url: "u", futureField: 1 },
});
assert.equal(r.success, true);
});
});
describe("CdpSession.call / callOnTarget / callBrowser", () => {
test("(a) a valid stub payload decodes to typed data", async () => {
const { transport } = makeStubTransport({
"Target.getTargetInfo": ok({ targetInfo: { targetId: "t1", type: "page", title: "T", url: "u" } }),
});
const session = createCdpSession(transport);
const r = await session.call("Target.getTargetInfo", {});
assert.equal(r.success, true);
if (r.success) {
const targetId: string = r.data.targetInfo.targetId;
assert.equal(targetId, "t1");
}
});
test("(b) a garbage stub payload becomes invalid_response, without throwing", async () => {
const { transport } = makeStubTransport({
"Target.getTargetInfo": ok({ targetInfo: { targetId: 42 } }),
});
const session = createCdpSession(transport);
const r = await session.call("Target.getTargetInfo", {});
assert.equal(r.success, false);
if (!r.success) assert.equal(r.error.kind, "invalid_response");
});
test("(c) a transport-level error passes through with its original kind", async () => {
const { transport } = makeStubTransport({
"Target.getTargetInfo": err(cdpError("timeout", "socket dead", "Target.getTargetInfo")),
});
const session = createCdpSession(transport);
const r = await session.call("Target.getTargetInfo", {});
assert.equal(r.success, false);
if (!r.success) {
assert.equal(r.error.kind, "timeout");
assert.equal(r.error.message, "socket dead");
}
});
test("(d) call/callOnTarget/callBrowser route the session id correctly", async () => {
const { transport, calls } = makeStubTransport({
"Target.attachToTarget": ok({ sessionId: "session-xyz" }),
});
const session = createCdpSession(transport);
const switched = await session.switchTo("target-1");
assert.equal(switched.success, true);
calls.length = 0;
await session.call("Page.navigate", { url: "http://example.com" });
await session.callOnTarget("Page.navigate", { url: "http://example.com" }, "explicit-sid");
await session.callBrowser("Target.getTargets");
const navigateCalls = calls.filter((c) => c.method === "Page.navigate");
assert.equal(navigateCalls[0]?.opts?.sessionId, "session-xyz");
assert.equal(navigateCalls[1]?.opts?.sessionId, "explicit-sid");
const browserCall = calls.find((c) => c.method === "Target.getTargets");
assert.equal(browserCall?.opts?.sessionId, null);
});
test("opts.timeoutMs reaches the transport only when provided (exactOptionalPropertyTypes)", async () => {
const { transport, calls } = makeStubTransport();
const session = createCdpSession(transport);
await session.callBrowser("Target.getTargets", {}, { timeoutMs: 5_000 });
const withTimeout = calls.find((c) => c.method === "Target.getTargets");
assert.equal(withTimeout?.opts?.timeoutMs, 5_000);
await session.callBrowser("Target.getTargets");
const withoutTimeout = calls.filter((c) => c.method === "Target.getTargets")[1];
// The key must be absent, not present-with-undefined — the distinction exactOptionalPropertyTypes enforces at the call site.
assert.equal(withoutTimeout?.opts && "timeoutMs" in withoutTimeout.opts, false);
});
});
|