diff options
| author | amankumarsingh77 <amankumarsingh7702@gmail.com> | 2026-07-27 01:07:05 +0530 |
|---|---|---|
| committer | Aman Kumar <145339995+amankumarsingh77@users.noreply.github.com> | 2026-08-02 19:12:46 +0530 |
| commit | d1b57558ff40ef6d6b19ee7eff0a44f7f4a585f6 (patch) | |
| tree | 9172819196434737d1ecb63e22a2ad102b97ebd9 /test | |
| parent | 9f2ef1cf87251ce12f6b6432c48c6e7d2017b6b5 (diff) | |
test(cdp): cover call/callOnTarget/callBrowser through a stub transport; collapse decode into one path
session-call-test.ts previously only exercised decodeResult (Task 3's
surface, and mostly duplicating commands-test.ts) — call, callOnTarget,
callBrowser, and the session's internal req helper had zero coverage.
Add a stub CdpTransport (no socket, no browser) and test: a valid payload
decodes to typed data, a garbage payload becomes invalid_response without
throwing, a transport-level error passes through with its original kind
(not relabeled), call/callOnTarget/callBrowser route the session id
correctly, and opts.timeoutMs reaches the transport only when provided.
Also collapse the four near-identical transport-then-decode bodies (req,
call, callOnTarget, callBrowser) into one: the three public methods now
delegate to req, which is the sole place unknown ends for every CDP call
the session makes, internal or public. Public signatures are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'test')
| -rw-r--r-- | test/cdp/session-call-test.ts | 128 |
1 files changed, 117 insertions, 11 deletions
diff --git a/test/cdp/session-call-test.ts b/test/cdp/session-call-test.ts index b318eb4..7b1386b 100644 --- a/test/cdp/session-call-test.ts +++ b/test/cdp/session-call-test.ts @@ -1,28 +1,134 @@ 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/transport"; +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; +}; + +/** + * A stub transport: no socket, no browser. `request()` looks up a canned + * Result by method name (defaulting to `ok({})` for methods the test doesn't + * care about — e.g. the six domain-enable calls switchTo fires as a side + * effect). Every call is recorded so tests can assert on exactly what the + * session sent, including the sessionId routing and the timeoutMs option. + */ +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; the session's background consumer just + // awaits forever. No timers involved, so it doesn't 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("a valid transport payload decodes to typed data", () => { - const fromTransport: unknown = { sessionId: "abc123" }; - const r = decodeResult("Target.attachToTarget", fromTransport); + 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 sid: string = r.data.sessionId; - assert.equal(sid, "abc123"); + const targetId: string = r.data.targetInfo.targetId; + assert.equal(targetId, "t1"); } }); - test("a garbage transport payload becomes invalid_response", () => { - const r = decodeResult("Target.attachToTarget", { sessionId: 42 }); + 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("extra Chrome fields on a result are tolerated", () => { - const r = decodeResult("Target.getTargetInfo", { - targetInfo: { targetId: "t", type: "page", title: "T", url: "u", futureField: 1 }, + 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")), }); - assert.equal(r.success, true); + const session = createCdpSession(transport); + const r = await session.call("Target.getTargetInfo", {}); + assert.equal(r.success, false); + if (!r.success) { + // Must stay "timeout" — decodeResult must never run on a failed + // transport call and relabel it "invalid_response". + 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); + + // Establish an internal sessionId distinct from both null and the + // explicit sid used below, via the public switchTo() — no attachFirstPage + // bookkeeping required. + const switched = await session.switchTo("target-1"); + assert.equal(switched.success, true); + + calls.length = 0; // discard switchTo's own requests (activate/attach/enable*6) + + 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"); // call() uses the active session + assert.equal(navigateCalls[1]?.opts?.sessionId, "explicit-sid"); // callOnTarget() uses the given sid, not the active one + + const browserCall = calls.find((c) => c.method === "Target.getTargets"); + assert.equal(browserCall?.opts?.sessionId, null); // callBrowser() forces null even though a session is active + }); + + 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 itself must be absent, not present-with-undefined — that's the + // distinction exactOptionalPropertyTypes exists to enforce at the call site. + assert.equal(withoutTimeout?.opts && "timeoutMs" in withoutTimeout.opts, false); }); }); |
