browser-commander 0.32.1 → 0.33.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/CHANGELOG.md +16 -0
- package/README.md +52 -5
- package/package.json +1 -1
- package/src/browser/browser-handoff.js +4 -0
- package/src/browser/chrome-for-testing-cache.js +198 -0
- package/src/browser/launch-executable.js +203 -0
- package/src/browser/launch-restrictions.json +7 -1
- package/src/browser/launcher.js +78 -65
- package/src/browser/migration/firefox.js +1 -1
- package/src/browser/migration/index.js +10 -0
- package/src/browser/migration/os-crypt-keys.js +36 -6
- package/src/browser/migration/passwords.js +1 -1
- package/src/browser/parity.js +6 -1
- package/src/browser/real-browser.js +53 -14
- package/src/browser/webdriver.js +63 -8
- package/src/capture/screenshot.js +16 -1
- package/src/capture/viewport.js +2 -1
- package/src/exports.js +5 -0
- package/src/traces/recorder.js +18 -2
- package/tests/e2e/chrome-for-testing.e2e.test.js +105 -0
- package/tests/e2e/startup-readiness.e2e.test.js +2 -0
- package/tests/unit/browser/browser-handoff.test.js +13 -0
- package/tests/unit/browser/chrome-for-testing-cache.test.js +139 -0
- package/tests/unit/browser/launch-executable.test.js +94 -0
- package/tests/unit/browser/launcher.test.js +235 -31
- package/tests/unit/browser/migration/os-crypt-keys.test.js +57 -0
- package/tests/unit/browser/migration/safari-data.test.js +43 -19
- package/tests/unit/browser/parity.test.js +19 -0
- package/tests/unit/browser/real-browser.test.js +125 -0
- package/tests/unit/browser/webdriver.test.js +95 -14
- package/tests/unit/scripts/check-web-archive.test.js +77 -2
- package/tests/unit/traces/issue-162-defaults.test.js +1 -1
- package/tests/unit/traces/issue-166-policy.test.js +1 -1
- package/tests/unit/traces/issue-180-screenshots.test.js +173 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
1
|
+
import { after, describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import { access } from 'node:fs/promises';
|
|
3
|
+
import { access, chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
launchBrowser,
|
|
@@ -52,21 +54,59 @@ function fakeRealLauncher(calls, { engine = 'playwright' } = {}) {
|
|
|
52
54
|
|
|
53
55
|
const systemChrome = async () => '/usr/bin/google-chrome';
|
|
54
56
|
|
|
57
|
+
// An executable laid out like the Chrome for Testing Playwright downloads.
|
|
58
|
+
const cftRoot = await mkdtemp(path.join(tmpdir(), 'browser-commander-cft-'));
|
|
59
|
+
const cftChrome = path.join(
|
|
60
|
+
cftRoot,
|
|
61
|
+
'chromium-1248',
|
|
62
|
+
'chrome-linux64',
|
|
63
|
+
'chrome'
|
|
64
|
+
);
|
|
65
|
+
await mkdir(path.dirname(cftChrome), { recursive: true });
|
|
66
|
+
await writeFile(cftChrome, '#!/bin/sh\n');
|
|
67
|
+
await chmod(cftChrome, 0o755);
|
|
68
|
+
after(() => rm(cftRoot, { recursive: true, force: true }));
|
|
69
|
+
const cftEngine = async () => ({ executablePath: () => cftChrome });
|
|
70
|
+
const missingEngine = async () => ({ executablePath: () => '/nonexistent' });
|
|
71
|
+
const noCachedBrowsers = () => [];
|
|
72
|
+
const noSystemChrome = async () => {
|
|
73
|
+
throw new Error('the installed Google Chrome must not be resolved');
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// A real launch with recorded calls: the result and launchRealBrowser's options.
|
|
77
|
+
async function realLaunch(options, dependencies, calls = []) {
|
|
78
|
+
const result = await launchBrowserWithDependencies(options, {
|
|
79
|
+
launchRealBrowser: fakeRealLauncher(calls),
|
|
80
|
+
settleMs: 0,
|
|
81
|
+
...dependencies,
|
|
82
|
+
});
|
|
83
|
+
const [, launched] = calls.find(([name]) => name === 'launchRealBrowser');
|
|
84
|
+
return [result, launched];
|
|
85
|
+
}
|
|
86
|
+
|
|
55
87
|
describe('launchBrowser', () => {
|
|
56
|
-
it('starts
|
|
88
|
+
it('starts Chrome for Testing by default, never the installed Google Chrome (issue #179)', async () => {
|
|
89
|
+
const [, options] = await realLaunch(
|
|
90
|
+
{},
|
|
91
|
+
{ resolveSystem: noSystemChrome, loadEngineModule: cftEngine }
|
|
92
|
+
);
|
|
93
|
+
assert.equal(options.executablePath, cftChrome);
|
|
94
|
+
// No channel is invented: the launch is not Google Chrome's.
|
|
95
|
+
assert.equal('channel' in options, false);
|
|
96
|
+
// The Chrome for Testing defaults are added by launchRealBrowser.
|
|
97
|
+
assert.deepEqual(options.restrictions, []);
|
|
98
|
+
assert.equal('defaultRestrictions' in options, false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('starts the requested Google Chrome with nothing added (issue #103)', async () => {
|
|
57
102
|
const calls = [];
|
|
58
103
|
const googleBefore = process.env.GOOGLE_API_KEY;
|
|
59
104
|
|
|
60
|
-
const result = await
|
|
61
|
-
{},
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
resolveSystem: systemChrome,
|
|
65
|
-
settleMs: 0,
|
|
66
|
-
}
|
|
105
|
+
const [result, options] = await realLaunch(
|
|
106
|
+
{ channel: 'chrome' },
|
|
107
|
+
{ resolveSystem: systemChrome },
|
|
108
|
+
calls
|
|
67
109
|
);
|
|
68
|
-
|
|
69
|
-
const [, options] = calls.find(([name]) => name === 'launchRealBrowser');
|
|
70
110
|
assert.equal(options.engine, 'playwright');
|
|
71
111
|
assert.equal(options.executablePath, '/usr/bin/google-chrome');
|
|
72
112
|
assert.equal('slowMo' in options, false);
|
|
@@ -91,7 +131,7 @@ describe('launchBrowser', () => {
|
|
|
91
131
|
{ engine: 'puppeteer', restrictions: ['no-sync'], slowMo: 20 },
|
|
92
132
|
{
|
|
93
133
|
launchRealBrowser: fakeRealLauncher(calls, { engine: 'puppeteer' }),
|
|
94
|
-
|
|
134
|
+
loadEngineModule: cftEngine,
|
|
95
135
|
settleMs: 0,
|
|
96
136
|
}
|
|
97
137
|
);
|
|
@@ -109,7 +149,7 @@ describe('launchBrowser', () => {
|
|
|
109
149
|
{ colorScheme: 'dark' },
|
|
110
150
|
{
|
|
111
151
|
launchRealBrowser: fakeRealLauncher(calls),
|
|
112
|
-
|
|
152
|
+
loadEngineModule: cftEngine,
|
|
113
153
|
settleMs: 0,
|
|
114
154
|
}
|
|
115
155
|
);
|
|
@@ -128,7 +168,7 @@ describe('launchBrowser', () => {
|
|
|
128
168
|
{ fingerprint: { timezone: 'Not/AZone' } },
|
|
129
169
|
{
|
|
130
170
|
launchRealBrowser: fakeRealLauncher(calls),
|
|
131
|
-
|
|
171
|
+
loadEngineModule: cftEngine,
|
|
132
172
|
settleMs: 0,
|
|
133
173
|
}
|
|
134
174
|
),
|
|
@@ -150,6 +190,10 @@ describe('launchBrowser', () => {
|
|
|
150
190
|
() => launchBrowser({ restrictions: ['no-such-restriction'] }),
|
|
151
191
|
/Unknown launch restriction "no-such-restriction"/
|
|
152
192
|
);
|
|
193
|
+
await assert.rejects(
|
|
194
|
+
() => launchBrowser({ defaultRestrictions: ['no-such-default'] }),
|
|
195
|
+
/Unknown launch restriction "no-such-default"/
|
|
196
|
+
);
|
|
153
197
|
});
|
|
154
198
|
|
|
155
199
|
describe("launch: 'engine'", () => {
|
|
@@ -233,6 +277,100 @@ describe('launchBrowser', () => {
|
|
|
233
277
|
assert.deepEqual(calls.at(-1), ['browser.close']);
|
|
234
278
|
});
|
|
235
279
|
|
|
280
|
+
it('adds the Chrome for Testing defaults to its engine launch (issue #179)', async () => {
|
|
281
|
+
const launchWith = async (options, executablePath) => {
|
|
282
|
+
const calls = [];
|
|
283
|
+
const engine = {
|
|
284
|
+
...fakePlaywright(calls),
|
|
285
|
+
executablePath: () => executablePath,
|
|
286
|
+
};
|
|
287
|
+
const result = await launchBrowserWithDependencies(
|
|
288
|
+
{ launch: 'engine', ...options },
|
|
289
|
+
{ loadEngineModule: async () => engine, settleMs: 0 }
|
|
290
|
+
);
|
|
291
|
+
await result.close();
|
|
292
|
+
return { args: calls[0][2].args, defaults: result.defaultRestrictions };
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const cft = await launchWith({}, cftChrome);
|
|
296
|
+
assert.deepEqual(cft.defaults, [
|
|
297
|
+
'no-chrome-for-testing-infobar',
|
|
298
|
+
'mock-keychain',
|
|
299
|
+
]);
|
|
300
|
+
assert.ok(cft.args.includes('--test-type=gpu'));
|
|
301
|
+
assert.ok(cft.args.includes('--use-mock-keychain'));
|
|
302
|
+
|
|
303
|
+
for (const [options, executablePath] of [
|
|
304
|
+
[{}, '/home/u/.cache/ms-playwright/chromium-1100/chrome-linux/chrome'],
|
|
305
|
+
[{ channel: 'chrome' }, cftChrome],
|
|
306
|
+
[{ defaultRestrictions: [] }, cftChrome],
|
|
307
|
+
]) {
|
|
308
|
+
const other = await launchWith(options, executablePath);
|
|
309
|
+
assert.deepEqual(other.defaults, [], JSON.stringify(options));
|
|
310
|
+
assert.equal(other.args.includes('--test-type=gpu'), false);
|
|
311
|
+
assert.equal(other.args.includes('--use-mock-keychain'), false);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("points a Selenium launch at the engines' Chrome for Testing (issue #179)", async () => {
|
|
316
|
+
const launches = [];
|
|
317
|
+
const launchWebDriver = async (options) => {
|
|
318
|
+
launches.push(options);
|
|
319
|
+
return { driver: {}, page: fakePage(), close: async () => {} };
|
|
320
|
+
};
|
|
321
|
+
await launchBrowserWithDependencies(
|
|
322
|
+
{
|
|
323
|
+
engine: 'selenium',
|
|
324
|
+
launch: 'engine',
|
|
325
|
+
restrictions: ['no-sync'],
|
|
326
|
+
defaultRestrictions: ['mock-keychain'],
|
|
327
|
+
},
|
|
328
|
+
{ launchWebDriver, loadEngineModule: cftEngine, settleMs: 0 }
|
|
329
|
+
);
|
|
330
|
+
assert.equal(launches[0].executablePath, cftChrome);
|
|
331
|
+
// launchWebDriver adds the defaults for the browser it resolves.
|
|
332
|
+
assert.deepEqual(launches[0].restrictions, ['no-sync']);
|
|
333
|
+
assert.deepEqual(launches[0].defaultRestrictions, ['mock-keychain']);
|
|
334
|
+
|
|
335
|
+
// An installed browser asked for by channel is left to Selenium.
|
|
336
|
+
await launchBrowserWithDependencies(
|
|
337
|
+
{ engine: 'selenium', launch: 'engine', channel: 'chrome' },
|
|
338
|
+
{ launchWebDriver, loadEngineModule: cftEngine, settleMs: 0 }
|
|
339
|
+
);
|
|
340
|
+
assert.equal(launches[1].executablePath, undefined);
|
|
341
|
+
|
|
342
|
+
// Selenium Manager's Chrome for Testing comes last...
|
|
343
|
+
const seleniumCft = process.execPath;
|
|
344
|
+
await launchBrowserWithDependencies(
|
|
345
|
+
{ engine: 'selenium', launch: 'engine' },
|
|
346
|
+
{
|
|
347
|
+
launchWebDriver,
|
|
348
|
+
loadEngineModule: missingEngine,
|
|
349
|
+
cachedBrowsers: noCachedBrowsers,
|
|
350
|
+
seleniumManagerBrowser: async () => seleniumCft,
|
|
351
|
+
settleMs: 0,
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
assert.equal(launches[2].executablePath, seleniumCft);
|
|
355
|
+
|
|
356
|
+
// ...and without any the launch fails instead of letting the driver
|
|
357
|
+
// pick the installed Google Chrome.
|
|
358
|
+
await assert.rejects(
|
|
359
|
+
launchBrowserWithDependencies(
|
|
360
|
+
{ engine: 'selenium', launch: 'engine' },
|
|
361
|
+
{
|
|
362
|
+
launchWebDriver,
|
|
363
|
+
loadEngineModule: missingEngine,
|
|
364
|
+
cachedBrowsers: noCachedBrowsers,
|
|
365
|
+
seleniumManagerBrowser: async () => undefined,
|
|
366
|
+
settleMs: 0,
|
|
367
|
+
}
|
|
368
|
+
),
|
|
369
|
+
/Chrome for Testing is not installed for selenium/
|
|
370
|
+
);
|
|
371
|
+
assert.equal(launches.length, 3);
|
|
372
|
+
});
|
|
373
|
+
|
|
236
374
|
it("leaves Ctrl+C to the caller with interruption: 'caller' (#174)", async () => {
|
|
237
375
|
for (const [interruption, expected] of [
|
|
238
376
|
[undefined, undefined],
|
|
@@ -279,34 +417,100 @@ describe('resolveLaunchExecutable', () => {
|
|
|
279
417
|
]);
|
|
280
418
|
});
|
|
281
419
|
|
|
282
|
-
it("
|
|
283
|
-
const notInstalled = async () => {
|
|
284
|
-
throw new Error('Chrome is not installed');
|
|
285
|
-
};
|
|
420
|
+
it("starts the engine's Chrome for Testing, never the installed Google Chrome", async () => {
|
|
286
421
|
const executable = await resolveLaunchExecutable(
|
|
287
422
|
{ engine: 'playwright' },
|
|
288
|
-
{
|
|
289
|
-
resolveSystem: notInstalled,
|
|
290
|
-
loadEngineModule: async () => ({
|
|
291
|
-
executablePath: () => process.execPath,
|
|
292
|
-
}),
|
|
293
|
-
}
|
|
423
|
+
{ resolveSystem: noSystemChrome, loadEngineModule: cftEngine }
|
|
294
424
|
);
|
|
295
|
-
assert.equal(executable,
|
|
425
|
+
assert.equal(executable, cftChrome);
|
|
426
|
+
});
|
|
296
427
|
|
|
428
|
+
it('says how to install Chrome for Testing or ask for Google Chrome without it', async () => {
|
|
429
|
+
const missing = async () => ({
|
|
430
|
+
executablePath: () => '/nonexistent/chrome',
|
|
431
|
+
});
|
|
297
432
|
await assert.rejects(
|
|
298
433
|
() =>
|
|
299
434
|
resolveLaunchExecutable(
|
|
300
435
|
{ engine: 'playwright' },
|
|
301
436
|
{
|
|
302
|
-
resolveSystem:
|
|
303
|
-
loadEngineModule:
|
|
304
|
-
|
|
305
|
-
|
|
437
|
+
resolveSystem: noSystemChrome,
|
|
438
|
+
loadEngineModule: missing,
|
|
439
|
+
cachedBrowsers: noCachedBrowsers,
|
|
440
|
+
}
|
|
441
|
+
),
|
|
442
|
+
/Chrome for Testing is not installed for playwright: run `npx playwright install chromium`, or pass channel: 'chrome'/
|
|
443
|
+
);
|
|
444
|
+
await assert.rejects(
|
|
445
|
+
() =>
|
|
446
|
+
resolveLaunchExecutable(
|
|
447
|
+
{ engine: 'selenium' },
|
|
448
|
+
{
|
|
449
|
+
resolveSystem: noSystemChrome,
|
|
450
|
+
loadEngineModule: async () => {
|
|
451
|
+
throw new Error('not installed');
|
|
452
|
+
},
|
|
453
|
+
seleniumManagerBrowser: async () => {
|
|
454
|
+
throw new Error('offline');
|
|
455
|
+
},
|
|
456
|
+
cachedBrowsers: noCachedBrowsers,
|
|
306
457
|
}
|
|
307
458
|
),
|
|
308
|
-
/
|
|
459
|
+
/run `npx playwright install chromium` or `npx puppeteer browsers install chrome` or `selenium-manager --browser chrome --force-browser-download`/
|
|
460
|
+
);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("falls back to Selenium Manager's Chrome for Testing for Selenium only", async () => {
|
|
464
|
+
const asked = [];
|
|
465
|
+
const seleniumManagerBrowser = async () => {
|
|
466
|
+
asked.push('selenium-manager');
|
|
467
|
+
return cftChrome;
|
|
468
|
+
};
|
|
469
|
+
assert.equal(
|
|
470
|
+
await resolveLaunchExecutable(
|
|
471
|
+
{ engine: 'selenium' },
|
|
472
|
+
{
|
|
473
|
+
resolveSystem: noSystemChrome,
|
|
474
|
+
loadEngineModule: missingEngine,
|
|
475
|
+
seleniumManagerBrowser,
|
|
476
|
+
cachedBrowsers: noCachedBrowsers,
|
|
477
|
+
}
|
|
478
|
+
),
|
|
479
|
+
cftChrome
|
|
480
|
+
);
|
|
481
|
+
await assert.rejects(
|
|
482
|
+
resolveLaunchExecutable(
|
|
483
|
+
{ engine: 'playwright' },
|
|
484
|
+
{
|
|
485
|
+
resolveSystem: noSystemChrome,
|
|
486
|
+
loadEngineModule: missingEngine,
|
|
487
|
+
seleniumManagerBrowser,
|
|
488
|
+
cachedBrowsers: noCachedBrowsers,
|
|
489
|
+
}
|
|
490
|
+
),
|
|
491
|
+
/Chrome for Testing is not installed for playwright/
|
|
492
|
+
);
|
|
493
|
+
assert.deepEqual(asked, ['selenium-manager']);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("falls back to a cached build when the engine's own is not installed", async () => {
|
|
497
|
+
const asked = [];
|
|
498
|
+
const resolved = await resolveLaunchExecutable(
|
|
499
|
+
{ engine: 'selenium' },
|
|
500
|
+
{
|
|
501
|
+
resolveSystem: noSystemChrome,
|
|
502
|
+
loadEngineModule: missingEngine,
|
|
503
|
+
cachedBrowsers: (engine) => {
|
|
504
|
+
asked.push(engine);
|
|
505
|
+
return ['/nonexistent/chrome-linux64/chrome', cftChrome];
|
|
506
|
+
},
|
|
507
|
+
seleniumManagerBrowser: async () => {
|
|
508
|
+
throw new Error('Selenium Manager is asked only without a cache');
|
|
509
|
+
},
|
|
510
|
+
}
|
|
309
511
|
);
|
|
512
|
+
assert.equal(resolved, cftChrome);
|
|
513
|
+
assert.deepEqual(asked, ['selenium']);
|
|
310
514
|
});
|
|
311
515
|
});
|
|
312
516
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createSourceKeyResolver,
|
|
7
7
|
localStatePathForProfile,
|
|
8
8
|
resolveTargetKey,
|
|
9
|
+
targetKeystoreFor,
|
|
9
10
|
} from '../../../../src/browser/migration/os-crypt-keys.js';
|
|
10
11
|
import { deriveChromiumCookieKey } from '../../../../src/browser/browser-cookie-crypto.js';
|
|
11
12
|
|
|
@@ -77,6 +78,45 @@ describe('resolveTargetKey', () => {
|
|
|
77
78
|
assert.equal(result.prefix, 'v11');
|
|
78
79
|
});
|
|
79
80
|
|
|
81
|
+
it('tags macOS keys v10, the only tag macOS Chromium reads', async () => {
|
|
82
|
+
const result = await resolveTargetKey({
|
|
83
|
+
browser: 'chromium',
|
|
84
|
+
platform: 'darwin',
|
|
85
|
+
readSafeStoragePassword: async () => 'target-pass',
|
|
86
|
+
});
|
|
87
|
+
assert.deepEqual(
|
|
88
|
+
result.key,
|
|
89
|
+
deriveChromiumCookieKey('target-pass', 'darwin')
|
|
90
|
+
);
|
|
91
|
+
assert.equal(result.prefix, 'v10');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("uses Chromium's fixed mock passwords for a mock keystore (issue #179)", async () => {
|
|
95
|
+
const unreadable = async () => {
|
|
96
|
+
throw new Error('the OS keystore must not be asked');
|
|
97
|
+
};
|
|
98
|
+
const mac = await resolveTargetKey({
|
|
99
|
+
browser: 'chromium',
|
|
100
|
+
platform: 'darwin',
|
|
101
|
+
keystore: 'mock',
|
|
102
|
+
readSafeStoragePassword: unreadable,
|
|
103
|
+
});
|
|
104
|
+
assert.deepEqual(mac, {
|
|
105
|
+
key: deriveChromiumCookieKey('mock_password', 'darwin'),
|
|
106
|
+
prefix: 'v10',
|
|
107
|
+
});
|
|
108
|
+
const linux = await resolveTargetKey({
|
|
109
|
+
browser: 'chrome',
|
|
110
|
+
platform: 'linux',
|
|
111
|
+
keystore: 'mock',
|
|
112
|
+
readSafeStoragePassword: unreadable,
|
|
113
|
+
});
|
|
114
|
+
assert.deepEqual(linux, {
|
|
115
|
+
key: deriveChromiumCookieKey('peanuts', 'linux'),
|
|
116
|
+
prefix: 'v10',
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
80
120
|
it('refuses to derive a Windows key', async () => {
|
|
81
121
|
await assert.rejects(
|
|
82
122
|
resolveTargetKey({ browser: 'chrome', platform: 'win32' }),
|
|
@@ -85,6 +125,23 @@ describe('resolveTargetKey', () => {
|
|
|
85
125
|
});
|
|
86
126
|
});
|
|
87
127
|
|
|
128
|
+
describe('targetKeystoreFor (issue #179)', () => {
|
|
129
|
+
it('reports the switches that bypass the OS keystore', () => {
|
|
130
|
+
assert.equal(targetKeystoreFor(['--use-mock-keychain'], 'darwin'), 'mock');
|
|
131
|
+
assert.equal(
|
|
132
|
+
targetKeystoreFor(['--password-store=basic'], 'linux'),
|
|
133
|
+
'mock'
|
|
134
|
+
);
|
|
135
|
+
assert.equal(targetKeystoreFor(['--use-mock-keychain'], 'linux'), 'os');
|
|
136
|
+
assert.equal(
|
|
137
|
+
targetKeystoreFor(['--password-store=gnome-libsecret'], 'linux'),
|
|
138
|
+
'os'
|
|
139
|
+
);
|
|
140
|
+
assert.equal(targetKeystoreFor([], 'darwin'), 'os');
|
|
141
|
+
assert.equal(targetKeystoreFor(['--use-mock-keychain'], 'win32'), 'os');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
88
145
|
describe('localStatePathForProfile', () => {
|
|
89
146
|
it('points at Local State next to the profile directory', () => {
|
|
90
147
|
assert.equal(
|
|
@@ -24,6 +24,24 @@ const fixture = (name) =>
|
|
|
24
24
|
)
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
+
function migratedPasswords(profile) {
|
|
28
|
+
const db = new Database(path.join(profile, 'Login Data'));
|
|
29
|
+
try {
|
|
30
|
+
return db.prepare('SELECT origin_url,password_value FROM logins').all();
|
|
31
|
+
} finally {
|
|
32
|
+
db.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const decryptPassword = (row, key, platform) =>
|
|
37
|
+
decryptChromiumCookie({
|
|
38
|
+
encryptedValue: row.password_value,
|
|
39
|
+
key,
|
|
40
|
+
platform,
|
|
41
|
+
databaseVersion: 0,
|
|
42
|
+
host: 'github.com',
|
|
43
|
+
});
|
|
44
|
+
|
|
27
45
|
describe('Safari data sources', () => {
|
|
28
46
|
for (const format of ['binary', 'xml']) {
|
|
29
47
|
it(`preserves hierarchy and reading list from ${format} plist`, async () => {
|
|
@@ -103,25 +121,31 @@ describe('Safari data sources', () => {
|
|
|
103
121
|
({ reason }) => reason === 'safari-reading-list-translated'
|
|
104
122
|
)
|
|
105
123
|
);
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
const rows = migratedPasswords(target);
|
|
125
|
+
assert.equal(rows.length, 1);
|
|
126
|
+
assert.equal(decryptPassword(rows[0], key, 'linux'), 'p"a\nss');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('encrypts migrated passwords with the mock keychain key of a Chrome for Testing launch (issue #179)', async () => {
|
|
130
|
+
const source = await temporary();
|
|
131
|
+
const target = await temporary();
|
|
132
|
+
// No keys are injected and the OS keystore is never asked: Chromium's
|
|
133
|
+
// --use-mock-keychain derives its key from the fixed "mock_password".
|
|
134
|
+
const report = await migrateProfile({
|
|
135
|
+
from: { browser: 'safari', userDataDir: source },
|
|
136
|
+
to: target,
|
|
137
|
+
include: ['passwords'],
|
|
138
|
+
domains: ['github.com'],
|
|
139
|
+
passwordCsv: fixture('Passwords.csv'),
|
|
140
|
+
platform: 'darwin',
|
|
141
|
+
targetBrowser: 'chromium',
|
|
142
|
+
targetKeystore: 'mock',
|
|
143
|
+
});
|
|
144
|
+
assert.equal(report.migrated.passwords, 1);
|
|
145
|
+
const [row] = migratedPasswords(target);
|
|
146
|
+
assert.equal(row.password_value.subarray(0, 3).toString(), 'v10');
|
|
147
|
+
const key = deriveChromiumCookieKey('mock_password', 'darwin');
|
|
148
|
+
assert.equal(decryptPassword(row, key, 'darwin'), 'p"a\nss');
|
|
125
149
|
});
|
|
126
150
|
|
|
127
151
|
it('names the executing app and retry link for non-cookie protection failures', async () => {
|
|
@@ -264,6 +264,25 @@ describe('measureParity', () => {
|
|
|
264
264
|
);
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
it("accepts Chrome for Testing's default switches as requested (issue #179)", async () => {
|
|
268
|
+
const { session, dependencies } = fakes({
|
|
269
|
+
candidateCommandLine: `${CHROME} --user-data-dir=/tmp/c --remote-debugging-port=2 --test-type=gpu --use-mock-keychain about:blank`,
|
|
270
|
+
});
|
|
271
|
+
session.args = [
|
|
272
|
+
'--user-data-dir=/tmp/c',
|
|
273
|
+
'--remote-debugging-port=2',
|
|
274
|
+
'--test-type=gpu',
|
|
275
|
+
'--use-mock-keychain',
|
|
276
|
+
];
|
|
277
|
+
session.defaultRestrictions = [
|
|
278
|
+
'no-chrome-for-testing-infobar',
|
|
279
|
+
'mock-keychain',
|
|
280
|
+
];
|
|
281
|
+
const report = await measureParity({}, dependencies);
|
|
282
|
+
assert.equal(report.ok, true);
|
|
283
|
+
assert.deepEqual(report.unlisted, []);
|
|
284
|
+
});
|
|
285
|
+
|
|
267
286
|
it('measures a provided session and leaves it open', async () => {
|
|
268
287
|
const { calls, session, dependencies } = fakes({
|
|
269
288
|
candidateCommandLine: `${CHROME} --user-data-dir=/tmp/c --remote-debugging-port=2 about:blank`,
|
|
@@ -282,6 +282,28 @@ describe('launchAndConnectRealBrowser', () => {
|
|
|
282
282
|
await access(path.join(temporaryDirectory, 'First Run'));
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
+
it('gives Selenium the started browser so its driver matches it (issue #179)', async () => {
|
|
286
|
+
temporaryDirectory = await makeDedicatedProfile();
|
|
287
|
+
const cft =
|
|
288
|
+
'/home/u/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome';
|
|
289
|
+
let connectOptions;
|
|
290
|
+
await launchAndConnectRealBrowserWithDependencies(
|
|
291
|
+
{ engine: 'selenium', userDataDir: temporaryDirectory },
|
|
292
|
+
{
|
|
293
|
+
resolveExecutable: async () => cft,
|
|
294
|
+
reservePort: async () => 9444,
|
|
295
|
+
spawnBrowser: () => fakeProcess([]),
|
|
296
|
+
waitForEndpoint: async () => 'http://127.0.0.1:9444',
|
|
297
|
+
connect: async (options) => {
|
|
298
|
+
connectOptions = options;
|
|
299
|
+
return { browser: null, page: null };
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
assert.equal(connectOptions.engine, 'selenium');
|
|
304
|
+
assert.equal(connectOptions.executablePath, cft);
|
|
305
|
+
});
|
|
306
|
+
|
|
285
307
|
it('migrates a profile before launch and seeds the migrated cookies', async () => {
|
|
286
308
|
temporaryDirectory = await makeDedicatedProfile();
|
|
287
309
|
const calls = [];
|
|
@@ -516,6 +538,109 @@ describe('launchAndConnectRealBrowser', () => {
|
|
|
516
538
|
await result.close();
|
|
517
539
|
});
|
|
518
540
|
|
|
541
|
+
it('adds the Chrome for Testing defaults to its command line only (issue #179)', async () => {
|
|
542
|
+
const cft =
|
|
543
|
+
'/home/u/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome';
|
|
544
|
+
const launch = async (executablePath, options = {}) => {
|
|
545
|
+
let spawnedArgs;
|
|
546
|
+
const result = await launchAndConnectRealBrowserWithDependencies(
|
|
547
|
+
{ closeTimeout: 50, ...options },
|
|
548
|
+
{
|
|
549
|
+
resolveExecutable: async () => executablePath,
|
|
550
|
+
reservePort: async () => 40006,
|
|
551
|
+
spawnBrowser: (_executablePath, args) => {
|
|
552
|
+
spawnedArgs = args;
|
|
553
|
+
return fakeProcess([]);
|
|
554
|
+
},
|
|
555
|
+
waitForEndpoint: async () => 'http://127.0.0.1:40006',
|
|
556
|
+
connect: async () => ({ browser: null, page: null }),
|
|
557
|
+
}
|
|
558
|
+
);
|
|
559
|
+
await result.close();
|
|
560
|
+
return { args: spawnedArgs, defaults: result.defaultRestrictions };
|
|
561
|
+
};
|
|
562
|
+
const defaultSwitches = (args) =>
|
|
563
|
+
args.filter((arg) =>
|
|
564
|
+
['--test-type=gpu', '--use-mock-keychain'].includes(arg)
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const testing = await launch(cft);
|
|
568
|
+
assert.deepEqual(testing.defaults, [
|
|
569
|
+
'no-chrome-for-testing-infobar',
|
|
570
|
+
'mock-keychain',
|
|
571
|
+
]);
|
|
572
|
+
assert.deepEqual(defaultSwitches(testing.args), [
|
|
573
|
+
'--test-type=gpu',
|
|
574
|
+
'--use-mock-keychain',
|
|
575
|
+
]);
|
|
576
|
+
|
|
577
|
+
// Google Chrome keeps the clean command line of issue #103.
|
|
578
|
+
const chrome = await launch('/opt/google/chrome');
|
|
579
|
+
assert.deepEqual(chrome.defaults, []);
|
|
580
|
+
assert.deepEqual(defaultSwitches(chrome.args), []);
|
|
581
|
+
|
|
582
|
+
// defaultRestrictions replaces the defaults; [] turns them off.
|
|
583
|
+
const off = await launch(cft, { defaultRestrictions: [] });
|
|
584
|
+
assert.deepEqual(defaultSwitches(off.args), []);
|
|
585
|
+
const keychainOnly = await launch(cft, {
|
|
586
|
+
defaultRestrictions: ['mock-keychain'],
|
|
587
|
+
});
|
|
588
|
+
assert.deepEqual(defaultSwitches(keychainOnly.args), [
|
|
589
|
+
'--use-mock-keychain',
|
|
590
|
+
]);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('migrates passwords with the key of the browser that starts (issue #179)', async () => {
|
|
594
|
+
const cft =
|
|
595
|
+
'/Users/u/Library/Caches/ms-playwright/chromium-1248/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing';
|
|
596
|
+
const migrateTarget = async (executablePath, options = {}) => {
|
|
597
|
+
let migrateOptions;
|
|
598
|
+
const result = await launchAndConnectRealBrowserWithDependencies(
|
|
599
|
+
{
|
|
600
|
+
closeTimeout: 50,
|
|
601
|
+
migrateFrom: { browser: 'chrome', include: ['passwords'] },
|
|
602
|
+
...options,
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
platform: 'darwin',
|
|
606
|
+
resolveExecutable: async () => executablePath,
|
|
607
|
+
reservePort: async () => 40007,
|
|
608
|
+
spawnBrowser: () => fakeProcess([]),
|
|
609
|
+
waitForEndpoint: async () => 'http://127.0.0.1:40007',
|
|
610
|
+
connect: async () => ({ browser: null, page: null }),
|
|
611
|
+
migrateProfile: async (migration) => {
|
|
612
|
+
migrateOptions = migration;
|
|
613
|
+
return { migrated: {}, skipped: [], warnings: [], cookies: [] };
|
|
614
|
+
},
|
|
615
|
+
}
|
|
616
|
+
);
|
|
617
|
+
await result.close();
|
|
618
|
+
const { targetBrowser, targetKeystore } = migrateOptions;
|
|
619
|
+
return { targetBrowser, targetKeystore };
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// Chrome for Testing keeps "Chromium Safe Storage", and its default
|
|
623
|
+
// mock keychain encrypts with Chromium's fixed mock password.
|
|
624
|
+
assert.deepEqual(await migrateTarget(cft), {
|
|
625
|
+
targetBrowser: 'chromium',
|
|
626
|
+
targetKeystore: 'mock',
|
|
627
|
+
});
|
|
628
|
+
assert.deepEqual(await migrateTarget(cft, { defaultRestrictions: [] }), {
|
|
629
|
+
targetBrowser: 'chromium',
|
|
630
|
+
targetKeystore: 'os',
|
|
631
|
+
});
|
|
632
|
+
assert.deepEqual(await migrateTarget('/opt/google/chrome'), {
|
|
633
|
+
targetBrowser: 'chrome',
|
|
634
|
+
targetKeystore: 'os',
|
|
635
|
+
});
|
|
636
|
+
assert.deepEqual(
|
|
637
|
+
await migrateTarget('/opt/google/chrome', {
|
|
638
|
+
restrictions: ['mock-keychain'],
|
|
639
|
+
}),
|
|
640
|
+
{ targetBrowser: 'chrome', targetKeystore: 'mock' }
|
|
641
|
+
);
|
|
642
|
+
});
|
|
643
|
+
|
|
519
644
|
it('terminates the spawned browser when connection fails', async () => {
|
|
520
645
|
temporaryDirectory = await makeDedicatedProfile();
|
|
521
646
|
let killed = false;
|