cdp-tunnel 1.0.10 → 1.0.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-tunnel",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
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",
@@ -1,5 +1,5 @@
1
1
  const CONFIG = {
2
- PORT: 9221,
2
+ PORT: process.env.PORT ? parseInt(process.env.PORT) : 9221,
3
3
  HEARTBEAT_INTERVAL: 30000,
4
4
  STATUS_PRINT_INTERVAL: 60000,
5
5
  TARGETS_CACHE_TTL: 2000,
@@ -11,6 +11,7 @@ const statusLogFile = path.join(logDir, 'server-status.log');
11
11
 
12
12
  const MAX_LOG_SIZE = 10 * 1024 * 1024;
13
13
  const MAX_LOG_FILES = 5;
14
+ const MAX_TOTAL_LOG_SIZE = 30 * 1024 * 1024;
14
15
 
15
16
  let logWriteQueue = [];
16
17
  let isWritingLog = false;
@@ -84,6 +85,25 @@ function flushStatusQueue() {
84
85
  function checkLogRotation() {
85
86
  checkAndRotateLog(logFile);
86
87
  checkAndRotateLog(statusLogFile);
88
+ cleanupOldLogs();
89
+ }
90
+
91
+ function cleanupOldLogs() {
92
+ try {
93
+ const files = fs.readdirSync(logDir).filter(f => f.endsWith('.log')).map(f => {
94
+ const fp = path.join(logDir, f);
95
+ try {
96
+ return { path: fp, stat: fs.statSync(fp) };
97
+ } catch { return null; }
98
+ }).filter(Boolean).sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
99
+
100
+ let totalSize = files.reduce((sum, f) => sum + f.stat.size, 0);
101
+ while (totalSize > MAX_TOTAL_LOG_SIZE && files.length > 1) {
102
+ const oldest = files.shift();
103
+ fs.unlinkSync(oldest.path);
104
+ totalSize -= oldest.stat.size;
105
+ }
106
+ } catch {}
87
107
  }
88
108
 
89
109
  setInterval(checkLogRotation, 60000);
@@ -105,7 +125,43 @@ const NOISY_METHODS = [
105
125
  'Network.responseReceivedExtraInfo',
106
126
  'Network.dataReceived',
107
127
  'Network.loadingFinished',
128
+ 'Network.resourceChangedPriority',
129
+ 'Network.requestServedFromCache',
130
+ 'Network.webSocketFrameSent',
131
+ 'Network.webSocketFrameReceived',
132
+ 'Network.eventSourceMessageReceived',
108
133
  'Input.dispatchMouseEvent',
134
+ 'Input.mouseMoved',
135
+ 'Input.keyDown',
136
+ 'Input.keyUp',
137
+ 'Input.char',
138
+ 'Input.dispatchKeyEvent',
139
+ 'Page.lifecycleEvent',
140
+ 'Page.frameStartedLoading',
141
+ 'Page.frameStoppedLoading',
142
+ 'Page.frameNavigated',
143
+ 'Page.frameRequestedNavigation',
144
+ 'Page.frameScheduledNavigation',
145
+ 'Page.frameStartedNavigating',
146
+ 'Page.frameAttached',
147
+ 'Page.frameClearedScheduledNavigation',
148
+ 'Page.navigatedWithinDocument',
149
+ 'Page.domContentEventFired',
150
+ 'Page.loadEventFired',
151
+ 'Page.screencastFrame',
152
+ 'Page.screencastFrameAck',
153
+ 'Runtime.executionContextCreated',
154
+ 'Runtime.executionContextDestroyed',
155
+ 'Runtime.executionContextsCleared',
156
+ 'Runtime.bindingCalled',
157
+ 'CSS.styleChanged',
158
+ 'CSS.fontsUpdated',
159
+ 'DOM.childNodeInserted',
160
+ 'DOM.childNodeRemoved',
161
+ 'DOM.attributeModified',
162
+ 'DOM.attributeRemoved',
163
+ 'DOM.childNodeCountUpdated',
164
+ 'Log.entryAdded',
109
165
  ];
110
166
 
111
167
  function truncateMessage(message) {
@@ -0,0 +1,304 @@
1
+ const { chromium } = require('playwright');
2
+ const { spawn } = require('child_process');
3
+ const http = require('http');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ const CHROMIUM = '/Applications/Chromium.app/Contents/MacOS/Chromium';
8
+ const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
9
+ const EXTENSION_PATH = path.join(__dirname, '..', 'extension-new');
10
+ const SERVER_SCRIPT = path.join(__dirname, '..', 'server', 'proxy-server.js');
11
+ const USER_DATA_DIR = '/tmp/cdp-tunnel-e2e-test';
12
+ const PROXY_PORT = 19221;
13
+ const CDP_URL = `http://localhost:${PROXY_PORT}`;
14
+ const TEST_EXT_DIR = '/tmp/cdp-tunnel-e2e-extension';
15
+
16
+ const CHROME_PATH = fs.existsSync(CHROMIUM) ? CHROMIUM : fs.existsSync(CHROME) ? CHROME : null;
17
+
18
+ if (!CHROME_PATH) {
19
+ console.error('Chrome/Chromium not found');
20
+ process.exit(1);
21
+ }
22
+
23
+ async function sleep(ms) {
24
+ return new Promise(r => setTimeout(r, ms));
25
+ }
26
+
27
+ function httpGet(url) {
28
+ return new Promise((resolve, reject) => {
29
+ http.get(url, (res) => {
30
+ let data = '';
31
+ res.on('data', (chunk) => data += chunk);
32
+ res.on('end', () => resolve(data));
33
+ }).on('error', reject);
34
+ });
35
+ }
36
+
37
+ async function waitForServer(port, maxWait = 10000) {
38
+ const start = Date.now();
39
+ while (Date.now() - start < maxWait) {
40
+ try {
41
+ await httpGet(`http://localhost:${port}/json/version`);
42
+ return true;
43
+ } catch {
44
+ await sleep(500);
45
+ }
46
+ }
47
+ return false;
48
+ }
49
+
50
+ async function waitForExtension(maxWait = 15000) {
51
+ const start = Date.now();
52
+ while (Date.now() - start < maxWait) {
53
+ try {
54
+ const data = await httpGet(`http://localhost:${PROXY_PORT}/json/version`);
55
+ const info = JSON.parse(data);
56
+ return true;
57
+ } catch {}
58
+ await sleep(500);
59
+ }
60
+ return false;
61
+ }
62
+
63
+ async function runTest() {
64
+ let serverProc = null;
65
+ let chromeProc = null;
66
+ let browser = null;
67
+ let passed = 0;
68
+ let failed = 0;
69
+
70
+ console.log('='.repeat(60));
71
+ console.log(' CDP Tunnel E2E Automated Test');
72
+ console.log('='.repeat(60));
73
+ console.log(` Chrome: ${CHROME_PATH}`);
74
+ console.log(` Extension: ${EXTENSION_PATH}`);
75
+ console.log(` Proxy: ${CDP_URL}`);
76
+ console.log('='.repeat(60));
77
+
78
+ try {
79
+ // 1. Cleanup
80
+ console.log('\n[Setup] Cleaning up...');
81
+ if (fs.existsSync(USER_DATA_DIR)) {
82
+ fs.rmSync(USER_DATA_DIR, { recursive: true });
83
+ }
84
+ if (fs.existsSync(TEST_EXT_DIR)) {
85
+ fs.rmSync(TEST_EXT_DIR, { recursive: true });
86
+ }
87
+
88
+ // Kill any existing process on port
89
+ const { execSync } = require('child_process');
90
+ try { execSync(`lsof -ti :${PROXY_PORT} | xargs kill -9 2>/dev/null`, { stdio: 'ignore' }); } catch {}
91
+ await sleep(500);
92
+
93
+ // 2. Copy extension and patch WS port
94
+ console.log('[Setup] Preparing test extension (port ' + PROXY_PORT + ')...');
95
+ execSync(`cp -r "${EXTENSION_PATH}" "${TEST_EXT_DIR}"`, { stdio: 'ignore' });
96
+ const configPath = path.join(TEST_EXT_DIR, 'utils', 'config.js');
97
+ let configContent = fs.readFileSync(configPath, 'utf8');
98
+ configContent = configContent.replace(
99
+ /WS_URL:\s*'ws:\/\/localhost:\d+\/plugin'/,
100
+ `WS_URL: 'ws://localhost:${PROXY_PORT}/plugin'`
101
+ );
102
+ fs.writeFileSync(configPath, configContent);
103
+
104
+ // 2. Start proxy server
105
+ console.log('[Setup] Starting proxy server...');
106
+ serverProc = spawn('node', [SERVER_SCRIPT], {
107
+ detached: false,
108
+ stdio: ['ignore', 'pipe', 'pipe'],
109
+ env: { ...process.env, PORT: String(PROXY_PORT) }
110
+ });
111
+ serverProc.stdout.on('data', (d) => {
112
+ const msg = d.toString().trim();
113
+ if (msg.includes('PLUGIN') || msg.includes('CLIENT') || msg.includes('IFRAME')) {
114
+ console.log(` [Server] ${msg.substring(0, 120)}`);
115
+ }
116
+ });
117
+ serverProc.stderr.on('data', () => {});
118
+
119
+ if (!(await waitForServer(PROXY_PORT))) {
120
+ throw new Error('Proxy server failed to start');
121
+ }
122
+ console.log('[Setup] Proxy server started');
123
+
124
+ // 3. Launch Chrome with extension
125
+ console.log('[Setup] Launching Chrome with extension...');
126
+ chromeProc = spawn(CHROME_PATH, [
127
+ `--user-data-dir=${USER_DATA_DIR}`,
128
+ '--no-first-run',
129
+ '--no-default-browser-check',
130
+ '--disable-background-timer-throttling',
131
+ '--disable-backgrounding-occluded-windows',
132
+ '--disable-renderer-backgrounding',
133
+ `--load-extension=${TEST_EXT_DIR}`,
134
+ '--enable-features=AutomationControlled',
135
+ 'about:blank'
136
+ ], {
137
+ detached: false,
138
+ stdio: 'ignore'
139
+ });
140
+
141
+ console.log('[Setup] Waiting for extension to connect...');
142
+ await sleep(5000);
143
+
144
+ // 4. Connect Playwright
145
+ console.log('[Setup] Connecting Playwright...');
146
+ browser = await chromium.connectOverCDP(CDP_URL, { timeout: 15000 });
147
+ console.log('[Setup] Connected!\n');
148
+
149
+ const context = browser.contexts()[0];
150
+ const pages = context.pages();
151
+ let page = pages.find(p => p.url() === 'about:blank') || pages[0];
152
+ if (!page) page = await context.newPage();
153
+
154
+ // === TESTS ===
155
+
156
+ // Test 1: Basic page navigation
157
+ console.log('[Test 1] Basic page navigation...');
158
+ try {
159
+ await page.goto('https://example.com', { waitUntil: 'domcontentloaded', timeout: 10000 });
160
+ const title = await page.title();
161
+ console.log(`[Test 1] PASS - title: "${title}"`);
162
+ passed++;
163
+ } catch (e) {
164
+ console.error(`[Test 1] FAIL - ${e.message}`);
165
+ failed++;
166
+ }
167
+
168
+ // Test 2: Fill input on main page
169
+ console.log('\n[Test 2] Fill input on main page...');
170
+ try {
171
+ await page.goto(`file://${path.join(__dirname, 'iframe-test-page.html')}`, { waitUntil: 'domcontentloaded', timeout: 10000 });
172
+ await page.fill('#main-input-1', 'E2E test');
173
+ const val = await page.inputValue('#main-input-1');
174
+ if (val === 'E2E test') {
175
+ console.log(`[Test 2] PASS - value: "${val}"`);
176
+ passed++;
177
+ } else {
178
+ console.error(`[Test 2] FAIL - expected "E2E test", got "${val}"`);
179
+ failed++;
180
+ }
181
+ } catch (e) {
182
+ console.error(`[Test 2] FAIL - ${e.message}`);
183
+ failed++;
184
+ }
185
+
186
+ // Test 3: Same-origin iframe fill
187
+ console.log('\n[Test 3] Same-origin iframe fill...');
188
+ try {
189
+ const frame = page.frameLocator('#same-origin-iframe');
190
+ await frame.locator('#iframe-input-1').fill('iframe works', { timeout: 10000 });
191
+ const val = await frame.locator('#iframe-input-1').inputValue({ timeout: 5000 });
192
+ if (val === 'iframe works') {
193
+ console.log(`[Test 3] PASS - value: "${val}"`);
194
+ passed++;
195
+ } else {
196
+ console.error(`[Test 3] FAIL - expected "iframe works", got "${val}"`);
197
+ failed++;
198
+ }
199
+ } catch (e) {
200
+ console.error(`[Test 3] FAIL - ${e.message.substring(0, 100)}`);
201
+ failed++;
202
+ }
203
+
204
+ // Test 4: Create new page
205
+ console.log('\n[Test 4] Create new page...');
206
+ try {
207
+ const newPage = await context.newPage();
208
+ await newPage.goto('https://example.com', { waitUntil: 'domcontentloaded', timeout: 10000 });
209
+ const title = await newPage.title();
210
+ await newPage.close();
211
+ console.log(`[Test 4] PASS - new page title: "${title}"`);
212
+ passed++;
213
+ } catch (e) {
214
+ console.error(`[Test 4] FAIL - ${e.message}`);
215
+ failed++;
216
+ }
217
+
218
+ // Test 5: Page.getFrameTree
219
+ console.log('\n[Test 5] CDP Page.getFrameTree...');
220
+ try {
221
+ const cdpSession = await page.context().newCDPSession(page);
222
+ const frameTree = await cdpSession.send('Page.getFrameTree');
223
+ const mainFrame = frameTree.frameTree.frame;
224
+ const childCount = frameTree.frameTree.childFrames?.length || 0;
225
+ await cdpSession.detach();
226
+ console.log(`[Test 5] PASS - main frame: ${mainFrame.id} children: ${childCount}`);
227
+ passed++;
228
+ } catch (e) {
229
+ console.error(`[Test 5] FAIL - ${e.message}`);
230
+ failed++;
231
+ }
232
+
233
+ // Test 6: Target.setAutoAttach event count
234
+ console.log('\n[Test 6] Target.setAutoAttach events...');
235
+ try {
236
+ const cdpSession = await page.context().newCDPSession(page);
237
+ let eventCount = 0;
238
+ cdpSession.on('Target.attachedToTarget', () => eventCount++);
239
+ await cdpSession.send('Target.setAutoAttach', {
240
+ autoAttach: true,
241
+ waitForDebuggerOnStart: false,
242
+ flatten: true
243
+ });
244
+ await sleep(3000);
245
+ await cdpSession.detach();
246
+ console.log(`[Test 6] PASS - attachedToTarget events: ${eventCount}`);
247
+ passed++;
248
+ } catch (e) {
249
+ console.error(`[Test 6] FAIL - ${e.message}`);
250
+ failed++;
251
+ }
252
+
253
+ // Test 7: Douyin page (custom protocol blocking) - no disconnect
254
+ console.log('\n[Test 7] Douyin custom protocol (no disconnect)...');
255
+ try {
256
+ await page.goto('https://www.douyin.com', { waitUntil: 'domcontentloaded', timeout: 20000 });
257
+ await sleep(3000);
258
+ // If we get here without disconnect, the test passes
259
+ const url = page.url();
260
+ if (url.includes('douyin')) {
261
+ console.log(`[Test 7] PASS - page alive at ${url.substring(0, 50)}`);
262
+ passed++;
263
+ } else {
264
+ console.error(`[Test 7] FAIL - unexpected URL: ${url}`);
265
+ failed++;
266
+ }
267
+ } catch (e) {
268
+ console.error(`[Test 7] FAIL (possible disconnect) - ${e.message.substring(0, 120)}`);
269
+ failed++;
270
+ }
271
+
272
+ } catch (e) {
273
+ console.error(`\n!!! FATAL: ${e.message}`);
274
+ failed++;
275
+ } finally {
276
+ // Cleanup
277
+ console.log('\n[Cleanup] Tearing down...');
278
+ if (browser) {
279
+ try { await browser.close(); } catch {}
280
+ }
281
+ if (chromeProc) {
282
+ try { chromeProc.kill('SIGKILL'); } catch {}
283
+ }
284
+ if (serverProc) {
285
+ try { serverProc.kill('SIGKILL'); } catch {}
286
+ }
287
+ // Kill any leftover processes on our port
288
+ const { execSync } = require('child_process');
289
+ try { execSync(`lsof -ti :${PROXY_PORT} | xargs kill -9 2>/dev/null`, { stdio: 'ignore' }); } catch {}
290
+ try {
291
+ if (fs.existsSync(USER_DATA_DIR)) {
292
+ fs.rmSync(USER_DATA_DIR, { recursive: true });
293
+ }
294
+ } catch {}
295
+
296
+ console.log('\n' + '='.repeat(60));
297
+ console.log(` Results: ${passed} passed, ${failed} failed`);
298
+ console.log('='.repeat(60));
299
+
300
+ process.exit(failed > 0 ? 1 : 0);
301
+ }
302
+ }
303
+
304
+ runTest();
@@ -0,0 +1,89 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>IFrame Test Page</title>
6
+ <style>
7
+ body { font-family: sans-serif; padding: 20px; }
8
+ .section { margin: 20px 0; padding: 15px; border: 1px solid #ccc; border-radius: 8px; }
9
+ h2 { margin-top: 0; color: #333; }
10
+ iframe { width: 100%; height: 200px; border: 2px solid #666; margin: 10px 0; }
11
+ input { padding: 8px; font-size: 14px; margin: 5px; }
12
+ button { padding: 8px 16px; font-size: 14px; cursor: pointer; }
13
+ #status { margin-top: 10px; padding: 10px; background: #f0f0f0; border-radius: 4px; }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <h1>CDP Tunnel - IFrame Test Page</h1>
18
+
19
+ <div class="section">
20
+ <h2>Main Page Inputs</h2>
21
+ <input id="main-input-1" type="text" placeholder="Main input 1" />
22
+ <input id="main-input-2" type="text" placeholder="Main input 2" />
23
+ <button id="main-btn" onclick="document.getElementById('status').textContent='Main button clicked at ' + new Date().toISOString()">Main Button</button>
24
+ <div id="status"></div>
25
+ </div>
26
+
27
+ <div class="section">
28
+ <h2>Same-Origin IFrame</h2>
29
+ <iframe id="same-origin-iframe" srcdoc="
30
+ <html>
31
+ <body style='padding:10px; font-family:sans-serif;'>
32
+ <h3>Same-Origin IFrame Content</h3>
33
+ <input id='iframe-input-1' type='text' placeholder='Same-origin input 1' />
34
+ <input id='iframe-input-2' type='text' placeholder='Same-origin input 2' />
35
+ <button id='iframe-btn' onclick=\"document.getElementById('iframe-result').textContent='Iframe button clicked!'\">
36
+ Iframe Button
37
+ </button>
38
+ <div id='iframe-result'></div>
39
+ </body>
40
+ </html>
41
+ "></iframe>
42
+ </div>
43
+
44
+ <div class="section">
45
+ <h2>Cross-Origin IFrame (Wikipedia)</h2>
46
+ <iframe id="cross-origin-iframe" src="https://en.wikipedia.org/wiki/Main_Page" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
47
+ </div>
48
+
49
+ <div class="section">
50
+ <h2>Nested IFrame (same-origin, 2 levels)</h2>
51
+ <iframe id="nested-iframe" srcdoc="
52
+ <html>
53
+ <body style='padding:10px; font-family:sans-serif;'>
54
+ <h3>Level 1 IFrame</h3>
55
+ <input id='l1-input' type='text' placeholder='Level 1 input' />
56
+ <iframe id='nested-inner' srcdoc=\"
57
+ <html>
58
+ <body style='padding:10px; font-family:sans-serif;'>
59
+ <h4>Level 2 Nested IFrame</h4>
60
+ <input id='l2-input' type='text' placeholder='Level 2 input' />
61
+ </body>
62
+ </html>
63
+ \" width='100%' height='120' style='border:1px solid #999;'></iframe>
64
+ </body>
65
+ </html>
66
+ "></iframe>
67
+ </div>
68
+
69
+ <div class="section">
70
+ <h2>Dynamic IFrame (created by JS)</h2>
71
+ <button id="create-iframe-btn" onclick="createDynamicIframe()">Create Dynamic IFrame</button>
72
+ <div id="dynamic-iframe-container"></div>
73
+ <script>
74
+ function createDynamicIframe() {
75
+ const container = document.getElementById('dynamic-iframe-container');
76
+ const iframe = document.createElement('iframe');
77
+ iframe.id = 'dynamic-iframe';
78
+ iframe.width = '100%';
79
+ iframe.height = '150';
80
+ iframe.style.border = '2px solid red';
81
+ iframe.style.marginTop = '10px';
82
+ iframe.srcdoc = '<html><body style="padding:10px; font-family:sans-serif;"><h3>Dynamic IFrame</h3><input id="dyn-input" type="text" placeholder="Dynamic iframe input" /></body></html>';
83
+ container.appendChild(iframe);
84
+ document.getElementById('status').textContent = 'Dynamic iframe created';
85
+ }
86
+ </script>
87
+ </div>
88
+ </body>
89
+ </html>
@@ -0,0 +1,171 @@
1
+ const { chromium } = require('playwright');
2
+
3
+ const SERVER_URL = process.env.CDP_SERVER || 'http://localhost:9221';
4
+ const DOUYIN_URL = 'https://www.douyin.com/user/MS4wLjABAAAAnKeRN8QUgooS1pPRqOf_N_jnuztzUyocl0_vUndQFJs?modal_id=7635666432337351530';
5
+
6
+ async function sleep(ms) {
7
+ return new Promise(r => setTimeout(r, ms));
8
+ }
9
+
10
+ async function main() {
11
+ console.log('=== Douyin IFrame Disconnect Test ===');
12
+ console.log(`Server: ${SERVER_URL}\n`);
13
+
14
+ let browser;
15
+ try {
16
+ console.log('[1] Connecting to CDP tunnel...');
17
+ browser = await chromium.connectOverCDP(SERVER_URL, { timeout: 15000 });
18
+ console.log('[1] Connected!\n');
19
+
20
+ const context = browser.contexts()[0];
21
+ const page = context.pages()[0] || await context.newPage();
22
+
23
+ console.log('[2] Navigating to Douyin page...');
24
+ await page.goto(DOUYIN_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
25
+ console.log(`[2] Page loaded: ${page.url()}\n`);
26
+
27
+ await sleep(3000);
28
+ console.log('[3] Waiting for page to fully render...');
29
+ await sleep(2000);
30
+
31
+ // Check frame tree first
32
+ console.log('[4] Checking frame tree...');
33
+ try {
34
+ const cdpSession = await page.context().newCDPSession(page);
35
+ const frameTree = await cdpSession.send('Page.getFrameTree');
36
+
37
+ function printFrame(tree, indent) {
38
+ if (!tree || !tree.frame) return;
39
+ const pad = ' '.repeat(indent);
40
+ console.log(`${pad}Frame: id=${tree.frame.id} url=${(tree.frame.url || '').substring(0, 80)}`);
41
+ if (tree.childFrames) {
42
+ for (const child of tree.childFrames) printFrame(child, indent + 1);
43
+ }
44
+ }
45
+ printFrame(frameTree.frameTree, 0);
46
+ await cdpSession.detach();
47
+ } catch (e) {
48
+ console.error('[4] Frame tree error:', e.message);
49
+ }
50
+ console.log('');
51
+
52
+ // Try to find and click the entry button
53
+ // The button has data-popupid attribute and contains an SVG
54
+ console.log('[5] Looking for the entry button...');
55
+ const selectors = [
56
+ '[data-popupid]',
57
+ 'svg.wNbQukcA',
58
+ '.r68hW_1W',
59
+ ];
60
+
61
+ let clicked = false;
62
+ for (const sel of selectors) {
63
+ try {
64
+ const el = page.locator(sel).first();
65
+ const count = await el.count();
66
+ if (count > 0) {
67
+ console.log(`[5] Found element with selector: ${sel}`);
68
+ await el.click({ timeout: 5000 });
69
+ console.log(`[5] Clicked!`);
70
+ clicked = true;
71
+ break;
72
+ }
73
+ } catch (e) {
74
+ console.log(`[5] Selector ${sel}: ${e.message.substring(0, 80)}`);
75
+ }
76
+ }
77
+
78
+ if (!clicked) {
79
+ console.log('[5] Could not find entry button with known selectors, trying to list clickable elements...');
80
+ const buttons = await page.$$eval('[data-popupid]', els => els.map(e => ({
81
+ tag: e.tagName,
82
+ popupid: e.getAttribute('data-popupid'),
83
+ html: e.innerHTML.substring(0, 100)
84
+ })));
85
+ console.log('[5] Elements with data-popupid:', JSON.stringify(buttons, null, 2));
86
+ }
87
+
88
+ await sleep(3000);
89
+
90
+ // Check frame tree again after click
91
+ console.log('\n[6] Checking frame tree AFTER click...');
92
+ try {
93
+ const cdpSession = await page.context().newCDPSession(page);
94
+ const frameTree = await cdpSession.send('Page.getFrameTree');
95
+ printFrameTree(frameTree.frameTree, 0);
96
+ await cdpSession.detach();
97
+ } catch (e) {
98
+ console.error('[6] Frame tree error:', e.message);
99
+ }
100
+
101
+ // List all iframes on the page
102
+ console.log('\n[7] Listing all iframes...');
103
+ const iframes = await page.$$eval('iframe', els => els.map(e => ({
104
+ id: e.id,
105
+ src: e.src || e.getAttribute('srcdoc')?.substring(0, 50) || '(empty)',
106
+ className: e.className,
107
+ visible: e.offsetParent !== null
108
+ })));
109
+ console.log('[7] Iframes found:', JSON.stringify(iframes, null, 2));
110
+
111
+ // Try to find and interact with iframe input
112
+ console.log('\n[8] Trying to find input in iframes...');
113
+ const frames = page.frames();
114
+ console.log(`[8] Total frames: ${frames.length}`);
115
+ for (let i = 0; i < frames.length; i++) {
116
+ const frame = frames[i];
117
+ console.log(`[8] Frame ${i}: url=${frame.url().substring(0, 80)} name=${frame.name()}`);
118
+
119
+ if (frame === page.mainFrame()) continue;
120
+
121
+ try {
122
+ const inputs = await frame.locator('input, textarea').count();
123
+ console.log(`[8] Found ${inputs} input/textarea elements`);
124
+
125
+ if (inputs > 0) {
126
+ console.log(`[8] Attempting to click first input in frame ${i}...`);
127
+ const input = frame.locator('input, textarea').first();
128
+ await input.click({ timeout: 5000 });
129
+ console.log(`[8] CLICK SUCCEEDED - no disconnect!`);
130
+
131
+ await sleep(1000);
132
+ await input.fill('test input');
133
+ console.log(`[8] FILL SUCCEEDED`);
134
+ }
135
+ } catch (e) {
136
+ console.error(`[8] Frame ${i} error: ${e.message.substring(0, 120)}`);
137
+ }
138
+ }
139
+
140
+ console.log('\n=== Test completed - Playwright did NOT disconnect ===');
141
+
142
+ } catch (e) {
143
+ console.error('\n!!! FATAL ERROR - Possible disconnect !!!');
144
+ console.error('Error:', e.message);
145
+ console.error('This might indicate Playwright disconnected');
146
+ } finally {
147
+ if (browser) {
148
+ try {
149
+ await browser.close();
150
+ console.log('\nBrowser closed normally');
151
+ } catch (e) {
152
+ console.error('\nBrowser close FAILED:', e.message);
153
+ console.error('This confirms Playwright disconnected');
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ function printFrameTree(tree, indent) {
160
+ if (!tree || !tree.frame) return;
161
+ const pad = ' '.repeat(indent);
162
+ console.log(`${pad}Frame: id=${tree.frame.id} url=${(tree.frame.url || '').substring(0, 80)}`);
163
+ if (tree.childFrames) {
164
+ for (const child of tree.childFrames) printFrameTree(child, indent + 1);
165
+ }
166
+ }
167
+
168
+ main().catch(e => {
169
+ console.error('UNHANDLED:', e);
170
+ process.exit(1);
171
+ });
@@ -0,0 +1,204 @@
1
+ const { chromium } = require('playwright');
2
+
3
+ const SERVER_URL = process.env.CDP_SERVER || 'http://localhost:9221';
4
+ const TEST_PAGE = `file://${__dirname}/iframe-test-page.html`;
5
+
6
+ async function sleep(ms) {
7
+ return new Promise(r => setTimeout(r, ms));
8
+ }
9
+
10
+ async function main() {
11
+ console.log('=== IFrame Debug Test ===');
12
+ console.log(`Server: ${SERVER_URL}`);
13
+ console.log(`Test page: ${TEST_PAGE}\n`);
14
+
15
+ let browser;
16
+ try {
17
+ console.log('[1] Connecting to CDP tunnel...');
18
+ browser = await chromium.connectOverCDP(SERVER_URL, {
19
+ timeout: 10000
20
+ });
21
+ console.log('[1] Connected!\n');
22
+
23
+ const context = browser.contexts()[0];
24
+ if (!context) {
25
+ throw new Error('No default context found');
26
+ }
27
+
28
+ const pages = context.pages();
29
+ let page;
30
+ if (pages.length > 0) {
31
+ page = pages[0];
32
+ console.log(`[2] Using existing page: ${page.url()}`);
33
+ } else {
34
+ page = await context.newPage();
35
+ console.log('[2] Created new page');
36
+ }
37
+
38
+ console.log(`[2] Navigating to test page...`);
39
+ await page.goto(TEST_PAGE, { waitUntil: 'domcontentloaded', timeout: 15000 });
40
+ console.log(`[2] Page loaded: ${page.url()}\n`);
41
+
42
+ // --- Test 1: Main page input ---
43
+ console.log('[Test 1] Main page input...');
44
+ try {
45
+ await page.fill('#main-input-1', 'Hello from main');
46
+ const val1 = await page.inputValue('#main-input-1');
47
+ console.log(`[Test 1] OK - value: "${val1}"\n`);
48
+ } catch (e) {
49
+ console.error(`[Test 1] FAIL: ${e.message}\n`);
50
+ }
51
+
52
+ // --- Test 2: Main page button ---
53
+ console.log('[Test 2] Main page button click...');
54
+ try {
55
+ await page.click('#main-btn');
56
+ await sleep(500);
57
+ const status = await page.textContent('#status');
58
+ console.log(`[Test 2] OK - status: "${status}"\n`);
59
+ } catch (e) {
60
+ console.error(`[Test 2] FAIL: ${e.message}\n`);
61
+ }
62
+
63
+ // --- Test 3: Same-origin iframe ---
64
+ console.log('[Test 3] Same-origin iframe input...');
65
+ try {
66
+ const frame = page.frameLocator('#same-origin-iframe');
67
+ const input = frame.locator('#iframe-input-1');
68
+ await input.fill('Hello from iframe');
69
+ const val = await input.inputValue();
70
+ console.log(`[Test 3] OK - value: "${val}"\n`);
71
+ } catch (e) {
72
+ console.error(`[Test 3] FAIL: ${e.message}\n`);
73
+ }
74
+
75
+ // --- Test 4: Same-origin iframe button ---
76
+ console.log('[Test 4] Same-origin iframe button click...');
77
+ try {
78
+ const frame = page.frameLocator('#same-origin-iframe');
79
+ await frame.locator('#iframe-btn').click();
80
+ await sleep(500);
81
+ const result = await frame.locator('#iframe-result').textContent();
82
+ console.log(`[Test 4] OK - result: "${result}"\n`);
83
+ } catch (e) {
84
+ console.error(`[Test 4] FAIL: ${e.message}\n`);
85
+ }
86
+
87
+ // --- Test 5: Cross-origin iframe (Wikipedia) ---
88
+ console.log('[Test 5] Cross-origin iframe (Wikipedia)...');
89
+ try {
90
+ const frame = page.frameLocator('#cross-origin-iframe');
91
+ // Just try to access the frame - this will likely fail
92
+ const title = await frame.locator('h1').first().textContent({ timeout: 5000 });
93
+ console.log(`[Test 5] OK - title: "${title}"\n`);
94
+ } catch (e) {
95
+ console.error(`[Test 5] FAIL (expected): ${e.message}\n`);
96
+ }
97
+
98
+ // --- Test 6: Nested iframe ---
99
+ console.log('[Test 6] Nested iframe (level 1 input)...');
100
+ try {
101
+ const frame = page.frameLocator('#nested-iframe');
102
+ await frame.locator('#l1-input').fill('Level 1');
103
+ const val = await frame.locator('#l1-input').inputValue();
104
+ console.log(`[Test 6] OK - value: "${val}"\n`);
105
+ } catch (e) {
106
+ console.error(`[Test 6] FAIL: ${e.message}\n`);
107
+ }
108
+
109
+ // --- Test 7: Nested iframe level 2 ---
110
+ console.log('[Test 7] Nested iframe (level 2 input)...');
111
+ try {
112
+ const frame = page.frameLocator('#nested-iframe').frameLocator('#nested-inner');
113
+ await frame.locator('#l2-input').fill('Level 2');
114
+ const val = await frame.locator('#l2-input').inputValue();
115
+ console.log(`[Test 7] OK - value: "${val}"\n`);
116
+ } catch (e) {
117
+ console.error(`[Test 7] FAIL: ${e.message}\n`);
118
+ }
119
+
120
+ // --- Test 8: Dynamic iframe ---
121
+ console.log('[Test 8] Dynamic iframe...');
122
+ try {
123
+ await page.click('#create-iframe-btn');
124
+ await sleep(1000);
125
+ const frame = page.frameLocator('#dynamic-iframe');
126
+ await frame.locator('#dyn-input').fill('Dynamic!');
127
+ const val = await frame.locator('#dyn-input').inputValue();
128
+ console.log(`[Test 8] OK - value: "${val}"\n`);
129
+ } catch (e) {
130
+ console.error(`[Test 8] FAIL: ${e.message}\n`);
131
+ }
132
+
133
+ // --- Test 9: Page.getFrameTree via CDP directly ---
134
+ console.log('[Test 9] CDP Page.getFrameTree...');
135
+ try {
136
+ const cdpSession = await page.context().newCDPSession(page);
137
+ const frameTree = await cdpSession.send('Page.getFrameTree');
138
+ console.log(`[Test 9] Frame tree:`);
139
+ printFrameTree(frameTree.frameTree, 0);
140
+ await cdpSession.detach();
141
+ console.log('');
142
+ } catch (e) {
143
+ console.error(`[Test 9] FAIL: ${e.message}\n`);
144
+ }
145
+
146
+ // --- Test 10: Target.setAutoAttach + Target.attachedToTarget ---
147
+ console.log('[Test 10] CDP Target.setAutoAttach + iframe sessions...');
148
+ try {
149
+ const cdpSession = await page.context().newCDPSession(page);
150
+
151
+ let attachedEventCount = 0;
152
+ cdpSession.on('Target.attachedToTarget', (event) => {
153
+ attachedEventCount++;
154
+ console.log(`[Test 10] Target.attachedToTarget event #${attachedEventCount}:`);
155
+ console.log(` sessionId: ${event.sessionId}`);
156
+ console.log(` targetInfo.type: ${event.targetInfo?.type}`);
157
+ console.log(` targetInfo.targetId: ${event.targetInfo?.targetId}`);
158
+ console.log(` targetInfo.url: ${event.targetInfo?.url}`);
159
+ console.log(` waitingForDebugger: ${event.waitingForDebugger}`);
160
+ });
161
+
162
+ cdpSession.on('Target.targetCreated', (event) => {
163
+ console.log(`[Test 10] Target.targetCreated: type=${event.targetInfo?.type} targetId=${event.targetInfo?.targetId}`);
164
+ });
165
+
166
+ await cdpSession.send('Target.setAutoAttach', {
167
+ autoAttach: true,
168
+ waitForDebuggerOnStart: false,
169
+ flatten: true
170
+ });
171
+
172
+ await sleep(3000);
173
+
174
+ console.log(`[Test 10] Total attachedToTarget events: ${attachedEventCount}`);
175
+ await cdpSession.detach();
176
+ console.log('');
177
+ } catch (e) {
178
+ console.error(`[Test 10] FAIL: ${e.message}\n`);
179
+ }
180
+
181
+ console.log('=== All tests completed ===');
182
+
183
+ } catch (e) {
184
+ console.error('Fatal error:', e);
185
+ } finally {
186
+ if (browser) {
187
+ await browser.close().catch(() => {});
188
+ }
189
+ }
190
+ }
191
+
192
+ function printFrameTree(tree, indent) {
193
+ if (!tree || !tree.frame) return;
194
+ const pad = ' '.repeat(indent);
195
+ const frame = tree.frame;
196
+ console.log(`${pad}Frame: id=${frame.id} url=${frame.url?.substring(0, 60)}`);
197
+ if (tree.childFrames) {
198
+ for (const child of tree.childFrames) {
199
+ printFrameTree(child, indent + 1);
200
+ }
201
+ }
202
+ }
203
+
204
+ main().catch(console.error);