all repos — sz-chrome-remote-cdp @ master

pi-browser-harness, patched to drive a Chrome on another host over CDP, with castle_cdp_search web search

test/manual/ipc-server-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
 121
 122
 123
 124
 125
 126
import { createConnection } from "node:net";
import { createInterface } from "node:readline";
import { createIpcServer } from "../../src/daemon/server";
import { DAEMON_SOCKET_PATH } from "../../src/daemon/protocol";
import type { WireEvent, WireMessage } from "../../src/daemon/protocol";

const TEST_CLIENT_ID = "pi-test-001";

async function main() {
  let passed = 0;
  let failed = 0;
  const check = (cond: boolean, label: string) => {
    if (cond) { passed++; console.log(`  ✓ ${label}`); }
    else { failed++; console.error(`  ✗ ${label}`); }
  };

  console.log("Starting IpcServer...");
  const server = createIpcServer();

  const receivedByServer: WireMessage[] = [];
  let connectCount = 0;
  let disconnectCount = 0;

  server.onMessage((msg) => { receivedByServer.push(msg); });
  server.onConnect(() => { connectCount++; });
  server.onDisconnect(() => { disconnectCount++; });

  await server.start();
  console.log("Server started ✓");

  console.log("\nConnecting client...");
  const socket = createConnection(DAEMON_SOCKET_PATH);
  const rl = createInterface({ input: socket, crlfDelay: Infinity });

  const receivedByClient: WireMessage[] = [];
  rl.on("line", (line) => {
    try { receivedByClient.push(JSON.parse(line)); } catch {}
  });

  await new Promise<void>((resolve) => socket.on("connect", resolve));
  console.log("Client connected ✓");

  socket.write(JSON.stringify({ type: "control", action: "register", clientId: TEST_CLIENT_ID }) + "\n");
  await sleep(200);

  const ack = receivedByClient.find((m: any) => m.type === "control" && m.action === "registered");
  check(ack != null, "Registration acknowledged");
  check(connectCount === 1, "Connect handler fired");

  receivedByServer.length = 0;
  const testRequest = { type: "request", id: 1, method: "Page.navigate", params: { url: "https://example.com" } };
  socket.write(JSON.stringify(testRequest) + "\n");
  await sleep(200);

  const received = receivedByServer.find((m: any) => m.type === "request" && m.id === 1);
  check(received != null, "Server received request from client");
  check((received as any)?.method === "Page.navigate", "Method matches");

  receivedByClient.length = 0;
  server.send(TEST_CLIENT_ID, { type: "response", id: 1, result: { frameId: "xyz" } });
  await sleep(200);

  const response = receivedByClient.find((m: any) => m.type === "response" && m.id === 1);
  check(response != null, "Client received response from server");

  receivedByClient.length = 0;
  const broadcastMsg: WireEvent = {
    type: "event",
    method: "Target.targetCreated",
    params: { targetInfo: { type: "page" } },
  };
  server.broadcast(broadcastMsg);
  await sleep(200);

  check(receivedByClient.length === 1, "Client received broadcast");
  const bc = receivedByClient[0] as any;
  check(bc?.method === "Target.targetCreated", "Broadcast content matches");

  socket.destroy();
  await sleep(500);

  check(disconnectCount === 1, "Disconnect handler fired");
  check(server.clientCount() === 0, "Client count is 0 after disconnect");

  const socket2 = createConnection(DAEMON_SOCKET_PATH);
  await new Promise<void>((resolve) => socket2.on("connect", resolve));

  socket2.write(JSON.stringify({ type: "control", action: "register", clientId: TEST_CLIENT_ID }) + "\n");
  await sleep(200);
  check(server.clientCount() === 1, "Re-registration works after disconnect");
  socket2.destroy();
  socket2.on("error", () => {}); // suppress EPIPE from server-side cleanup
  await sleep(300);

  const testSockets: ReturnType<typeof createConnection>[] = [];
  for (let i = 0; i < 20; i++) {
    const s = createConnection(DAEMON_SOCKET_PATH);
    s.on("error", () => {});
    testSockets.push(s);
    await new Promise<void>((resolve) => s.on("connect", resolve));
    try { s.write(JSON.stringify({ type: "control", action: "register", clientId: `pi-test-max-${i}` }) + "\n"); } catch {}
    await sleep(20);
  }
  await sleep(300);
  check(server.clientCount() <= 16, "Max client enforcement works");
  for (const s of testSockets) {
    try { s.destroy(); } catch {}
    s.removeAllListeners();
  }

  console.log(`\nCleaning up...`);
  await server.stop();
  console.log("Server stopped ✓");

  console.log(`\n${passed} passed, ${failed} failed`);
  process.exit(failed > 0 ? 1 : 0);
}

function sleep(ms: number): Promise<void> {
  return new Promise((r) => setTimeout(r, ms));
}

main().catch((e) => {
  console.error("Test failed:", e);
  process.exit(1);
});