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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-browser tests of the `browser-commander` CLI (issue #104).
|
|
3
|
+
*
|
|
4
|
+
* The binary runs as a separate process through command-stream, the way a
|
|
5
|
+
* shell script, a Rust program or a Python program would use it:
|
|
6
|
+
*
|
|
7
|
+
* - `run tests/cli-contract/basic.json` must print, after normalization,
|
|
8
|
+
* exactly `tests/cli-contract/basic.expected.json`. The Rust and Python
|
|
9
|
+
* CLIs are checked against the same file, so all three agree.
|
|
10
|
+
* - `launch --keep-open` leaves a browser running that later `goto` and
|
|
11
|
+
* `eval` commands reach with `--cdp-endpoint`.
|
|
12
|
+
*
|
|
13
|
+
* Run with: npm run test:e2e:cli
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it } from 'node:test';
|
|
16
|
+
import assert from 'node:assert';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
import { ProcessRunner } from 'command-stream/process-runner';
|
|
21
|
+
|
|
22
|
+
import { normalizeRunOutput } from '../../../tests/cli-contract/normalize.mjs';
|
|
23
|
+
import { runCommand } from '../../src/utilities/subprocess.js';
|
|
24
|
+
import { SANDBOX_ARGS } from '../helpers/e2e-browser.js';
|
|
25
|
+
import { repoPath } from '../helpers/repo.js';
|
|
26
|
+
|
|
27
|
+
const BIN = fileURLToPath(
|
|
28
|
+
new URL('../../bin/browser-commander.js', import.meta.url)
|
|
29
|
+
);
|
|
30
|
+
const ENGINES = ['playwright', 'puppeteer'];
|
|
31
|
+
const E2E = { skip: !process.env.RUN_E2E, timeout: 180_000 };
|
|
32
|
+
|
|
33
|
+
/** Options that pick the test browser: CHROME_PATH and sandbox arguments. */
|
|
34
|
+
function browserOptions(engine) {
|
|
35
|
+
return [
|
|
36
|
+
'--engine',
|
|
37
|
+
engine,
|
|
38
|
+
...(process.env.CHROME_PATH
|
|
39
|
+
? ['--executable-path', process.env.CHROME_PATH]
|
|
40
|
+
: []),
|
|
41
|
+
// "--arg=VALUE": a value that starts with "--" cannot follow a space.
|
|
42
|
+
...SANDBOX_ARGS.map((arg) => `--arg=${arg}`),
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Run the CLI to completion and parse its one JSON document. */
|
|
47
|
+
async function browserCommander(args) {
|
|
48
|
+
const { code, stdout, stderr } = await runCommand(
|
|
49
|
+
process.execPath,
|
|
50
|
+
[BIN, ...args],
|
|
51
|
+
{ check: false }
|
|
52
|
+
);
|
|
53
|
+
let document;
|
|
54
|
+
try {
|
|
55
|
+
document = JSON.parse(stdout);
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error(`No JSON from ${args.join(' ')}: ${stdout}\n${stderr}`);
|
|
58
|
+
}
|
|
59
|
+
return { code, document };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Start `launch --keep-open` with stdin kept open, and resolve with its
|
|
64
|
+
* document once it has been printed.
|
|
65
|
+
*/
|
|
66
|
+
function startKeptOpenBrowser(engine) {
|
|
67
|
+
const runner = new ProcessRunner(
|
|
68
|
+
{
|
|
69
|
+
mode: 'exec',
|
|
70
|
+
file: process.execPath,
|
|
71
|
+
args: [
|
|
72
|
+
BIN,
|
|
73
|
+
'launch',
|
|
74
|
+
'--keep-open',
|
|
75
|
+
'--headless',
|
|
76
|
+
...browserOptions(engine),
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{ mirror: false, capture: true, stdin: 'pipe' }
|
|
80
|
+
);
|
|
81
|
+
let output = '';
|
|
82
|
+
const printed = new Promise((resolve, reject) => {
|
|
83
|
+
runner.on('stdout', (chunk) => {
|
|
84
|
+
output += chunk.toString();
|
|
85
|
+
if (output.includes('\n')) {
|
|
86
|
+
resolve(JSON.parse(output.split('\n')[0]));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
Promise.resolve(runner).then(
|
|
90
|
+
(result) =>
|
|
91
|
+
reject(new Error(`launch exited early: ${output}${result.stderr}`)),
|
|
92
|
+
reject
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
runner.start();
|
|
96
|
+
return { runner, printed };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const engine of ENGINES) {
|
|
100
|
+
describe(`E2E - browser-commander CLI with ${engine} (issue #104)`, () => {
|
|
101
|
+
it(
|
|
102
|
+
'run basic.json matches the cross-language expected output',
|
|
103
|
+
E2E,
|
|
104
|
+
async () => {
|
|
105
|
+
const { code, document } = await browserCommander([
|
|
106
|
+
'run',
|
|
107
|
+
repoPath('tests', 'cli-contract', 'basic.json'),
|
|
108
|
+
...browserOptions(engine),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const expected = JSON.parse(
|
|
112
|
+
readFileSync(
|
|
113
|
+
repoPath('tests', 'cli-contract', 'basic.expected.json'),
|
|
114
|
+
'utf8'
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
assert.deepEqual(normalizeRunOutput(document), expected);
|
|
118
|
+
assert.equal(code, 0);
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
it(
|
|
123
|
+
'launch --keep-open serves goto and eval over --cdp-endpoint',
|
|
124
|
+
E2E,
|
|
125
|
+
async () => {
|
|
126
|
+
const { runner, printed } = startKeptOpenBrowser(engine);
|
|
127
|
+
try {
|
|
128
|
+
const launched = await printed;
|
|
129
|
+
assert.match(launched.cdpEndpoint, /^http:\/\/127\.0\.0\.1:\d+$/u);
|
|
130
|
+
const attach = [
|
|
131
|
+
'--cdp-endpoint',
|
|
132
|
+
launched.cdpEndpoint,
|
|
133
|
+
'--engine',
|
|
134
|
+
engine,
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const url = `data:text/html,${encodeURIComponent(
|
|
138
|
+
'<title>Kept open</title><p id="x">42</p>'
|
|
139
|
+
)}`;
|
|
140
|
+
const goto = await browserCommander(['goto', url, ...attach]);
|
|
141
|
+
assert.equal(goto.code, 0);
|
|
142
|
+
assert.deepEqual(goto.document, { url, title: 'Kept open' });
|
|
143
|
+
|
|
144
|
+
// The second command attaches again and sees the page the first
|
|
145
|
+
// one left behind: the browser outlived both commands.
|
|
146
|
+
const evaluated = await browserCommander([
|
|
147
|
+
'eval',
|
|
148
|
+
"document.title + ':' + document.querySelector('#x').textContent",
|
|
149
|
+
...attach,
|
|
150
|
+
]);
|
|
151
|
+
assert.equal(evaluated.code, 0);
|
|
152
|
+
assert.deepEqual(evaluated.document, { value: 'Kept open:42' });
|
|
153
|
+
} finally {
|
|
154
|
+
runner.kill('SIGTERM');
|
|
155
|
+
await Promise.resolve(runner).catch(() => {});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -17,8 +17,12 @@ import fs from 'node:fs/promises';
|
|
|
17
17
|
import os from 'node:os';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
|
|
20
|
-
import {
|
|
21
|
-
|
|
20
|
+
import {
|
|
21
|
+
browserContextIdForPage,
|
|
22
|
+
openBrowserCdpSession,
|
|
23
|
+
} from '../../src/downloads/sources.js';
|
|
24
|
+
import { makeBrowserCommander } from '../../src/factory.js';
|
|
25
|
+
import { launchE2EBrowser, SANDBOX_ARGS } from '../helpers/e2e-browser.js';
|
|
22
26
|
import {
|
|
23
27
|
BLOB_PDF_BODY,
|
|
24
28
|
PDF_BODY,
|
|
@@ -202,9 +206,16 @@ function describeDownloads(engine) {
|
|
|
202
206
|
// than through the engine's download API. Issue #88 asks for "one
|
|
203
207
|
// lifecycle for automated *and* manual downloads".
|
|
204
208
|
const session = await openBrowserCdpSession({ engine, browser, page });
|
|
209
|
+
const browserContextId = await browserContextIdForPage({
|
|
210
|
+
engine,
|
|
211
|
+
browser,
|
|
212
|
+
page,
|
|
213
|
+
browserSession: session,
|
|
214
|
+
});
|
|
205
215
|
const captured = downloads.capture({ timeout: 20000 });
|
|
206
216
|
await session.send('Target.createTarget', {
|
|
207
217
|
url: `${server.baseUrl}/file/manual.pdf`,
|
|
218
|
+
...(browserContextId ? { browserContextId } : {}),
|
|
208
219
|
});
|
|
209
220
|
|
|
210
221
|
const artifact = await captured;
|
|
@@ -231,5 +242,71 @@ describe(
|
|
|
231
242
|
for (const engine of ENGINES) {
|
|
232
243
|
describeDownloads(engine);
|
|
233
244
|
}
|
|
245
|
+
|
|
246
|
+
it('should persist a Blob from browser.newPage() (issue #97)', async () => {
|
|
247
|
+
const { chromium } = await import('playwright');
|
|
248
|
+
const directory = await fs.mkdtemp(
|
|
249
|
+
path.join(os.tmpdir(), 'bc-e2e-context-downloads-')
|
|
250
|
+
);
|
|
251
|
+
const browser = await chromium.launch({
|
|
252
|
+
headless: true,
|
|
253
|
+
args: SANDBOX_ARGS,
|
|
254
|
+
});
|
|
255
|
+
const page = await browser.newPage();
|
|
256
|
+
const commander = makeBrowserCommander({
|
|
257
|
+
page,
|
|
258
|
+
enableNetworkTracking: false,
|
|
259
|
+
enableNavigationManager: false,
|
|
260
|
+
enableDialogManager: false,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const downloads = await commander.configureDownloads({
|
|
265
|
+
directory,
|
|
266
|
+
persist: true,
|
|
267
|
+
});
|
|
268
|
+
const completed = [];
|
|
269
|
+
downloads.on('completed', (artifact) => completed.push(artifact));
|
|
270
|
+
await page.setContent(`
|
|
271
|
+
<button id="download">Download</button>
|
|
272
|
+
<script>
|
|
273
|
+
document.querySelector('#download').addEventListener('click', () => {
|
|
274
|
+
const blob = new Blob([${JSON.stringify(BLOB_PDF_BODY)}], {
|
|
275
|
+
type: 'application/pdf',
|
|
276
|
+
});
|
|
277
|
+
const anchor = document.createElement('a');
|
|
278
|
+
anchor.href = URL.createObjectURL(blob);
|
|
279
|
+
anchor.download = 'declaration.pdf';
|
|
280
|
+
anchor.click();
|
|
281
|
+
});
|
|
282
|
+
</script>
|
|
283
|
+
`);
|
|
284
|
+
|
|
285
|
+
const artifact = await downloads.capture({
|
|
286
|
+
action: page.click.bind(page, '#download'),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
assert.strictEqual(artifact.state, 'completed');
|
|
290
|
+
assert.strictEqual(path.basename(artifact.path), 'declaration.pdf');
|
|
291
|
+
assert.strictEqual(
|
|
292
|
+
await fs.readFile(artifact.path, 'utf8'),
|
|
293
|
+
BLOB_PDF_BODY
|
|
294
|
+
);
|
|
295
|
+
assert.strictEqual(completed.length, 1);
|
|
296
|
+
const staged = await fs.readdir(
|
|
297
|
+
path.join(directory, '.browser-commander-staging')
|
|
298
|
+
);
|
|
299
|
+
assert.deepStrictEqual(staged, []);
|
|
300
|
+
assert.ok(
|
|
301
|
+
!(await fs.readdir(directory)).some((entry) =>
|
|
302
|
+
entry.endsWith('.partial')
|
|
303
|
+
)
|
|
304
|
+
);
|
|
305
|
+
} finally {
|
|
306
|
+
await commander.destroy();
|
|
307
|
+
await browser.close();
|
|
308
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
309
|
+
}
|
|
310
|
+
});
|
|
234
311
|
}
|
|
235
312
|
);
|
|
@@ -23,10 +23,6 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { describe, it, before, after } from 'node:test';
|
|
25
25
|
import assert from 'node:assert';
|
|
26
|
-
import { existsSync } from 'node:fs';
|
|
27
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
28
|
-
import { tmpdir } from 'node:os';
|
|
29
|
-
import path from 'node:path';
|
|
30
26
|
import { randomUUID } from 'node:crypto';
|
|
31
27
|
|
|
32
28
|
import {
|
|
@@ -34,25 +30,26 @@ import {
|
|
|
34
30
|
diffReports,
|
|
35
31
|
readProbeSource,
|
|
36
32
|
startProbeServer,
|
|
37
|
-
} from '
|
|
38
|
-
import { launchBrowser } from '../../src/index.js';
|
|
33
|
+
} from '../../src/parity/harness.js';
|
|
34
|
+
import { launchBrowser, launchRealBrowser } from '../../src/index.js';
|
|
39
35
|
import { createFingerprintPreset } from '../../src/fingerprint/presets.js';
|
|
36
|
+
import {
|
|
37
|
+
PARITY_CHROME as CHROME,
|
|
38
|
+
paritySkipReason as skipReason,
|
|
39
|
+
} from '../helpers/e2e-browser.js';
|
|
40
40
|
|
|
41
|
-
const CHROME = process.env.CHROME_PATH || '/usr/bin/google-chrome';
|
|
42
41
|
const TEST_TIMEOUT = 180000;
|
|
43
42
|
|
|
44
|
-
/**
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return false;
|
|
43
|
+
/** Fail with every differing path when `report` is not `reference`. */
|
|
44
|
+
function assertIndistinguishable(reference, report, label) {
|
|
45
|
+
const differences = diffReports(reference, report);
|
|
46
|
+
assert.deepEqual(
|
|
47
|
+
differences,
|
|
48
|
+
[],
|
|
49
|
+
`${label} differs from a hand-started browser in: ${differences
|
|
50
|
+
.map((entry) => entry.path)
|
|
51
|
+
.join(', ')}`
|
|
52
|
+
);
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
function defineParitySuite({ headless }) {
|
|
@@ -64,29 +61,48 @@ function defineParitySuite({ headless }) {
|
|
|
64
61
|
() => {
|
|
65
62
|
let server;
|
|
66
63
|
let reference;
|
|
67
|
-
const userDataDirs = [];
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Drive one capture through the shipped launcher. With no `launch`
|
|
67
|
+
* option that is the default real launch: a fresh temporary profile and
|
|
68
|
+
* a clean command line (issue #103).
|
|
69
|
+
*/
|
|
70
|
+
const captureWithLibrary = async (options) => {
|
|
71
|
+
const token = randomUUID();
|
|
72
|
+
const launched = await launchBrowser({
|
|
73
|
+
headless,
|
|
74
|
+
executablePath: CHROME,
|
|
75
|
+
...options,
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
await launched.page.goto(server.url(token), { waitUntil: 'load' });
|
|
79
|
+
return {
|
|
80
|
+
report: await server.waitForReport(token),
|
|
81
|
+
token,
|
|
82
|
+
launched,
|
|
83
|
+
};
|
|
84
|
+
} finally {
|
|
85
|
+
await launched.browser.close();
|
|
86
|
+
}
|
|
73
87
|
};
|
|
74
88
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Drive one capture through the real-browser launcher: Chrome is spawned
|
|
91
|
+
* with `--user-data-dir` and a reserved `--remote-debugging-port` only,
|
|
92
|
+
* and the engine attaches over CDP afterwards (issue #101).
|
|
93
|
+
*/
|
|
94
|
+
const captureWithRealBrowser = async (options) => {
|
|
77
95
|
const token = randomUUID();
|
|
78
|
-
const
|
|
96
|
+
const session = await launchRealBrowser({
|
|
79
97
|
headless,
|
|
80
|
-
slowMo: 0,
|
|
81
98
|
executablePath: CHROME,
|
|
82
|
-
userDataDir: await nextUserDataDir(),
|
|
83
99
|
...options,
|
|
84
100
|
});
|
|
85
101
|
try {
|
|
86
|
-
await page.goto(server.url(token), { waitUntil: 'load' });
|
|
87
|
-
return { report: await server.waitForReport(token),
|
|
102
|
+
await session.page.goto(server.url(token), { waitUntil: 'load' });
|
|
103
|
+
return { report: await server.waitForReport(token), session };
|
|
88
104
|
} finally {
|
|
89
|
-
await
|
|
105
|
+
await session.close();
|
|
90
106
|
}
|
|
91
107
|
};
|
|
92
108
|
|
|
@@ -108,27 +124,49 @@ function defineParitySuite({ headless }) {
|
|
|
108
124
|
if (server) {
|
|
109
125
|
await server.close();
|
|
110
126
|
}
|
|
111
|
-
await Promise.all(
|
|
112
|
-
userDataDirs.map((directory) =>
|
|
113
|
-
rm(directory, { recursive: true, force: true, maxRetries: 10 })
|
|
114
|
-
)
|
|
115
|
-
);
|
|
116
127
|
});
|
|
117
128
|
|
|
129
|
+
// Nothing but the profile, the fixed port, --headless=new when headless
|
|
130
|
+
// and the about:blank start page, so there is no switch that could show
|
|
131
|
+
// the unsupported-flag infobar.
|
|
132
|
+
const assertPersonCommandLine = (args) => {
|
|
133
|
+
assert.deepEqual(
|
|
134
|
+
args.map((argument) => argument.split('=')[0]),
|
|
135
|
+
[
|
|
136
|
+
'--user-data-dir',
|
|
137
|
+
'--remote-debugging-port',
|
|
138
|
+
...(headless ? ['--headless'] : []),
|
|
139
|
+
'about:blank',
|
|
140
|
+
],
|
|
141
|
+
args.join(' ')
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
118
145
|
for (const engine of ['playwright', 'puppeteer']) {
|
|
119
146
|
it(
|
|
120
147
|
`${engine} is indistinguishable from a hand-started Chrome`,
|
|
121
148
|
{ timeout: TEST_TIMEOUT },
|
|
122
149
|
async () => {
|
|
123
|
-
const { report } = await captureWithLibrary({ engine });
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
const { report, launched } = await captureWithLibrary({ engine });
|
|
151
|
+
assert.equal(launched.launch, 'real');
|
|
152
|
+
assert.equal(launched.temporaryProfile, true);
|
|
153
|
+
assertPersonCommandLine(launched.args);
|
|
154
|
+
assertIndistinguishable(reference, report, `${engine} (${mode})`);
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
it(
|
|
159
|
+
`${engine} with launch: 'engine' is indistinguishable from a hand-started Chrome`,
|
|
160
|
+
{ timeout: TEST_TIMEOUT },
|
|
161
|
+
async () => {
|
|
162
|
+
const { report } = await captureWithLibrary({
|
|
163
|
+
engine,
|
|
164
|
+
launch: 'engine',
|
|
165
|
+
});
|
|
166
|
+
assertIndistinguishable(
|
|
167
|
+
reference,
|
|
168
|
+
report,
|
|
169
|
+
`engine-launched ${engine} (${mode})`
|
|
132
170
|
);
|
|
133
171
|
}
|
|
134
172
|
);
|
|
@@ -140,8 +178,11 @@ function defineParitySuite({ headless }) {
|
|
|
140
178
|
// Negative control: without this the parity assertion above could
|
|
141
179
|
// pass for the wrong reason, for example because the probe stopped
|
|
142
180
|
// reporting the field.
|
|
181
|
+
// An engine launch passes switches that turn AutomationControlled
|
|
182
|
+
// on; the default real launch has none to undo.
|
|
143
183
|
const { report } = await captureWithLibrary({
|
|
144
184
|
engine,
|
|
185
|
+
launch: 'engine',
|
|
145
186
|
automationParity: false,
|
|
146
187
|
});
|
|
147
188
|
const paths = diffReports(reference, report).map(
|
|
@@ -155,6 +196,23 @@ function defineParitySuite({ headless }) {
|
|
|
155
196
|
}
|
|
156
197
|
);
|
|
157
198
|
|
|
199
|
+
it(
|
|
200
|
+
`launchRealBrowser with ${engine} is indistinguishable from a hand-started Chrome`,
|
|
201
|
+
{ timeout: TEST_TIMEOUT },
|
|
202
|
+
async () => {
|
|
203
|
+
const { report, session } = await captureWithRealBrowser({
|
|
204
|
+
engine,
|
|
205
|
+
});
|
|
206
|
+
assertPersonCommandLine(session.args);
|
|
207
|
+
assert.equal(report.navigator.webdriver, false);
|
|
208
|
+
assertIndistinguishable(
|
|
209
|
+
reference,
|
|
210
|
+
report,
|
|
211
|
+
`real ${engine} session (${mode})`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
|
|
158
216
|
it(
|
|
159
217
|
`${engine} presents the fingerprint profile it was launched with`,
|
|
160
218
|
{ timeout: TEST_TIMEOUT },
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E acceptance probe for issue #102: a clean default real-browser launch must
|
|
3
|
+
* let a person sign in with a Google account.
|
|
4
|
+
*
|
|
5
|
+
* The tell-tale is what Google says after an address is submitted. A browser
|
|
6
|
+
* Google trusts answers "Couldn't find your Google Account" for an address that
|
|
7
|
+
* does not exist; a browser it flags as automated answers "This browser or app
|
|
8
|
+
* may not be secure" and never reaches the account lookup. The default launch
|
|
9
|
+
* carries no automation switches (issue #101/#103), so `navigator.webdriver` is
|
|
10
|
+
* false and this probe should see the former, not the latter.
|
|
11
|
+
*
|
|
12
|
+
* This talks to the live accounts.google.com, so it is off by default and never
|
|
13
|
+
* runs in CI. It is headful (Google's checks differ under headless), so on Linux
|
|
14
|
+
* it needs a display:
|
|
15
|
+
*
|
|
16
|
+
* RUN_GOOGLE_PROBE=true xvfb-run -a --server-args="-screen 0 1920x1080x24" \
|
|
17
|
+
* npm run test:e2e:google-probe
|
|
18
|
+
*
|
|
19
|
+
* On success it writes docs/screenshots/google-signin-probe.png as evidence.
|
|
20
|
+
*/
|
|
21
|
+
import { describe, it } from 'node:test';
|
|
22
|
+
import assert from 'node:assert';
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { mkdir } from 'node:fs/promises';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
import { launchRealBrowser } from '../../src/index.js';
|
|
29
|
+
|
|
30
|
+
const CHROME = process.env.CHROME_PATH || '/usr/bin/google-chrome';
|
|
31
|
+
const TEST_TIMEOUT = 180000;
|
|
32
|
+
|
|
33
|
+
const SECURE_WARNING = 'This browser or app may not be secure';
|
|
34
|
+
const ACCOUNT_NOT_FOUND = "Couldn't find your Google Account";
|
|
35
|
+
|
|
36
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const SCREENSHOT = path.resolve(
|
|
38
|
+
HERE,
|
|
39
|
+
'../../../docs/screenshots/google-signin-probe.png'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/** Why the probe cannot run here, or `false` when it can. */
|
|
43
|
+
function skipReason() {
|
|
44
|
+
if (process.env.RUN_GOOGLE_PROBE !== 'true') {
|
|
45
|
+
return 'set RUN_GOOGLE_PROBE=true to run the live Google sign-in probe';
|
|
46
|
+
}
|
|
47
|
+
if (!existsSync(CHROME)) {
|
|
48
|
+
return `no Chrome binary at ${CHROME}; set CHROME_PATH`;
|
|
49
|
+
}
|
|
50
|
+
if (process.platform === 'linux' && !process.env.DISPLAY) {
|
|
51
|
+
return 'the headful Google probe needs a display; run under xvfb-run';
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe(
|
|
57
|
+
'E2E Tests - Google sign-in probe (#102)',
|
|
58
|
+
{ skip: skipReason() },
|
|
59
|
+
() => {
|
|
60
|
+
it(
|
|
61
|
+
'a clean default launch reaches the account lookup, not the "not secure" wall',
|
|
62
|
+
{ timeout: TEST_TIMEOUT },
|
|
63
|
+
async () => {
|
|
64
|
+
// A clearly nonexistent address, so a trusted browser lands on
|
|
65
|
+
// "Couldn't find your Google Account" instead of a password prompt.
|
|
66
|
+
const address = `bc-parity-probe-${Date.now()}@gmail.com`;
|
|
67
|
+
const session = await launchRealBrowser({
|
|
68
|
+
headless: false,
|
|
69
|
+
executablePath: CHROME,
|
|
70
|
+
args:
|
|
71
|
+
process.env.CHROME_NO_SANDBOX === 'true' ? ['--no-sandbox'] : [],
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
const { page } = session;
|
|
75
|
+
await page.goto('https://accounts.google.com/signin/v2/identifier', {
|
|
76
|
+
waitUntil: 'load',
|
|
77
|
+
timeout: 60000,
|
|
78
|
+
});
|
|
79
|
+
await page.locator('input[type="email"]').fill(address);
|
|
80
|
+
await page.keyboard.press('Enter');
|
|
81
|
+
|
|
82
|
+
// Wait for either outcome so the assertion is not racing the network.
|
|
83
|
+
await page
|
|
84
|
+
.waitForFunction(
|
|
85
|
+
(needles) =>
|
|
86
|
+
needles.some((needle) =>
|
|
87
|
+
document.body.innerText.includes(needle)
|
|
88
|
+
),
|
|
89
|
+
[ACCOUNT_NOT_FOUND, SECURE_WARNING],
|
|
90
|
+
{ timeout: 60000 }
|
|
91
|
+
)
|
|
92
|
+
.catch(() => {});
|
|
93
|
+
|
|
94
|
+
await mkdir(path.dirname(SCREENSHOT), { recursive: true });
|
|
95
|
+
await page.screenshot({ path: SCREENSHOT, fullPage: true });
|
|
96
|
+
|
|
97
|
+
const bodyText = await page.evaluate(() => document.body.innerText);
|
|
98
|
+
assert.ok(
|
|
99
|
+
!bodyText.includes(SECURE_WARNING),
|
|
100
|
+
`Google flagged the clean launch as not secure. Screenshot: ${SCREENSHOT}`
|
|
101
|
+
);
|
|
102
|
+
assert.ok(
|
|
103
|
+
bodyText.includes(ACCOUNT_NOT_FOUND),
|
|
104
|
+
`expected "${ACCOUNT_NOT_FOUND}"; got a different page. Screenshot: ${SCREENSHOT}`
|
|
105
|
+
);
|
|
106
|
+
} finally {
|
|
107
|
+
await session.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
);
|