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.
package/src/cli/args.js CHANGED
@@ -78,11 +78,17 @@ export const COMMANDS = Object.freeze({
78
78
  domain: STRINGS,
79
79
  },
80
80
  },
81
+ 'cookies sources': {
82
+ positionals: [],
83
+ options: { domain: STRINGS },
84
+ },
81
85
  'profile migrate': {
82
86
  positionals: [],
83
87
  options: {
84
88
  from: STRING,
85
89
  profile: STRING,
90
+ 'user-data-dir': STRING,
91
+ 'target-browser': STRING,
86
92
  to: STRING,
87
93
  include: STRING,
88
94
  domain: STRINGS,
@@ -92,6 +98,10 @@ export const COMMANDS = Object.freeze({
92
98
  positionals: [],
93
99
  options: { from: STRING, profile: STRING, to: STRING },
94
100
  },
101
+ 'profile sources': {
102
+ positionals: [],
103
+ options: { browser: STRING },
104
+ },
95
105
  attach: {
96
106
  positionals: [],
97
107
  options: { mode: STRING, port: STRING, timeout: STRING },
@@ -210,6 +210,8 @@ async function profileMigrate(parsed, io) {
210
210
  await dispatchOnce(io, 'profile.migrate', {
211
211
  from: requireOption(options, 'from', 'profile migrate'),
212
212
  profile: options.profile,
213
+ userDataDir: options.userDataDir,
214
+ targetBrowser: options.targetBrowser,
213
215
  to: options.to,
214
216
  include: options.include
215
217
  ?.split(',')
@@ -220,6 +222,29 @@ async function profileMigrate(parsed, io) {
220
222
  );
221
223
  }
222
224
 
225
+ /**
226
+ * List the installed browser profiles that hold cookies (names and counts
227
+ * only, never values), optionally filtered and counted by `--domain`.
228
+ */
229
+ async function cookieSources(parsed, io) {
230
+ const { options } = parsed;
231
+ return done(
232
+ await dispatchOnce(io, 'cookies.sources', {
233
+ domains: options.domain,
234
+ })
235
+ );
236
+ }
237
+
238
+ /** List installed browser profiles, optionally for one `--browser`. */
239
+ async function profileSources(parsed, io) {
240
+ const { options } = parsed;
241
+ return done(
242
+ await dispatchOnce(io, 'profile.sources', {
243
+ browser: options.browser,
244
+ })
245
+ );
246
+ }
247
+
223
248
  /** Copy a real profile into `--to` (or a new temporary directory). */
224
249
  async function profileSnapshot(parsed, io) {
225
250
  const { options } = parsed;
@@ -323,8 +348,10 @@ export const COMMAND_HANDLERS = Object.freeze({
323
348
  profile: options.profile,
324
349
  domains: options.domain,
325
350
  })),
351
+ 'cookies sources': cookieSources,
326
352
  'profile migrate': profileMigrate,
327
353
  'profile snapshot': profileSnapshot,
354
+ 'profile sources': profileSources,
328
355
  attach,
329
356
  doctor,
330
357
  run,
@@ -191,12 +191,18 @@ async function importCookies(state, params) {
191
191
  }
192
192
 
193
193
  function migrateProfile(state, params) {
194
+ // The CLI passes the source browser flat (`--from`, `--profile`,
195
+ // `--user-data-dir`); migrateProfile expects them grouped under `from`.
194
196
  return state.dependencies.migrateProfile({
195
- from: params.from,
196
- profile: params.profile,
197
+ from: {
198
+ browser: requireString(params, 'from'),
199
+ profile: params.profile,
200
+ userDataDir: params.userDataDir,
201
+ },
197
202
  to: params.to,
198
203
  include: params.include,
199
204
  domains: params.domains,
205
+ targetBrowser: params.targetBrowser,
200
206
  });
201
207
  }
202
208
 
@@ -209,6 +215,24 @@ function snapshotProfile(state, params) {
209
215
  });
210
216
  }
211
217
 
218
+ /** List browsers/profiles holding cookies, with optional per-domain counts. */
219
+ async function cookieSources(state, params) {
220
+ const domains = params.domains;
221
+ if (domains !== undefined && !Array.isArray(domains)) {
222
+ throw invalidParams('domains must be an array of strings');
223
+ }
224
+ return { sources: await state.dependencies.listCookieSources({ domains }) };
225
+ }
226
+
227
+ /** List the installed browser profiles (never cookie values). */
228
+ async function profileSources(state, params) {
229
+ return {
230
+ profiles: await state.dependencies.listBrowserProfiles({
231
+ browser: params.browser,
232
+ }),
233
+ };
234
+ }
235
+
212
236
  function relayOf(state) {
213
237
  if (!state.relay) {
214
238
  throw invalidParams('No extension is attached; call attach first');
@@ -298,8 +322,10 @@ export const HIGH_LEVEL_METHODS = Object.freeze({
298
322
  'trace.start': traceStart,
299
323
  'trace.stop': traceStop,
300
324
  'cookies.import': importCookies,
325
+ 'cookies.sources': cookieSources,
301
326
  'profile.migrate': migrateProfile,
302
327
  'profile.snapshot': snapshotProfile,
328
+ 'profile.sources': profileSources,
303
329
  attach,
304
330
  'attach.tabs': attachTabs,
305
331
  'attach.send': attachSend,
@@ -64,6 +64,12 @@ export const DEFAULT_DEPENDENCIES = Object.freeze({
64
64
  writeTraceViewer,
65
65
  readBrowserCookies: async (options) =>
66
66
  (await import('../browser/browser-cookies.js')).readBrowserCookies(options),
67
+ listCookieSources: async (options) =>
68
+ (await import('../browser/browser-cookies.js')).listCookieSources(options),
69
+ listBrowserProfiles: async (options) =>
70
+ (await import('../browser/browser-profiles.js')).listBrowserProfiles(
71
+ options
72
+ ),
67
73
  loadEngine: (name) => importOptional(name, `The ${name} engine`),
68
74
  openInUserBrowser: async (url) =>
69
75
  (
package/src/exports.js CHANGED
@@ -60,8 +60,12 @@ export {
60
60
  export { connectBrowser } from './browser/connector.js';
61
61
  export {
62
62
  listBrowserProfiles,
63
+ listCookieSources,
63
64
  readBrowserCookies,
65
+ resolveImportSource,
64
66
  } from './browser/browser-cookies.js';
67
+ export { resolveDefaultBrowser } from './browser/default-browser.js';
68
+ export { BROWSER_IDS, findBrowserSource } from './browser/browser-sources.js';
65
69
  export {
66
70
  launchAndConnectRealBrowser,
67
71
  launchRealBrowser,
@@ -158,6 +158,27 @@ export function createFakeDependencies() {
158
158
  { name: 'sid', value: '1', domain: `.${domainFilter}`, path: '/' },
159
159
  { name: 'bad', value: '2', domain: `.${domainFilter}`, path: '/' },
160
160
  ],
161
+ listCookieSources: async ({ domains } = {}) => [
162
+ {
163
+ browser: 'chrome',
164
+ profile: 'Default',
165
+ path: '/home/user/.config/google-chrome/Default',
166
+ isDefault: true,
167
+ cookies: 7,
168
+ byDomain: domains
169
+ ? Object.fromEntries(domains.map((d) => [d, 1]))
170
+ : null,
171
+ },
172
+ ],
173
+ listBrowserProfiles: async ({ browser } = {}) => [
174
+ {
175
+ browser: browser ?? 'chrome',
176
+ name: 'Default',
177
+ displayName: 'Default',
178
+ path: `/home/user/.config/${browser ?? 'google-chrome'}/Default`,
179
+ isDefault: true,
180
+ },
181
+ ],
161
182
  loadEngine: async (name) => ({ default: new FakeEngine(name) }),
162
183
  openInUserBrowser: async (url) => ({ opened: url, command: ['open', url] }),
163
184
  migrateProfile: async (options) => ({ migrated: {}, options }),
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import assert from 'node:assert';
11
- import { readFile, stat, writeFile } from 'node:fs/promises';
11
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
12
12
  import path from 'node:path';
13
13
 
14
14
  import BetterSqlite3 from 'better-sqlite3';
@@ -148,6 +148,35 @@ export async function writeFirefoxCookies(profileDir, rows) {
148
148
  return cookiePath;
149
149
  }
150
150
 
151
+ /**
152
+ * Write a Linux Firefox-family install under `home` with one default profile,
153
+ * listed in `profiles.ini`, whose `cookies.sqlite` holds `rows`.
154
+ *
155
+ * @param {string} home - Home directory the install lives under
156
+ * @param {Array<Object>} rows - Cookie rows, as for `writeFirefoxCookies`
157
+ * @param {Object} [options]
158
+ * @param {string} [options.root='.mozilla/firefox'] - Install root, relative
159
+ * to `home` (a fork such as LibreWolf uses its own)
160
+ * @param {string} [options.name='default-release'] - Profile name
161
+ * @returns {Promise<string>} The profile directory
162
+ */
163
+ export async function writeFirefoxProfile(
164
+ home,
165
+ rows,
166
+ { root = '.mozilla/firefox', name = 'default-release' } = {}
167
+ ) {
168
+ const rootPath = path.join(home, ...root.split('/'));
169
+ const profileName = `xyz.${name}`;
170
+ const profilePath = path.join(rootPath, profileName);
171
+ await mkdir(profilePath, { recursive: true });
172
+ await writeFile(
173
+ path.join(rootPath, 'profiles.ini'),
174
+ `[Profile0]\nName=${name}\nIsRelative=1\nPath=${profileName}\nDefault=1\n`
175
+ );
176
+ await writeFirefoxCookies(profilePath, rows);
177
+ return profilePath;
178
+ }
179
+
151
180
  /**
152
181
  * Write a Firefox `places.sqlite` with a toolbar bookmark and, unless
153
182
  * `withMenuBookmark` is false, a bookmarks-menu one. Each bookmark has its own
@@ -0,0 +1,36 @@
1
+ import assert from 'node:assert';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { describe, it } from 'node:test';
5
+
6
+ import { readBrowserCookiesWithDependencies } from '../../../src/browser/browser-cookies.js';
7
+ import { writeFirefoxCookies } from '../../helpers/migration-fixtures.js';
8
+ import { useTempDirectories } from '../../helpers/temp-directory.js';
9
+
10
+ const makeDirectory = useTempDirectories();
11
+
12
+ describe('readBrowserCookies honours an explicit profileDir', () => {
13
+ it('reads cookies from the given directory, not the default profile root', async () => {
14
+ const temporaryDirectory = await makeDirectory('bc-profiledir-');
15
+ // A custom userDataDir that is NOT under the default profile root.
16
+ const customProfile = path.join(temporaryDirectory, 'custom', 'profile');
17
+ await mkdir(customProfile, { recursive: true });
18
+ await writeFirefoxCookies(customProfile, [
19
+ { name: 'sid', value: 'abc', host: '.example.com' },
20
+ ]);
21
+
22
+ // homeDir points somewhere with no browser data, so a reader that ignored
23
+ // profileDir and re-resolved the default profile would find nothing.
24
+ const emptyHome = path.join(temporaryDirectory, 'empty-home');
25
+ await mkdir(emptyHome, { recursive: true });
26
+
27
+ const cookies = await readBrowserCookiesWithDependencies(
28
+ { browser: 'firefox', profileDir: customProfile },
29
+ { platform: 'linux', homeDir: emptyHome }
30
+ );
31
+
32
+ assert.equal(cookies.length, 1);
33
+ assert.equal(cookies[0].name, 'sid');
34
+ assert.equal(cookies[0].domain, '.example.com');
35
+ });
36
+ });
@@ -37,7 +37,7 @@ import {
37
37
  listBrowserProfiles as publicListBrowserProfiles,
38
38
  readBrowserCookies as publicReadBrowserCookies,
39
39
  } from '../../../src/index.js';
40
- import { writeFirefoxCookies } from '../../helpers/migration-fixtures.js';
40
+ import { writeFirefoxProfile } from '../../helpers/migration-fixtures.js';
41
41
 
42
42
  const CHROME_EPOCH_OFFSET_SECONDS = 11_644_473_600;
43
43
  const execFile = promisify(execFileCallback);
@@ -134,16 +134,7 @@ async function createChromiumProfile({ homeDir, rows, profile = 'Default' }) {
134
134
  }
135
135
 
136
136
  async function createFirefoxProfile({ homeDir, rows }) {
137
- const root = path.join(homeDir, '.mozilla', 'firefox');
138
- const profileName = 'fixture.default-release';
139
- const profilePath = path.join(root, profileName);
140
- await mkdir(profilePath, { recursive: true });
141
- await writeFile(
142
- path.join(root, 'profiles.ini'),
143
- `[Profile0]\nName=default-release\nIsRelative=1\nPath=${profileName}\nDefault=1\n`
144
- );
145
- const cookiePath = await writeFirefoxCookies(profilePath, rows);
146
- return { cookiePath, profilePath, root };
137
+ return { profilePath: await writeFirefoxProfile(homeDir, rows) };
147
138
  }
148
139
 
149
140
  describe('installed browser cookie import', () => {
@@ -0,0 +1,150 @@
1
+ import assert from 'node:assert';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { describe, it } from 'node:test';
5
+
6
+ import {
7
+ listBrowserProfiles,
8
+ resolveBrowserProfile,
9
+ resolveSourceBrowser,
10
+ } from '../../../src/browser/browser-profiles.js';
11
+ import { writeFirefoxProfile } from '../../helpers/migration-fixtures.js';
12
+ import { useTempDirectories } from '../../helpers/temp-directory.js';
13
+
14
+ const makeDirectory = useTempDirectories();
15
+
16
+ /** The browser of every profile `listBrowserProfiles` finds. */
17
+ async function listedBrowsers(options) {
18
+ const profiles = await listBrowserProfiles(options);
19
+ return profiles.map((profile) => profile.browser);
20
+ }
21
+
22
+ async function writeChromiumCookieDatabase(profilePath) {
23
+ const cookiePath = path.join(profilePath, 'Network', 'Cookies');
24
+ await mkdir(path.dirname(cookiePath), { recursive: true });
25
+ await writeFile(cookiePath, 'SQLite format 3\u0000');
26
+ return cookiePath;
27
+ }
28
+
29
+ describe('listBrowserProfiles with the expanded catalogue', () => {
30
+ it('reads an Opera single-profile layout from the root itself', async () => {
31
+ const temporaryDirectory = await makeDirectory('bc-opera-');
32
+ const root = path.join(temporaryDirectory, '.config', 'opera');
33
+ await mkdir(root, { recursive: true });
34
+ const cookiePath = await writeChromiumCookieDatabase(root);
35
+
36
+ const profiles = await listBrowserProfiles({
37
+ browser: 'opera',
38
+ platform: 'linux',
39
+ homeDir: temporaryDirectory,
40
+ environment: {},
41
+ });
42
+
43
+ assert.deepEqual(profiles, [
44
+ {
45
+ browser: 'opera',
46
+ name: 'Default',
47
+ displayName: 'Default',
48
+ path: root,
49
+ isDefault: true,
50
+ },
51
+ ]);
52
+ assert.ok(cookiePath.startsWith(root));
53
+ });
54
+
55
+ it('discovers a Firefox fork (LibreWolf) by its own root', async () => {
56
+ const temporaryDirectory = await makeDirectory('bc-librewolf-');
57
+ const profilePath = await writeFirefoxProfile(
58
+ temporaryDirectory,
59
+ [{ name: 'a', value: '1', host: '.example.com' }],
60
+ { root: '.librewolf', name: 'default' }
61
+ );
62
+
63
+ const profiles = await listBrowserProfiles({
64
+ browser: 'librewolf',
65
+ platform: 'linux',
66
+ homeDir: temporaryDirectory,
67
+ });
68
+
69
+ assert.deepEqual(profiles, [
70
+ {
71
+ browser: 'librewolf',
72
+ name: 'default',
73
+ displayName: 'default',
74
+ path: profilePath,
75
+ isDefault: true,
76
+ },
77
+ ]);
78
+ });
79
+
80
+ it('lists Firefox once when its channels share a root', async () => {
81
+ const temporaryDirectory = await makeDirectory('bc-ffshare-');
82
+ await writeFirefoxProfile(temporaryDirectory, [
83
+ { name: 'a', value: '1', host: '.example.com' },
84
+ ]);
85
+
86
+ assert.deepEqual(
87
+ await listedBrowsers({ platform: 'linux', homeDir: temporaryDirectory }),
88
+ ['firefox']
89
+ );
90
+ });
91
+
92
+ it('resolves browser: "default" to the system default browser', async () => {
93
+ const runCommand = async (command, args) => {
94
+ if (
95
+ command === 'xdg-settings' &&
96
+ args.join(' ') === 'get default-web-browser'
97
+ ) {
98
+ return 'firefox.desktop\n';
99
+ }
100
+ throw new Error('unexpected command');
101
+ };
102
+ assert.equal(
103
+ await resolveSourceBrowser('default', { platform: 'linux', runCommand }),
104
+ 'firefox'
105
+ );
106
+ assert.equal(
107
+ await resolveSourceBrowser('AUTO', { platform: 'linux', runCommand }),
108
+ 'firefox'
109
+ );
110
+ });
111
+
112
+ it('lists the default browser profile when browser is "default"', async () => {
113
+ const temporaryDirectory = await makeDirectory('bc-default-');
114
+ const profilePath = await writeFirefoxProfile(temporaryDirectory, [
115
+ { name: 'a', value: '1', host: '.example.com' },
116
+ ]);
117
+ const runCommand = async () => 'firefox.desktop\n';
118
+
119
+ const resolved = await resolveBrowserProfile({
120
+ browser: 'default',
121
+ platform: 'linux',
122
+ homeDir: temporaryDirectory,
123
+ runCommand,
124
+ });
125
+ assert.equal(resolved.browser, 'firefox');
126
+ assert.equal(resolved.path, profilePath);
127
+
128
+ assert.deepEqual(
129
+ await listedBrowsers({
130
+ browser: 'auto',
131
+ platform: 'linux',
132
+ homeDir: temporaryDirectory,
133
+ runCommand,
134
+ }),
135
+ ['firefox']
136
+ );
137
+ });
138
+
139
+ it('reports a clear error when the default browser is unknown', async () => {
140
+ await assert.rejects(
141
+ resolveSourceBrowser('default', {
142
+ platform: 'linux',
143
+ runCommand: async () => {
144
+ throw new Error('no xdg');
145
+ },
146
+ }),
147
+ /Could not determine the system default browser/
148
+ );
149
+ });
150
+ });
@@ -0,0 +1,154 @@
1
+ // feature-parity: sources.catalogue@native-typed
2
+ import assert from 'node:assert';
3
+ import path from 'node:path';
4
+ import { describe, it } from 'node:test';
5
+
6
+ import {
7
+ BROWSER_IDS,
8
+ browserFamily,
9
+ defaultBrowserIdentifiers,
10
+ findBrowserSource,
11
+ isSingleProfileBrowser,
12
+ normalizeBrowserId,
13
+ resolveBrowserRoots,
14
+ safeStorageIdentity,
15
+ } from '../../../src/browser/browser-sources.js';
16
+
17
+ describe('browser-sources catalogue', () => {
18
+ it('keeps the original five browsers resolvable by id and alias', () => {
19
+ assert.equal(normalizeBrowserId('chrome'), 'chrome');
20
+ assert.equal(normalizeBrowserId('msedge'), 'edge');
21
+ assert.equal(normalizeBrowserId('microsoft-edge'), 'edge');
22
+ assert.equal(normalizeBrowserId('google-chrome'), 'chrome');
23
+ assert.equal(normalizeBrowserId('CHROME'), 'chrome');
24
+ });
25
+
26
+ it('adds the new source browsers from #114', () => {
27
+ for (const id of [
28
+ 'opera',
29
+ 'opera-gx',
30
+ 'vivaldi',
31
+ 'arc',
32
+ 'yandex',
33
+ 'chrome-beta',
34
+ 'chrome-dev',
35
+ 'chrome-canary',
36
+ 'edge-beta',
37
+ 'edge-dev',
38
+ 'librewolf',
39
+ 'waterfox',
40
+ 'zen',
41
+ 'floorp',
42
+ 'firefox-developer',
43
+ 'firefox-nightly',
44
+ ]) {
45
+ assert.ok(BROWSER_IDS.includes(id), `missing ${id}`);
46
+ assert.ok(findBrowserSource(id), `unresolvable ${id}`);
47
+ }
48
+ });
49
+
50
+ it('rejects an unknown browser with the catalogue listed', () => {
51
+ assert.throws(
52
+ () => normalizeBrowserId('netscape'),
53
+ /Unsupported browser: netscape\. Expected one of .*chrome/
54
+ );
55
+ });
56
+
57
+ it('classifies browser families', () => {
58
+ assert.equal(browserFamily('chrome'), 'chromium');
59
+ assert.equal(browserFamily('opera'), 'chromium');
60
+ assert.equal(browserFamily('firefox'), 'firefox');
61
+ assert.equal(browserFamily('librewolf'), 'firefox');
62
+ });
63
+
64
+ // POSIX targets join with the host separator, so a Windows host running
65
+ // these Linux/macOS cases yields backslashes, like every other path here.
66
+ it('expands per-platform roots with the home directory', () => {
67
+ assert.deepEqual(
68
+ resolveBrowserRoots('chrome', {
69
+ platform: 'linux',
70
+ homeDir: '/home/me',
71
+ environment: {},
72
+ }),
73
+ [path.join('/home/me', '.config', 'google-chrome')]
74
+ );
75
+ assert.deepEqual(
76
+ resolveBrowserRoots('firefox', {
77
+ platform: 'darwin',
78
+ homeDir: '/Users/me',
79
+ }),
80
+ [path.join('/Users/me', 'Library', 'Application Support', 'Firefox')]
81
+ );
82
+ });
83
+
84
+ it('honours XDG_CONFIG_HOME on Linux', () => {
85
+ assert.deepEqual(
86
+ resolveBrowserRoots('chromium', {
87
+ platform: 'linux',
88
+ homeDir: '/home/me',
89
+ environment: { XDG_CONFIG_HOME: '/cfg' },
90
+ }),
91
+ [path.join('/cfg', 'chromium')]
92
+ );
93
+ });
94
+
95
+ it('builds Windows roots with backslashes from %APPDATA%/%LOCALAPPDATA%', () => {
96
+ assert.deepEqual(
97
+ resolveBrowserRoots('opera', {
98
+ platform: 'win32',
99
+ homeDir: 'C:\\Users\\me',
100
+ environment: { APPDATA: 'C:\\Users\\me\\AppData\\Roaming' },
101
+ }),
102
+ ['C:\\Users\\me\\AppData\\Roaming\\Opera Software\\Opera Stable']
103
+ );
104
+ assert.deepEqual(
105
+ resolveBrowserRoots('chrome', {
106
+ platform: 'win32',
107
+ homeDir: 'C:\\Users\\me',
108
+ environment: { LOCALAPPDATA: 'C:\\Users\\me\\AppData\\Local' },
109
+ }),
110
+ ['C:\\Users\\me\\AppData\\Local\\Google\\Chrome\\User Data']
111
+ );
112
+ });
113
+
114
+ it('returns no root where a browser does not run', () => {
115
+ assert.deepEqual(
116
+ resolveBrowserRoots('chrome-canary', {
117
+ platform: 'linux',
118
+ homeDir: '/home/me',
119
+ }),
120
+ []
121
+ );
122
+ assert.deepEqual(
123
+ resolveBrowserRoots('arc', { platform: 'linux', homeDir: '/home/me' }),
124
+ []
125
+ );
126
+ });
127
+
128
+ it('marks Opera-style browsers as single-profile', () => {
129
+ assert.equal(isSingleProfileBrowser('opera'), true);
130
+ assert.equal(isSingleProfileBrowser('opera-gx'), true);
131
+ assert.equal(isSingleProfileBrowser('chrome'), false);
132
+ assert.equal(isSingleProfileBrowser('vivaldi'), false);
133
+ });
134
+
135
+ it('exposes a Safe Storage identity for Chromium and none for Firefox', () => {
136
+ assert.deepEqual(safeStorageIdentity('brave'), {
137
+ service: 'Brave Safe Storage',
138
+ application: 'brave',
139
+ folder: 'Brave Keys',
140
+ });
141
+ assert.equal(safeStorageIdentity('firefox'), undefined);
142
+ });
143
+
144
+ it('exposes default-browser identifiers per platform', () => {
145
+ assert.deepEqual(defaultBrowserIdentifiers('chrome', 'darwin'), [
146
+ 'com.google.chrome',
147
+ ]);
148
+ assert.deepEqual(defaultBrowserIdentifiers('firefox', 'win32'), [
149
+ 'FirefoxHTML',
150
+ 'FirefoxURL',
151
+ ]);
152
+ assert.deepEqual(defaultBrowserIdentifiers('chrome', 'nope'), []);
153
+ });
154
+ });