browser-commander 0.19.0 → 0.21.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/CHANGELOG.md +156 -0
- package/README.md +216 -32
- package/bin/browser-commander.js +16 -0
- package/eslint.config.js +10 -0
- package/examples/react-test-app/src/App.jsx +24 -23
- package/extension/background.js +14 -0
- package/extension/manifest.json +12 -0
- package/extension/relay-handler.js +353 -0
- package/package.json +25 -6
- package/scripts/run-tests.mjs +4 -2
- package/src/browser/attach/differences.js +131 -0
- package/src/browser/attach/extension-relay.js +402 -0
- package/src/browser/attach/index.js +81 -0
- package/src/browser/attach/snapshot-launch.js +146 -0
- package/src/browser/attach/snapshot.js +374 -0
- package/src/browser/attach/websocket.js +615 -0
- package/src/browser/browser-cookie-cache.js +12 -17
- package/src/browser/browser-cookie-credentials.js +6 -9
- package/src/browser/browser-cookies.js +5 -2
- package/src/browser/cdp-session.js +94 -0
- package/src/browser/connector.js +42 -4
- package/src/browser/debugging-port.js +155 -0
- package/src/browser/launch-options.js +63 -52
- package/src/browser/launch-restrictions.json +147 -0
- package/src/browser/launcher.js +329 -133
- package/src/browser/media.js +5 -0
- package/src/browser/migration/bookmarks.js +73 -0
- package/src/browser/migration/chromium-crypto.js +89 -0
- package/src/browser/migration/cookies.js +155 -0
- package/src/browser/migration/extensions.js +187 -0
- package/src/browser/migration/firefox-bookmarks.js +144 -0
- package/src/browser/migration/firefox-der.js +84 -0
- package/src/browser/migration/firefox-nss.js +281 -0
- package/src/browser/migration/firefox.js +334 -0
- package/src/browser/migration/fs-utils.js +51 -0
- package/src/browser/migration/history.js +103 -0
- package/src/browser/migration/index.js +307 -0
- package/src/browser/migration/os-crypt-keys.js +106 -0
- package/src/browser/migration/passwords.js +171 -0
- package/src/browser/migration/preferences.js +132 -0
- package/src/browser/migration/sqlite-snapshot.js +138 -0
- package/src/browser/open-in-user-browser.js +112 -0
- package/src/browser/parity.js +439 -0
- package/src/browser/profile-directory.js +102 -0
- package/src/browser/real-browser.js +513 -306
- package/src/browser/restrictions.js +148 -0
- package/src/browser/system-browser.js +232 -0
- package/src/browser/webdriver.js +572 -0
- package/src/cli/args.js +253 -0
- package/src/cli/commands.js +332 -0
- package/src/cli/dispatcher.js +99 -0
- package/src/cli/handle-methods.js +136 -0
- package/src/cli/handles.js +273 -0
- package/src/cli/high-level-methods.js +310 -0
- package/src/cli/main.js +67 -0
- package/src/cli/modules.js +100 -0
- package/src/cli/rpc-error.js +77 -0
- package/src/cli/script.js +77 -0
- package/src/cli/serve.js +144 -0
- package/src/cli/sessions.js +195 -0
- package/src/core/constants.js +7 -1
- package/src/core/engine-adapter.js +107 -1
- package/src/core/engine-detection.js +18 -3
- package/src/core/navigation-safety.js +4 -0
- package/src/core/webdriver-conversions.js +347 -0
- package/src/core/webdriver-events.js +229 -0
- package/src/core/webdriver-page.js +716 -0
- package/src/downloads/attach.js +6 -0
- package/src/downloads/manager.js +9 -0
- package/src/downloads/sources.js +71 -0
- package/src/downloads/staging.js +21 -9
- package/src/elements/selectors.js +3 -2
- package/src/elements/visibility.js +4 -0
- package/src/exports.js +61 -2
- package/src/factory.js +19 -4
- package/src/fingerprint/apply.js +6 -0
- package/src/fingerprint/automation-parity.js +7 -8
- package/src/fingerprint/limitations.js +1 -1
- package/src/fingerprint/limitations.json +45 -2
- package/src/parity/harness.js +303 -0
- package/src/parity/probe.js +838 -0
- package/src/parity/version-page.js +144 -0
- package/src/traces/bundle.js +10 -2
- package/src/traces/recorder.js +16 -3
- package/src/utilities/subprocess.js +199 -0
- package/tests/e2e/api-coverage.e2e.test.js +174 -0
- package/tests/e2e/cli.e2e.test.js +160 -0
- package/tests/e2e/downloads.e2e.test.js +79 -2
- package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
- package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
- package/tests/e2e/measure-parity.e2e.test.js +118 -0
- package/tests/e2e/playwright.e2e.test.js +25 -17
- package/tests/e2e/puppeteer.e2e.test.js +17 -23
- package/tests/e2e/webdriver.e2e.test.js +239 -0
- package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
- package/tests/helpers/cli-fakes.js +214 -0
- package/tests/helpers/declared-api.js +80 -0
- package/tests/helpers/e2e-browser.js +40 -14
- package/tests/helpers/fake-chrome.js +124 -0
- package/tests/helpers/migration-fixtures.js +242 -0
- package/tests/helpers/temp-directory.js +24 -0
- package/tests/helpers/trace-fixtures.js +18 -0
- package/tests/helpers/webdriver-mocks.js +183 -0
- package/tests/unit/browser/attach/extension-relay.test.js +360 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +312 -0
- package/tests/unit/browser/attach/snapshot.test.js +390 -0
- package/tests/unit/browser/attach/websocket.test.js +240 -0
- package/tests/unit/browser/browser-cookies.test.js +2 -32
- package/tests/unit/browser/cdp-session.test.js +162 -0
- package/tests/unit/browser/connector.test.js +23 -0
- package/tests/unit/browser/launch-building-blocks.test.js +238 -0
- package/tests/unit/browser/launch-options.test.js +41 -22
- package/tests/unit/browser/launcher.test.js +281 -102
- package/tests/unit/browser/media.test.js +13 -1
- package/tests/unit/browser/migration/bookmarks.test.js +69 -0
- package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
- package/tests/unit/browser/migration/cookies.test.js +94 -0
- package/tests/unit/browser/migration/extensions.test.js +104 -0
- package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
- package/tests/unit/browser/migration/firefox.test.js +139 -0
- package/tests/unit/browser/migration/history.test.js +48 -0
- package/tests/unit/browser/migration/index.test.js +117 -0
- package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
- package/tests/unit/browser/migration/passwords.test.js +144 -0
- package/tests/unit/browser/migration/preferences.test.js +77 -0
- package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
- package/tests/unit/browser/open-in-user-browser.test.js +123 -0
- package/tests/unit/browser/parity.test.js +281 -0
- package/tests/unit/browser/real-browser.test.js +458 -33
- package/tests/unit/browser/webdriver.test.js +473 -0
- package/tests/unit/cli/bin.test.js +69 -0
- package/tests/unit/cli/declared-api.test.js +49 -0
- package/tests/unit/cli/dispatcher.test.js +429 -0
- package/tests/unit/cli/handles.test.js +170 -0
- package/tests/unit/cli/main.test.js +409 -0
- package/tests/unit/cli/serve.test.js +167 -0
- package/tests/unit/core/webdriver-conversions.test.js +235 -0
- package/tests/unit/core/webdriver-page.test.js +534 -0
- package/tests/unit/downloads/manager.test.js +82 -9
- package/tests/unit/downloads/staging.test.js +24 -2
- package/tests/unit/fingerprint/automation-parity.test.js +17 -4
- package/tests/unit/fingerprint/limitations.test.js +1 -1
- package/tests/unit/parity/harness.test.js +138 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +4 -0
- package/tests/unit/scripts/ci-workflow-policy.test.js +8 -0
- package/tests/unit/scripts/feature-parity.test.js +221 -0
- package/tests/unit/scripts/npm-release-helpers.test.js +3 -2
- package/tests/unit/scripts/pre-commit-config.test.js +2 -1
- package/tests/unit/traces/bundle.test.js +24 -3
- package/tests/unit/traces/links.test.js +7 -0
- package/tests/unit/traces/reader.test.js +11 -4
- package/tests/unit/traces/recorder.test.js +4 -0
- package/tests/unit/traces/viewer.test.js +4 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- package/tests/unit/utilities/subprocess.test.js +80 -0
|
@@ -22,7 +22,7 @@ describe('AutomationControlled triggers', () => {
|
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
it('finds each switch
|
|
25
|
+
it('finds each switch that turns the feature on', () => {
|
|
26
26
|
const found = detectAutomationControlledTriggers([
|
|
27
27
|
'--enable-automation',
|
|
28
28
|
'--headless=new',
|
|
@@ -32,16 +32,29 @@ describe('AutomationControlled triggers', () => {
|
|
|
32
32
|
|
|
33
33
|
assert.deepEqual(found, [
|
|
34
34
|
'--enable-automation',
|
|
35
|
-
'--headless',
|
|
36
35
|
'--remote-debugging-pipe',
|
|
37
36
|
'--remote-debugging-port=0',
|
|
38
37
|
]);
|
|
39
38
|
});
|
|
40
39
|
|
|
40
|
+
it('does not treat headless as a trigger', () => {
|
|
41
|
+
// Measured: a hand-started headless Chrome reports navigator.webdriver
|
|
42
|
+
// false, because the renderer never sees the --headless switch.
|
|
43
|
+
assert.deepEqual(
|
|
44
|
+
detectAutomationControlledTriggers([
|
|
45
|
+
'--headless',
|
|
46
|
+
'--headless=new',
|
|
47
|
+
'--headless=old',
|
|
48
|
+
]),
|
|
49
|
+
[]
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
41
53
|
it('reports the argument as written next to the switch it matched', () => {
|
|
42
54
|
assert.deepEqual(
|
|
43
|
-
detectAutomationControlledTriggers(['--
|
|
44
|
-
|
|
55
|
+
detectAutomationControlledTriggers(['--remote-debugging-port=0'])[0]
|
|
56
|
+
.argument,
|
|
57
|
+
'--remote-debugging-port=0'
|
|
45
58
|
);
|
|
46
59
|
});
|
|
47
60
|
|
|
@@ -44,7 +44,7 @@ describe('the documented limitations', () => {
|
|
|
44
44
|
|
|
45
45
|
it('is the shared JSON asset Python and Rust also publish', () => {
|
|
46
46
|
// The catalogue is data, not code, so the three packages ship one file
|
|
47
|
-
// rather than three translations of the same
|
|
47
|
+
// rather than three translations of the same sixteen paragraphs.
|
|
48
48
|
const asset = JSON.parse(
|
|
49
49
|
readFileSync(
|
|
50
50
|
new URL('../../../src/fingerprint/limitations.json', import.meta.url),
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference capture starts a browser nothing automates and waits for the
|
|
3
|
+
* probe page to POST its report. A browser that cannot start at all (no usable
|
|
4
|
+
* sandbox, a rejected switch) never reports, so the capture has to fail with
|
|
5
|
+
* the browser's own exit code and stderr rather than a bare 60 s timeout.
|
|
6
|
+
*/
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { describe, it } from 'node:test';
|
|
9
|
+
|
|
10
|
+
import { captureReferenceReport } from '../../../src/parity/harness.js';
|
|
11
|
+
|
|
12
|
+
function fakeProcess({ exitCode, stderr = '' }) {
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
let resolveExit;
|
|
15
|
+
const child = {
|
|
16
|
+
exited: new Promise((resolve) => {
|
|
17
|
+
resolveExit = resolve;
|
|
18
|
+
}),
|
|
19
|
+
stderr: {
|
|
20
|
+
on(event, listener) {
|
|
21
|
+
listeners.add(listener);
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
kill() {
|
|
25
|
+
resolveExit(143);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
for (const listener of listeners) {
|
|
30
|
+
listener(stderr);
|
|
31
|
+
}
|
|
32
|
+
if (exitCode !== null) {
|
|
33
|
+
resolveExit(exitCode);
|
|
34
|
+
}
|
|
35
|
+
}, 0);
|
|
36
|
+
return child;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fakeServer({ report = null } = {}) {
|
|
40
|
+
return {
|
|
41
|
+
url: (token) => `http://127.0.0.1:1/probe/${token}`,
|
|
42
|
+
waitForReport: (token, timeoutMs) =>
|
|
43
|
+
report
|
|
44
|
+
? new Promise((resolve) => setTimeout(() => resolve(report), 20))
|
|
45
|
+
: new Promise((resolve, reject) =>
|
|
46
|
+
setTimeout(
|
|
47
|
+
() => reject(new Error(`timed out waiting for report ${token}`)),
|
|
48
|
+
timeoutMs
|
|
49
|
+
)
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('captureReferenceReport', () => {
|
|
55
|
+
it('fails fast with the exit code and stderr of a browser that died', async () => {
|
|
56
|
+
const started = Date.now();
|
|
57
|
+
await assert.rejects(
|
|
58
|
+
captureReferenceReport({
|
|
59
|
+
executablePath: '/usr/bin/chromium',
|
|
60
|
+
server: fakeServer(),
|
|
61
|
+
token: 't1',
|
|
62
|
+
timeoutMs: 3000,
|
|
63
|
+
start: () =>
|
|
64
|
+
fakeProcess({
|
|
65
|
+
exitCode: 1,
|
|
66
|
+
stderr: 'FATAL: No usable sandbox!',
|
|
67
|
+
}),
|
|
68
|
+
}),
|
|
69
|
+
(error) => {
|
|
70
|
+
assert.match(error.message, /\/usr\/bin\/chromium exited with code 1/);
|
|
71
|
+
assert.match(error.message, /No usable sandbox/);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
assert.ok(Date.now() - started < 5000);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('includes stderr when the report times out', async () => {
|
|
79
|
+
await assert.rejects(
|
|
80
|
+
captureReferenceReport({
|
|
81
|
+
server: fakeServer(),
|
|
82
|
+
token: 't2',
|
|
83
|
+
timeoutMs: 50,
|
|
84
|
+
start: () => fakeProcess({ exitCode: null, stderr: 'GPU init failed' }),
|
|
85
|
+
}),
|
|
86
|
+
/timed out waiting for report t2\nbrowser stderr:\nGPU init failed/
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('retries a fresh profile after the browser network service crashes', async () => {
|
|
91
|
+
const tokens = [];
|
|
92
|
+
let starts = 0;
|
|
93
|
+
const report = await captureReferenceReport({
|
|
94
|
+
server: {
|
|
95
|
+
url: (token) => {
|
|
96
|
+
tokens.push(token);
|
|
97
|
+
return `http://127.0.0.1:1/probe/${token}`;
|
|
98
|
+
},
|
|
99
|
+
waitForReport: (token) =>
|
|
100
|
+
token === 't4'
|
|
101
|
+
? new Promise((resolve, reject) =>
|
|
102
|
+
setTimeout(
|
|
103
|
+
() =>
|
|
104
|
+
reject(new Error(`timed out waiting for report ${token}`)),
|
|
105
|
+
20
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
: Promise.resolve({ navigator: { webdriver: false } }),
|
|
109
|
+
},
|
|
110
|
+
token: 't4',
|
|
111
|
+
timeoutMs: 50,
|
|
112
|
+
start: () => {
|
|
113
|
+
starts += 1;
|
|
114
|
+
return fakeProcess({
|
|
115
|
+
exitCode: null,
|
|
116
|
+
stderr:
|
|
117
|
+
starts === 1
|
|
118
|
+
? 'Network service crashed or was terminated, restarting service.'
|
|
119
|
+
: '',
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
assert.deepEqual(report, { navigator: { webdriver: false } });
|
|
124
|
+
assert.equal(starts, 2);
|
|
125
|
+
assert.equal(tokens.length, 2);
|
|
126
|
+
assert.notEqual(tokens[0], tokens[1]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('keeps waiting when a launcher script exits cleanly', async () => {
|
|
130
|
+
const report = await captureReferenceReport({
|
|
131
|
+
server: fakeServer({ report: { navigator: { webdriver: false } } }),
|
|
132
|
+
token: 't3',
|
|
133
|
+
start: () => fakeProcess({ exitCode: 0 }),
|
|
134
|
+
});
|
|
135
|
+
assert.deepEqual(report, { navigator: { webdriver: false } });
|
|
136
|
+
assert.ok(Array.isArray(report.commandLine));
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -196,7 +196,11 @@ describe('CI execution budgets', () => {
|
|
|
196
196
|
assert.deepEqual(budgeted, [
|
|
197
197
|
'docs.yml build-docs Rust API docs',
|
|
198
198
|
'js.yml test Node.js test suite',
|
|
199
|
+
'parity.yml cli CLI and API coverage suites',
|
|
200
|
+
'parity.yml cli Cross-language CLI contract',
|
|
201
|
+
'parity.yml cli Rust CLI build',
|
|
199
202
|
'parity.yml parity Fingerprint parity suite',
|
|
203
|
+
'parity.yml parity WebDriver suite',
|
|
200
204
|
'python.yml test pytest suite',
|
|
201
205
|
'rust.yml coverage Rust code coverage',
|
|
202
206
|
'rust.yml test Rust doc tests',
|
|
@@ -155,6 +155,14 @@ jobs:
|
|
|
155
155
|
assert.equal(countFailures(content), 2);
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
it('rejects download-artifact v7 after its runtime emitted DEP0005', () => {
|
|
159
|
+
const content = workflow({
|
|
160
|
+
steps: ' - uses: actions/download-artifact@v7',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
assert.equal(countFailures(content), 1);
|
|
164
|
+
});
|
|
165
|
+
|
|
158
166
|
it('accepts a base_ref bound to any environment variable name', () => {
|
|
159
167
|
const content = workflow({
|
|
160
168
|
on: 'pull_request',
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated feature-parity matrix (issue #104).
|
|
3
|
+
*
|
|
4
|
+
* `scripts/generate-feature-parity.mjs` builds the per-language matrix in
|
|
5
|
+
* docs/feature-parity.md from the `feature-parity:` tags in the three test
|
|
6
|
+
* suites, and fails when a feature is tested in one language and missing in
|
|
7
|
+
* another without a reason in docs/feature-parity/limitations.json. These
|
|
8
|
+
* tests pin those rules against a fixture tree, then pin the repository
|
|
9
|
+
* itself: the committed document must be what the generator writes.
|
|
10
|
+
*
|
|
11
|
+
* Fixture tags are assembled at run time, because the generator scans this
|
|
12
|
+
* file too and a literal tag here would claim a feature.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { describe, it } from 'node:test';
|
|
20
|
+
import { pathToFileURL } from 'node:url';
|
|
21
|
+
|
|
22
|
+
import { repoPath } from '../../helpers/repo.js';
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
BEGIN_MARKER,
|
|
26
|
+
END_MARKER,
|
|
27
|
+
escapeCell,
|
|
28
|
+
generate,
|
|
29
|
+
parseTags,
|
|
30
|
+
renderTable,
|
|
31
|
+
} = await import(
|
|
32
|
+
pathToFileURL(repoPath('scripts/generate-feature-parity.mjs'))
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const TAG = ['feature', 'parity:'].join('-');
|
|
36
|
+
|
|
37
|
+
function tag(comment, ids) {
|
|
38
|
+
return `${comment} ${TAG} ${ids}\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fixture({ tests, limitations = [] }) {
|
|
42
|
+
const root = fs.mkdtempSync(path.join(tmpdir(), 'feature-parity-'));
|
|
43
|
+
const write = (relative, content) => {
|
|
44
|
+
const full = path.join(root, relative);
|
|
45
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
46
|
+
fs.writeFileSync(full, content);
|
|
47
|
+
};
|
|
48
|
+
write(
|
|
49
|
+
'docs/feature-parity/features.json',
|
|
50
|
+
JSON.stringify({
|
|
51
|
+
sections: [
|
|
52
|
+
{
|
|
53
|
+
title: 'Launch',
|
|
54
|
+
features: [
|
|
55
|
+
{ id: 'launch.real', title: 'Real launch' },
|
|
56
|
+
{ id: 'launch.attach', title: 'Attach | snapshot' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
);
|
|
62
|
+
write(
|
|
63
|
+
'docs/feature-parity/limitations.json',
|
|
64
|
+
JSON.stringify(limitations, null, 2)
|
|
65
|
+
);
|
|
66
|
+
write(
|
|
67
|
+
'docs/feature-parity.md',
|
|
68
|
+
`# Parity\n\nprose\n\n${BEGIN_MARKER}\n${END_MARKER}\n\ntail\n`
|
|
69
|
+
);
|
|
70
|
+
for (const [relative, content] of Object.entries(tests)) {
|
|
71
|
+
write(relative, content);
|
|
72
|
+
}
|
|
73
|
+
return root;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ALL_LANGUAGES = {
|
|
77
|
+
'js/tests/unit/launch.test.js': tag('//', 'launch.real launch.attach'),
|
|
78
|
+
'python/tests/unit/test_launch.py': tag('#', 'launch.real, launch.attach'),
|
|
79
|
+
'rust/src/launch.rs': `fn x() {}\n${tag(' //', 'launch.real')}${tag('//', 'launch.attach')}`,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
describe('feature parity generator', () => {
|
|
83
|
+
it('reads space- and comma-separated tags from line comments only', () => {
|
|
84
|
+
assert.deepEqual(parseTags(tag('//', 'a.b c-d')), ['a.b', 'c-d']);
|
|
85
|
+
assert.deepEqual(parseTags(tag(' #', 'a.b,c.d')), ['a.b', 'c.d']);
|
|
86
|
+
assert.deepEqual(parseTags(`const s = '${tag('//', 'a.b').trim()}';`), []);
|
|
87
|
+
assert.deepEqual(parseTags(tag('//', 'a.b').replaceAll('\n', '\r\n')), [
|
|
88
|
+
'a.b',
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('escapes both Markdown pipes and backslashes in feature names', () => {
|
|
93
|
+
assert.equal(escapeCell('a\\b|c\nx'), 'a\\\\b\\|c x');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('accepts a generated document checked out with CRLF endings', () => {
|
|
97
|
+
const root = fixture({ tests: ALL_LANGUAGES });
|
|
98
|
+
try {
|
|
99
|
+
const documentPath = path.join(root, 'docs/feature-parity.md');
|
|
100
|
+
const { next } = generate(root);
|
|
101
|
+
fs.writeFileSync(documentPath, next.replaceAll('\n', '\r\n'));
|
|
102
|
+
const generated = generate(root);
|
|
103
|
+
assert.equal(generated.current, generated.next);
|
|
104
|
+
assert.equal(generated.next.includes('\r'), false);
|
|
105
|
+
} finally {
|
|
106
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('pads tables so the output is stable under Prettier', () => {
|
|
111
|
+
assert.equal(
|
|
112
|
+
renderTable(['A', 'Long header'], [['wide cell', 'x']]),
|
|
113
|
+
[
|
|
114
|
+
'| A | Long header |',
|
|
115
|
+
'| --------- | ----------- |',
|
|
116
|
+
'| wide cell | x |',
|
|
117
|
+
].join('\n')
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('writes a matrix with links to the claiming tests', () => {
|
|
122
|
+
const root = fixture({ tests: ALL_LANGUAGES });
|
|
123
|
+
try {
|
|
124
|
+
const { next, errors } = generate(root);
|
|
125
|
+
assert.deepEqual(errors, []);
|
|
126
|
+
assert.match(next, /^# Parity\n\nprose\n\n<!-- feature-parity/u);
|
|
127
|
+
assert.match(
|
|
128
|
+
next,
|
|
129
|
+
/\| Real launch +\| \[Supported\]\(\.\.\/js\/tests\/unit\/launch\.test\.js\) +\| \[Supported\]\(\.\.\/python\/tests\/unit\/test_launch\.py\) +\| \[Supported\]\(\.\.\/rust\/src\/launch\.rs\) +\|/u
|
|
130
|
+
);
|
|
131
|
+
assert.match(next, /Attach \\\| snapshot/u);
|
|
132
|
+
assert.match(next, /None: every feature is tested in every language/u);
|
|
133
|
+
assert.match(next, /\n\ntail\n$/u);
|
|
134
|
+
} finally {
|
|
135
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('fails on a gap without a limitation', () => {
|
|
140
|
+
const tests = { ...ALL_LANGUAGES };
|
|
141
|
+
tests['rust/src/launch.rs'] = tag('//', 'launch.real');
|
|
142
|
+
const root = fixture({ tests });
|
|
143
|
+
try {
|
|
144
|
+
const { errors, next } = generate(root);
|
|
145
|
+
assert.deepEqual(errors, [
|
|
146
|
+
'feature "launch.attach" is tested in javascript, python but not in ' +
|
|
147
|
+
'rust, and docs/feature-parity/limitations.json gives no reason',
|
|
148
|
+
]);
|
|
149
|
+
assert.match(next, /\| Missing +\|/u);
|
|
150
|
+
} finally {
|
|
151
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('accepts a gap with a technical reason and links to it', () => {
|
|
156
|
+
const tests = { ...ALL_LANGUAGES };
|
|
157
|
+
tests['rust/src/launch.rs'] = tag('//', 'launch.real');
|
|
158
|
+
const root = fixture({
|
|
159
|
+
tests,
|
|
160
|
+
limitations: [
|
|
161
|
+
{
|
|
162
|
+
id: 'rust-no-attach',
|
|
163
|
+
features: ['launch.attach'],
|
|
164
|
+
languages: ['rust'],
|
|
165
|
+
reason: 'The crate has no WebSocket server.',
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
try {
|
|
170
|
+
const { errors, next } = generate(root);
|
|
171
|
+
assert.deepEqual(errors, []);
|
|
172
|
+
assert.match(next, /\[Limitation\]\(#rust-no-attach\)/u);
|
|
173
|
+
assert.match(
|
|
174
|
+
next,
|
|
175
|
+
/<a id="rust-no-attach"><\/a>\*\*`rust-no-attach`\*\* \(Rust\): The crate has no WebSocket server\./u
|
|
176
|
+
);
|
|
177
|
+
} finally {
|
|
178
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('rejects stale, reasonless and unknown entries', () => {
|
|
183
|
+
const tests = {
|
|
184
|
+
...ALL_LANGUAGES,
|
|
185
|
+
'js/tests/unit/extra.test.js': tag('//', 'launch.unknown'),
|
|
186
|
+
};
|
|
187
|
+
const root = fixture({
|
|
188
|
+
tests,
|
|
189
|
+
limitations: [
|
|
190
|
+
{
|
|
191
|
+
id: 'stale',
|
|
192
|
+
features: ['launch.real', 'launch.missing'],
|
|
193
|
+
languages: ['rust', 'cobol'],
|
|
194
|
+
reason: ' ',
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
try {
|
|
199
|
+
const { errors } = generate(root);
|
|
200
|
+
assert.deepEqual(errors.sort(), [
|
|
201
|
+
'limitation "stale" covers launch.real in rust, but rust/src/launch.rs tests it; remove the stale entry',
|
|
202
|
+
'limitation "stale" gives no technical reason',
|
|
203
|
+
'limitation "stale" names unknown feature "launch.missing"',
|
|
204
|
+
'limitation "stale" names unknown language "cobol"',
|
|
205
|
+
'unknown feature "launch.unknown" claimed by js/tests/unit/extra.test.js; add it to docs/feature-parity/features.json',
|
|
206
|
+
]);
|
|
207
|
+
} finally {
|
|
208
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('keeps docs/feature-parity.md in sync with the tests', () => {
|
|
213
|
+
const { current, next, errors } = generate(repoPath());
|
|
214
|
+
assert.deepEqual(errors, []);
|
|
215
|
+
assert.equal(
|
|
216
|
+
current,
|
|
217
|
+
next,
|
|
218
|
+
'run node scripts/generate-feature-parity.mjs and commit the result'
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -131,6 +131,7 @@ describe('test runner helper', () => {
|
|
|
131
131
|
buildNodeTestArgs(options, ['tests/unit/a.test.js'], '24.18.0'),
|
|
132
132
|
[
|
|
133
133
|
'--test',
|
|
134
|
+
'--throw-deprecation',
|
|
134
135
|
'--test-isolation=none',
|
|
135
136
|
'--experimental-test-coverage',
|
|
136
137
|
'--test-reporter',
|
|
@@ -144,8 +145,8 @@ describe('test runner helper', () => {
|
|
|
144
145
|
const options = parseTestRunnerArgs(['tests/unit']);
|
|
145
146
|
|
|
146
147
|
assert.deepEqual(
|
|
147
|
-
buildNodeTestArgs(options, ['tests/unit/a.test.js'], '
|
|
148
|
-
['--test', 'tests/unit/a.test.js']
|
|
148
|
+
buildNodeTestArgs(options, ['tests/unit/a.test.js'], '22.22.3'),
|
|
149
|
+
['--test', '--throw-deprecation', 'tests/unit/a.test.js']
|
|
149
150
|
);
|
|
150
151
|
});
|
|
151
152
|
});
|
|
@@ -48,7 +48,8 @@ const MIRRORED_COMMANDS = [
|
|
|
48
48
|
{
|
|
49
49
|
id: 'python-mypy-scripts',
|
|
50
50
|
workflow: 'python.yml',
|
|
51
|
-
command:
|
|
51
|
+
command:
|
|
52
|
+
'mypy --no-warn-unused-configs --python-version 3.13 scripts tests/unit/scripts',
|
|
52
53
|
},
|
|
53
54
|
{
|
|
54
55
|
id: 'rust-fmt',
|
|
@@ -16,10 +16,14 @@ import {
|
|
|
16
16
|
TRACE_MODE,
|
|
17
17
|
TRACE_OUTCOME,
|
|
18
18
|
} from '../../../src/traces/schema.js';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
useTempTraceDirectory,
|
|
21
|
+
useTraceBundleCleanup,
|
|
22
|
+
} from '../../helpers/trace-fixtures.js';
|
|
20
23
|
|
|
21
24
|
describe('trace bundle writer (issue #87)', () => {
|
|
22
25
|
const directory = useTempTraceDirectory();
|
|
26
|
+
const cleanup = useTraceBundleCleanup();
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Open a bundle inside the suite's temporary directory.
|
|
@@ -27,8 +31,13 @@ describe('trace bundle writer (issue #87)', () => {
|
|
|
27
31
|
* @param {Object} [options] - Options for `openTraceBundle`
|
|
28
32
|
* @returns {Promise<Object>} The bundle writer
|
|
29
33
|
*/
|
|
30
|
-
const open = (options = {}) =>
|
|
31
|
-
|
|
34
|
+
const open = async (options = {}) =>
|
|
35
|
+
cleanup(
|
|
36
|
+
await openTraceBundle({
|
|
37
|
+
output: path.join(directory.path, 'run'),
|
|
38
|
+
...options,
|
|
39
|
+
})
|
|
40
|
+
);
|
|
32
41
|
|
|
33
42
|
/**
|
|
34
43
|
* List a bundle's members, relative to its root.
|
|
@@ -66,6 +75,18 @@ describe('trace bundle writer (issue #87)', () => {
|
|
|
66
75
|
await assert.rejects(openTraceBundle({}), /trace output must be a path/);
|
|
67
76
|
});
|
|
68
77
|
|
|
78
|
+
it('should abort an unfinished bundle without writing a manifest', async () => {
|
|
79
|
+
const bundle = await open();
|
|
80
|
+
await bundle.appendEvent({ kind: TRACE_EVENT.CHECKPOINT });
|
|
81
|
+
|
|
82
|
+
await bundle.abort();
|
|
83
|
+
await bundle.abort();
|
|
84
|
+
|
|
85
|
+
await assert.rejects(
|
|
86
|
+
fs.access(path.join(bundle.root, TRACE_FILES.MANIFEST))
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
69
90
|
it('should write the layout the format documents', async () => {
|
|
70
91
|
const bundle = await open();
|
|
71
92
|
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
createFakePage,
|
|
28
28
|
makeSnapshot,
|
|
29
29
|
useTempTraceDirectory,
|
|
30
|
+
useTraceBundleCleanup,
|
|
30
31
|
} from '../../helpers/trace-fixtures.js';
|
|
31
32
|
|
|
32
33
|
/**
|
|
@@ -66,6 +67,7 @@ function parseExport(text) {
|
|
|
66
67
|
|
|
67
68
|
describe('trace links export (issue #94)', () => {
|
|
68
69
|
const directory = useTempTraceDirectory();
|
|
70
|
+
const trackOpenBundle = useTraceBundleCleanup();
|
|
69
71
|
|
|
70
72
|
/**
|
|
71
73
|
* Write a bundle holding one of everything the export has to say.
|
|
@@ -76,6 +78,7 @@ describe('trace links export (issue #94)', () => {
|
|
|
76
78
|
const writeBundle = async ({ close = true } = {}) => {
|
|
77
79
|
const root = path.join(directory.path, 'run.bc-trace');
|
|
78
80
|
const bundle = await openTraceBundle({ output: root });
|
|
81
|
+
trackOpenBundle(bundle);
|
|
79
82
|
const owner = {
|
|
80
83
|
traceId: 'trace-1',
|
|
81
84
|
browserContextId: 'context-1',
|
|
@@ -457,6 +460,10 @@ describe('trace links export (issue #94)', () => {
|
|
|
457
460
|
);
|
|
458
461
|
// No closing link, which is how a reader tells this run never finished.
|
|
459
462
|
assert.ok(!links.some((link) => link.id === 'result'));
|
|
463
|
+
|
|
464
|
+
// The assertions above model the process being killed. The test process
|
|
465
|
+
// itself survives, so release its open descriptor explicitly.
|
|
466
|
+
await trace.stop();
|
|
460
467
|
});
|
|
461
468
|
|
|
462
469
|
it('should take the export away with a discarded bundle', async () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// feature-parity: trace.read
|
|
1
2
|
import { describe, it } from 'node:test';
|
|
2
3
|
import assert from 'node:assert';
|
|
3
4
|
import fs from 'node:fs/promises';
|
|
@@ -16,10 +17,14 @@ import {
|
|
|
16
17
|
TRACE_MODE,
|
|
17
18
|
TRACE_OUTCOME,
|
|
18
19
|
} from '../../../src/traces/schema.js';
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
useTempTraceDirectory,
|
|
22
|
+
useTraceBundleCleanup,
|
|
23
|
+
} from '../../helpers/trace-fixtures.js';
|
|
20
24
|
|
|
21
25
|
describe('trace reader (issue #87)', () => {
|
|
22
26
|
const directory = useTempTraceDirectory();
|
|
27
|
+
const cleanup = useTraceBundleCleanup();
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Write a small bundle the tests can read back.
|
|
@@ -29,9 +34,11 @@ describe('trace reader (issue #87)', () => {
|
|
|
29
34
|
* @returns {Promise<Object>} The bundle writer
|
|
30
35
|
*/
|
|
31
36
|
const writeBundle = async ({ close = true } = {}) => {
|
|
32
|
-
const bundle =
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
const bundle = cleanup(
|
|
38
|
+
await openTraceBundle({
|
|
39
|
+
output: path.join(directory.path, 'run'),
|
|
40
|
+
})
|
|
41
|
+
);
|
|
35
42
|
await bundle.appendEvent({
|
|
36
43
|
kind: TRACE_EVENT.TRACE_START,
|
|
37
44
|
mode: TRACE_MODE.CHECKPOINTS,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// feature-parity: trace.record
|
|
1
2
|
import { describe, it } from 'node:test';
|
|
2
3
|
import assert from 'node:assert';
|
|
3
4
|
import { setImmediate as yieldToEngine } from 'node:timers/promises';
|
|
@@ -172,6 +173,7 @@ describe('trace recorder (issue #87)', () => {
|
|
|
172
173
|
await trace.checkpoint('start');
|
|
173
174
|
|
|
174
175
|
assert.strictEqual(page.state.screenshots, 0);
|
|
176
|
+
await trace.stop();
|
|
175
177
|
});
|
|
176
178
|
|
|
177
179
|
it('should take a screenshot only on failure when asked', async () => {
|
|
@@ -182,6 +184,7 @@ describe('trace recorder (issue #87)', () => {
|
|
|
182
184
|
|
|
183
185
|
await trace.checkpoint('the end', { reason: 'failure' });
|
|
184
186
|
assert.strictEqual(page.state.screenshots, 1);
|
|
187
|
+
await trace.stop();
|
|
185
188
|
});
|
|
186
189
|
|
|
187
190
|
it('should keep a gap rather than fail when the page has closed', async () => {
|
|
@@ -237,6 +240,7 @@ describe('trace recorder (issue #87)', () => {
|
|
|
237
240
|
// Once at the start, and once after each of the two checkpoints - the
|
|
238
241
|
// base one the recorder takes itself and the one named above.
|
|
239
242
|
assert.strictEqual(installs.length, 3);
|
|
243
|
+
await trace.stop();
|
|
240
244
|
});
|
|
241
245
|
});
|
|
242
246
|
|
|
@@ -282,6 +282,10 @@ describe('offline trace viewer (issue #87)', () => {
|
|
|
282
282
|
|
|
283
283
|
assert.match(body, /outcome truncated/);
|
|
284
284
|
assert.match(body, /the last thing it did|1 checkpoints/);
|
|
285
|
+
|
|
286
|
+
// The assertions model an interrupted process; this test process keeps
|
|
287
|
+
// running, so explicitly release the descriptor afterwards.
|
|
288
|
+
await trace.stop();
|
|
285
289
|
});
|
|
286
290
|
|
|
287
291
|
it('should refuse a directory that is not a bundle', async () => {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { repoPath } from '../../helpers/repo.js';
|
|
7
|
+
|
|
8
|
+
const SOURCE_ROOT = repoPath('js', 'src');
|
|
9
|
+
const DIRECT_SUBPROCESS = /from\s+['"](?:node:)?child_process['"]/u;
|
|
10
|
+
|
|
11
|
+
function listSourceFiles(directory) {
|
|
12
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
13
|
+
const entryPath = path.join(directory, entry.name);
|
|
14
|
+
if (entry.isDirectory()) {
|
|
15
|
+
return listSourceFiles(entryPath);
|
|
16
|
+
}
|
|
17
|
+
return /\.[cm]?js$/u.test(entry.name) ? [entryPath] : [];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('subprocesses go through command-stream (issue #104)', () => {
|
|
22
|
+
it('no library module imports node:child_process directly', () => {
|
|
23
|
+
const offenders = listSourceFiles(SOURCE_ROOT)
|
|
24
|
+
.filter((file) => DIRECT_SUBPROCESS.test(readFileSync(file, 'utf8')))
|
|
25
|
+
.map((file) => path.relative(SOURCE_ROOT, file));
|
|
26
|
+
assert.deepEqual(offenders, []);
|
|
27
|
+
});
|
|
28
|
+
});
|