aboutsummaryrefslogtreecommitdiff
path: root/test/manual/bridge-test.ts
blob: 5402aae953a0122121275f5a944df63a86a1d2ad (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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/**
 * Manual integration test for CdpBridge against real Chrome.
 * Requires: Chrome with remote debugging enabled (chrome://inspect/#remote-debugging).
 * Run: npx tsx test/manual/bridge-test.ts
 */
import { createCdpBridge, type SendToClient } from "../../src/daemon/bridge";
import type { WireRequest, WireResponse, WireEvent } from "../../src/daemon/protocol";

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() {
  console.log("Starting CdpBridge...");
  const bridge = createCdpBridge();

  // ── Test 1: Connect to Chrome ──────────────────────────────────────────
  // start() now runs auto-retry in the background. Wait a moment for
  // the initial connection attempt to complete.
  await bridge.start();
  await sleep(3000); // allow time for WebSocket handshake
  if (!bridge.isAlive()) {
    console.error("Cannot continue — Chrome not reachable. Run /browser-setup first.");
    console.error("(The bridge will keep retrying in the background.)");
    process.exit(1);
  }
  console.log("  ✓ Connect to Chrome: ok");
  check(bridge.isAlive(), "Bridge reports alive after connect");

  // ── Test 2: Send Target.getTargets ─────────────────────────────────────
  const responses: WireResponse[] = [];
  const events: WireEvent[] = [];
  let eventCount = 0;

  const send: SendToClient = (_clientId, msg) => {
    if (msg.type === "response") responses.push(msg);
    else events.push(msg);
  };

  bridge.onEvent((_evt, _targets) => { eventCount++; });

  const req1: WireRequest = { type: "request", id: 1, method: "Target.getTargets" };
  bridge.handleRequest(req1, "pi-test", send);

  await sleep(2000);

  const res1 = responses.find(r => r.id === 1);
  check(res1 != null, "Target.getTargets response received");
  if (res1?.result) {
    const r = res1.result as any;
    check(Array.isArray(r.targetInfos), "targetInfos is array");
    console.log(`    Found ${r.targetInfos?.length ?? 0} targets`);
  }

  // ── Test 3: Create a new tab ───────────────────────────────────────────
  responses.length = 0;
  const req2: WireRequest = {
    type: "request", id: 2, method: "Target.createTarget",
    params: { url: "about:blank", newWindow: true },
  };
  bridge.handleRequest(req2, "pi-test", send);
  await sleep(2000);

  const res2 = responses.find(r => r.id === 2);
  check(res2 != null, "Target.createTarget response received");
  const targetId = (res2?.result as any)?.targetId as string | undefined;
  check(typeof targetId === "string", `Created target: ${targetId ?? "FAILED"}`);

  if (!targetId) {
    console.error("Cannot continue without a target");
    await bridge.stop();
    process.exit(1);
  }

  // ── Test 4: Attach to target ───────────────────────────────────────────
  responses.length = 0;
  const req3: WireRequest = {
    type: "request", id: 3, method: "Target.attachToTarget",
    params: { targetId, flatten: true },
  };
  bridge.handleRequest(req3, "pi-test", send);
  await sleep(2000);

  const res3 = responses.find(r => r.id === 3);
  check(res3 != null, "Target.attachToTarget response received");
  const sessionId = (res3?.result as any)?.sessionId as string | undefined;
  check(typeof sessionId === "string", `Session ID: ${sessionId ?? "FAILED"}`);

  // Verify session ownership was tracked
  if (sessionId) {
    const owner = bridge.getSessionOwner(sessionId);
    check(owner === "pi-test", "Session ownership tracked correctly");
  }

  if (!sessionId) {
    await bridge.stop();
    process.exit(1);
  }

  // ── Test 5: Navigate tab via session ───────────────────────────────────
  responses.length = 0;
  const req4: WireRequest = {
    type: "request", id: 4, method: "Page.navigate",
    params: { url: "https://example.com" },
    sessionId,
  };
  bridge.handleRequest(req4, "pi-test", send);
  await sleep(3000);

  const res4 = responses.find(r => r.id === 4);
  check(res4 != null, "Page.navigate response received");
  const frameId = (res4?.result as any)?.frameId as string | undefined;
  check(typeof frameId === "string", `Navigated to example.com (frame: ${frameId ?? "FAILED"})`);

  // ── Test 6: Evaluate JS in the page ────────────────────────────────────
  responses.length = 0;
  const req5: WireRequest = {
    type: "request", id: 5, method: "Runtime.evaluate",
    params: { expression: "document.title", returnByValue: true },
    sessionId,
  };
  bridge.handleRequest(req5, "pi-test", send);
  await sleep(2000);

  const res5 = responses.find(r => r.id === 5);
  check(res5 != null, "Runtime.evaluate response received");
  const title = (res5?.result as any)?.result?.value as string | undefined;
  check(title === "Example Domain", `Page title: "${title ?? "FAILED"}" (expected "Example Domain")`);

  // ── Test 7: Detach from target ─────────────────────────────────────────
  responses.length = 0;
  const req6: WireRequest = {
    type: "request", id: 6, method: "Target.detachFromTarget",
    params: { sessionId },
  };
  bridge.handleRequest(req6, "pi-test", send);
  await sleep(1000);

  const res6 = responses.find(r => r.id === 6);
  check(res6 != null, "Target.detachFromTarget response received");

  // Session ownership should be released
  const ownerAfter = bridge.getSessionOwner(sessionId);
  check(ownerAfter === undefined, "Session ownership released after detach");

  // ── Test 8: Close target ───────────────────────────────────────────────
  responses.length = 0;
  const req7: WireRequest = {
    type: "request", id: 7, method: "Target.closeTarget",
    params: { targetId },
  };
  bridge.handleRequest(req7, "pi-test", send);
  await sleep(1000);

  const res7 = responses.find(r => r.id === 7);
  check(res7 != null, "Target.closeTarget response received");

  // ── Test 9: Remove client ──────────────────────────────────────────────
  bridge.removeClient("pi-test");
  // After removal, no pending requests should remain — verify by checking
  // no errors are thrown when handling subsequent events.

  // ── Cleanup ────────────────────────────────────────────────────────────
  console.log("\nStopping bridge...");
  await bridge.stop();
  console.log("Bridge 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);
});