browser-commander 0.21.2 → 0.23.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,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
+ });
@@ -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
  });