browser-commander 0.18.0 → 0.19.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 +8 -0
- package/CHANGELOG.md +34 -0
- package/README.md +50 -2
- package/package.json +2 -1
- package/src/downloads/manager.js +31 -8
- package/src/downloads/sources.js +56 -6
- package/src/downloads/staging.js +146 -0
- package/src/exports.js +14 -0
- package/src/traces/bundle.js +16 -1
- package/src/traces/deadline.js +33 -0
- package/src/traces/identity.js +127 -0
- package/src/traces/init-script.js +95 -0
- package/src/traces/links.js +623 -0
- package/src/traces/mutation-stream.js +232 -0
- package/src/traces/observers.js +29 -3
- package/src/traces/page-capture.js +251 -15
- package/src/traces/recorder.js +117 -100
- package/src/traces/schema.js +65 -1
- package/src/traces/viewer.js +157 -11
- package/tests/e2e/downloads.e2e.test.js +47 -0
- package/tests/e2e/traces.e2e.test.js +296 -11
- package/tests/helpers/download-server.js +26 -6
- package/tests/helpers/trace-fixtures.js +79 -3
- package/tests/helpers/trace-server.js +57 -1
- package/tests/unit/downloads/manager.test.js +131 -0
- package/tests/unit/downloads/staging.test.js +117 -0
- package/tests/unit/traces/links.test.js +557 -0
- package/tests/unit/traces/recorder.test.js +169 -1
- package/tests/unit/traces/viewer.test.js +5 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable identities for the things a trace talks about (issue #93).
|
|
3
|
+
*
|
|
4
|
+
* A timeline that says "a mutation happened" without saying *where* is only
|
|
5
|
+
* readable when exactly one document existed. Two tabs, or one page with an
|
|
6
|
+
* iframe, and every record becomes a guess. Issue #93 calls this out: "no
|
|
7
|
+
* browser-context / page / navigation / frame / action identifiers", so every
|
|
8
|
+
* record this recorder writes now names its owner.
|
|
9
|
+
*
|
|
10
|
+
* Identities are assigned here rather than taken from the engine because the
|
|
11
|
+
* two engines name the same things differently, and a trace is supposed to
|
|
12
|
+
* read the same whichever one recorded it. They are stable within a process:
|
|
13
|
+
* tracing the same page twice reports the same `pageId`, which is what makes
|
|
14
|
+
* two bundles of one run comparable.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Identities already handed out, so the same object keeps the same name. */
|
|
18
|
+
const assigned = new WeakMap();
|
|
19
|
+
|
|
20
|
+
const counters = { context: 0, page: 0, trace: 0 };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Name one engine object, stably.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object|null|undefined} subject - The object to name
|
|
26
|
+
* @param {string} kind - Counter to draw from, such as `page`
|
|
27
|
+
* @param {string} prefix - Identifier prefix, such as `page`
|
|
28
|
+
* @returns {string|null} The identifier, or null when there is nothing to name
|
|
29
|
+
*/
|
|
30
|
+
function identify(subject, kind, prefix) {
|
|
31
|
+
if (
|
|
32
|
+
!subject ||
|
|
33
|
+
(typeof subject !== 'object' && typeof subject !== 'function')
|
|
34
|
+
) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const existing = assigned.get(subject);
|
|
38
|
+
if (existing) {
|
|
39
|
+
return existing;
|
|
40
|
+
}
|
|
41
|
+
const id = `${prefix}-${++counters[kind]}`;
|
|
42
|
+
assigned.set(subject, id);
|
|
43
|
+
return id;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The browser context a page belongs to, whichever engine owns it.
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} page - Playwright or Puppeteer page
|
|
50
|
+
* @returns {Object|null} The context object, or null when it cannot be reached
|
|
51
|
+
*/
|
|
52
|
+
export function browserContextOf(page) {
|
|
53
|
+
for (const accessor of ['context', 'browserContext']) {
|
|
54
|
+
if (typeof page?.[accessor] === 'function') {
|
|
55
|
+
try {
|
|
56
|
+
return page[accessor]();
|
|
57
|
+
} catch {
|
|
58
|
+
// An engine that closed the page answers by throwing; an unnamed
|
|
59
|
+
// context is better than a failed trace.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Open an identity for one running trace.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} options - `{page}`
|
|
70
|
+
* @param {Object} options.page - The page being traced
|
|
71
|
+
* @returns {Object} The trace's identity
|
|
72
|
+
*/
|
|
73
|
+
export function createTraceIdentity({ page } = {}) {
|
|
74
|
+
const traceId = `trace-${++counters.trace}`;
|
|
75
|
+
const browserContextId = identify(
|
|
76
|
+
browserContextOf(page),
|
|
77
|
+
'context',
|
|
78
|
+
'context'
|
|
79
|
+
);
|
|
80
|
+
const pageId = identify(page, 'page', 'page');
|
|
81
|
+
|
|
82
|
+
// A navigation is what separates "the same element" from "an element with
|
|
83
|
+
// the same path in a different document", so it is numbered from the start
|
|
84
|
+
// rather than from the first navigation the recorder happens to see.
|
|
85
|
+
let navigation = 1;
|
|
86
|
+
let actions = 0;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
traceId,
|
|
90
|
+
browserContextId,
|
|
91
|
+
pageId,
|
|
92
|
+
get navigationId() {
|
|
93
|
+
return `nav-${navigation}`;
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* Start a new navigation generation.
|
|
97
|
+
*
|
|
98
|
+
* @returns {string} The identifier of the document that is now current
|
|
99
|
+
*/
|
|
100
|
+
navigated() {
|
|
101
|
+
navigation += 1;
|
|
102
|
+
return `nav-${navigation}`;
|
|
103
|
+
},
|
|
104
|
+
/**
|
|
105
|
+
* Name one action, so its start, its result and its DOM effects agree.
|
|
106
|
+
*
|
|
107
|
+
* @returns {string} A fresh action identifier
|
|
108
|
+
*/
|
|
109
|
+
nextActionId() {
|
|
110
|
+
actions += 1;
|
|
111
|
+
return `${traceId}-action-${actions}`;
|
|
112
|
+
},
|
|
113
|
+
/**
|
|
114
|
+
* The owner fields every timeline record carries.
|
|
115
|
+
*
|
|
116
|
+
* @returns {Object} `{traceId, browserContextId, pageId, navigationId}`
|
|
117
|
+
*/
|
|
118
|
+
owner() {
|
|
119
|
+
return {
|
|
120
|
+
traceId,
|
|
121
|
+
browserContextId,
|
|
122
|
+
pageId,
|
|
123
|
+
navigationId: `nav-${navigation}`,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting the in-page recorder in before the page's own code (issue #93).
|
|
3
|
+
*
|
|
4
|
+
* A `MutationObserver` lives on the document, so a navigation destroys it. The
|
|
5
|
+
* recorder used to be reinstalled from `checkpoint()`, which meant everything a
|
|
6
|
+
* new page did while it was loading - the whole first render of a
|
|
7
|
+
* single-page app, every `DOMContentLoaded` handler - happened before anything
|
|
8
|
+
* was watching. Replaying such a trace showed a page that had apparently always
|
|
9
|
+
* looked finished.
|
|
10
|
+
*
|
|
11
|
+
* Both engines can run a script at document creation, ahead of the page's own
|
|
12
|
+
* scripts, in every frame. That hook is the only place an observer can be
|
|
13
|
+
* installed early enough to see a document's first mutation, so this module
|
|
14
|
+
* wraps the two spellings of it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Which engine's init-script hook this page offers.
|
|
19
|
+
*
|
|
20
|
+
* The page is asked rather than the commander, because a caller may trace a raw
|
|
21
|
+
* Playwright or Puppeteer page with no commander at all.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} page - The page being traced
|
|
24
|
+
* @returns {string|null} 'playwright', 'puppeteer', or null when neither
|
|
25
|
+
*/
|
|
26
|
+
export function initScriptEngine(page) {
|
|
27
|
+
if (typeof page?.addInitScript === 'function') {
|
|
28
|
+
return 'playwright';
|
|
29
|
+
}
|
|
30
|
+
if (typeof page?.evaluateOnNewDocument === 'function') {
|
|
31
|
+
return 'puppeteer';
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run a function in every document this page creates from now on.
|
|
38
|
+
*
|
|
39
|
+
* Playwright offers no way to unregister a single init script, so the returned
|
|
40
|
+
* detach is best-effort: it removes the registration on Puppeteer, and
|
|
41
|
+
* everywhere else the trace relies on `stop()` switching the recorder off in
|
|
42
|
+
* the documents that exist. A recorder installed into a document created after
|
|
43
|
+
* `stop()` therefore queues into a queue nobody drains, bounded by the same
|
|
44
|
+
* `maxQueuedMutations` limit as everything else.
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} options - `{page, fn, arg, note}`
|
|
47
|
+
* @returns {Promise<Function|null>} Detach, or null when the page has no hook
|
|
48
|
+
*/
|
|
49
|
+
export async function installInitScript(options) {
|
|
50
|
+
const { page, fn, arg, note } = options;
|
|
51
|
+
const engine = initScriptEngine(page);
|
|
52
|
+
|
|
53
|
+
if (engine === 'playwright') {
|
|
54
|
+
await page.addInitScript(fn, arg);
|
|
55
|
+
return () => {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (engine === 'puppeteer') {
|
|
59
|
+
const handle = await page.evaluateOnNewDocument(fn, arg);
|
|
60
|
+
const identifier = handle?.identifier ?? handle;
|
|
61
|
+
return async () => {
|
|
62
|
+
if (identifier === undefined || identifier === null) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await page.removeScriptToEvaluateOnNewDocument?.(identifier);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
note?.(`could not remove the init script: ${error.message}`);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Every frame of a page, including the page itself.
|
|
78
|
+
*
|
|
79
|
+
* Frames come and go while a trace is running, so this is read at the moment it
|
|
80
|
+
* is needed rather than kept.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} page - The page being traced
|
|
83
|
+
* @returns {Object[]} Frames, main frame first when the engine orders them so
|
|
84
|
+
*/
|
|
85
|
+
export function framesOf(page) {
|
|
86
|
+
if (typeof page?.frames !== 'function') {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return page.frames() ?? [];
|
|
91
|
+
} catch {
|
|
92
|
+
// A page that closed mid-drain has no frames, which is not an error here.
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|