cdp-tunnel 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -137,7 +137,27 @@ var LocalHandler = (function() {
137
137
 
138
138
  function getTargetInfos() {
139
139
  return chrome.debugger.getTargets().then(function(targets) {
140
- return targets.map(mapToTargetInfo).filter(Boolean);
140
+ // 为每个有 tabId 的 target 查询 openerTabId
141
+ const promises = targets.map(function(target) {
142
+ if (target.tabId) {
143
+ return new Promise(function(resolve) {
144
+ chrome.tabs.get(target.tabId, function(tab) {
145
+ if (tab && tab.openerTabId) {
146
+ const openerMatch = targets.find(function(t) {
147
+ return String(t.tabId) === String(tab.openerTabId);
148
+ });
149
+ if (openerMatch) {
150
+ target.openerId = openerMatch.id;
151
+ }
152
+ }
153
+ resolve(mapToTargetInfo(target));
154
+ });
155
+ });
156
+ } else {
157
+ return Promise.resolve(mapToTargetInfo(target));
158
+ }
159
+ });
160
+ return Promise.all(promises);
141
161
  });
142
162
  }
143
163
 
@@ -146,7 +166,28 @@ var LocalHandler = (function() {
146
166
  var match = targets.find(function(t) {
147
167
  return t.id === targetId || String(t.tabId) === String(targetId);
148
168
  });
149
- return match ? mapToTargetInfo(match) : null;
169
+ if (!match) return null;
170
+
171
+ // 获取 tab 信息以获取 openerTabId
172
+ var tabId = match.tabId;
173
+ if (tabId) {
174
+ return new Promise(function(resolve) {
175
+ chrome.tabs.get(tabId, function(tab) {
176
+ if (tab && tab.openerTabId) {
177
+ // 查找 opener 的 targetId
178
+ var openerMatch = targets.find(function(t) {
179
+ return String(t.tabId) === String(tab.openerTabId);
180
+ });
181
+ if (openerMatch) {
182
+ match.openerId = openerMatch.id;
183
+ }
184
+ }
185
+ resolve(mapToTargetInfo(match));
186
+ });
187
+ });
188
+ }
189
+
190
+ return mapToTargetInfo(match);
150
191
  });
151
192
  }
152
193
 
@@ -194,7 +235,7 @@ var LocalHandler = (function() {
194
235
 
195
236
  function mapToTargetInfo(target) {
196
237
  if (!target) return null;
197
- return {
238
+ var info = {
198
239
  targetId: target.id || String(target.tabId),
199
240
  type: target.type || 'page',
200
241
  title: target.title || '',
@@ -203,6 +244,11 @@ var LocalHandler = (function() {
203
244
  canAccessOpener: false,
204
245
  browserContextId: 'default'
205
246
  };
247
+ // 添加 openerId(如果有)
248
+ if (target.openerId) {
249
+ info.openerId = target.openerId;
250
+ }
251
+ return info;
206
252
  }
207
253
 
208
254
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-tunnel",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Chrome Extension CDP Proxy - 通过 Chrome 扩展将 chrome.debugger API 暴露为 WebSocket 端点",
5
5
  "main": "server/proxy-server.js",
6
6
  "bin": "./cli/index.js",
@@ -50,6 +50,7 @@ const globalRequestIdMap = new Map();
50
50
  const targetIdToClientId = new Map();
51
51
  const pendingAttachedEvents = new Map();
52
52
  const browserContextToClientId = new Map();
53
+ const clientIdToBrowserContext = new Map();
53
54
  let globalRequestIdCounter = 0;
54
55
 
55
56
  let cachedTargets = [];
@@ -309,6 +310,8 @@ function handlePluginConnection(ws, clientInfo) {
309
310
 
310
311
  // 消息转发: Plugin -> Client
311
312
  ws.on('message', (data) => {
313
+ console.log(`[PLUGIN MESSAGE] size=${data.length}`);
314
+
312
315
  const messageSize = data.length;
313
316
  let messagePreview;
314
317
  let parsed;
@@ -331,69 +334,77 @@ function handlePluginConnection(ws, clientInfo) {
331
334
  return;
332
335
  }
333
336
 
334
- // 记录所有 PLUGIN -> CLIENT 消息到日志文件
335
- logCDP('PLUGIN -> CLIENT', data.toString().substring(0, CONFIG.LOG_MESSAGE_PREVIEW_LENGTH), parsed?.sessionId, ws.pluginType);
336
-
337
337
  // 调试:打印所有收到的消息
338
338
  console.log(`[PLUGIN MSG] id=${parsed?.id} method=${parsed?.method || 'none'} type=${parsed?.type || 'none'} sessionId=${parsed?.sessionId?.substring(0,8) || 'none'}`);
339
339
 
340
+ // 记录所有 PLUGIN -> CLIENT 消息到日志文件
341
+ logCDP('PLUGIN -> CLIENT', data.toString().substring(0, CONFIG.LOG_MESSAGE_PREVIEW_LENGTH), parsed?.sessionId, ws.pluginType);
342
+
340
343
  // 处理 type: 'event' 消息(来自 background.js 的 screencast 等事件)
341
344
  if (parsed && parsed.type === 'event' && parsed.method) {
342
- logCDP('DEBUG', `Converting type:event message: ${parsed.method}`, parsed?.sessionId);
345
+ // 对于 Target 事件,始终广播给所有客户端
346
+ const targetEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
347
+ if (targetEvents.includes(parsed.method)) {
348
+ const cdpMsg = {
349
+ method: parsed.method,
350
+ params: parsed.params,
351
+ type: 'event'
352
+ };
353
+ if (parsed.sessionId) {
354
+ cdpMsg.params.sessionId = parsed.sessionId;
355
+ cdpMsg.sessionId = parsed.sessionId;
356
+ }
357
+
358
+ if (parsed.method === 'Target.targetCreated') {
359
+ const targetId = parsed.params?.targetInfo?.targetId;
360
+ const openerId = parsed.params?.targetInfo?.openerId;
361
+ if (openerId && targetId) {
362
+ const openerClientId = targetIdToClientId.get(openerId);
363
+ if (openerClientId) {
364
+ targetIdToClientId.set(targetId, openerClientId);
365
+ console.log(`[TARGET CREATED with opener] targetId=${targetId.substring(0,8)} openerId=${openerId.substring(0,8)} -> clientId=${openerClientId}`);
366
+ }
367
+ }
368
+ }
369
+
370
+ rewriteBrowserContextId(cdpMsg);
371
+ const cdpData = JSON.stringify(cdpMsg);
372
+ logCDP('BROADCAST', `Broadcasting ${parsed.method}, full data: ${cdpData}`, parsed?.sessionId);
373
+ broadcastToClients(cdpData, null);
374
+ logCDP('BROADCAST', `Done broadcasting ${parsed.method}`, parsed?.sessionId);
375
+ }
343
376
 
344
- // 处理 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
377
+ // 对于 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
345
378
  if (parsed.method === 'Target.attachedToTarget') {
346
379
  const targetId = parsed.params?.targetInfo?.targetId;
347
380
  const sessionId = parsed.params?.sessionId;
348
381
 
349
- console.log(`[ATTACHED EVENT (type:event)] targetId=${targetId} sessionId=${sessionId?.substring(0,8)}`);
350
-
351
- // 查找 targetId 对应的 clientId
352
- const clientId = targetIdToClientId.get(targetId);
353
- if (clientId && sessionId) {
354
- sessionToClientId.set(sessionId, clientId);
355
- console.log(`[SESSION MAPPED from event] sessionId=${sessionId.substring(0,8)} -> clientId=${clientId} (targetId=${targetId})`);
356
- targetIdToClientId.delete(targetId);
357
-
358
- // 转换为 CDP 格式并发送给对应的客户端
359
- const cdpMsg = {
360
- method: parsed.method,
361
- params: parsed.params
362
- };
363
-
364
- const clientWs = clientById.get(clientId);
365
- if (clientWs && clientWs.readyState === WebSocket.OPEN) {
366
- clientWs.send(JSON.stringify(cdpMsg));
367
- console.log(`[ATTACHED EVENT] Sent to client: ${clientId}`);
368
- }
369
- return;
370
- } else if (targetId && sessionId) {
371
- // targetId 还没有映射,缓存事件等待 Target.createTarget 响应
372
- pendingAttachedEvents.set(targetId, { sessionId, parsed, data });
373
- console.log(`[ATTACHED EVENT] Cached for targetId=${targetId}, waiting for createTarget response`);
374
- return;
382
+ if (targetId && sessionId) {
383
+ sessionToClientId.set(sessionId, targetId);
384
+ console.log(`[SESSION MAPPED] sessionId=${sessionId.substring(0,8)} -> targetId=${targetId.substring(0,8)}`);
375
385
  }
376
386
  }
377
387
 
378
- const cdpMsg = {
379
- method: parsed.method,
380
- params: parsed.params
381
- };
382
- if (parsed.sessionId) {
383
- cdpMsg.sessionId = parsed.sessionId;
384
- }
385
- const cdpData = JSON.stringify(cdpMsg);
386
-
387
- // 发送给配对的 client
388
- if (ws.pairedClientId) {
389
- const clientWs = clientById.get(ws.pairedClientId);
390
- if (safeSend(clientWs, cdpData, 'client')) {
391
- logCDP('DEBUG', `Sent converted event to client: ${parsed.method}`, parsed?.sessionId);
392
- return;
388
+ // 如果不是 Target 事件,按照原来的逻辑发送
389
+ if (!targetEvents.includes(parsed.method)) {
390
+ const cdpMsg = {
391
+ method: parsed.method,
392
+ params: parsed.params
393
+ };
394
+ if (parsed.sessionId) {
395
+ cdpMsg.sessionId = parsed.sessionId;
393
396
  }
397
+ const cdpData = JSON.stringify(cdpMsg);
398
+
399
+ if (ws.pairedClientId) {
400
+ const clientWs = clientById.get(ws.pairedClientId);
401
+ if (safeSend(clientWs, cdpData, 'client')) {
402
+ logCDP('DEBUG', `Sent converted event to client: ${parsed.method}`, parsed?.sessionId);
403
+ return;
404
+ }
405
+ }
406
+ broadcastToClients(cdpData, ws);
394
407
  }
395
- // 广播给所有 client
396
- broadcastToClients(cdpData, ws);
397
408
  return;
398
409
  }
399
410
 
@@ -418,6 +429,7 @@ function handlePluginConnection(ws, clientInfo) {
418
429
  if (mapping.isCreateBrowserContext && parsed.result?.browserContextId) {
419
430
  const browserContextId = parsed.result.browserContextId;
420
431
  browserContextToClientId.set(browserContextId, mapping.clientId);
432
+ clientIdToBrowserContext.set(mapping.clientId, browserContextId);
421
433
  console.log(`[BROWSER CONTEXT MAPPED] browserContextId=${browserContextId} -> clientId=${mapping.clientId}`);
422
434
  }
423
435
 
@@ -484,120 +496,16 @@ function handlePluginConnection(ws, clientInfo) {
484
496
  return;
485
497
  }
486
498
 
487
- // 3. 事件广播:无 id 和 sessionId 的消息(如 Target.targetCreated)
488
- // 只广播特定类型的事件,避免干扰其他客户端
489
- if (parsed && parsed.method) {
490
- // 处理 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
491
- if (parsed.method === 'Target.attachedToTarget') {
492
- const targetId = parsed.params?.targetInfo?.targetId;
493
- const sessionId = parsed.params?.sessionId;
494
- const openerId = parsed.params?.targetInfo?.openerId;
495
-
496
- // 查找 targetId 对应的 clientId
497
- let clientId = targetIdToClientId.get(targetId);
498
-
499
- // 如果没有直接映射,检查 openerId(window.open 打开的新 tab)
500
- if (!clientId && openerId) {
501
- // 查找 openerId 对应的 clientId
502
- // openerId 可能是某个已知的 targetId
503
- clientId = targetIdToClientId.get(openerId);
504
- if (clientId) {
505
- console.log(`[OPENER TRACKING] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0,8)} -> clientId=${clientId}`);
506
- // 记录新 targetId 的映射
507
- targetIdToClientId.set(targetId, clientId);
508
- }
509
- }
510
-
511
- if (clientId && sessionId) {
512
- sessionToClientId.set(sessionId, clientId);
513
- console.log(`[SESSION MAPPED from event] sessionId=${sessionId.substring(0,8)} -> clientId=${clientId} (targetId=${targetId?.substring(0,8)})`);
514
- targetIdToClientId.delete(targetId);
515
-
516
- // 只发送给对应的客户端
517
- const clientWs = clientById.get(clientId);
518
- if (clientWs && clientWs.readyState === WebSocket.OPEN) {
519
- clientWs.send(data);
520
- }
521
- return;
522
- }
523
- }
524
-
525
- // 处理 Target.targetInfoChanged 事件
526
- if (parsed.method === 'Target.targetInfoChanged') {
527
- const targetId = parsed.params?.targetInfo?.targetId;
528
- const openerId = parsed.params?.targetInfo?.openerId;
529
- console.log(`[TARGET INFO CHANGED] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0, 8) || 'none'}`);
530
- }
531
-
532
- if (parsed.method === 'Target.targetCreated') {
533
- const targetId = parsed.params?.targetInfo?.targetId;
534
- const openerId = parsed.params?.targetInfo?.openerId;
535
- const browserContextId = parsed.params?.targetInfo?.browserContextId;
536
- const targetType = parsed.params?.targetInfo?.type;
537
-
538
- console.log(`[TARGET CREATED] targetId=${targetId?.substring(0,8)} type=${targetType} openerId=${openerId?.substring(0, 8) || 'none'} browserContextId=${browserContextId?.substring(0, 8) || 'none'}`);
539
-
540
- // 如果有 openerId,尝试找到对应的 clientId
541
- if (openerId && targetId) {
542
- const openerClientId = targetIdToClientId.get(openerId);
543
- if (openerClientId) {
544
- targetIdToClientId.set(targetId, openerClientId);
545
- console.log(`[TARGET CREATED with opener] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0,8)} -> clientId=${openerClientId}`);
546
- }
547
- }
548
-
549
- // 如果有 browserContextId,尝试找到对应的 clientId
550
- // browserContextId 是通过 Target.createBrowserContext 创建的
551
- if (browserContextId && targetId) {
552
- const contextClientId = browserContextToClientId.get(browserContextId);
553
- if (contextClientId && !targetIdToClientId.has(targetId)) {
554
- targetIdToClientId.set(targetId, contextClientId);
555
- console.log(`[TARGET CREATED in context] targetId=${targetId?.substring(0,8)} browserContextId=${browserContextId?.substring(0,8)} -> clientId=${contextClientId}`);
556
- }
557
- }
558
-
559
- // Service Worker 处理:Service Worker 通常属于创建它的页面所在的客户端
560
- // 通过 browserContextId 来判断归属
561
- if (targetType === 'service_worker' && browserContextId && targetId) {
562
- const contextClientId = browserContextToClientId.get(browserContextId);
563
- if (contextClientId) {
564
- targetIdToClientId.set(targetId, contextClientId);
565
- console.log(`[SERVICE WORKER] targetId=${targetId?.substring(0,8)} -> clientId=${contextClientId}`);
566
- }
567
- }
568
-
569
- // iframe (OOPIF) 处理:跨域 iframe 可能有独立的 target
570
- // 通过 openerId 或 browserContextId 来判断归属
571
- if (targetType === 'iframe' && targetId) {
572
- // 优先使用 openerId
573
- if (openerId) {
574
- const openerClientId = targetIdToClientId.get(openerId);
575
- if (openerClientId) {
576
- targetIdToClientId.set(targetId, openerClientId);
577
- console.log(`[IFRAME with opener] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0,8)} -> clientId=${openerClientId}`);
578
- }
579
- } else if (browserContextId) {
580
- // 否则使用 browserContextId
581
- const contextClientId = browserContextToClientId.get(browserContextId);
582
- if (contextClientId) {
583
- targetIdToClientId.set(targetId, contextClientId);
584
- console.log(`[IFRAME in context] targetId=${targetId?.substring(0,8)} browserContextId=${browserContextId?.substring(0,8)} -> clientId=${contextClientId}`);
585
- }
586
- }
587
- }
588
- }
589
-
590
- const broadcastMethods = [
591
- 'Target.targetCreated',
592
- 'Target.targetDestroyed',
593
- 'Target.targetInfoChanged'
499
+ // 3. 其他事件:无 id 和 sessionId 的消息
500
+ // 注意:Target 事件已在上方 type: 'event' 分支处理
501
+ // 这里只处理非 Target 事件
502
+ if (parsed && parsed.method && !parsed.sessionId) {
503
+ const nonTargetBroadcastMethods = [
504
+ 'Inspector.detached',
505
+ 'Log.entryAdded'
594
506
  ];
595
- if (broadcastMethods.includes(parsed.method)) {
596
- for (const clientWs of clientConnections) {
597
- if (clientWs.readyState === WebSocket.OPEN) {
598
- clientWs.send(data);
599
- }
600
- }
507
+ if (nonTargetBroadcastMethods.includes(parsed.method)) {
508
+ broadcastToClients(data, null);
601
509
  }
602
510
  }
603
511
  });
@@ -1016,11 +924,16 @@ function handlePageConnection(ws, clientInfo, targetId) {
1016
924
  cdpMsg.sessionId = msg.sessionId;
1017
925
  }
1018
926
 
1019
- if (msg.method === 'Page.screencastFrame' && shouldLog('debug')) {
1020
- console.log(`[PLUGIN -> PAGE] ${id}: Page.screencastFrame`);
927
+ // 对于全局 Target 事件,需要广播给所有客户端
928
+ const broadcastEvents = ['Target.targetCreated', 'Target.attachedToTarget', 'Target.targetDestroyed', 'Target.targetInfoChanged'];
929
+ if (broadcastEvents.includes(msg.method)) {
930
+ rewriteBrowserContextId(cdpMsg);
931
+ console.log(`[PLUGIN -> ALL CLIENTS] Broadcasting ${msg.method}`);
932
+ broadcastToClients(JSON.stringify(cdpMsg), null);
933
+ } else {
934
+ ws.lastActivityTime = Date.now();
935
+ ws.send(JSON.stringify(cdpMsg));
1021
936
  }
1022
- ws.lastActivityTime = Date.now();
1023
- ws.send(JSON.stringify(cdpMsg));
1024
937
  return;
1025
938
  }
1026
939
 
@@ -1109,16 +1022,51 @@ function handlePageConnection(ws, clientInfo, targetId) {
1109
1022
  });
1110
1023
  }
1111
1024
 
1025
+ /**
1026
+ * 重写 Target 事件中的 browserContextId
1027
+ * 插件总是报告 'default',但 Playwright 期望自己创建的 context ID
1028
+ * 通过 openerId 找到对应的 clientId,再找到该 client 的 browserContextId
1029
+ */
1030
+ function rewriteBrowserContextId(cdpMsg) {
1031
+ const targetInfo = cdpMsg.params?.targetInfo;
1032
+ if (!targetInfo || targetInfo.browserContextId !== 'default') {
1033
+ return cdpMsg;
1034
+ }
1035
+
1036
+ let clientId = null;
1037
+
1038
+ if (targetInfo.openerId) {
1039
+ clientId = targetIdToClientId.get(targetInfo.openerId);
1040
+ }
1041
+ if (!clientId && targetInfo.targetId) {
1042
+ clientId = targetIdToClientId.get(targetInfo.targetId);
1043
+ }
1044
+
1045
+ if (clientId) {
1046
+ const contextId = clientIdToBrowserContext.get(clientId);
1047
+ if (contextId) {
1048
+ console.log(`[CONTEXT REWRITE] targetId=${targetInfo.targetId?.substring(0,8)} browserContextId: 'default' -> '${contextId}' (via openerId=${targetInfo.openerId?.substring(0,8) || 'none'}, clientId=${clientId})`);
1049
+ targetInfo.browserContextId = contextId;
1050
+ }
1051
+ }
1052
+
1053
+ return cdpMsg;
1054
+ }
1055
+
1112
1056
  /**
1113
1057
  * 广播消息给所有客户端
1114
1058
  */
1115
1059
  function broadcastToClients(data, excludeWs = null) {
1116
1060
  let sent = 0;
1061
+ logCDP('BROADCAST', `Starting broadcast to ${clientConnections.size} clients, data preview: ${data.substring(0, 200)}`);
1117
1062
  clientConnections.forEach((client) => {
1063
+ logCDP('BROADCAST', `Checking client ${client.id}, state=${client.readyState}, excluded=${client === excludeWs}`);
1118
1064
  if (client !== excludeWs && safeSend(client, data, 'client')) {
1119
1065
  sent++;
1066
+ logCDP('BROADCAST', `Sent to client ${client.id}`);
1120
1067
  }
1121
1068
  });
1069
+ logCDP('BROADCAST', `Finished: sent to ${sent} clients`);
1122
1070
  return sent;
1123
1071
  }
1124
1072
 
@@ -0,0 +1,192 @@
1
+ const { chromium } = require('playwright');
2
+ const { spawn } = require('child_process');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ let serverPort = 9050;
7
+
8
+ async function testThreePages(port, label) {
9
+ serverPort++;
10
+ const currentPort = serverPort;
11
+
12
+ console.log(`\n${'='.repeat(60)}`);
13
+ console.log(`Testing ${label} (port ${port})`);
14
+ console.log('='.repeat(60));
15
+
16
+ try {
17
+ const sharedState = { counter: 0 };
18
+
19
+ const htmlContent1 = `
20
+ <!DOCTYPE html>
21
+ <html>
22
+ <head><title>Page 1</title></head>
23
+ <body>
24
+ <h1>Page 1</h1>
25
+ <a href="page2.html" id="link1">Go to Page 2 (same tab)</a>
26
+ <button id="btn" onclick="window.getCounter().then(r => document.getElementById('result').innerText = r)">Get Counter</button>
27
+ <div id="result">-</div>
28
+ </body>
29
+ </html>
30
+ `;
31
+
32
+ const htmlContent2 = `
33
+ <!DOCTYPE html>
34
+ <html>
35
+ <head><title>Page 2</title></head>
36
+ <body>
37
+ <h1>Page 2</h1>
38
+ <a href="page3.html" target="_blank" id="link2">Open Page 3 (new tab)</a>
39
+ <button id="btn" onclick="window.getCounter().then(r => document.getElementById('result').innerText = r)">Get Counter</button>
40
+ <div id="result">-</div>
41
+ </body>
42
+ </html>
43
+ `;
44
+
45
+ const htmlContent3 = `
46
+ <!DOCTYPE html>
47
+ <html>
48
+ <head><title>Page 3</title></head>
49
+ <body>
50
+ <h1>Page 3 - New Tab</h1>
51
+ <button id="btn" onclick="window.getCounter().then(r => document.getElementById('result').innerText = r)">Get Counter</button>
52
+ <div id="result">-</div>
53
+ </body>
54
+ </html>
55
+ `;
56
+
57
+ const serverDir = '/tmp/test-three-pages';
58
+ if (!fs.existsSync(serverDir)) {
59
+ fs.mkdirSync(serverDir, { recursive: true });
60
+ }
61
+ fs.writeFileSync(path.join(serverDir, 'page1.html'), htmlContent1);
62
+ fs.writeFileSync(path.join(serverDir, 'page2.html'), htmlContent2);
63
+ fs.writeFileSync(path.join(serverDir, 'page3.html'), htmlContent3);
64
+
65
+ const http = require('http');
66
+ const server = http.createServer((req, res) => {
67
+ let filePath = path.join(serverDir, req.url === '/' ? 'page1.html' : req.url);
68
+ if (fs.existsSync(filePath)) {
69
+ const content = fs.readFileSync(filePath);
70
+ res.writeHead(200, { 'Content-Type': 'text/html' });
71
+ res.end(content);
72
+ } else {
73
+ res.writeHead(404);
74
+ res.end('Not found');
75
+ }
76
+ });
77
+
78
+ await new Promise(resolve => server.listen(currentPort, resolve));
79
+ console.log(`[${label}] Test server on port ${currentPort}`);
80
+
81
+ const browser = await chromium.connectOverCDP(`http://localhost:${port}`);
82
+ console.log(`[${label}] Connected!`);
83
+
84
+ const context = await browser.newContext();
85
+ console.log(`[${label}] Context created`);
86
+
87
+ await context.exposeFunction('getCounter', () => {
88
+ sharedState.counter += 1;
89
+ console.log(`[${label}] getCounter called -> counter: ${sharedState.counter}`);
90
+ return sharedState.counter;
91
+ });
92
+ console.log(`[${label}] exposeFunction registered`);
93
+
94
+ const page1 = await context.newPage();
95
+ await page1.goto(`http://localhost:${currentPort}/page1.html`, { waitUntil: 'domcontentloaded' });
96
+ console.log(`[${label}] Page 1 loaded: ${page1.url()}`);
97
+
98
+ const r1 = await page1.evaluate(() => window.getCounter());
99
+ console.log(`[${label}] Page 1 counter: ${r1} (expect 1)`);
100
+
101
+ console.log(`[${label}] --- Step 1: Click link in Page 1 -> navigate to Page 2 (same tab) ---`);
102
+ await Promise.all([
103
+ page1.waitForURL('**/page2.html'),
104
+ page1.click('#link1')
105
+ ]);
106
+ console.log(`[${label}] Page 1 navigated to Page 2: ${page1.url()}`);
107
+
108
+ const r2 = await page1.evaluate(() => window.getCounter());
109
+ console.log(`[${label}] Page 2 counter: ${r2} (expect 2)`);
110
+
111
+ console.log(`[${label}] --- Step 2: Click link in Page 2 -> open Page 3 (new tab) ---`);
112
+ const [page3] = await Promise.all([
113
+ context.waitForEvent('page', { timeout: 10000 }),
114
+ page1.click('#link2')
115
+ ]);
116
+ await page3.waitForLoadState('domcontentloaded');
117
+ console.log(`[${label}] Page 3 opened in new tab: ${page3.url()}`);
118
+
119
+ const r3 = await page3.evaluate(() => window.getCounter());
120
+ console.log(`[${label}] Page 3 counter: ${r3} (expect 3)`);
121
+
122
+ console.log(`[${label}] --- Step 3: Verify all pages ---`);
123
+ const allPages = context.pages();
124
+ console.log(`[${label}] Total pages in context: ${allPages.length} (expect 2)`);
125
+
126
+ const r2again = await page1.evaluate(() => window.getCounter());
127
+ console.log(`[${label}] Page 2 counter again: ${r2again} (expect 4)`);
128
+
129
+ const r3again = await page3.evaluate(() => window.getCounter());
130
+ console.log(`[${label}] Page 3 counter again: ${r3again} (expect 5)`);
131
+
132
+ const success = r1 === 1 && r2 === 2 && r3 === 3 && r2again === 4 && r3again === 5;
133
+
134
+ server.close();
135
+
136
+ if (success) {
137
+ console.log(`[${label}] ✓ PASS! Counter: ${r1} -> ${r2} -> ${r3} -> ${r2again} -> ${r3again}`);
138
+ } else {
139
+ console.log(`[${label}] ✗ FAIL! Expected: 1,2,3,4,5 Got: ${r1},${r2},${r3},${r2again},${r3again}`);
140
+ }
141
+
142
+ await browser.close();
143
+ return success;
144
+
145
+ } catch (error) {
146
+ console.error(`[${label}] ✗ Error:`, error.message);
147
+ return false;
148
+ }
149
+ }
150
+
151
+ async function main() {
152
+ console.log('Testing: Page1 -> navigate to Page2 (same tab) -> open Page3 (new tab)');
153
+ console.log('Counter: 1 -> 2 -> 3 -> 4 -> 5 (shared state via exposeFunction)\n');
154
+
155
+ console.log('Step 1: Starting Chromium on port 9230...');
156
+ const chromiumProcess = spawn('/Applications/Chromium.app/Contents/MacOS/Chromium', [
157
+ '--remote-debugging-port=9230',
158
+ '--user-data-dir=/tmp/chromium-test-three',
159
+ '--no-first-run',
160
+ '--no-default-browser-check'
161
+ ], { detached: true, stdio: 'ignore' });
162
+
163
+ await new Promise(resolve => setTimeout(resolve, 3000));
164
+
165
+ console.log('\nStep 2: Testing Native CDP...');
166
+ const nativeResult = await testThreePages(9230, 'Native CDP');
167
+
168
+ await new Promise(resolve => setTimeout(resolve, 1000));
169
+
170
+ console.log('\nStep 3: Testing CDP Tunnel...');
171
+ const tunnelResult = await testThreePages(9221, 'CDP Tunnel');
172
+
173
+ try { process.kill(-chromiumProcess.pid); } catch (e) {}
174
+
175
+ console.log('\n' + '='.repeat(60));
176
+ console.log(`Native CDP: ${nativeResult ? '✓ PASS' : '✗ FAIL'}`);
177
+ console.log(`CDP Tunnel: ${tunnelResult ? '✓ PASS' : '✗ FAIL'}`);
178
+
179
+ if (nativeResult && tunnelResult) {
180
+ console.log('\n✓ Both work identically!');
181
+ } else if (!nativeResult && tunnelResult) {
182
+ console.log('\n✓ CDP Tunnel works! Native CDP has limitations.');
183
+ } else if (nativeResult && !tunnelResult) {
184
+ console.log('\n✗ CDP Tunnel has issues!');
185
+ } else {
186
+ console.log('\n✗ Both have issues!');
187
+ }
188
+
189
+ process.exit(0);
190
+ }
191
+
192
+ main();