browser-commander 0.23.0 → 0.25.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.
Files changed (77) hide show
  1. package/.jscpd-baseline.json +48 -30
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +19 -7
  4. package/eslint.config.js +1 -1
  5. package/examples/react-test-app/package-lock.json +890 -0
  6. package/examples/react-test-app/package.json +4 -4
  7. package/package.json +20 -17
  8. package/src/browser/attach/snapshot.js +4 -28
  9. package/src/browser/browser-cookie-credentials.js +15 -8
  10. package/src/browser/browser-cookies.js +36 -30
  11. package/src/browser/browser-profile-files.js +31 -0
  12. package/src/browser/browser-profiles.js +11 -13
  13. package/src/browser/browser-sources.js +39 -9
  14. package/src/browser/browser-sources.json +600 -23
  15. package/src/browser/connector.js +46 -4
  16. package/src/browser/launcher.js +26 -1
  17. package/src/browser/migration/capabilities.json +117 -0
  18. package/src/browser/migration/chromium-history.sql +22 -0
  19. package/src/browser/migration/chromium-writers.js +85 -0
  20. package/src/browser/migration/cookies.js +4 -1
  21. package/src/browser/migration/data-classes.js +68 -0
  22. package/src/browser/migration/domains.js +32 -0
  23. package/src/browser/migration/firefox-history.js +106 -0
  24. package/src/browser/migration/firefox.js +39 -25
  25. package/src/browser/migration/history.js +161 -2
  26. package/src/browser/migration/index.js +229 -83
  27. package/src/browser/migration/os-crypt-keys.js +1 -5
  28. package/src/browser/migration/password-metadata.js +95 -0
  29. package/src/browser/migration/passwords.js +78 -41
  30. package/src/browser/migration/safari-import.js +100 -0
  31. package/src/browser/migration/safari.js +110 -0
  32. package/src/browser/migration/sqlite-snapshot.js +31 -65
  33. package/src/browser/migration/validation.js +48 -0
  34. package/src/browser/real-browser.js +21 -3
  35. package/src/browser/safari-access.js +45 -0
  36. package/src/browser/safari-cookies.js +39 -36
  37. package/src/browser/safari-profiles.js +83 -0
  38. package/src/browser/storage-state.js +27 -0
  39. package/src/browser/system-browser.js +31 -143
  40. package/src/browser/webdriver.js +18 -2
  41. package/src/cli/args.js +7 -0
  42. package/src/cli/commands.js +12 -6
  43. package/src/cli/handle-methods.js +19 -2
  44. package/src/cli/high-level-methods.js +22 -3
  45. package/src/cli/modules.js +5 -1
  46. package/src/cli/sessions.js +14 -2
  47. package/src/tests/index.js +1 -0
  48. package/tests/e2e/engine-matrix.e2e.test.js +107 -0
  49. package/tests/e2e/playwright.e2e.test.js +11 -17
  50. package/tests/e2e/puppeteer.e2e.test.js +13 -17
  51. package/tests/unit/browser/attach/snapshot.test.js +30 -0
  52. package/tests/unit/browser/browser-cookies.test.js +35 -0
  53. package/tests/unit/browser/catalogue-launch.test.js +93 -0
  54. package/tests/unit/browser/cookie-sources.test.js +92 -17
  55. package/tests/unit/browser/credential-catalogue.test.js +50 -0
  56. package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
  57. package/tests/unit/browser/migration/data-classes.test.js +89 -0
  58. package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
  59. package/tests/unit/browser/migration/firefox-history.test.js +130 -0
  60. package/tests/unit/browser/migration/history-metadata.test.js +74 -0
  61. package/tests/unit/browser/migration/index.test.js +16 -1
  62. package/tests/unit/browser/migration/opera-keys.test.js +22 -0
  63. package/tests/unit/browser/migration/safari-data.test.js +143 -0
  64. package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
  65. package/tests/unit/browser/migration/validation.test.js +63 -0
  66. package/tests/unit/browser/migration/yandex.test.js +84 -0
  67. package/tests/unit/browser/real-browser.test.js +19 -0
  68. package/tests/unit/browser/safari-cookies.test.js +26 -1
  69. package/tests/unit/browser/safari-profiles.test.js +111 -0
  70. package/tests/unit/cli/dispatcher.test.js +6 -2
  71. package/tests/unit/cli/main.test.js +5 -0
  72. package/tests/unit/cli/selenium-parity.test.js +186 -0
  73. package/tests/unit/scripts/browser-support.test.js +14 -7
  74. package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
  75. package/tests/unit/scripts/migration-support.test.js +31 -0
  76. package/tests/unit/tests/index.test.js +2 -1
  77. package/tests/unit/traces/links.test.js +2 -2
@@ -0,0 +1,143 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFile, copyFile, writeFile } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { describe, it } from 'node:test';
5
+ import {
6
+ readSafariBookmarks,
7
+ readSafariHistory,
8
+ readSafariPasswords,
9
+ } from '../../../../src/browser/migration/safari.js';
10
+ import { migrateProfile } from '../../../../src/browser/migration/index.js';
11
+ import { useTempDirectories } from '../../../helpers/temp-directory.js';
12
+ import Database from 'better-sqlite3';
13
+ import path from 'node:path';
14
+ import {
15
+ deriveChromiumCookieKey,
16
+ decryptChromiumCookie,
17
+ } from '../../../../src/browser/browser-cookie-crypto.js';
18
+
19
+ const fixture = (name) =>
20
+ fileURLToPath(
21
+ new URL(
22
+ `../../../../../tests/fixtures/safari-data/${name}`,
23
+ import.meta.url
24
+ )
25
+ );
26
+
27
+ describe('Safari data sources', () => {
28
+ for (const format of ['binary', 'xml']) {
29
+ it(`preserves hierarchy and reading list from ${format} plist`, async () => {
30
+ const filename = fixture(`Bookmarks-${format}.plist`);
31
+ const before = await readFile(filename);
32
+ const tree = await readSafariBookmarks(filename);
33
+ assert.equal(tree[0].name, 'Work');
34
+ assert.equal(tree[0].children[0].name, 'Foundation ☃');
35
+ assert.equal(tree[1].children[0].readingList, true);
36
+ assert.deepEqual(await readFile(filename), before);
37
+ });
38
+ }
39
+
40
+ it('preserves all matching visits and converts the Cocoa epoch', async () => {
41
+ const filename = fixture('History.db');
42
+ const before = await readFile(filename);
43
+ const entries = await readSafariHistory(filename, ['github.com']);
44
+ assert.equal(entries.length, 2);
45
+ assert.equal(entries[0].time, 1778307200250000);
46
+ assert.deepEqual(await readFile(filename), before);
47
+ });
48
+
49
+ it('reads explicit BOM UTF-8 CSV with quoted newlines and filters domains', async () => {
50
+ const entries = await readSafariPasswords(fixture('Passwords.csv'), [
51
+ 'github.com',
52
+ ]);
53
+ assert.deepEqual(entries, [
54
+ {
55
+ origin: 'https://github.com/login',
56
+ username: 'a,b',
57
+ password: 'p"a\nss',
58
+ },
59
+ ]);
60
+ });
61
+
62
+ const temporary = useTempDirectories('bc-safari-data-');
63
+ it('rejects password CSV records with missing or extra fields without changing the source', async () => {
64
+ for (const record of [
65
+ 'https://github.com,a\n',
66
+ 'https://github.com,a,b,c\n',
67
+ ]) {
68
+ const filename = path.join(await temporary(), 'Passwords.csv');
69
+ await writeFile(filename, `URL,Username,Password\n${record}`);
70
+ const before = await readFile(filename);
71
+ await assert.rejects(
72
+ readSafariPasswords(filename),
73
+ /Invalid Record Length/u
74
+ );
75
+ assert.deepEqual(await readFile(filename), before);
76
+ }
77
+ });
78
+
79
+ it('migrates custom Safari sources into native Chromium stores with target encryption', async () => {
80
+ const source = await temporary();
81
+ const target = await temporary();
82
+ await copyFile(
83
+ fixture('Bookmarks-binary.plist'),
84
+ path.join(source, 'Bookmarks.plist')
85
+ );
86
+ await copyFile(fixture('History.db'), path.join(source, 'History.db'));
87
+ const key = deriveChromiumCookieKey('dedicated-target', 'linux');
88
+ const report = await migrateProfile({
89
+ from: { browser: 'safari', userDataDir: source },
90
+ to: target,
91
+ include: ['bookmarks', 'history', 'passwords'],
92
+ domains: ['github.com'],
93
+ passwordCsv: fixture('Passwords.csv'),
94
+ platform: 'linux',
95
+ keys: { targetKey: key, targetPrefix: 'v11' },
96
+ });
97
+ assert.equal(report.migrated.bookmarks, 2);
98
+ assert.equal(report.migrated.history, 2);
99
+ assert.equal(report.migrated.passwords, 1);
100
+ assert.deepEqual(report.skipped, []);
101
+ assert.ok(
102
+ report.warnings.some(
103
+ ({ reason }) => reason === 'safari-reading-list-translated'
104
+ )
105
+ );
106
+ const db = new Database(path.join(target, 'Login Data'));
107
+ try {
108
+ const rows = db
109
+ .prepare('SELECT origin_url,password_value FROM logins')
110
+ .all();
111
+ assert.equal(rows.length, 1);
112
+ assert.equal(
113
+ decryptChromiumCookie({
114
+ encryptedValue: rows[0].password_value,
115
+ key,
116
+ platform: 'linux',
117
+ databaseVersion: 0,
118
+ host: 'github.com',
119
+ }),
120
+ 'p"a\nss'
121
+ );
122
+ } finally {
123
+ db.close();
124
+ }
125
+ });
126
+
127
+ it('names the executing app and retry link for non-cookie protection failures', async () => {
128
+ const { withSafariAccess } =
129
+ await import('../../../../src/browser/safari-access.js');
130
+ for (const code of ['EPERM', 'EACCES']) {
131
+ await assert.rejects(
132
+ withSafariAccess(
133
+ '/Safari/History.db',
134
+ () => {
135
+ throw Object.assign(new Error('protected'), { code });
136
+ },
137
+ { TERM_PROGRAM: 'FixtureTerminal' }
138
+ ),
139
+ /Full Disk Access to FixtureTerminal.*Privacy_AllFiles/u
140
+ );
141
+ }
142
+ });
143
+ });
@@ -1,5 +1,6 @@
1
1
  import assert from 'node:assert';
2
- import { stat } from 'node:fs/promises';
2
+ import { stat, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
3
4
  import { describe, it } from 'node:test';
4
5
 
5
6
  import BetterSqlite3 from 'better-sqlite3';
@@ -58,6 +59,60 @@ describe('withDatabaseSnapshot', () => {
58
59
  /does not exist/
59
60
  );
60
61
  });
62
+
63
+ it('does not substitute an unverified file copy when backup fails', async () => {
64
+ const sourcePath = path.join(await makeTempDir(), 'History');
65
+ await writeFile(sourcePath, 'not a SQLite database');
66
+ let readerCalled = false;
67
+ await assert.rejects(
68
+ withDatabaseSnapshot({
69
+ sourcePath,
70
+ read: () => {
71
+ readerCalled = true;
72
+ },
73
+ }),
74
+ /Consistent SQLite snapshot unavailable.*close the source browser/iu
75
+ );
76
+ assert.equal(readerCalled, false);
77
+ });
78
+
79
+ it(
80
+ 'reports guidance for an exclusive lock without invoking the reader',
81
+ { timeout: 5000 },
82
+ async () => {
83
+ const sourcePath = await makeDatabase(1);
84
+ const writer = new BetterSqlite3(sourcePath);
85
+ writer.exec('BEGIN EXCLUSIVE');
86
+ try {
87
+ await assert.rejects(
88
+ withDatabaseSnapshot({
89
+ sourcePath,
90
+ read: () => assert.fail('locked snapshot reader invoked'),
91
+ }),
92
+ /Consistent SQLite snapshot unavailable.*close the source browser/iu
93
+ );
94
+ } finally {
95
+ writer.exec('ROLLBACK');
96
+ writer.close();
97
+ }
98
+ }
99
+ );
100
+
101
+ it('includes committed WAL records while the source connection stays open', async () => {
102
+ const sourcePath = await makeDatabase(1);
103
+ const writer = new BetterSqlite3(sourcePath);
104
+ writer.pragma('journal_mode = WAL');
105
+ writer.prepare('UPDATE urls SET title = ?').run('committed WAL title');
106
+ try {
107
+ const title = await readDatabaseSnapshot({
108
+ sourcePath,
109
+ read: (db) => db.prepare('SELECT title FROM urls').get().title,
110
+ });
111
+ assert.equal(title, 'committed WAL title');
112
+ } finally {
113
+ writer.close();
114
+ }
115
+ });
61
116
  });
62
117
 
63
118
  describe('readDatabaseSnapshot', () => {
@@ -0,0 +1,63 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readdir } from 'node:fs/promises';
3
+ import { describe, it } from 'node:test';
4
+ import { migrateProfile } from '../../../../src/browser/migration/index.js';
5
+ import { useTempDirectories } from '../../../helpers/temp-directory.js';
6
+
7
+ const temporary = useTempDirectories('bc-validation-');
8
+
9
+ describe('migration validation before target mutation', () => {
10
+ it('rejects unknown classes and invalid domain filters', async () => {
11
+ for (const options of [
12
+ { include: ['invented'] },
13
+ { domains: ['https://example.com'] },
14
+ ]) {
15
+ const target = await temporary();
16
+ await assert.rejects(
17
+ migrateProfile({ from: { browser: 'chrome' }, to: target, ...options })
18
+ );
19
+ assert.deepEqual(await readdir(target), []);
20
+ }
21
+ });
22
+
23
+ it('rejects unsupported target layouts before creating Chromium files', async () => {
24
+ const target = await temporary();
25
+ await assert.rejects(
26
+ migrateProfile({
27
+ from: { browser: 'chrome' },
28
+ to: target,
29
+ include: ['bookmarks'],
30
+ targetBrowser: 'firefox',
31
+ }),
32
+ /target/u
33
+ );
34
+ assert.deepEqual(await readdir(target), []);
35
+ });
36
+
37
+ it('rejects detection-only sources before target mutation', async () => {
38
+ const source = await temporary();
39
+ const target = await temporary();
40
+ await assert.rejects(
41
+ migrateProfile({
42
+ from: { browser: 'duckduckgo', userDataDir: source },
43
+ to: target,
44
+ include: ['history'],
45
+ }),
46
+ /detection/u
47
+ );
48
+ assert.deepEqual(await readdir(target), []);
49
+ });
50
+
51
+ it('rejects a target that overlaps the source profile', async () => {
52
+ const source = await temporary();
53
+ await assert.rejects(
54
+ migrateProfile({
55
+ from: { browser: 'opera', userDataDir: source },
56
+ to: source,
57
+ include: ['bookmarks'],
58
+ }),
59
+ /overlap/u
60
+ );
61
+ assert.deepEqual(await readdir(source), []);
62
+ });
63
+ });
@@ -0,0 +1,84 @@
1
+ import assert from 'node:assert/strict';
2
+ import { it } from 'node:test';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { randomBytes } from 'node:crypto';
5
+ import path from 'node:path';
6
+ import Database from 'better-sqlite3';
7
+ import { useTempDirectories } from '../../../helpers/temp-directory.js';
8
+ import { migrateProfile } from '../../../../src/browser/migration/index.js';
9
+ import { encryptChromiumValue } from '../../../../src/browser/migration/chromium-crypto.js';
10
+ import { readMigratedLogins } from '../../../helpers/migration-fixtures.js';
11
+
12
+ const temporary = useTempDirectories('bc-yandex-');
13
+ function migrateYandexPasswords(source, to, options = {}) {
14
+ return migrateProfile({
15
+ from: { browser: 'yandex', userDataDir: source },
16
+ to,
17
+ include: ['passwords'],
18
+ ...options,
19
+ });
20
+ }
21
+
22
+ it('identifies Ya Passman Data before prompting for target credentials', async () => {
23
+ const source = await temporary();
24
+ const target = await temporary();
25
+ await mkdir(path.join(source, 'Default'));
26
+ const db = new Database(path.join(source, 'Default', 'Ya Passman Data'));
27
+ db.exec(
28
+ "CREATE TABLE meta(key TEXT,value BLOB); INSERT INTO meta VALUES('local_encryptor_data',x'01')"
29
+ );
30
+ db.close();
31
+ const report = await migrateYandexPasswords(source, target);
32
+ assert.equal(report.migrated.passwords, 0);
33
+ assert.equal(
34
+ report.skipped[0].reason,
35
+ 'yandex-passman-encryption-unsupported'
36
+ );
37
+ assert.match(
38
+ report.skipped[0].detail,
39
+ /local_encryptor_data.*master password/u
40
+ );
41
+ });
42
+
43
+ it('retains supported Login Data beside unsupported Ya Passman Data', async () => {
44
+ const source = await temporary();
45
+ const target = await temporary();
46
+ const profile = path.join(source, 'Default');
47
+ await mkdir(profile);
48
+ const passman = path.join(profile, 'Ya Passman Data');
49
+ await writeFile(passman, 'unsupported-store');
50
+ const sourceKey = randomBytes(16);
51
+ const targetKey = randomBytes(16);
52
+ const loginData = path.join(profile, 'Login Data');
53
+ const db = new Database(loginData);
54
+ db.exec(
55
+ 'CREATE TABLE logins(origin_url TEXT,username_value TEXT,password_value BLOB)'
56
+ );
57
+ db.prepare('INSERT INTO logins VALUES(?,?,?)').run(
58
+ 'https://example.com',
59
+ 'alice',
60
+ encryptChromiumValue({
61
+ plaintext: 'retained-password',
62
+ key: sourceKey,
63
+ platform: 'linux',
64
+ prefix: 'v11',
65
+ })
66
+ );
67
+ db.close();
68
+ const original = await readFile(loginData);
69
+ const report = await migrateYandexPasswords(source, target, {
70
+ platform: 'linux',
71
+ keys: { targetKey, resolveSourceKey: () => sourceKey },
72
+ });
73
+ assert.equal(report.migrated.passwords, 1);
74
+ assert.equal(
75
+ report.skipped[0].reason,
76
+ 'yandex-passman-encryption-unsupported'
77
+ );
78
+ assert.equal(
79
+ readMigratedLogins(target, targetKey)[0].password,
80
+ 'retained-password'
81
+ );
82
+ assert.deepEqual(await readFile(loginData), original);
83
+ assert.equal(await readFile(passman, 'utf8'), 'unsupported-store');
84
+ });
@@ -203,6 +203,21 @@ describe('launchAndConnectRealBrowser', () => {
203
203
  }
204
204
  });
205
205
 
206
+ it('rejects non-CDP catalogue browsers before resolving or spawning them', async () => {
207
+ for (const channel of ['firefox', 'librewolf', 'safari', 'duckduckgo']) {
208
+ await assert.rejects(
209
+ launchAndConnectRealBrowserWithDependencies(
210
+ { channel },
211
+ {
212
+ resolveExecutable: () =>
213
+ assert.fail('non-CDP executable resolution reached'),
214
+ }
215
+ ),
216
+ /does not support CDP/u
217
+ );
218
+ }
219
+ });
220
+
206
221
  it('spawns, waits, connects, and returns process metadata', async () => {
207
222
  temporaryDirectory = await makeDedicatedProfile();
208
223
  const calls = [];
@@ -285,6 +300,8 @@ describe('launchAndConnectRealBrowser', () => {
285
300
  profile: 'Default',
286
301
  include: ['cookies', 'bookmarks'],
287
302
  domains: ['google.com'],
303
+ passwordCsv: '/tmp/safari-export.csv',
304
+ includePaymentCards: true,
288
305
  },
289
306
  },
290
307
  {
@@ -330,6 +347,8 @@ describe('launchAndConnectRealBrowser', () => {
330
347
  assert.equal(migrateOptions.targetBrowser, 'chrome');
331
348
  assert.deepEqual(migrateOptions.include, ['cookies', 'bookmarks']);
332
349
  assert.deepEqual(migrateOptions.domains, ['google.com']);
350
+ assert.equal(migrateOptions.passwordCsv, '/tmp/safari-export.csv');
351
+ assert.equal(migrateOptions.includePaymentCards, true);
333
352
  assert.deepEqual(migrateOptions.from, {
334
353
  browser: 'chrome',
335
354
  profile: 'Default',
@@ -49,6 +49,22 @@ async function installed(browser = 'safari', legacy = false) {
49
49
  }
50
50
 
51
51
  describe('Safari cookies', { timeout: 10000 }, () => {
52
+ it('uses whole domains for discovery while retaining reader substring filters', async () => {
53
+ const options = await installed();
54
+ const sources = await listCookieSources({
55
+ ...options,
56
+ domains: ['hub.com', 'github.co'],
57
+ });
58
+ assert.deepEqual(sources, []);
59
+ assert.deepEqual(
60
+ await readBrowserCookiesWithDependencies(
61
+ { browser: 'safari', domainFilter: 'hub.com', cache: false },
62
+ options
63
+ ),
64
+ expected.slice(0, 2)
65
+ );
66
+ });
67
+
52
68
  it('decodes multiple pages, flags, UTF-8 and Apple epoch without guessing session cookies', () => {
53
69
  assert.deepEqual(parseSafariCookies(fixture), expected);
54
70
  });
@@ -133,7 +149,16 @@ describe('Safari cookies', { timeout: 10000 }, () => {
133
149
  });
134
150
  assert.deepEqual(report.cookies, expected.slice(0, 2));
135
151
  assert.equal(report.migrated.cookies, 2);
136
- assert.equal(report.skipped.length, 5);
152
+ const classes = JSON.parse(
153
+ await readFile(
154
+ repoPath('tests/fixtures/migration-data-classes.json'),
155
+ 'utf8'
156
+ )
157
+ );
158
+ assert.deepEqual(
159
+ new Set(report.skipped.map((entry) => entry.type)),
160
+ new Set(classes.filter((type) => type !== 'cookies'))
161
+ );
137
162
  assert.equal(
138
163
  report.skipped.find((entry) => entry.type === 'passwords').reason,
139
164
  'safari-password-export-required'
@@ -0,0 +1,111 @@
1
+ import assert from 'node:assert/strict';
2
+ import { copyFile, mkdir, readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { it } from 'node:test';
5
+ import Database from 'better-sqlite3';
6
+ import { listBrowserProfiles } from '../../../src/browser/browser-profiles.js';
7
+ import { listCookieSources } from '../../../src/browser/browser-cookies.js';
8
+ import { findSafariCookieFile } from '../../../src/browser/safari-cookies.js';
9
+ import { repoPath } from '../../helpers/repo.js';
10
+ import { useTempDirectories } from '../../helpers/temp-directory.js';
11
+
12
+ const temporary = useTempDirectories('bc-safari-stores-');
13
+ const uuid = '11111111-2222-3333-4444-555555555555';
14
+
15
+ it('discovers Safari named profiles and prefers WebKit stores to stale legacy cookies', async () => {
16
+ const homeDir = await temporary();
17
+ const root = path.join(
18
+ homeDir,
19
+ 'Library/Containers/com.apple.Safari/Data/Library'
20
+ );
21
+ const named = path.join(root, 'Safari/Profiles', uuid);
22
+ await mkdir(named, { recursive: true });
23
+ const db = new Database(path.join(root, 'Safari/SafariTabs.db'));
24
+ db.exec(
25
+ await readFile(
26
+ repoPath('tests/fixtures/safari-data/SafariTabs.sql'),
27
+ 'utf8'
28
+ )
29
+ );
30
+ db.close();
31
+ const locations = [
32
+ 'Cookies',
33
+ 'WebKit/WebsiteData/Default/Cookies',
34
+ `WebKit/WebsiteDataStore/${uuid}/Cookies`,
35
+ ];
36
+ for (const location of locations) {
37
+ await mkdir(path.join(root, location), { recursive: true });
38
+ await copyFile(
39
+ repoPath('tests/fixtures/safari/Cookies.binarycookies'),
40
+ path.join(root, location, 'Cookies.binarycookies')
41
+ );
42
+ }
43
+ const profiles = await listBrowserProfiles({
44
+ browser: 'safari',
45
+ platform: 'darwin',
46
+ homeDir,
47
+ environment: {},
48
+ });
49
+ assert.deepEqual(
50
+ profiles.map(({ name }) => name),
51
+ ['Default', uuid]
52
+ );
53
+ assert.equal(profiles[1].displayName, 'Work');
54
+ assert.equal(profiles[1].path, named);
55
+ assert.equal(
56
+ await findSafariCookieFile(root),
57
+ path.join(root, locations[1], 'Cookies.binarycookies')
58
+ );
59
+ assert.equal(
60
+ await findSafariCookieFile(named),
61
+ path.join(root, locations[2], 'Cookies.binarycookies')
62
+ );
63
+ });
64
+
65
+ it('preserves a Safari catalogue error while listing its readable cookies and other browsers', async () => {
66
+ const homeDir = await temporary();
67
+ const root = path.join(
68
+ homeDir,
69
+ 'Library/Containers/com.apple.Safari/Data/Library'
70
+ );
71
+ await mkdir(path.join(root, 'Safari'), { recursive: true });
72
+ await mkdir(path.join(root, 'Cookies'));
73
+ await copyFile(
74
+ repoPath('tests/fixtures/safari/Cookies.binarycookies'),
75
+ path.join(root, 'Cookies/Cookies.binarycookies')
76
+ );
77
+ const tabs = path.join(root, 'Safari/SafariTabs.db');
78
+ new Database(tabs).close();
79
+ const chromium = path.join(
80
+ homeDir,
81
+ 'Library/Application Support/Google/Chrome/Default'
82
+ );
83
+ await mkdir(chromium, { recursive: true });
84
+ const db = new Database(path.join(chromium, 'Cookies'));
85
+ db.exec(
86
+ "CREATE TABLE cookies(host_key TEXT); INSERT INTO cookies VALUES ('.github.com')"
87
+ );
88
+ db.close();
89
+ const before = await readFile(tabs);
90
+ const options = { platform: 'darwin', homeDir, environment: {} };
91
+ const profiles = await listBrowserProfiles(options);
92
+ assert.equal(
93
+ profiles.filter(({ browser }) => browser === 'safari').length,
94
+ 2
95
+ );
96
+ const sources = await listCookieSources({
97
+ ...options,
98
+ domains: ['github.com'],
99
+ });
100
+ assert.equal(sources.find(({ browser }) => browser === 'chrome').cookies, 1);
101
+ assert.equal(
102
+ sources.find(({ browser, error }) => browser === 'safari' && !error)
103
+ .cookies,
104
+ 4
105
+ );
106
+ assert.match(
107
+ sources.find(({ browser, error }) => browser === 'safari' && error).error,
108
+ /no such table/u
109
+ );
110
+ assert.deepEqual(await readFile(tabs), before);
111
+ });
@@ -182,8 +182,12 @@ describe('dispatcher: high-level methods', () => {
182
182
  const migrated = await dispatch('profile.migrate', {
183
183
  from: 'chrome',
184
184
  include: ['cookies'],
185
+ passwordCsv: '/tmp/export.csv',
186
+ includePaymentCards: true,
185
187
  });
186
188
  assert.deepEqual(migrated.options.include, ['cookies']);
189
+ assert.equal(migrated.options.passwordCsv, '/tmp/export.csv');
190
+ assert.equal(migrated.options.includePaymentCards, true);
187
191
  assert.deepEqual(await dispatch('doctor', {}), { ok: true, unlisted: [] });
188
192
  assert.equal((await dispatch('version')).language, 'js');
189
193
  });
@@ -272,7 +276,7 @@ describe('dispatcher: high-level methods', () => {
272
276
  await assert.rejects(dispatcher.dispatch('nope'), { code: -32601 });
273
277
  await assert.rejects(dispatcher.dispatch('version', []), { code: -32602 });
274
278
  await assert.rejects(
275
- dispatcher.dispatch('session.launch', { engine: 'selenium' }),
279
+ dispatcher.dispatch('session.launch', { engine: 'unknown' }),
276
280
  { code: -32602 }
277
281
  );
278
282
  await assert.rejects(
@@ -351,7 +355,7 @@ describe('dispatcher: generic handle methods', () => {
351
355
  'launched with puppeteer'
352
356
  );
353
357
  await assert.rejects(
354
- dispatcher.dispatch('handle.root', { name: 'selenium' }),
358
+ dispatcher.dispatch('handle.root', { name: 'unknown' }),
355
359
  { code: -32602 }
356
360
  );
357
361
  });
@@ -247,6 +247,9 @@ describe('runCli', () => {
247
247
  'cookies, bookmarks',
248
248
  '--domain',
249
249
  'a.test',
250
+ '--password-csv',
251
+ '/tmp/export.csv',
252
+ '--include-payment-cards',
250
253
  ]);
251
254
 
252
255
  // The source browser is grouped under `from` so migrateProfile receives
@@ -254,6 +257,8 @@ describe('runCli', () => {
254
257
  assert.equal(document.options.from.browser, 'chrome');
255
258
  assert.deepEqual(document.options.include, ['cookies', 'bookmarks']);
256
259
  assert.deepEqual(document.options.domains, ['a.test']);
260
+ assert.equal(document.options.passwordCsv, '/tmp/export.csv');
261
+ assert.equal(document.options.includePaymentCards, true);
257
262
  });
258
263
 
259
264
  it('lists cookie sources with per-domain counts', async () => {