browser-commander 0.20.0 → 0.21.1
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.json +2 -1
- package/.prettierignore +1 -0
- package/CHANGELOG.md +159 -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 +21 -4
- 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 +26 -19
- 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 +205 -0
- package/src/browser/real-browser.js +545 -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 +300 -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 +199 -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/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 +3 -2
- package/src/traces/observers.js +14 -5
- package/src/traces/recorder.js +3 -0
- package/src/traces/viewer.js +16 -8
- 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/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 +26 -17
- package/tests/e2e/puppeteer.e2e.test.js +18 -23
- package/tests/e2e/webdriver.e2e.test.js +239 -0
- package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.state.json +54 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.state.json +60 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.png +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.state.json +60 -0
- package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +18 -0
- package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +77 -0
- package/tests/fixtures/traces/conformance/expected/bundle/mutations/0001.ndjson +2 -0
- package/tests/fixtures/traces/conformance/expected/bundle/mutations/0002.ndjson +1 -0
- package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +309 -0
- package/tests/fixtures/traces/conformance/expected/redaction.json +86 -0
- package/tests/fixtures/traces/conformance/expected/trace.lino +27 -0
- package/tests/fixtures/traces/conformance/scenario.json +249 -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/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 +321 -0
- package/tests/unit/browser/attach/snapshot.test.js +409 -0
- package/tests/unit/browser/attach/websocket.test.js +240 -0
- package/tests/unit/browser/browser-cookie-lock.test.js +102 -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 +361 -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/storage-state.test.js +2 -0
- 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 +437 -0
- package/tests/unit/cli/serve.test.js +167 -0
- package/tests/unit/core/navigation-manager.test.js +11 -1
- package/tests/unit/core/webdriver-conversions.test.js +235 -0
- package/tests/unit/core/webdriver-page.test.js +534 -0
- 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/playwright-protocol-coverage.test.js +231 -0
- package/tests/unit/puppeteer-bindings-coverage.test.js +214 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +27 -0
- package/tests/unit/scripts/feature-parity.test.js +327 -0
- package/tests/unit/traces/conformance.test.js +92 -0
- package/tests/unit/traces/reader.test.js +1 -0
- package/tests/unit/traces/recorder.test.js +1 -0
- package/tests/unit/traces/viewer.test.js +78 -0
- package/tests/unit/utilities/command-stream-only.test.js +28 -0
- package/tests/unit/utilities/subprocess.test.js +80 -0
|
@@ -1,126 +1,305 @@
|
|
|
1
|
-
import { describe, it
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
+
import { access } from 'node:fs/promises';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
5
|
+
import {
|
|
6
|
+
launchBrowser,
|
|
7
|
+
launchBrowserWithDependencies,
|
|
8
|
+
resolveLaunchExecutable,
|
|
9
|
+
} from '../../../src/browser/launcher.js';
|
|
10
|
+
import { TEMPORARY_PROFILE_PREFIX } from '../../../src/browser/profile-directory.js';
|
|
11
|
+
|
|
12
|
+
const exists = (file) =>
|
|
13
|
+
access(file).then(
|
|
14
|
+
() => true,
|
|
15
|
+
() => false
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
function fakePage(calls = []) {
|
|
19
|
+
return {
|
|
20
|
+
bringToFront: async () => calls.push(['bringToFront']),
|
|
21
|
+
emulateMedia: async (options) => calls.push(['emulateMedia', options]),
|
|
22
|
+
emulateMediaFeatures: async (features) =>
|
|
23
|
+
calls.push(['emulateMediaFeatures', features]),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A launchRealBrowser stand-in that records what it was asked for. */
|
|
28
|
+
function fakeRealLauncher(calls, { engine = 'playwright' } = {}) {
|
|
29
|
+
return async (options) => {
|
|
30
|
+
calls.push(['launchRealBrowser', options]);
|
|
31
|
+
const page = fakePage(calls);
|
|
32
|
+
const context = { pages: () => [page], close: async () => {} };
|
|
33
|
+
const browser =
|
|
34
|
+
engine === 'playwright'
|
|
35
|
+
? { contexts: () => [context], close: async () => {} }
|
|
36
|
+
: { pages: async () => [page], close: async () => {} };
|
|
37
|
+
const close = async () => calls.push(['close']);
|
|
38
|
+
if (engine === 'puppeteer') {
|
|
39
|
+
browser.close = close;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
browser,
|
|
43
|
+
page,
|
|
44
|
+
downloads: null,
|
|
45
|
+
close,
|
|
46
|
+
userDataDir: '/tmp/browser-commander-profile-x',
|
|
47
|
+
temporaryProfile: true,
|
|
48
|
+
args: ['--user-data-dir=/tmp/browser-commander-profile-x'],
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const systemChrome = async () => '/usr/bin/google-chrome';
|
|
54
|
+
|
|
55
|
+
describe('launchBrowser', () => {
|
|
56
|
+
it('starts the real browser by default, with nothing added (issue #103)', async () => {
|
|
57
|
+
const calls = [];
|
|
58
|
+
const googleBefore = process.env.GOOGLE_API_KEY;
|
|
59
|
+
|
|
60
|
+
const result = await launchBrowserWithDependencies(
|
|
61
|
+
{},
|
|
62
|
+
{
|
|
63
|
+
launchRealBrowser: fakeRealLauncher(calls),
|
|
64
|
+
resolveSystem: systemChrome,
|
|
65
|
+
settleMs: 0,
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const [, options] = calls.find(([name]) => name === 'launchRealBrowser');
|
|
70
|
+
assert.equal(options.engine, 'playwright');
|
|
71
|
+
assert.equal(options.executablePath, '/usr/bin/google-chrome');
|
|
72
|
+
assert.equal('slowMo' in options, false);
|
|
73
|
+
assert.equal(options.userDataDir, undefined);
|
|
74
|
+
assert.deepEqual(options.restrictions, []);
|
|
75
|
+
assert.deepEqual(options.args, []);
|
|
76
|
+
assert.equal(result.launch, 'real');
|
|
77
|
+
assert.equal(result.temporaryProfile, true);
|
|
78
|
+
// The host process environment is never changed.
|
|
79
|
+
assert.equal(process.env.GOOGLE_API_KEY, googleBefore);
|
|
80
|
+
|
|
81
|
+
// Playwright gets the attached default context, like the persistent
|
|
82
|
+
// context it used to get, and closing it closes the browser.
|
|
83
|
+
assert.equal(typeof result.browser.pages, 'function');
|
|
84
|
+
await result.browser.close();
|
|
85
|
+
assert.deepEqual(calls.at(-1), ['close']);
|
|
86
|
+
});
|
|
30
87
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
88
|
+
it('returns the connected Puppeteer browser in real mode', async () => {
|
|
89
|
+
const calls = [];
|
|
90
|
+
const result = await launchBrowserWithDependencies(
|
|
91
|
+
{ engine: 'puppeteer', restrictions: ['no-sync'], slowMo: 20 },
|
|
92
|
+
{
|
|
93
|
+
launchRealBrowser: fakeRealLauncher(calls, { engine: 'puppeteer' }),
|
|
94
|
+
resolveSystem: systemChrome,
|
|
95
|
+
settleMs: 0,
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const [, options] = calls[0];
|
|
100
|
+
assert.deepEqual(options.restrictions, ['no-sync']);
|
|
101
|
+
assert.equal(options.slowMo, 20);
|
|
102
|
+
await result.close();
|
|
103
|
+
assert.deepEqual(calls.at(-1), ['close']);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('emulates the colour scheme on the attached page', async () => {
|
|
107
|
+
const calls = [];
|
|
108
|
+
await launchBrowserWithDependencies(
|
|
109
|
+
{ colorScheme: 'dark' },
|
|
110
|
+
{
|
|
111
|
+
launchRealBrowser: fakeRealLauncher(calls),
|
|
112
|
+
resolveSystem: systemChrome,
|
|
113
|
+
settleMs: 0,
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
assert.deepEqual(
|
|
118
|
+
calls.find(([name]) => name === 'emulateMedia'),
|
|
119
|
+
['emulateMedia', { colorScheme: 'dark' }]
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('closes the browser when post-launch setup fails', async () => {
|
|
124
|
+
const calls = [];
|
|
125
|
+
await assert.rejects(
|
|
126
|
+
() =>
|
|
127
|
+
launchBrowserWithDependencies(
|
|
128
|
+
{ fingerprint: { timezone: 'Not/AZone' } },
|
|
34
129
|
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
130
|
+
launchRealBrowser: fakeRealLauncher(calls),
|
|
131
|
+
resolveSystem: systemChrome,
|
|
132
|
+
settleMs: 0,
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
/timezone|Not\/AZone/i
|
|
136
|
+
);
|
|
137
|
+
assert.deepEqual(calls.at(-1), ['close']);
|
|
138
|
+
});
|
|
40
139
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
140
|
+
it('rejects an invalid engine, launch mode or restriction before launching', async () => {
|
|
141
|
+
await assert.rejects(
|
|
142
|
+
() => launchBrowser({ engine: 'invalid-engine' }),
|
|
143
|
+
/Invalid engine: invalid-engine/
|
|
144
|
+
);
|
|
145
|
+
await assert.rejects(
|
|
146
|
+
() => launchBrowser({ launch: 'attach' }),
|
|
147
|
+
/Invalid launch mode: attach/
|
|
148
|
+
);
|
|
149
|
+
await assert.rejects(
|
|
150
|
+
() => launchBrowser({ restrictions: ['no-such-restriction'] }),
|
|
151
|
+
/Unknown launch restriction "no-such-restriction"/
|
|
152
|
+
);
|
|
153
|
+
});
|
|
48
154
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
155
|
+
describe("launch: 'engine'", () => {
|
|
156
|
+
function fakePlaywright(calls) {
|
|
157
|
+
const page = fakePage(calls);
|
|
158
|
+
return {
|
|
159
|
+
launchPersistentContext: async (userDataDir, options) => {
|
|
160
|
+
calls.push(['launchPersistentContext', userDataDir, options]);
|
|
161
|
+
return {
|
|
162
|
+
pages: () => [page],
|
|
163
|
+
close: async () => calls.push(['context.close']),
|
|
164
|
+
};
|
|
56
165
|
},
|
|
57
166
|
};
|
|
167
|
+
}
|
|
58
168
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Restore original environment
|
|
66
|
-
process.env = originalEnv;
|
|
67
|
-
});
|
|
169
|
+
it('uses a fresh temporary profile, deleted on close', async () => {
|
|
170
|
+
const calls = [];
|
|
171
|
+
const result = await launchBrowserWithDependencies(
|
|
172
|
+
{ launch: 'engine' },
|
|
173
|
+
{ loadEngineModule: async () => fakePlaywright(calls), settleMs: 0 }
|
|
174
|
+
);
|
|
68
175
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
assert.
|
|
72
|
-
assert.
|
|
73
|
-
|
|
176
|
+
const [, userDataDir, options] = calls[0];
|
|
177
|
+
assert.ok(userDataDir.includes(TEMPORARY_PROFILE_PREFIX));
|
|
178
|
+
assert.equal(await exists(userDataDir), true);
|
|
179
|
+
assert.equal(options.slowMo, 0);
|
|
180
|
+
// Only the automation off switch the engine launch needs; none of the
|
|
181
|
+
// pre-#103 CHROME_ARGS.
|
|
182
|
+
assert.deepEqual(options.args, [
|
|
183
|
+
'--disable-blink-features=AutomationControlled',
|
|
184
|
+
]);
|
|
185
|
+
assert.equal('env' in options, false);
|
|
186
|
+
assert.equal(result.launch, 'engine');
|
|
74
187
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
assert.
|
|
78
|
-
assert.ok(CHROME_ARGS.includes('--no-first-run'));
|
|
79
|
-
assert.ok(CHROME_ARGS.includes('--no-default-browser-check'));
|
|
188
|
+
await result.browser.close();
|
|
189
|
+
assert.deepEqual(calls.at(-1), ['context.close']);
|
|
190
|
+
assert.equal(await exists(userDataDir), false);
|
|
80
191
|
});
|
|
81
192
|
|
|
82
|
-
it('
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
193
|
+
it('passes restriction environment to the browser process only', async () => {
|
|
194
|
+
const calls = [];
|
|
195
|
+
const googleBefore = process.env.GOOGLE_API_KEY;
|
|
196
|
+
const result = await launchBrowserWithDependencies(
|
|
197
|
+
{ launch: 'engine', restrictions: ['legacy-launch-browser'] },
|
|
198
|
+
{ loadEngineModule: async () => fakePlaywright(calls), settleMs: 0 }
|
|
199
|
+
);
|
|
88
200
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
201
|
+
const [, , options] = calls[0];
|
|
202
|
+
assert.equal(options.env.GOOGLE_API_KEY, 'no');
|
|
203
|
+
assert.equal(process.env.GOOGLE_API_KEY, googleBefore);
|
|
204
|
+
assert.ok(options.args.includes('--no-first-run'));
|
|
205
|
+
assert.ok(options.args.includes('--disable-features=Translate'));
|
|
206
|
+
await result.close();
|
|
92
207
|
});
|
|
93
208
|
|
|
94
|
-
it('
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
209
|
+
it('launches Puppeteer without --start-maximized', async () => {
|
|
210
|
+
const calls = [];
|
|
211
|
+
const page = fakePage(calls);
|
|
212
|
+
const puppeteer = {
|
|
213
|
+
launch: async (options) => {
|
|
214
|
+
calls.push(['launch', options]);
|
|
215
|
+
return {
|
|
216
|
+
pages: async () => [page],
|
|
217
|
+
close: async () => calls.push(['browser.close']),
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const result = await launchBrowserWithDependencies(
|
|
222
|
+
{ engine: 'puppeteer', launch: 'engine' },
|
|
223
|
+
{ loadEngineModule: async () => puppeteer, settleMs: 0 }
|
|
105
224
|
);
|
|
225
|
+
|
|
226
|
+
const [, options] = calls[0];
|
|
227
|
+
assert.equal(options.args.includes('--start-maximized'), false);
|
|
228
|
+
await result.close();
|
|
229
|
+
assert.deepEqual(calls.at(-1), ['browser.close']);
|
|
106
230
|
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
107
233
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
234
|
+
describe('resolveLaunchExecutable', () => {
|
|
235
|
+
it('honours an explicit channel or executable path', async () => {
|
|
236
|
+
const requests = [];
|
|
237
|
+
const resolveSystem = async (request) => {
|
|
238
|
+
requests.push(request);
|
|
239
|
+
return '/resolved';
|
|
240
|
+
};
|
|
241
|
+
await resolveLaunchExecutable(
|
|
242
|
+
{ engine: 'playwright', channel: 'msedge' },
|
|
243
|
+
{ resolveSystem }
|
|
244
|
+
);
|
|
245
|
+
await resolveLaunchExecutable(
|
|
246
|
+
{ engine: 'playwright', executablePath: '/opt/chrome' },
|
|
247
|
+
{ resolveSystem }
|
|
248
|
+
);
|
|
249
|
+
assert.deepEqual(requests, [
|
|
250
|
+
{ channel: 'msedge', executablePath: undefined },
|
|
251
|
+
{ channel: 'chrome', executablePath: '/opt/chrome' },
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
111
254
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
255
|
+
it("falls back to the engine's downloaded Chromium without an installed Chrome", async () => {
|
|
256
|
+
const notInstalled = async () => {
|
|
257
|
+
throw new Error('Chrome is not installed');
|
|
258
|
+
};
|
|
259
|
+
const executable = await resolveLaunchExecutable(
|
|
260
|
+
{ engine: 'playwright' },
|
|
261
|
+
{
|
|
262
|
+
resolveSystem: notInstalled,
|
|
263
|
+
loadEngineModule: async () => ({
|
|
264
|
+
executablePath: () => process.execPath,
|
|
265
|
+
}),
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
assert.equal(executable, process.execPath);
|
|
117
269
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
270
|
+
await assert.rejects(
|
|
271
|
+
() =>
|
|
272
|
+
resolveLaunchExecutable(
|
|
273
|
+
{ engine: 'playwright' },
|
|
274
|
+
{
|
|
275
|
+
resolveSystem: notInstalled,
|
|
276
|
+
loadEngineModule: async () => ({
|
|
277
|
+
executablePath: () => '/nonexistent/chrome',
|
|
278
|
+
}),
|
|
279
|
+
}
|
|
280
|
+
),
|
|
281
|
+
/Chrome is not installed/
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
122
285
|
|
|
123
|
-
|
|
124
|
-
|
|
286
|
+
describe('public launch API', () => {
|
|
287
|
+
it('exports the launch modes, restrictions and profile helpers', async () => {
|
|
288
|
+
const api = await import('../../../src/index.js');
|
|
289
|
+
assert.deepEqual(api.LAUNCH_MODES, ['real', 'engine']);
|
|
290
|
+
assert.ok(api.LAUNCH_RESTRICTIONS.some(({ id }) => id === 'no-sync'));
|
|
291
|
+
assert.ok(api.LAUNCH_RESTRICTION_PRESETS['legacy-defaults']);
|
|
292
|
+
for (const name of [
|
|
293
|
+
'resolveRestrictions',
|
|
294
|
+
'mergeFeatureSwitches',
|
|
295
|
+
'resolveLaunchExecutable',
|
|
296
|
+
'createTemporaryUserDataDir',
|
|
297
|
+
'prepareUserDataDir',
|
|
298
|
+
'removeUserDataDir',
|
|
299
|
+
'reserveLoopbackPort',
|
|
300
|
+
'PortRaceError',
|
|
301
|
+
]) {
|
|
302
|
+
assert.equal(typeof api[name], 'function', name);
|
|
303
|
+
}
|
|
125
304
|
});
|
|
126
305
|
});
|
|
@@ -38,12 +38,24 @@ describe('emulateMedia', () => {
|
|
|
38
38
|
() =>
|
|
39
39
|
emulateMedia({
|
|
40
40
|
page: mockPage,
|
|
41
|
-
engine: '
|
|
41
|
+
engine: 'webkit-classic',
|
|
42
42
|
colorScheme: 'dark',
|
|
43
43
|
}),
|
|
44
44
|
/Unsupported engine/
|
|
45
45
|
);
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
it('should explain that the selenium engine has no media emulation', async () => {
|
|
49
|
+
await assert.rejects(
|
|
50
|
+
() =>
|
|
51
|
+
emulateMedia({
|
|
52
|
+
page: {},
|
|
53
|
+
engine: 'selenium',
|
|
54
|
+
colorScheme: 'dark',
|
|
55
|
+
}),
|
|
56
|
+
/not available with the selenium engine.*webdriver-no-cdp-emulation/
|
|
57
|
+
);
|
|
58
|
+
});
|
|
47
59
|
});
|
|
48
60
|
|
|
49
61
|
describe('Playwright engine', () => {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
countBookmarks,
|
|
6
|
+
migrateBookmarks,
|
|
7
|
+
} from '../../../../src/browser/migration/bookmarks.js';
|
|
8
|
+
import {
|
|
9
|
+
assertNothingMigrated,
|
|
10
|
+
migrateBetween,
|
|
11
|
+
readProfileJson,
|
|
12
|
+
writeProfileJson,
|
|
13
|
+
} from '../../../helpers/migration-fixtures.js';
|
|
14
|
+
import { useTempDirectories } from '../../../helpers/temp-directory.js';
|
|
15
|
+
|
|
16
|
+
const makeTempDir = useTempDirectories('bc-bookmarks-');
|
|
17
|
+
|
|
18
|
+
const SAMPLE_BOOKMARKS = {
|
|
19
|
+
checksum: 'source-checksum',
|
|
20
|
+
roots: {
|
|
21
|
+
bookmark_bar: {
|
|
22
|
+
type: 'folder',
|
|
23
|
+
children: [
|
|
24
|
+
{ type: 'url', name: 'A', url: 'https://a.example/' },
|
|
25
|
+
{
|
|
26
|
+
type: 'folder',
|
|
27
|
+
children: [{ type: 'url', name: 'B', url: 'https://b.example/' }],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
other: { type: 'folder', children: [] },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('countBookmarks', () => {
|
|
36
|
+
it('counts url nodes recursively across roots', () => {
|
|
37
|
+
assert.equal(countBookmarks(SAMPLE_BOOKMARKS), 2);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns 0 for empty or invalid trees', () => {
|
|
41
|
+
assert.equal(countBookmarks({}), 0);
|
|
42
|
+
assert.equal(countBookmarks(null), 0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('migrateBookmarks', () => {
|
|
47
|
+
it('copies the Bookmarks JSON verbatim and reports the count', async () => {
|
|
48
|
+
const source = await makeTempDir();
|
|
49
|
+
const target = await makeTempDir();
|
|
50
|
+
await writeProfileJson(source, 'Bookmarks', SAMPLE_BOOKMARKS);
|
|
51
|
+
|
|
52
|
+
const report = await migrateBetween(migrateBookmarks, source, target);
|
|
53
|
+
|
|
54
|
+
assert.equal(report.migrated, 2);
|
|
55
|
+
assert.deepEqual(report.skipped, []);
|
|
56
|
+
const copied = await readProfileJson(target, 'Bookmarks');
|
|
57
|
+
assert.deepEqual(copied, SAMPLE_BOOKMARKS);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('reports a skip when the source has no bookmarks', async () => {
|
|
61
|
+
const source = await makeTempDir();
|
|
62
|
+
const target = await makeTempDir();
|
|
63
|
+
|
|
64
|
+
const report = await migrateBetween(migrateBookmarks, source, target);
|
|
65
|
+
|
|
66
|
+
assertNothingMigrated(report, 'source-has-no-bookmarks');
|
|
67
|
+
assert.equal(report.skipped.length, 1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { describe, it } from 'node:test';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createWindowsProfileKey,
|
|
7
|
+
encryptChromiumValue,
|
|
8
|
+
} from '../../../../src/browser/migration/chromium-crypto.js';
|
|
9
|
+
import {
|
|
10
|
+
decryptChromiumCookie,
|
|
11
|
+
deriveChromiumCookieKey,
|
|
12
|
+
} from '../../../../src/browser/browser-cookie-crypto.js';
|
|
13
|
+
|
|
14
|
+
describe('encryptChromiumValue', () => {
|
|
15
|
+
for (const platform of ['darwin', 'linux']) {
|
|
16
|
+
it(`round-trips through AES-128-CBC on ${platform}`, () => {
|
|
17
|
+
const key = deriveChromiumCookieKey('safe-storage-pass', platform);
|
|
18
|
+
const encrypted = encryptChromiumValue({
|
|
19
|
+
plaintext: 'hunter2',
|
|
20
|
+
key,
|
|
21
|
+
platform,
|
|
22
|
+
prefix: 'v11',
|
|
23
|
+
});
|
|
24
|
+
assert.equal(encrypted.subarray(0, 3).toString('ascii'), 'v11');
|
|
25
|
+
const decrypted = decryptChromiumCookie({
|
|
26
|
+
encryptedValue: encrypted,
|
|
27
|
+
host: 'accounts.example.com',
|
|
28
|
+
databaseVersion: 0,
|
|
29
|
+
platform,
|
|
30
|
+
key,
|
|
31
|
+
});
|
|
32
|
+
assert.equal(decrypted, 'hunter2');
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it('round-trips through AES-256-GCM on win32', () => {
|
|
37
|
+
const key = randomBytes(32);
|
|
38
|
+
const encrypted = encryptChromiumValue({
|
|
39
|
+
plaintext: 'p@ssw0rd',
|
|
40
|
+
key,
|
|
41
|
+
platform: 'win32',
|
|
42
|
+
prefix: 'v10',
|
|
43
|
+
});
|
|
44
|
+
assert.equal(encrypted.subarray(0, 3).toString('ascii'), 'v10');
|
|
45
|
+
const decrypted = decryptChromiumCookie({
|
|
46
|
+
encryptedValue: encrypted,
|
|
47
|
+
host: 'login.example.com',
|
|
48
|
+
databaseVersion: 0,
|
|
49
|
+
platform: 'win32',
|
|
50
|
+
key,
|
|
51
|
+
});
|
|
52
|
+
assert.equal(decrypted, 'p@ssw0rd');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('requires a Buffer key and a supported platform', () => {
|
|
56
|
+
assert.throws(
|
|
57
|
+
() =>
|
|
58
|
+
encryptChromiumValue({
|
|
59
|
+
plaintext: 'x',
|
|
60
|
+
key: 'nope',
|
|
61
|
+
platform: 'linux',
|
|
62
|
+
}),
|
|
63
|
+
/target encryption key/
|
|
64
|
+
);
|
|
65
|
+
assert.throws(
|
|
66
|
+
() =>
|
|
67
|
+
encryptChromiumValue({
|
|
68
|
+
plaintext: 'x',
|
|
69
|
+
key: randomBytes(16),
|
|
70
|
+
platform: 'sunos',
|
|
71
|
+
}),
|
|
72
|
+
/unsupported/
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('createWindowsProfileKey', () => {
|
|
78
|
+
it('tags the DPAPI-protected key and base64-encodes it', async () => {
|
|
79
|
+
const encryptDpapi = (input) => Buffer.concat([Buffer.from('ENC'), input]);
|
|
80
|
+
const { key, encryptedKeyBase64 } = await createWindowsProfileKey({
|
|
81
|
+
encryptDpapi,
|
|
82
|
+
generateKey: () => Buffer.alloc(32, 7),
|
|
83
|
+
});
|
|
84
|
+
assert.equal(key.length, 32);
|
|
85
|
+
const decoded = Buffer.from(encryptedKeyBase64, 'base64');
|
|
86
|
+
assert.equal(decoded.subarray(0, 5).toString('ascii'), 'DPAPI');
|
|
87
|
+
assert.equal(decoded.subarray(5, 8).toString('ascii'), 'ENC');
|
|
88
|
+
assert.deepEqual(decoded.subarray(8), Buffer.alloc(32, 7));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('requires an encryptDpapi function', async () => {
|
|
92
|
+
await assert.rejects(createWindowsProfileKey({}), /encryptDpapi/);
|
|
93
|
+
});
|
|
94
|
+
});
|