browser-commander 0.21.2 → 0.22.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.
@@ -0,0 +1,146 @@
1
+ // feature-parity: sources.cookie-listing@native-typed
2
+ import assert from 'node:assert';
3
+ import { describe, it } from 'node:test';
4
+
5
+ import {
6
+ listCookieSources,
7
+ resolveImportSource,
8
+ } from '../../../src/browser/browser-cookies.js';
9
+ import { writeFirefoxProfile } from '../../helpers/migration-fixtures.js';
10
+ import { useTempDirectories } from '../../helpers/temp-directory.js';
11
+
12
+ const makeDirectory = useTempDirectories();
13
+
14
+ describe('listCookieSources', () => {
15
+ it('reports cookie counts per profile without reading values', async () => {
16
+ const temporaryDirectory = await makeDirectory('bc-src-');
17
+ const profilePath = await writeFirefoxProfile(temporaryDirectory, [
18
+ { name: 'a', value: 'secret-1', host: '.example.com' },
19
+ { name: 'b', value: 'secret-2', host: '.example.com' },
20
+ { name: 'c', value: 'secret-3', host: '.other.test' },
21
+ ]);
22
+
23
+ const sources = await listCookieSources({
24
+ platform: 'linux',
25
+ homeDir: temporaryDirectory,
26
+ });
27
+
28
+ assert.equal(sources.length, 1);
29
+ const [source] = sources;
30
+ assert.equal(source.browser, 'firefox');
31
+ assert.equal(source.path, profilePath);
32
+ assert.equal(source.cookies, 3);
33
+ assert.equal(source.byDomain, null);
34
+ // Never expose a value anywhere in the payload.
35
+ assert.ok(!JSON.stringify(sources).includes('secret-'));
36
+ });
37
+
38
+ it('counts per domain and omits profiles that hold none', async () => {
39
+ const temporaryDirectory = await makeDirectory('bc-src2-');
40
+ await writeFirefoxProfile(temporaryDirectory, [
41
+ { name: 'a', value: '1', host: '.example.com' },
42
+ { name: 'b', value: '2', host: '.example.com' },
43
+ ]);
44
+
45
+ const matched = await listCookieSources({
46
+ domains: ['example.com'],
47
+ platform: 'linux',
48
+ homeDir: temporaryDirectory,
49
+ });
50
+ assert.equal(matched.length, 1);
51
+ assert.deepEqual(matched[0].byDomain, { 'example.com': 2 });
52
+
53
+ const none = await listCookieSources({
54
+ domains: ['absent.test'],
55
+ platform: 'linux',
56
+ homeDir: temporaryDirectory,
57
+ });
58
+ assert.deepEqual(none, []);
59
+ });
60
+ });
61
+
62
+ describe('resolveImportSource', () => {
63
+ // feature-parity: sources.default-domain-fallback@native-typed
64
+ const firefoxIsDefault = async () => 'firefox.desktop\n';
65
+ const githubCookie = [{ name: 'a', value: '1', host: '.github.com' }];
66
+ const otherCookie = [{ name: 'b', value: '2', host: '.other.test' }];
67
+
68
+ async function resolve(homeDir, overrides = {}) {
69
+ return await resolveImportSource({
70
+ browser: 'default',
71
+ domains: ['github.com'],
72
+ platform: 'linux',
73
+ homeDir,
74
+ environment: {},
75
+ runCommand: firefoxIsDefault,
76
+ ...overrides,
77
+ });
78
+ }
79
+
80
+ it('keeps the system default when it holds the domains', async () => {
81
+ const homeDir = await makeDirectory('bc-import-default-');
82
+ await writeFirefoxProfile(homeDir, githubCookie);
83
+ await writeFirefoxProfile(homeDir, githubCookie, {
84
+ root: '.librewolf',
85
+ name: 'default',
86
+ });
87
+
88
+ assert.deepEqual(await resolve(homeDir), {
89
+ browser: 'firefox',
90
+ profile: 'default-release',
91
+ warning: null,
92
+ });
93
+ });
94
+
95
+ it('falls back to the installed browser that holds the domains', async () => {
96
+ const homeDir = await makeDirectory('bc-import-fallback-');
97
+ await writeFirefoxProfile(homeDir, otherCookie);
98
+ await writeFirefoxProfile(homeDir, githubCookie, {
99
+ root: '.librewolf',
100
+ name: 'default',
101
+ });
102
+
103
+ const source = await resolve(homeDir);
104
+ assert.equal(source.browser, 'librewolf');
105
+ assert.equal(source.profile, 'default');
106
+ assert.equal(source.warning.reason, 'default-browser-fallback');
107
+ assert.equal(source.warning.item, 'librewolf');
108
+ assert.match(
109
+ source.warning.detail,
110
+ /default browser \(firefox\) holds no cookies for github\.com; imported from librewolf/
111
+ );
112
+ });
113
+
114
+ it('falls back when the system default cannot be determined', async () => {
115
+ const homeDir = await makeDirectory('bc-import-unknown-');
116
+ await writeFirefoxProfile(homeDir, githubCookie, {
117
+ root: '.librewolf',
118
+ name: 'default',
119
+ });
120
+ const runCommand = async () => {
121
+ throw new Error('no xdg');
122
+ };
123
+
124
+ const source = await resolve(homeDir, { runCommand });
125
+ assert.equal(source.browser, 'librewolf');
126
+ assert.equal(source.warning.reason, 'default-browser-unknown');
127
+ await assert.rejects(
128
+ resolve(await makeDirectory('bc-import-none-'), { runCommand }),
129
+ /no installed browser holds cookies for github\.com/
130
+ );
131
+ });
132
+
133
+ it('resolves the default plainly without domains', async () => {
134
+ const homeDir = await makeDirectory('bc-import-plain-');
135
+ assert.deepEqual(await resolve(homeDir, { domains: undefined }), {
136
+ browser: 'firefox',
137
+ profile: undefined,
138
+ warning: null,
139
+ });
140
+ assert.deepEqual(await resolve(homeDir, { browser: 'Opera' }), {
141
+ browser: 'opera',
142
+ profile: undefined,
143
+ warning: null,
144
+ });
145
+ });
146
+ });
@@ -0,0 +1,121 @@
1
+ // feature-parity: sources.default-browser@native-typed
2
+ import assert from 'node:assert';
3
+ import { describe, it } from 'node:test';
4
+
5
+ import {
6
+ browserForIdentifier,
7
+ parseMacLaunchServicesHandler,
8
+ parseWindowsProgId,
9
+ resolveDefaultBrowser,
10
+ } from '../../../src/browser/default-browser.js';
11
+
12
+ function runnerReturning(map) {
13
+ return async (command, args) => {
14
+ const key = [command, ...args].join(' ');
15
+ if (!(key in map)) {
16
+ throw new Error(`unexpected command: ${key}`);
17
+ }
18
+ return map[key];
19
+ };
20
+ }
21
+
22
+ describe('resolveDefaultBrowser', () => {
23
+ it('maps a macOS LaunchServices https handler to a browser id', async () => {
24
+ const output = `(
25
+ {
26
+ LSHandlerRoleAll = "com.apple.safari";
27
+ LSHandlerURLScheme = mailto;
28
+ },
29
+ {
30
+ LSHandlerRoleAll = "com.google.chrome";
31
+ LSHandlerURLScheme = https;
32
+ }
33
+ )`;
34
+ const runCommand = runnerReturning({
35
+ 'defaults read com.apple.LaunchServices/com.apple.launchservices.secure LSHandlers':
36
+ output,
37
+ });
38
+ assert.equal(
39
+ await resolveDefaultBrowser({ platform: 'darwin', runCommand }),
40
+ 'chrome'
41
+ );
42
+ });
43
+
44
+ it('maps a Linux xdg-settings .desktop name to a browser id', async () => {
45
+ const runCommand = runnerReturning({
46
+ 'xdg-settings get default-web-browser': 'firefox.desktop\n',
47
+ });
48
+ assert.equal(
49
+ await resolveDefaultBrowser({ platform: 'linux', runCommand }),
50
+ 'firefox'
51
+ );
52
+ });
53
+
54
+ it('falls back to xdg-mime when xdg-settings is unknown', async () => {
55
+ const runCommand = async (command, args) => {
56
+ const key = [command, ...args].join(' ');
57
+ if (key === 'xdg-settings get default-web-browser') {
58
+ return 'some-unknown.desktop\n';
59
+ }
60
+ if (key === 'xdg-mime query default x-scheme-handler/https') {
61
+ return 'brave-browser.desktop\n';
62
+ }
63
+ throw new Error(`unexpected command: ${key}`);
64
+ };
65
+ assert.equal(
66
+ await resolveDefaultBrowser({ platform: 'linux', runCommand }),
67
+ 'brave'
68
+ );
69
+ });
70
+
71
+ it('maps a Windows UserChoice ProgId to a browser id', async () => {
72
+ const output = [
73
+ '',
74
+ 'HKEY_CURRENT_USER\\...\\https\\UserChoice',
75
+ ' ProgId REG_SZ MSEdgeHTM',
76
+ '',
77
+ ].join('\r\n');
78
+ const runCommand = runnerReturning({
79
+ 'reg query HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\https\\UserChoice /v ProgId':
80
+ output,
81
+ });
82
+ assert.equal(
83
+ await resolveDefaultBrowser({ platform: 'win32', runCommand }),
84
+ 'edge'
85
+ );
86
+ });
87
+
88
+ it('returns null when nothing matches or the tool fails', async () => {
89
+ assert.equal(
90
+ await resolveDefaultBrowser({
91
+ platform: 'linux',
92
+ runCommand: async () => {
93
+ throw new Error('no xdg');
94
+ },
95
+ }),
96
+ null
97
+ );
98
+ assert.equal(
99
+ await resolveDefaultBrowser({
100
+ platform: 'sunos',
101
+ runCommand: async () => '',
102
+ }),
103
+ null
104
+ );
105
+ });
106
+
107
+ it('parses handler blocks and ProgId lines directly', () => {
108
+ assert.equal(
109
+ parseMacLaunchServicesHandler(
110
+ '{ LSHandlerURLScheme = https; LSHandlerRoleAll = "com.brave.browser"; }'
111
+ ),
112
+ 'com.brave.browser'
113
+ );
114
+ assert.equal(
115
+ parseWindowsProgId(' ProgId REG_SZ FirefoxHTML'),
116
+ 'FirefoxHTML'
117
+ );
118
+ assert.equal(browserForIdentifier('COM.GOOGLE.CHROME', 'darwin'), 'chrome');
119
+ assert.equal(browserForIdentifier('', 'darwin'), null);
120
+ });
121
+ });
@@ -1,6 +1,6 @@
1
1
  // feature-parity: migration.profile@native-typed
2
2
  import assert from 'node:assert';
3
- import { readFile } from 'node:fs/promises';
3
+ import { mkdir, readFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { describe, it } from 'node:test';
6
6
 
@@ -14,6 +14,8 @@ import {
14
14
  writeFirefoxCookies,
15
15
  writeFirefoxLogins,
16
16
  writeFirefoxPlaces,
17
+ writeFirefoxProfile,
18
+ writeProfileJson,
17
19
  } from '../../../helpers/migration-fixtures.js';
18
20
  import { useTempDirectories } from '../../../helpers/temp-directory.js';
19
21
 
@@ -114,4 +116,63 @@ describe('migrateProfile', () => {
114
116
  );
115
117
  assert.equal(report.warnings[0].reason, 'target-key-unavailable');
116
118
  });
119
+
120
+ it('imports a domain from whichever installed browser holds it', async () => {
121
+ const homeDir = await makeTempDir();
122
+ const target = await makeTempDir();
123
+ await writeFirefoxProfile(homeDir, [
124
+ { name: 'a', value: '1', host: '.other.test' },
125
+ ]);
126
+ await writeFirefoxProfile(
127
+ homeDir,
128
+ [{ name: 'session', value: 's', host: '.github.com' }],
129
+ { root: '.librewolf', name: 'default' }
130
+ );
131
+
132
+ const report = await migrateProfile({
133
+ from: { browser: 'default' },
134
+ to: target,
135
+ include: ['cookies'],
136
+ domains: ['github.com'],
137
+ platform: 'linux',
138
+ homeDir,
139
+ environment: {},
140
+ runCommand: async () => 'firefox.desktop\n',
141
+ });
142
+
143
+ assert.equal(report.source.browser, 'librewolf');
144
+ assert.equal(report.source.profile, 'default');
145
+ assert.equal(report.migrated.cookies, 1);
146
+ assert.deepEqual(
147
+ report.warnings.map(({ reason }) => reason),
148
+ ['default-browser-fallback']
149
+ );
150
+ });
151
+
152
+ it('reads a single-profile Chromium browser (Opera) from its root', async () => {
153
+ const homeDir = await makeTempDir();
154
+ const target = await makeTempDir();
155
+ const root = path.join(homeDir, '.config', 'opera');
156
+ await mkdir(root, { recursive: true });
157
+ await writeProfileJson(root, 'Bookmarks', {
158
+ roots: {
159
+ bookmark_bar: {
160
+ type: 'folder',
161
+ children: [{ type: 'url', name: 'A', url: 'https://a.example/' }],
162
+ },
163
+ },
164
+ });
165
+
166
+ const report = await migrateProfile({
167
+ from: { browser: 'opera' },
168
+ to: target,
169
+ include: ['bookmarks'],
170
+ platform: 'linux',
171
+ homeDir,
172
+ environment: {},
173
+ });
174
+
175
+ assert.equal(report.migrated.bookmarks, 1);
176
+ assert.deepEqual(report.skipped, []);
177
+ });
117
178
  });
@@ -249,11 +249,38 @@ describe('runCli', () => {
249
249
  'a.test',
250
250
  ]);
251
251
 
252
- assert.deepEqual(document.options, {
253
- from: 'chrome',
254
- include: ['cookies', 'bookmarks'],
255
- domains: ['a.test'],
256
- });
252
+ // The source browser is grouped under `from` so migrateProfile receives
253
+ // `from.browser` (a flat string here was a bug that threw).
254
+ assert.equal(document.options.from.browser, 'chrome');
255
+ assert.deepEqual(document.options.include, ['cookies', 'bookmarks']);
256
+ assert.deepEqual(document.options.domains, ['a.test']);
257
+ });
258
+
259
+ it('lists cookie sources with per-domain counts', async () => {
260
+ const { exitCode, document } = await cli([
261
+ 'cookies',
262
+ 'sources',
263
+ '--domain',
264
+ 'example.com',
265
+ ]);
266
+
267
+ assert.equal(exitCode, 0);
268
+ assert.equal(document.sources[0].browser, 'chrome');
269
+ assert.equal(document.sources[0].cookies, 7);
270
+ assert.deepEqual(document.sources[0].byDomain, { 'example.com': 1 });
271
+ });
272
+
273
+ it('lists installed profiles for profile sources', async () => {
274
+ const { exitCode, document } = await cli([
275
+ 'profile',
276
+ 'sources',
277
+ '--browser',
278
+ 'firefox',
279
+ ]);
280
+
281
+ assert.equal(exitCode, 0);
282
+ assert.equal(document.profiles[0].browser, 'firefox');
283
+ assert.equal(document.profiles[0].isDefault, true);
257
284
  });
258
285
 
259
286
  it('prints a profile snapshot report', async () => {
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The generated importable-browser support matrix (issue #114).
3
+ *
4
+ * `scripts/generate-browser-support.mjs` turns the shared catalogue
5
+ * `js/src/browser/browser-sources.json` into the table between the
6
+ * `browser-support:generated` markers in README.md. These tests pin the
7
+ * rendering against a fixture catalogue, then pin the repository itself: the
8
+ * committed README must be what the generator writes, so a browser added to
9
+ * the JSON without regenerating fails CI.
10
+ */
11
+
12
+ import assert from 'node:assert/strict';
13
+ import * as fs from 'node:fs';
14
+ import { describe, it } from 'node:test';
15
+ import { pathToFileURL } from 'node:url';
16
+
17
+ import { repoPath } from '../../helpers/repo.js';
18
+
19
+ const { BEGIN_MARKER, END_MARKER, readCatalogue, renderBrowserSupport } =
20
+ await import(pathToFileURL(repoPath('scripts/generate-browser-support.mjs')));
21
+
22
+ describe('generate-browser-support', () => {
23
+ it('renders a row per browser with per-platform availability', () => {
24
+ const table = renderBrowserSupport([
25
+ {
26
+ id: 'chrome',
27
+ family: 'chromium',
28
+ roots: { darwin: ['a'], win32: ['b'], linux: ['c'] },
29
+ },
30
+ {
31
+ id: 'arc',
32
+ family: 'chromium',
33
+ roots: { darwin: ['a'], win32: ['b'] },
34
+ },
35
+ { id: 'firefox', family: 'firefox', roots: { linux: ['c'] } },
36
+ ]);
37
+ // Columns are padded the way Prettier formats Markdown tables.
38
+ const lines = table.split('\n');
39
+ assert.equal(lines[0], '| Browser | Family | macOS | Windows | Linux |');
40
+ assert.equal(lines[2], '| chrome | Chromium | Yes | Yes | Yes |');
41
+ // A browser absent on a platform shows an em dash, not a blank cell.
42
+ assert.equal(lines[3], '| arc | Chromium | Yes | Yes | — |');
43
+ assert.equal(lines[4], '| firefox | Firefox | — | — | Yes |');
44
+ });
45
+
46
+ it('keeps the committed README in sync with the catalogue', () => {
47
+ const readme = fs
48
+ .readFileSync(repoPath('README.md'), 'utf8')
49
+ .replaceAll('\r\n', '\n');
50
+ const begin = readme.indexOf(BEGIN_MARKER);
51
+ const end = readme.indexOf(END_MARKER);
52
+ assert.ok(begin !== -1 && end > begin, 'README is missing the markers');
53
+ const between = readme.slice(begin + BEGIN_MARKER.length, end).trim();
54
+ assert.equal(between, renderBrowserSupport(readCatalogue(repoPath('.'))));
55
+ });
56
+ });