cdp.test.ts
// Cdp against a fake socket: no Chrome, no network. The fake records every
// frame sent and lets a test script the replies and the socket's lifecycle.
import { describe, expect, it } from "vitest";
import { Cdp, type CdpSocket } from "./cdp.js";
interface Frame {
id: number;
method: string;
params: Record<string, unknown>;
}
class FakeSocket implements CdpSocket {
readyState: number = WebSocket.OPEN;
readonly sent: Frame[] = [];
private readonly listeners = new Map<string, ((ev: { data: unknown }) => void)[]>();
// How to answer each frame; the default acknowledges with an empty result, undefined never answers.
reply: (frame: Frame) => Record<string, unknown> | undefined = () => ({ result: {} });
addEventListener(type: string, listener: (ev: { data: unknown }) => void): void {
const list = this.listeners.get(type) ?? [];
list.push(listener);
this.listeners.set(type, list);
}
// Like the real thing, a frame sent on a socket that isn't open is dropped silently.
send(data: string): void {
if (this.readyState !== WebSocket.OPEN) return;
const frame = JSON.parse(data) as Frame;
this.sent.push(frame);
const reply = this.reply(frame);
if (reply !== undefined) this.emit("message", { data: JSON.stringify({ id: frame.id, ...reply }) });
}
close(): void {
this.readyState = WebSocket.CLOSED;
this.emit("close", { data: undefined });
}
emit(type: string, ev: { data: unknown }): void {
for (const l of this.listeners.get(type) ?? []) l(ev);
}
}
// Whether a promise has settled (either way) once the microtask queue drains.
async function settled(p: Promise<unknown>): Promise<boolean> {
let done = false;
p.then(() => (done = true), () => (done = true));
await new Promise<void>((r) => setImmediate(r));
return done;
}
describe("Cdp.key", () => {
async function keyParams(key: string, opts?: { shift?: boolean }): Promise<Record<string, unknown>[]> {
const sock = new FakeSocket();
await Cdp.fromSocket(sock).key(key, opts);
for (const f of sock.sent) expect(f.method).toBe("Input.dispatchKeyEvent");
return sock.sent.map((f) => f.params);
}
it("sends Tab as a rawKeyDown/keyUp pair with the virtual key code", async () => {
expect(await keyParams("Tab")).toEqual([
{ type: "rawKeyDown", windowsVirtualKeyCode: 9, code: "Tab", key: "Tab", modifiers: 0 },
{ type: "keyUp", windowsVirtualKeyCode: 9, code: "Tab", key: "Tab", modifiers: 0 },
]);
});
it("sends shift+Tab with the shift modifier bit on both halves", async () => {
const params = await keyParams("Tab", { shift: true });
expect(params.map((p) => p.modifiers)).toEqual([8, 8]);
});
it("sends Enter as a full keyDown carrying its text", async () => {
expect(await keyParams("Enter")).toEqual([
{ type: "keyDown", windowsVirtualKeyCode: 13, code: "Enter", key: "Enter", modifiers: 0, text: "\r" },
{ type: "keyUp", windowsVirtualKeyCode: 13, code: "Enter", key: "Enter", modifiers: 0 },
]);
});
it("sends space as a full keyDown carrying its text", async () => {
expect(await keyParams(" ")).toEqual([
{ type: "keyDown", windowsVirtualKeyCode: 32, code: "Space", key: " ", modifiers: 0, text: " " },
{ type: "keyUp", windowsVirtualKeyCode: 32, code: "Space", key: " ", modifiers: 0 },
]);
});
it("sends a printable character with its text and KeyX code", async () => {
expect(await keyParams("a")).toEqual([
{ type: "keyDown", windowsVirtualKeyCode: 65, code: "KeyA", key: "a", modifiers: 0, text: "a" },
{ type: "keyUp", windowsVirtualKeyCode: 65, code: "KeyA", key: "a", modifiers: 0 },
]);
});
it("rejects a multi-character name that isn't a known key, sending nothing", async () => {
const sock = new FakeSocket();
await expect(Cdp.fromSocket(sock).key("F1")).rejects.toThrow(/KEYDEFS/);
expect(sock.sent).toEqual([]);
});
it("rejects shift on a printable character (type the shifted character instead)", async () => {
const sock = new FakeSocket();
await expect(Cdp.fromSocket(sock).key("a", { shift: true })).rejects.toThrow(/shift/);
expect(sock.sent).toEqual([]);
});
});
describe("Cdp.cmd", () => {
it("resolves with the reply's result", async () => {
const sock = new FakeSocket();
sock.reply = () => ({ result: { answer: 42 } });
expect(await Cdp.fromSocket(sock).cmd("Any.thing", { x: 1 })).toEqual({ answer: 42 });
expect(sock.sent).toEqual([{ id: 1, method: "Any.thing", params: { x: 1 } }]);
});
it("rejects on an error reply, with the error in the message", async () => {
const sock = new FakeSocket();
sock.reply = () => ({ error: { code: -32601, message: "no such method" } });
await expect(Cdp.fromSocket(sock).cmd("Nope.nope")).rejects.toThrow(/no such method/);
});
it("records a method message as an event", async () => {
const sock = new FakeSocket();
const cdp = Cdp.fromSocket(sock);
sock.emit("message", { data: JSON.stringify({ method: "Runtime.consoleAPICalled", params: { type: "log" } }) });
expect(cdp.events).toEqual([{ method: "Runtime.consoleAPICalled", params: { type: "log" } }]);
});
it("rejects an outstanding command when the socket closes", async () => {
const sock = new FakeSocket();
sock.reply = () => undefined;
const pending = Cdp.fromSocket(sock).cmd("Slow.thing");
sock.close();
expect(await settled(pending)).toBe(true);
await expect(pending).rejects.toThrow(/closed/);
});
it("rejects a command sent after the socket closed", async () => {
const sock = new FakeSocket();
const cdp = Cdp.fromSocket(sock);
sock.close();
const late = cdp.cmd("Late.thing");
expect(await settled(late)).toBe(true);
await expect(late).rejects.toThrow(/closed/);
});
});
describe("Cdp.js", () => {
it("returns the evaluated value", async () => {
const sock = new FakeSocket();
sock.reply = () => ({ result: { result: { type: "number", value: 2 } } });
expect(await Cdp.fromSocket(sock).js("1+1")).toBe(2);
});
it("rejects when the expression throws in the page, with the exception text", async () => {
const sock = new FakeSocket();
sock.reply = () => ({
result: {
result: { type: "object", subtype: "error", description: "ReferenceError: nosuchvar is not defined" },
exceptionDetails: {
text: "Uncaught",
exception: { description: "ReferenceError: nosuchvar is not defined\n at <anonymous>:1:1" },
},
},
});
await expect(Cdp.fromSocket(sock).js("nosuchvar.x")).rejects.toThrow(/nosuchvar is not defined/);
});
});
packages/yukey-testkit/src/cdp.test.ts · 6.3 KiB · 9993779312