aboutsummaryrefslogtreecommitdiff
path: root/test/manual/e2e-test.ts
blob: 963efaffc1557a54f78ca47919b26e34a03a8ee3 (plain)
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/**
 * End-to-end integration test: full daemon pipeline.
 *
 * Starts the daemon (IpcServer + CdpBridge), connects via DaemonTransport,
 * exercises the full CDP request/response/event lifecycle.
 *
 * Run: npx tsx test/manual/e2e-test.ts
 */
import { createIpcServer } from "../../src/daemon/server";
import { createCdpBridge, type SendToClient } from "../../src/daemon/bridge";
import { createDaemonTransport } from "../../src/cdp/daemon-transport";
import { DAEMON_SOCKET_PATH } from "../../src/daemon/protocol";
import type { WireRequest } from "../../src/daemon/protocol";
import { unlinkSync } from "node:fs";

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

async function main() {
  // Clean up stale socket
  try { unlinkSync(DAEMON_SOCKET_PATH); } catch {}

  // ── Start daemon ───────────────────────────────────────────────────────
  console.log("Starting daemon (IpcServer + CdpBridge)...");
  const ipcServer = createIpcServer();
  const cdpBridge = createCdpBridge();

  // Wire IpcServer ↔ CdpBridge
  ipcServer.onMessage((msg, client) => {
    if (msg.type !== "request") return;
    const send: SendToClient = (cid, resp) => ipcServer.send(cid, resp);
    cdpBridge.handleRequest(msg, client.id, send);
  });

  cdpBridge.onEvent((event, targetClientIds) => {
    for (const cid of targetClientIds) ipcServer.send(cid, event);
  });

  ipcServer.onDisconnect((client) => cdpBridge.removeClient(client.id));

  await ipcServer.start();
  console.log("Daemon started ✓");

  // The bridge will try to connect to Chrome — might fail, that's ok for
  // this test. We'll simulate CDP responses from the server side.

  // ── Connect pi client via DaemonTransport ──────────────────────────────
  console.log("\nConnecting pi client (DaemonTransport)...");
  const transport = createDaemonTransport("pi-e2e-test");

  const connectResult = await transport.connect("");
  check(connectResult.success, `Client connect: ${connectResult.success ? "ok" : connectResult.error.message}`);
  check(transport.state() === "open", "Transport state is open");

  // ── Test 1: Request → simulated CDP response ───────────────────────────
  // The IpcServer's onMessage handler forwards requests to the bridge.
  // The bridge tries to send to Chrome (which may be down). For this test,
  // we intercept at the IpcServer level and simulate CDP responses.

  // Override the message handler to simulate CDP responses directly.
  let rawRequests: WireRequest[] = [];
  ipcServer.onMessage((msg, client) => {
    if (msg.type === "request") {
      rawRequests.push(msg);
      // Simulate a CDP success response
      ipcServer.send(client.id, {
        type: "response",
        id: msg.id,
        result: { simulated: true, method: msg.method },
      });
    }
  });

  const res1 = await transport.request("Page.navigate", { url: "https://example.com" });
  check(res1.success, "CDP request succeeded");
  if (res1.success) {
    const d = res1.data as any;
    check(d?.simulated === true, "Response contains simulated flag");
    check(d?.method === "Page.navigate", "Response echoes method");
  }
  check(rawRequests.length === 1, "Server received exactly 1 request");
  check(rawRequests[0]!.method === "Page.navigate", "Correct method forwarded");

  // ── Test 2: Request with sessionId ─────────────────────────────────────
  rawRequests.length = 0;
  const res2 = await transport.request("Runtime.evaluate", { expression: "1+1" }, { sessionId: "session-abc" });
  check(res2.success, "Session-scoped request succeeded");
  check(rawRequests[0]?.sessionId === "session-abc", "SessionId forwarded correctly");

  // ── Test 3: Multiple parallel requests ─────────────────────────────────
  rawRequests.length = 0;
  const [r3a, r3b, r3c] = await Promise.all([
    transport.request("Target.getTargets", {}),
    transport.request("Browser.getVersion", {}),
    transport.request("Page.captureScreenshot", {}),
  ]);
  check(r3a.success && r3b.success && r3c.success, "All 3 parallel requests succeeded");
  check(rawRequests.length === 3, "Server received all 3 requests");

  // ── Test 4: Events ─────────────────────────────────────────────────────
  const eventPromise = new Promise<string | null>((resolve) => {
    const timeout = setTimeout(() => resolve(null), 2000);
    (async () => {
      for await (const evt of transport.events()) {
        clearTimeout(timeout);
        resolve(evt.method);
        break;
      }
    })();
  });

  // Simulate a CDP event broadcast from the daemon
  ipcServer.broadcast({
    type: "event",
    method: "Target.targetCreated",
    params: { targetInfo: { type: "page", targetId: "e2e-test-target" } },
  });

  const eventMethod = await eventPromise;
  check(eventMethod === "Target.targetCreated", `Event received: ${eventMethod ?? "TIMEOUT"}`);

  // ── Test 5: Session persistence (detach simulation) ────────────────────
  // Simulate a session shutdown: close the transport, then reconnect.
  // The transport.close() sends deregister, server cleans up.
  await transport.close();
  check(transport.state() === "closed", "Transport closed");

  // Small delay for server cleanup
  await new Promise(r => setTimeout(r, 500));

  // Reconnect — simulates next pi session
  const reconnectResult = await transport.connect("");
  check(reconnectResult.success, "Transport reconnected (simulated session restart)");

  // Send a request through the reconnected transport
  const res4 = await transport.request("Target.getTargets", {});
  check(res4.success, "Request after reconnect succeeded");
  check(transport.state() === "open", "Transport state is open after reconnect");

  // ── Test 6: Server-side client count ───────────────────────────────────
  check(ipcServer.clientCount() === 1, "Server reports 1 connected client");

  // ── Test 7: Client disconnect cleanup ──────────────────────────────────
  await transport.close();
  await new Promise(r => setTimeout(r, 500));
  check(ipcServer.clientCount() === 0, "Server reports 0 clients after disconnect");

  // ── Cleanup ────────────────────────────────────────────────────────────
  await ipcServer.stop();
  console.log("\nDaemon stopped ✓");

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

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