browser-commander 0.28.0 → 0.30.0
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/.jscpd-baseline.json +7 -20
- package/CHANGELOG.md +12 -0
- package/README.md +27 -10
- package/package.json +1 -1
- package/scripts/run-tests.mjs +3 -1
- package/src/browser/browser-profile-files.js +5 -2
- package/src/browser/connector.js +140 -65
- package/src/browser/launcher.js +19 -8
- package/src/browser/persistent-session.js +110 -38
- package/src/browser/session-adoption.js +96 -0
- package/src/browser/session-worker.js +12 -0
- package/src/cli/args.js +3 -0
- package/src/cli/commands.js +3 -0
- package/src/cli/high-level-methods.js +7 -0
- package/src/cli/sessions.js +3 -0
- package/src/core/commander-features.js +33 -25
- package/src/core/navigation-safety.js +1 -0
- package/src/core/page-trigger-manager.js +173 -134
- package/src/index.js +2 -0
- package/src/interactions/overlays.js +59 -14
- package/src/traces/bundle.js +41 -3
- package/src/traces/checkpoints.js +7 -3
- package/src/traces/commander-actions.js +146 -0
- package/src/traces/debug-session.js +119 -0
- package/src/traces/drain-timer.js +25 -0
- package/src/traces/links.js +44 -5
- package/src/traces/mutation-stream.js +12 -5
- package/src/traces/network.js +65 -13
- package/src/traces/observers.js +50 -46
- package/src/traces/page-capture.js +65 -21
- package/src/traces/recorder.js +79 -28
- package/src/traces/redaction.js +89 -6
- package/src/traces/rolling-bundle.js +240 -0
- package/src/traces/rolling.js +10 -113
- package/src/traces/stop.js +1 -0
- package/src/traces/user-actions.js +405 -0
- package/tests/e2e/issue-160.e2e.test.js +291 -0
- package/tests/e2e/issue-162-recording.e2e.test.js +175 -0
- package/tests/e2e/traces.e2e.test.js +4 -1
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +1 -1
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +1 -1
- package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +4 -4
- package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +4 -0
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +1 -1
- package/tests/fixtures/traces/conformance/expected/trace.lino +4 -4
- package/tests/fixtures/traces/conformance/scenario.json +1 -0
- package/tests/helpers/e2e-browser.js +2 -1
- package/tests/unit/browser/issue-160-connector.test.js +157 -0
- package/tests/unit/core/interrupted-navigation.test.js +32 -0
- package/tests/unit/core/trigger-stop-race.test.js +80 -0
- package/tests/unit/interactions/overlays.test.js +58 -0
- package/tests/unit/traces/issue-160-privacy.test.js +110 -0
- package/tests/unit/traces/issue-160-rotation.test.js +140 -0
- package/tests/unit/traces/issue-162-defaults.test.js +223 -0
- package/tests/unit/traces/issue-162-user-actions.test.js +181 -0
- package/tests/unit/traces/network.test.js +55 -3
- package/tests/unit/traces/recorder.test.js +4 -1
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { connectBrowser, pickForegroundPage } from './connector.js';
|
|
6
6
|
import { launchAndConnectRealBrowserWithDependencies } from './real-browser.js';
|
|
7
7
|
import { assertDedicatedUserDataDir } from './system-browser.js';
|
|
8
|
+
import { findSessionProcess } from './session-adoption.js';
|
|
8
9
|
import { startDetachedProcess } from '../utilities/subprocess.js';
|
|
9
10
|
|
|
10
11
|
export const SESSION_METADATA = '.browser-commander-session.json';
|
|
@@ -108,8 +109,22 @@ async function open(options, file, endpoint) {
|
|
|
108
109
|
let metadata = await read(file);
|
|
109
110
|
const version = await probeSession(endpoint);
|
|
110
111
|
if (version) {
|
|
112
|
+
let adopted = false;
|
|
113
|
+
if (!metadata && options.adoptExisting) {
|
|
114
|
+
const pid = await findSessionProcess(options);
|
|
115
|
+
metadata = {
|
|
116
|
+
token: randomUUID(),
|
|
117
|
+
pid,
|
|
118
|
+
userDataDir: path.resolve(options.userDataDir),
|
|
119
|
+
remoteDebuggingPort: options.remoteDebuggingPort,
|
|
120
|
+
webSocketDebuggerUrl: version.webSocketDebuggerUrl,
|
|
121
|
+
};
|
|
122
|
+
adopted = true;
|
|
123
|
+
}
|
|
111
124
|
if (
|
|
112
125
|
!metadata ||
|
|
126
|
+
path.resolve(metadata.userDataDir ?? '') !==
|
|
127
|
+
path.resolve(options.userDataDir) ||
|
|
113
128
|
metadata.remoteDebuggingPort !== options.remoteDebuggingPort ||
|
|
114
129
|
metadata.webSocketDebuggerUrl !== version.webSocketDebuggerUrl
|
|
115
130
|
) {
|
|
@@ -119,9 +134,13 @@ async function open(options, file, endpoint) {
|
|
|
119
134
|
...options,
|
|
120
135
|
cdpEndpoint: endpoint,
|
|
121
136
|
targetId:
|
|
122
|
-
options.targetId ??
|
|
137
|
+
options.targetId ??
|
|
138
|
+
(!options.url && !options.urlMatchers?.length
|
|
139
|
+
? metadata.targetId
|
|
140
|
+
: undefined),
|
|
141
|
+
fallback: !options.targetId,
|
|
123
142
|
});
|
|
124
|
-
return { session, metadata, reused: true };
|
|
143
|
+
return { session, metadata, reused: true, adopted };
|
|
125
144
|
}
|
|
126
145
|
const session = await launchAndConnectRealBrowserWithDependencies(
|
|
127
146
|
{ ...options, keepOpen: false },
|
|
@@ -177,7 +196,7 @@ export async function closeOwnedSession(file, metadata) {
|
|
|
177
196
|
await fs.rm(file, { force: true });
|
|
178
197
|
}
|
|
179
198
|
async function lifecycle(
|
|
180
|
-
{ session, metadata, reused },
|
|
199
|
+
{ session, metadata, reused, adopted },
|
|
181
200
|
options,
|
|
182
201
|
file,
|
|
183
202
|
endpoint
|
|
@@ -212,11 +231,48 @@ async function lifecycle(
|
|
|
212
231
|
value.off?.('framenavigated', touch);
|
|
213
232
|
};
|
|
214
233
|
await remember(page, engine, file, metadata, idleTimeoutMs);
|
|
234
|
+
if (!reused || adopted) {
|
|
235
|
+
await startDetachedProcess(process.execPath, [
|
|
236
|
+
fileURLToPath(new URL('./session-watchdog.js', import.meta.url)),
|
|
237
|
+
file,
|
|
238
|
+
metadata.token,
|
|
239
|
+
]);
|
|
240
|
+
}
|
|
215
241
|
bind(page);
|
|
242
|
+
const heartbeat = setInterval(
|
|
243
|
+
touch,
|
|
244
|
+
Math.max(20, Math.min(30_000, idleTimeoutMs / 3))
|
|
245
|
+
);
|
|
246
|
+
heartbeat.unref?.();
|
|
247
|
+
const tabSource = engine === 'playwright' ? page.context() : session.browser;
|
|
248
|
+
const tabTasks = new Set();
|
|
249
|
+
let selecting = false;
|
|
250
|
+
const closeNewTab = (value) => {
|
|
251
|
+
const task = Promise.resolve()
|
|
252
|
+
.then(async () => {
|
|
253
|
+
const candidate =
|
|
254
|
+
engine === 'playwright' ? value : await value.page?.();
|
|
255
|
+
if (candidate && candidate !== page && !disconnected && !selecting) {
|
|
256
|
+
await candidate.close();
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
.catch((error) =>
|
|
260
|
+
options.log?.debug?.(() => `session tab close: ${error.message}`)
|
|
261
|
+
);
|
|
262
|
+
tabTasks.add(task);
|
|
263
|
+
void task.finally(() => tabTasks.delete(task));
|
|
264
|
+
};
|
|
265
|
+
const tabEvent = engine === 'playwright' ? 'page' : 'targetcreated';
|
|
266
|
+
if (options.closeNewTabs) {
|
|
267
|
+
tabSource.on?.(tabEvent, closeNewTab);
|
|
268
|
+
}
|
|
216
269
|
const detach = async () => {
|
|
217
270
|
if (disconnected) {
|
|
218
271
|
return;
|
|
219
272
|
}
|
|
273
|
+
clearInterval(heartbeat);
|
|
274
|
+
tabSource.off?.(tabEvent, closeNewTab);
|
|
275
|
+
await Promise.allSettled([...tabTasks]);
|
|
220
276
|
touch();
|
|
221
277
|
await activityQueue;
|
|
222
278
|
unbind(page);
|
|
@@ -248,51 +304,65 @@ async function lifecycle(
|
|
|
248
304
|
close,
|
|
249
305
|
touch,
|
|
250
306
|
reused,
|
|
307
|
+
adopted: Boolean(adopted),
|
|
251
308
|
cdpEndpoint: endpoint,
|
|
252
309
|
userDataDir: path.resolve(options.userDataDir),
|
|
253
310
|
remoteDebuggingPort: options.remoteDebuggingPort,
|
|
254
311
|
temporaryProfile: false,
|
|
255
312
|
};
|
|
256
313
|
result.reusePage = async (selection = {}) => {
|
|
257
|
-
|
|
258
|
-
throw new Error('Persistent controller is detached');
|
|
259
|
-
}
|
|
260
|
-
const release = await acquireLease(`${file}.lock`);
|
|
314
|
+
selecting = true;
|
|
261
315
|
try {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
throw new Error('Persistent session ownership changed');
|
|
316
|
+
if (disconnected) {
|
|
317
|
+
throw new Error('Persistent controller is detached');
|
|
265
318
|
}
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
319
|
+
const release = await acquireLease(`${file}.lock`);
|
|
320
|
+
try {
|
|
321
|
+
const current = await read(file);
|
|
322
|
+
if (current?.token !== metadata.token) {
|
|
323
|
+
throw new Error('Persistent session ownership changed');
|
|
324
|
+
}
|
|
325
|
+
const pages =
|
|
326
|
+
engine === 'playwright'
|
|
327
|
+
? page.context().pages()
|
|
328
|
+
: await session.browser.pages();
|
|
329
|
+
const selected =
|
|
330
|
+
(await pickForegroundPage(pages, {
|
|
331
|
+
targetId:
|
|
332
|
+
selection.targetId ??
|
|
333
|
+
(!selection.url && !selection.urlMatchers?.length
|
|
334
|
+
? metadata.targetId
|
|
335
|
+
: undefined),
|
|
336
|
+
fallback: !selection.targetId,
|
|
337
|
+
...selection,
|
|
338
|
+
})) ??
|
|
339
|
+
(engine === 'playwright'
|
|
340
|
+
? await page.context().newPage()
|
|
341
|
+
: await session.browser.newPage());
|
|
342
|
+
unbind(page);
|
|
343
|
+
page = selected;
|
|
344
|
+
bind(page);
|
|
345
|
+
result.page = page;
|
|
346
|
+
await remember(page, engine, file, metadata, idleTimeoutMs);
|
|
347
|
+
} finally {
|
|
348
|
+
await release();
|
|
278
349
|
}
|
|
279
|
-
|
|
280
|
-
page = selected;
|
|
281
|
-
bind(page);
|
|
282
|
-
result.page = page;
|
|
283
|
-
await remember(page, engine, file, metadata, idleTimeoutMs);
|
|
350
|
+
return page;
|
|
284
351
|
} finally {
|
|
285
|
-
|
|
352
|
+
selecting = false;
|
|
353
|
+
if (options.closeNewTabs && !disconnected) {
|
|
354
|
+
const pages =
|
|
355
|
+
engine === 'playwright'
|
|
356
|
+
? page.context().pages()
|
|
357
|
+
: await session.browser.pages();
|
|
358
|
+
await Promise.allSettled(
|
|
359
|
+
pages
|
|
360
|
+
.filter((candidate) => candidate !== page)
|
|
361
|
+
.map((candidate) => candidate.close())
|
|
362
|
+
);
|
|
363
|
+
}
|
|
286
364
|
}
|
|
287
|
-
return page;
|
|
288
365
|
};
|
|
289
|
-
if (!reused) {
|
|
290
|
-
await startDetachedProcess(process.execPath, [
|
|
291
|
-
fileURLToPath(new URL('./session-watchdog.js', import.meta.url)),
|
|
292
|
-
file,
|
|
293
|
-
metadata.token,
|
|
294
|
-
]);
|
|
295
|
-
}
|
|
296
366
|
return result;
|
|
297
367
|
}
|
|
298
368
|
export async function closeRemoteBrowser(endpoint) {
|
|
@@ -350,8 +420,10 @@ export async function connectOrLaunch(settings = {}) {
|
|
|
350
420
|
owned = await open(options, file, endpoint);
|
|
351
421
|
return await lifecycle(owned, options, file, endpoint);
|
|
352
422
|
} catch (error) {
|
|
353
|
-
if (owned
|
|
354
|
-
await
|
|
423
|
+
if (owned) {
|
|
424
|
+
await (
|
|
425
|
+
owned.reused ? owned.session.detach() : owned.session.close()
|
|
426
|
+
).catch(() => {});
|
|
355
427
|
}
|
|
356
428
|
throw error;
|
|
357
429
|
} finally {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { runCommand } from '../utilities/subprocess.js';
|
|
4
|
+
|
|
5
|
+
/** Match both the dedicated profile and fixed port before adopting ownership. */
|
|
6
|
+
export function matchesSessionProcess(args, options) {
|
|
7
|
+
if (
|
|
8
|
+
args.filter((arg) => arg.startsWith('--user-data-dir=')).length !== 1 ||
|
|
9
|
+
args.filter((arg) => arg.startsWith('--remote-debugging-port=')).length !==
|
|
10
|
+
1
|
|
11
|
+
) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const profile = args
|
|
15
|
+
.find((arg) => arg.startsWith('--user-data-dir='))
|
|
16
|
+
?.slice(16);
|
|
17
|
+
const port = args
|
|
18
|
+
.find((arg) => arg.startsWith('--remote-debugging-port='))
|
|
19
|
+
?.slice(24);
|
|
20
|
+
return (
|
|
21
|
+
/^(?:google[ -]chrome(?:[ -](?:for testing|stable|beta|unstable|canary|dev))?|chrome|chromium(?:-browser)?|brave(?: browser)?|microsoft edge|msedge)(?:\.exe)?$/i.test(
|
|
22
|
+
path.basename(args[0] ?? '')
|
|
23
|
+
) &&
|
|
24
|
+
Boolean(profile) &&
|
|
25
|
+
path.resolve(profile) === path.resolve(options.userDataDir) &&
|
|
26
|
+
Number(port) === options.remoteDebuggingPort &&
|
|
27
|
+
!args.some((arg) => arg.startsWith('--type='))
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
export async function findSessionProcess(options) {
|
|
31
|
+
if (process.platform === 'linux') {
|
|
32
|
+
for (const name of await fs.readdir('/proc')) {
|
|
33
|
+
if (!/^\d+$/.test(name)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const args = await fs.readFile(`/proc/${name}/cmdline`, 'utf8').then(
|
|
37
|
+
(text) => text.split('\0'),
|
|
38
|
+
() => []
|
|
39
|
+
);
|
|
40
|
+
if (matchesSessionProcess(args, options)) {
|
|
41
|
+
return Number(name);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} else if (process.platform === 'darwin') {
|
|
45
|
+
const { stdout } = await runCommand('ps', ['-axo', 'pid=,command=']);
|
|
46
|
+
for (const line of stdout.split('\n')) {
|
|
47
|
+
const match = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
48
|
+
if (!match) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const args = processArguments(match[2]);
|
|
52
|
+
if (matchesSessionProcess(args, options)) {
|
|
53
|
+
return Number(match[1]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else if (process.platform === 'win32') {
|
|
57
|
+
const { stdout } = await runCommand('powershell.exe', [
|
|
58
|
+
'-NoProfile',
|
|
59
|
+
'-NonInteractive',
|
|
60
|
+
'-Command',
|
|
61
|
+
'Get-CimInstance Win32_Process | Where-Object { $_.Name -match "^(chrome|chromium|brave|msedge)\\.exe$" } | Select-Object ProcessId,ExecutablePath,CommandLine | ConvertTo-Json -Compress',
|
|
62
|
+
]);
|
|
63
|
+
const records = JSON.parse(stdout || '[]');
|
|
64
|
+
for (const record of Array.isArray(records) ? records : [records]) {
|
|
65
|
+
const args = processArguments(record.CommandLine ?? '');
|
|
66
|
+
args[0] = record.ExecutablePath ?? args[0];
|
|
67
|
+
if (matchesSessionProcess(args, options)) {
|
|
68
|
+
return record.ProcessId;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Cannot verify the running browser profile and port for adoption'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Process listings omit argv boundaries; only accept unambiguous managed switches. */
|
|
78
|
+
export function processArguments(command) {
|
|
79
|
+
if (
|
|
80
|
+
command.match(/--user-data-dir=/g)?.length !== 1 ||
|
|
81
|
+
command.match(/--remote-debugging-port=/g)?.length !== 1
|
|
82
|
+
) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const executable = command.match(/^"([^"\n]+)"|^(.*?)(?=\s--)/);
|
|
86
|
+
const profile = command.match(
|
|
87
|
+
/--user-data-dir=(?:"([^"\n]+)"|'([^'\n]+)'|(.+?))(?=\s--|$)/
|
|
88
|
+
);
|
|
89
|
+
const port = command.match(/--remote-debugging-port=(\d+)(?=\s|$)/);
|
|
90
|
+
return [
|
|
91
|
+
executable?.[1] ?? executable?.[2] ?? '',
|
|
92
|
+
`--user-data-dir=${profile?.[1] ?? profile?.[2] ?? profile?.[3] ?? ''}`,
|
|
93
|
+
`--remote-debugging-port=${port?.[1] ?? ''}`,
|
|
94
|
+
...(command.includes('--type=') ? ['--type=child'] : []),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { connectBrowser } from './connector.js';
|
|
3
4
|
import {
|
|
4
5
|
connectOrLaunch,
|
|
5
6
|
SESSION_METADATA,
|
|
@@ -45,6 +46,15 @@ if (options.operation) {
|
|
|
45
46
|
metadata.targetId = options.targetId;
|
|
46
47
|
}
|
|
47
48
|
await writeSessionMetadata(file, metadata);
|
|
49
|
+
if (options.closeNewTabs) {
|
|
50
|
+
const connection = await connectBrowser({
|
|
51
|
+
cdpEndpoint: `http://127.0.0.1:${metadata.remoteDebuggingPort}`,
|
|
52
|
+
targetId: metadata.targetId,
|
|
53
|
+
singleTab: true,
|
|
54
|
+
noDefaults: true,
|
|
55
|
+
});
|
|
56
|
+
await connection.detach();
|
|
57
|
+
}
|
|
48
58
|
} else {
|
|
49
59
|
throw new TypeError('Unknown session operation');
|
|
50
60
|
}
|
|
@@ -65,6 +75,8 @@ if (options.operation) {
|
|
|
65
75
|
reused: session.reused,
|
|
66
76
|
token: metadata.token,
|
|
67
77
|
targetId: metadata.targetId,
|
|
78
|
+
idleTimeoutMs: metadata.idleTimeoutMs,
|
|
79
|
+
adopted: session.adopted,
|
|
68
80
|
})
|
|
69
81
|
);
|
|
70
82
|
}
|
package/src/cli/args.js
CHANGED
|
@@ -35,6 +35,9 @@ const BROWSER_OPTIONS = Object.freeze({
|
|
|
35
35
|
bidi: FLAG,
|
|
36
36
|
'remote-debugging-port': STRING,
|
|
37
37
|
'idle-timeout-ms': STRING,
|
|
38
|
+
'adopt-existing': FLAG,
|
|
39
|
+
'close-new-tabs': FLAG,
|
|
40
|
+
'no-defaults': FLAG,
|
|
38
41
|
});
|
|
39
42
|
|
|
40
43
|
/** Options accepted by every page command. */
|
package/src/cli/commands.js
CHANGED
|
@@ -186,6 +186,9 @@ async function withPageSession(parsed, io, work, { skipUrl = false } = {}) {
|
|
|
186
186
|
engine: options.engine,
|
|
187
187
|
targetId: options.targetId,
|
|
188
188
|
singleTab: options.singleTab,
|
|
189
|
+
...(options.noDefaults !== undefined
|
|
190
|
+
? { noDefaults: options.noDefaults }
|
|
191
|
+
: {}),
|
|
189
192
|
...(options.serverUrl ? { serverUrl: options.serverUrl } : {}),
|
|
190
193
|
...(options.driverPath ? { driverPath: options.driverPath } : {}),
|
|
191
194
|
...(options.bidi !== undefined ? { bidi: options.bidi } : {}),
|
|
@@ -54,6 +54,13 @@ async function connectSession(state, params) {
|
|
|
54
54
|
...endpoint,
|
|
55
55
|
...(params.targetId ? { targetId: params.targetId } : {}),
|
|
56
56
|
...(params.urlMatcher ? { url: params.urlMatcher } : {}),
|
|
57
|
+
...(params.noDefaults !== undefined
|
|
58
|
+
? { noDefaults: params.noDefaults }
|
|
59
|
+
: {}),
|
|
60
|
+
...(params.urlMatchers !== undefined
|
|
61
|
+
? { urlMatchers: params.urlMatchers }
|
|
62
|
+
: {}),
|
|
63
|
+
...(params.fallback !== undefined ? { fallback: params.fallback } : {}),
|
|
57
64
|
...(params.singleTab !== undefined ? { singleTab: params.singleTab } : {}),
|
|
58
65
|
...(params.driverPath ? { driverPath: params.driverPath } : {}),
|
|
59
66
|
...(params.bidi !== undefined ? { bidi: params.bidi } : {}),
|
package/src/cli/sessions.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { startTrace } from '../traces/recorder.js';
|
|
2
|
+
import { debugEnabled, ensureDebugSession } from '../traces/debug-session.js';
|
|
3
|
+
import { observeCommanderActions } from '../traces/commander-actions.js';
|
|
4
|
+
import { TRACED_INTERACTIONS, interactionTarget } from '../traces/observers.js';
|
|
2
5
|
import { touchSessionPage } from '../browser/persistent-session.js';
|
|
3
6
|
import { dismissOverlays } from '../interactions/overlays.js';
|
|
4
7
|
import {
|
|
@@ -15,7 +18,7 @@ const INTERACTIONS = new Set([
|
|
|
15
18
|
'pressKey',
|
|
16
19
|
]);
|
|
17
20
|
|
|
18
|
-
/** Bind capture, persistence and
|
|
21
|
+
/** Bind capture, persistence and automatic debugging to the commander lifecycle. */
|
|
19
22
|
export function attachCommanderFeatures(commander, options, recreate) {
|
|
20
23
|
const { page, engine } = commander;
|
|
21
24
|
const managed = new Set();
|
|
@@ -38,27 +41,18 @@ export function attachCommanderFeatures(commander, options, recreate) {
|
|
|
38
41
|
commander.gif = (frames, animationOptions = {}) =>
|
|
39
42
|
encodeAnimation(frames, { ...animationOptions, format: 'gif' });
|
|
40
43
|
commander.dismissOverlays = (overlays = options.overlays) =>
|
|
41
|
-
dismissOverlays({
|
|
44
|
+
dismissOverlays({
|
|
45
|
+
page,
|
|
46
|
+
engine,
|
|
47
|
+
overlays,
|
|
48
|
+
onDismiss: options.onOverlayDismiss,
|
|
49
|
+
});
|
|
42
50
|
commander.startTrace = async (traceOptions = {}) =>
|
|
43
51
|
track(await startTrace({ commander, page, ...traceOptions }));
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let debug;
|
|
49
|
-
const ensureDebug = async () => {
|
|
50
|
-
if (!output || (!options.debug && !process.env.BROWSER_COMMANDER_TRACE)) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
debug ??= commander.startTrace({
|
|
54
|
-
output,
|
|
55
|
-
mode: 'continuous',
|
|
56
|
-
network: { har: true },
|
|
57
|
-
links: { output: `${output}.links`, dom: 'text' },
|
|
58
|
-
checkpointOnNavigation: true,
|
|
59
|
-
});
|
|
60
|
-
await debug;
|
|
61
|
-
};
|
|
52
|
+
let debugSession;
|
|
53
|
+
let detachActions;
|
|
54
|
+
commander.tracePath = null;
|
|
55
|
+
const ensureDebug = () => commander.ready;
|
|
62
56
|
for (const name of [...INTERACTIONS, 'goto', 'evaluate', 'count']) {
|
|
63
57
|
const method = commander[name];
|
|
64
58
|
if (!method) {
|
|
@@ -76,10 +70,10 @@ export function attachCommanderFeatures(commander, options, recreate) {
|
|
|
76
70
|
const destroy = commander.destroy;
|
|
77
71
|
commander.destroy = async () => {
|
|
78
72
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
73
|
+
await commander.ready;
|
|
74
|
+
await debugSession?.stop();
|
|
82
75
|
await Promise.all([...managed].map((item) => item.stop()));
|
|
76
|
+
detachActions?.();
|
|
83
77
|
} finally {
|
|
84
78
|
await destroy();
|
|
85
79
|
}
|
|
@@ -99,7 +93,21 @@ export function attachCommanderFeatures(commander, options, recreate) {
|
|
|
99
93
|
touchSessionPage(selected);
|
|
100
94
|
return selected;
|
|
101
95
|
};
|
|
102
|
-
if (options
|
|
103
|
-
|
|
96
|
+
if (debugEnabled({ ...options, page })) {
|
|
97
|
+
detachActions = observeCommanderActions({
|
|
98
|
+
commander,
|
|
99
|
+
page,
|
|
100
|
+
names: TRACED_INTERACTIONS,
|
|
101
|
+
targetOf: interactionTarget,
|
|
102
|
+
});
|
|
104
103
|
}
|
|
104
|
+
commander.ready = ensureDebugSession({
|
|
105
|
+
...options,
|
|
106
|
+
page,
|
|
107
|
+
engine,
|
|
108
|
+
commander,
|
|
109
|
+
}).then((session) => {
|
|
110
|
+
debugSession = session;
|
|
111
|
+
commander.tracePath = session?.path ?? null;
|
|
112
|
+
});
|
|
105
113
|
}
|