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 +1 -1
- package/server/modules/config.js +1 -1
- package/server/modules/logger.js +56 -0
- package/tests/e2e-auto-test.js +304 -0
- package/tests/iframe-test-page.html +89 -0
- package/tests/test-douyin-iframe.js +171 -0
- package/tests/test-iframe-debug.js +204 -0
package/package.json
CHANGED
package/server/modules/config.js
CHANGED
package/server/modules/logger.js
CHANGED
|
@@ -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);
|