browser-commander 0.20.0 → 0.21.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.
Files changed (169) hide show
  1. package/.jscpd.json +2 -1
  2. package/.prettierignore +1 -0
  3. package/CHANGELOG.md +159 -0
  4. package/README.md +216 -32
  5. package/bin/browser-commander.js +16 -0
  6. package/eslint.config.js +10 -0
  7. package/examples/react-test-app/src/App.jsx +24 -23
  8. package/extension/background.js +14 -0
  9. package/extension/manifest.json +12 -0
  10. package/extension/relay-handler.js +353 -0
  11. package/package.json +21 -4
  12. package/src/browser/attach/differences.js +131 -0
  13. package/src/browser/attach/extension-relay.js +402 -0
  14. package/src/browser/attach/index.js +81 -0
  15. package/src/browser/attach/snapshot-launch.js +146 -0
  16. package/src/browser/attach/snapshot.js +374 -0
  17. package/src/browser/attach/websocket.js +615 -0
  18. package/src/browser/browser-cookie-cache.js +26 -19
  19. package/src/browser/browser-cookie-credentials.js +6 -9
  20. package/src/browser/browser-cookies.js +5 -2
  21. package/src/browser/cdp-session.js +94 -0
  22. package/src/browser/connector.js +42 -4
  23. package/src/browser/debugging-port.js +155 -0
  24. package/src/browser/launch-options.js +63 -52
  25. package/src/browser/launch-restrictions.json +147 -0
  26. package/src/browser/launcher.js +329 -133
  27. package/src/browser/media.js +5 -0
  28. package/src/browser/migration/bookmarks.js +73 -0
  29. package/src/browser/migration/chromium-crypto.js +89 -0
  30. package/src/browser/migration/cookies.js +155 -0
  31. package/src/browser/migration/extensions.js +187 -0
  32. package/src/browser/migration/firefox-bookmarks.js +144 -0
  33. package/src/browser/migration/firefox-der.js +84 -0
  34. package/src/browser/migration/firefox-nss.js +281 -0
  35. package/src/browser/migration/firefox.js +334 -0
  36. package/src/browser/migration/fs-utils.js +51 -0
  37. package/src/browser/migration/history.js +103 -0
  38. package/src/browser/migration/index.js +307 -0
  39. package/src/browser/migration/os-crypt-keys.js +106 -0
  40. package/src/browser/migration/passwords.js +171 -0
  41. package/src/browser/migration/preferences.js +132 -0
  42. package/src/browser/migration/sqlite-snapshot.js +138 -0
  43. package/src/browser/open-in-user-browser.js +112 -0
  44. package/src/browser/parity.js +439 -0
  45. package/src/browser/profile-directory.js +205 -0
  46. package/src/browser/real-browser.js +545 -306
  47. package/src/browser/restrictions.js +148 -0
  48. package/src/browser/system-browser.js +232 -0
  49. package/src/browser/webdriver.js +572 -0
  50. package/src/cli/args.js +300 -0
  51. package/src/cli/commands.js +332 -0
  52. package/src/cli/dispatcher.js +99 -0
  53. package/src/cli/handle-methods.js +136 -0
  54. package/src/cli/handles.js +273 -0
  55. package/src/cli/high-level-methods.js +310 -0
  56. package/src/cli/main.js +67 -0
  57. package/src/cli/modules.js +100 -0
  58. package/src/cli/rpc-error.js +77 -0
  59. package/src/cli/script.js +77 -0
  60. package/src/cli/serve.js +144 -0
  61. package/src/cli/sessions.js +199 -0
  62. package/src/core/constants.js +7 -1
  63. package/src/core/engine-adapter.js +107 -1
  64. package/src/core/engine-detection.js +18 -3
  65. package/src/core/navigation-safety.js +4 -0
  66. package/src/core/webdriver-conversions.js +347 -0
  67. package/src/core/webdriver-events.js +229 -0
  68. package/src/core/webdriver-page.js +716 -0
  69. package/src/downloads/attach.js +6 -0
  70. package/src/elements/selectors.js +3 -2
  71. package/src/elements/visibility.js +4 -0
  72. package/src/exports.js +61 -2
  73. package/src/factory.js +19 -4
  74. package/src/fingerprint/apply.js +6 -0
  75. package/src/fingerprint/automation-parity.js +7 -8
  76. package/src/fingerprint/limitations.js +1 -1
  77. package/src/fingerprint/limitations.json +45 -2
  78. package/src/parity/harness.js +303 -0
  79. package/src/parity/probe.js +838 -0
  80. package/src/parity/version-page.js +144 -0
  81. package/src/traces/bundle.js +3 -2
  82. package/src/traces/observers.js +14 -5
  83. package/src/traces/recorder.js +3 -0
  84. package/src/traces/viewer.js +16 -8
  85. package/src/utilities/subprocess.js +199 -0
  86. package/tests/e2e/api-coverage.e2e.test.js +174 -0
  87. package/tests/e2e/cli.e2e.test.js +160 -0
  88. package/tests/e2e/fingerprint-parity.e2e.test.js +104 -46
  89. package/tests/e2e/google-signin-probe.e2e.test.js +112 -0
  90. package/tests/e2e/measure-parity.e2e.test.js +118 -0
  91. package/tests/e2e/playwright.e2e.test.js +26 -17
  92. package/tests/e2e/puppeteer.e2e.test.js +18 -23
  93. package/tests/e2e/webdriver.e2e.test.js +239 -0
  94. package/tests/fixtures/firefox-nss-fixtures.mjs +211 -0
  95. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.html +2 -0
  96. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.png +1 -0
  97. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0001.state.json +54 -0
  98. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.html +2 -0
  99. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.png +1 -0
  100. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0002.state.json +60 -0
  101. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.html +2 -0
  102. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.png +1 -0
  103. package/tests/fixtures/traces/conformance/expected/bundle/checkpoints/0003.state.json +60 -0
  104. package/tests/fixtures/traces/conformance/expected/bundle/events.ndjson +18 -0
  105. package/tests/fixtures/traces/conformance/expected/bundle/manifest.json +77 -0
  106. package/tests/fixtures/traces/conformance/expected/bundle/mutations/0001.ndjson +2 -0
  107. package/tests/fixtures/traces/conformance/expected/bundle/mutations/0002.ndjson +1 -0
  108. package/tests/fixtures/traces/conformance/expected/bundle/viewer.html +309 -0
  109. package/tests/fixtures/traces/conformance/expected/redaction.json +86 -0
  110. package/tests/fixtures/traces/conformance/expected/trace.lino +27 -0
  111. package/tests/fixtures/traces/conformance/scenario.json +249 -0
  112. package/tests/helpers/cli-fakes.js +214 -0
  113. package/tests/helpers/declared-api.js +80 -0
  114. package/tests/helpers/e2e-browser.js +40 -14
  115. package/tests/helpers/fake-chrome.js +124 -0
  116. package/tests/helpers/migration-fixtures.js +242 -0
  117. package/tests/helpers/temp-directory.js +24 -0
  118. package/tests/helpers/webdriver-mocks.js +183 -0
  119. package/tests/unit/browser/attach/extension-relay.test.js +360 -0
  120. package/tests/unit/browser/attach/snapshot-launch.test.js +321 -0
  121. package/tests/unit/browser/attach/snapshot.test.js +409 -0
  122. package/tests/unit/browser/attach/websocket.test.js +240 -0
  123. package/tests/unit/browser/browser-cookie-lock.test.js +102 -0
  124. package/tests/unit/browser/browser-cookies.test.js +2 -32
  125. package/tests/unit/browser/cdp-session.test.js +162 -0
  126. package/tests/unit/browser/connector.test.js +23 -0
  127. package/tests/unit/browser/launch-building-blocks.test.js +361 -0
  128. package/tests/unit/browser/launch-options.test.js +41 -22
  129. package/tests/unit/browser/launcher.test.js +281 -102
  130. package/tests/unit/browser/media.test.js +13 -1
  131. package/tests/unit/browser/migration/bookmarks.test.js +69 -0
  132. package/tests/unit/browser/migration/chromium-crypto.test.js +94 -0
  133. package/tests/unit/browser/migration/cookies.test.js +94 -0
  134. package/tests/unit/browser/migration/extensions.test.js +104 -0
  135. package/tests/unit/browser/migration/firefox-nss.test.js +113 -0
  136. package/tests/unit/browser/migration/firefox.test.js +139 -0
  137. package/tests/unit/browser/migration/history.test.js +48 -0
  138. package/tests/unit/browser/migration/index.test.js +117 -0
  139. package/tests/unit/browser/migration/os-crypt-keys.test.js +95 -0
  140. package/tests/unit/browser/migration/passwords.test.js +144 -0
  141. package/tests/unit/browser/migration/preferences.test.js +77 -0
  142. package/tests/unit/browser/migration/sqlite-snapshot.test.js +75 -0
  143. package/tests/unit/browser/open-in-user-browser.test.js +123 -0
  144. package/tests/unit/browser/parity.test.js +281 -0
  145. package/tests/unit/browser/real-browser.test.js +458 -33
  146. package/tests/unit/browser/storage-state.test.js +2 -0
  147. package/tests/unit/browser/webdriver.test.js +473 -0
  148. package/tests/unit/cli/bin.test.js +69 -0
  149. package/tests/unit/cli/declared-api.test.js +49 -0
  150. package/tests/unit/cli/dispatcher.test.js +429 -0
  151. package/tests/unit/cli/handles.test.js +170 -0
  152. package/tests/unit/cli/main.test.js +437 -0
  153. package/tests/unit/cli/serve.test.js +167 -0
  154. package/tests/unit/core/navigation-manager.test.js +11 -1
  155. package/tests/unit/core/webdriver-conversions.test.js +235 -0
  156. package/tests/unit/core/webdriver-page.test.js +534 -0
  157. package/tests/unit/fingerprint/automation-parity.test.js +17 -4
  158. package/tests/unit/fingerprint/limitations.test.js +1 -1
  159. package/tests/unit/parity/harness.test.js +138 -0
  160. package/tests/unit/playwright-protocol-coverage.test.js +231 -0
  161. package/tests/unit/puppeteer-bindings-coverage.test.js +214 -0
  162. package/tests/unit/scripts/ci-timeout-budgets.test.js +27 -0
  163. package/tests/unit/scripts/feature-parity.test.js +327 -0
  164. package/tests/unit/traces/conformance.test.js +92 -0
  165. package/tests/unit/traces/reader.test.js +1 -0
  166. package/tests/unit/traces/recorder.test.js +1 -0
  167. package/tests/unit/traces/viewer.test.js +78 -0
  168. package/tests/unit/utilities/command-stream-only.test.js +28 -0
  169. package/tests/unit/utilities/subprocess.test.js +80 -0
@@ -0,0 +1,124 @@
1
+ /**
2
+ * A fake `chrome` namespace and browser `WebSocket` for the companion
3
+ * extension tests (issue #102, addendum, mode 2).
4
+ *
5
+ * The extension code in js/extension only touches the handful of APIs below,
6
+ * so the relay handler and its service-worker client run unchanged in Node.
7
+ */
8
+ import { connectWebSocket } from '../../src/browser/attach/websocket.js';
9
+
10
+ /** A `chrome.*.onSomething` event whose listeners the test can fire. */
11
+ function fakeEvent() {
12
+ const listeners = [];
13
+ return {
14
+ listeners,
15
+ addListener: (listener) => listeners.push(listener),
16
+ fire: (...args) => listeners.forEach((listener) => listener(...args)),
17
+ };
18
+ }
19
+
20
+ /**
21
+ * @param {Object} [options]
22
+ * @param {Array<Object>} [options.tabs] - `chrome.tabs.Tab`-like objects
23
+ * @param {Object} [options.storage] - `chrome.storage.local` contents
24
+ * @returns {Object} The fake namespace; `calls` records every API call
25
+ */
26
+ export function createFakeChrome({ tabs = [], storage = {} } = {}) {
27
+ const calls = [];
28
+ let nextTabId = 100;
29
+ const chrome = {
30
+ calls,
31
+ tabs: {
32
+ query: async (query) => {
33
+ calls.push(['tabs.query', query]);
34
+ return tabs;
35
+ },
36
+ create: async (properties) => {
37
+ calls.push(['tabs.create', properties]);
38
+ const tab = { id: nextTabId++, pendingUrl: properties.url };
39
+ tabs.push(tab);
40
+ return tab;
41
+ },
42
+ },
43
+ debugger: {
44
+ attach: async (target, version) => {
45
+ calls.push(['debugger.attach', target.tabId, version]);
46
+ },
47
+ detach: async (target) => {
48
+ calls.push(['debugger.detach', target.tabId]);
49
+ },
50
+ sendCommand: async (target, method, params) => {
51
+ calls.push(['debugger.sendCommand', target.tabId, method, params]);
52
+ if (method === 'Bad.method') {
53
+ throw new Error("'Bad.method' wasn't found");
54
+ }
55
+ return method === 'Runtime.evaluate'
56
+ ? { result: { type: 'number', value: 2 } }
57
+ : undefined;
58
+ },
59
+ onEvent: fakeEvent(),
60
+ onDetach: fakeEvent(),
61
+ },
62
+ alarms: {
63
+ create: (name, info) => calls.push(['alarms.create', name, info]),
64
+ onAlarm: fakeEvent(),
65
+ },
66
+ storage: {
67
+ local: { get: async (key) => ({ [key]: storage[key] }) },
68
+ onChanged: fakeEvent(),
69
+ },
70
+ runtime: {
71
+ getManifest: () => ({ version: '1.2.3' }),
72
+ onStartup: fakeEvent(),
73
+ onInstalled: fakeEvent(),
74
+ },
75
+ };
76
+ return chrome;
77
+ }
78
+
79
+ /**
80
+ * The browser `WebSocket` API on top of {@link connectWebSocket}, sending the
81
+ * `Origin` an extension service worker sends.
82
+ */
83
+ export function createExtensionWebSocket(origin = 'chrome-extension://abc') {
84
+ return class ExtensionWebSocket {
85
+ static CONNECTING = 0;
86
+ static OPEN = 1;
87
+ static CLOSED = 3;
88
+
89
+ constructor(url) {
90
+ this.url = url;
91
+ this.readyState = ExtensionWebSocket.CONNECTING;
92
+ connectWebSocket(url, { headers: { Origin: origin } }).then(
93
+ (connection) => {
94
+ this.connection = connection;
95
+ this.readyState = ExtensionWebSocket.OPEN;
96
+ connection.on('message', (data) => this.onmessage?.({ data }));
97
+ connection.on('error', () => {});
98
+ connection.once('close', () => this.closed());
99
+ this.onopen?.();
100
+ },
101
+ (error) => {
102
+ this.error = error;
103
+ this.onerror?.(error);
104
+ this.closed();
105
+ }
106
+ );
107
+ }
108
+
109
+ send(text) {
110
+ this.connection.send(text);
111
+ }
112
+
113
+ close() {
114
+ this.connection?.close();
115
+ }
116
+
117
+ closed() {
118
+ if (this.readyState !== ExtensionWebSocket.CLOSED) {
119
+ this.readyState = ExtensionWebSocket.CLOSED;
120
+ this.onclose?.();
121
+ }
122
+ }
123
+ };
124
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Profile fixtures for the migration tests.
3
+ *
4
+ * Every migration suite moves data from a source profile into a target one and
5
+ * checks the same report shape, and the Firefox suites (plus the installed
6
+ * browser cookie reader) need the same `cookies.sqlite`, `places.sqlite` and
7
+ * `logins.json` files, so they are built here once instead of per test file.
8
+ */
9
+
10
+ import assert from 'node:assert';
11
+ import { readFile, stat, writeFile } from 'node:fs/promises';
12
+ import path from 'node:path';
13
+
14
+ import BetterSqlite3 from 'better-sqlite3';
15
+
16
+ import { decryptChromiumCookie } from '../../src/browser/browser-cookie-crypto.js';
17
+ import { openSqliteDatabase } from '../../src/browser/browser-cookie-database.js';
18
+ import {
19
+ buildKey4Database,
20
+ buildLoginsJson,
21
+ } from '../fixtures/firefox-nss-fixtures.mjs';
22
+
23
+ /**
24
+ * Run a Chromium migration step from one profile directory into another.
25
+ *
26
+ * @param {Function} migrate - A `migrate*` data-class function
27
+ * @param {string} source - Source profile directory
28
+ * @param {string} target - Target profile directory
29
+ * @param {Object} [options] - Extra options for the step
30
+ * @returns {Promise<Object>} The step's report
31
+ */
32
+ export function migrateBetween(migrate, source, target, options = {}) {
33
+ return migrate({
34
+ sourceProfileDir: source,
35
+ targetProfileDir: target,
36
+ ...options,
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Assert that a step migrated nothing, and why.
42
+ *
43
+ * @param {Object} report - A data-class report
44
+ * @param {string} reason - The expected first skip reason
45
+ */
46
+ export function assertNothingMigrated(report, reason) {
47
+ assert.equal(report.migrated, 0);
48
+ assert.equal(report.skipped[0].reason, reason);
49
+ }
50
+
51
+ /**
52
+ * Assert that `action` leaves a source file untouched. Migration only ever
53
+ * reads a snapshot of the source, so its modification time must not move.
54
+ *
55
+ * @param {string} filePath - The source file
56
+ * @param {() => Promise<void>} action
57
+ */
58
+ export async function assertSourceUnchanged(filePath, action) {
59
+ const before = (await stat(filePath)).mtimeMs;
60
+ await action();
61
+ const after = (await stat(filePath)).mtimeMs;
62
+ assert.equal(before, after);
63
+ }
64
+
65
+ /**
66
+ * Write a JSON profile file such as `Bookmarks` or `Preferences`.
67
+ *
68
+ * @param {string} profileDir
69
+ * @param {string} name - File name relative to the profile
70
+ * @param {*} value - Serialised with `JSON.stringify`
71
+ * @returns {Promise<void>}
72
+ */
73
+ export function writeProfileJson(profileDir, name, value) {
74
+ return writeFile(path.join(profileDir, name), JSON.stringify(value));
75
+ }
76
+
77
+ /**
78
+ * Read and parse a JSON profile file a migration wrote.
79
+ *
80
+ * @param {string} profileDir
81
+ * @param {string} name - File name relative to the profile
82
+ * @returns {Promise<*>}
83
+ */
84
+ export async function readProfileJson(profileDir, name) {
85
+ return JSON.parse(await readFile(path.join(profileDir, name), 'utf8'));
86
+ }
87
+
88
+ /**
89
+ * Write a Chromium `History` database with `urlCount` visited URLs.
90
+ *
91
+ * @param {string} profileDir - Chromium profile directory
92
+ * @param {number} urlCount
93
+ * @returns {string} The database path
94
+ */
95
+ export function writeChromiumHistory(profileDir, urlCount) {
96
+ const historyPath = path.join(profileDir, 'History');
97
+ const db = new BetterSqlite3(historyPath);
98
+ db.exec('CREATE TABLE urls (id INTEGER PRIMARY KEY, url TEXT, title TEXT)');
99
+ const insert = db.prepare('INSERT INTO urls (url, title) VALUES (?, ?)');
100
+ for (let i = 0; i < urlCount; i += 1) {
101
+ insert.run(`https://example.com/${i}`, `Page ${i}`);
102
+ }
103
+ db.close();
104
+ return historyPath;
105
+ }
106
+
107
+ /**
108
+ * Write a Firefox `cookies.sqlite` holding the given rows.
109
+ *
110
+ * @param {string} profileDir - Firefox profile directory
111
+ * @param {Array<Object>} rows - `{name, value, host}` plus optional `path`,
112
+ * `expiry`, `secure`, `httpOnly` and `sameSite`
113
+ * @returns {Promise<string>} The database path
114
+ */
115
+ export async function writeFirefoxCookies(profileDir, rows) {
116
+ const cookiePath = path.join(profileDir, 'cookies.sqlite');
117
+ const database = await openSqliteDatabase(cookiePath);
118
+ database.exec(`
119
+ CREATE TABLE moz_cookies (
120
+ name TEXT,
121
+ value TEXT,
122
+ host TEXT,
123
+ path TEXT,
124
+ expiry INTEGER,
125
+ isSecure INTEGER,
126
+ isHttpOnly INTEGER,
127
+ sameSite INTEGER
128
+ );
129
+ `);
130
+ const insert = database.prepare(`
131
+ INSERT INTO moz_cookies
132
+ (name, value, host, path, expiry, isSecure, isHttpOnly, sameSite)
133
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
134
+ `);
135
+ for (const row of rows) {
136
+ insert.run(
137
+ row.name,
138
+ row.value,
139
+ row.host,
140
+ row.path ?? '/',
141
+ row.expiry ?? 0,
142
+ row.secure ? 1 : 0,
143
+ row.httpOnly ? 1 : 0,
144
+ row.sameSite ?? 0
145
+ );
146
+ }
147
+ database.close();
148
+ return cookiePath;
149
+ }
150
+
151
+ /**
152
+ * Write a Firefox `places.sqlite` with a toolbar bookmark and, unless
153
+ * `withMenuBookmark` is false, a bookmarks-menu one. Each bookmark has its own
154
+ * `moz_places` row, so the history count equals the bookmark count.
155
+ *
156
+ * @param {string} profileDir - Firefox profile directory
157
+ * @param {Object} [options]
158
+ * @param {boolean} [options.withMenuBookmark=true]
159
+ */
160
+ export function writeFirefoxPlaces(
161
+ profileDir,
162
+ { withMenuBookmark = true } = {}
163
+ ) {
164
+ const db = new BetterSqlite3(path.join(profileDir, 'places.sqlite'));
165
+ db.exec(
166
+ `CREATE TABLE moz_places (id INTEGER PRIMARY KEY, url TEXT);
167
+ CREATE TABLE moz_bookmarks (id INTEGER PRIMARY KEY, type INTEGER, fk INTEGER,
168
+ parent INTEGER, position INTEGER, title TEXT, guid TEXT);`
169
+ );
170
+ const insertPlace = db.prepare(
171
+ 'INSERT INTO moz_places (id, url) VALUES (?, ?)'
172
+ );
173
+ const insertBookmark = db.prepare(
174
+ 'INSERT INTO moz_bookmarks (id, type, fk, parent, position, title, guid) VALUES (?, ?, ?, ?, ?, ?, ?)'
175
+ );
176
+ insertBookmark.run(1, 2, null, 0, 0, '', 'root________');
177
+ insertBookmark.run(3, 2, null, 1, 0, 'Bookmarks Toolbar', 'toolbar_____');
178
+ insertPlace.run(10, 'https://toolbar.example/');
179
+ insertBookmark.run(100, 1, 10, 3, 0, 'Toolbar Site', 'aaaaaaaaaaaa');
180
+ if (withMenuBookmark) {
181
+ insertBookmark.run(2, 2, null, 1, 1, 'Bookmarks Menu', 'menu________');
182
+ insertPlace.run(11, 'https://menu.example/');
183
+ insertBookmark.run(101, 1, 11, 2, 0, 'Menu Site', 'bbbbbbbbbbbb');
184
+ }
185
+ db.close();
186
+ }
187
+
188
+ /**
189
+ * Write a Firefox `key4.db` and a `logins.json` encrypted with its login key.
190
+ *
191
+ * @param {string} profileDir - Firefox profile directory
192
+ * @param {Object} options
193
+ * @param {Array<{hostname: string, username: string, password: string}>} options.entries
194
+ * @param {Buffer} [options.primaryPassword] - Locks the key when set
195
+ * @returns {Promise<{key4Path: string, loginKey: Buffer}>}
196
+ */
197
+ export async function writeFirefoxLogins(
198
+ profileDir,
199
+ { entries, primaryPassword }
200
+ ) {
201
+ const fixture = buildKey4Database({ dir: profileDir, primaryPassword });
202
+ await writeFile(
203
+ path.join(profileDir, 'logins.json'),
204
+ buildLoginsJson({ key: fixture.loginKey, entries })
205
+ );
206
+ return fixture;
207
+ }
208
+
209
+ /**
210
+ * Read back the logins a migration wrote to a target `Login Data`, decrypting
211
+ * each password with the target profile's key (Linux `v10`/`v11` scheme).
212
+ *
213
+ * @param {string} targetProfileDir
214
+ * @param {Buffer} targetKey
215
+ * @returns {Array<{origin: string, username: string, password: string}>}
216
+ * Sorted by origin
217
+ */
218
+ export function readMigratedLogins(targetProfileDir, targetKey) {
219
+ const db = new BetterSqlite3(path.join(targetProfileDir, 'Login Data'), {
220
+ readonly: true,
221
+ });
222
+ try {
223
+ return db
224
+ .prepare(
225
+ 'SELECT origin_url, username_value, password_value FROM logins ORDER BY origin_url'
226
+ )
227
+ .all()
228
+ .map((row) => ({
229
+ origin: row.origin_url,
230
+ username: row.username_value,
231
+ password: decryptChromiumCookie({
232
+ encryptedValue: row.password_value,
233
+ host: new URL(row.origin_url).hostname,
234
+ databaseVersion: 0,
235
+ platform: 'linux',
236
+ key: targetKey,
237
+ }),
238
+ }));
239
+ } finally {
240
+ db.close();
241
+ }
242
+ }
@@ -42,3 +42,27 @@ export function useTempDirectory(prefix) {
42
42
 
43
43
  return holder;
44
44
  }
45
+
46
+ /**
47
+ * Let each test create as many directories as it needs (a source and a target
48
+ * profile, say), and remove all of them after the test.
49
+ *
50
+ * @param {string} [prefix] - Default name prefix for the directories
51
+ * @returns {(prefix?: string) => Promise<string>} Creates and tracks one more
52
+ * directory, optionally with its own prefix
53
+ */
54
+ export function useTempDirectories(prefix) {
55
+ const created = [];
56
+
57
+ afterEach(async () => {
58
+ while (created.length > 0) {
59
+ await fs.rm(created.pop(), { recursive: true, force: true });
60
+ }
61
+ });
62
+
63
+ return async (directoryPrefix = prefix) => {
64
+ const directory = await makeTempDirectory(directoryPrefix);
65
+ created.push(directory);
66
+ return directory;
67
+ };
68
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Stand-ins for a selenium-webdriver WebDriver, its WebElements and its BiDi
3
+ * connection (issue #104). Every call is recorded in `calls` so tests can
4
+ * assert on the WebDriver commands an operation produced.
5
+ */
6
+
7
+ /**
8
+ * A mock WebElement.
9
+ *
10
+ * @param {Object} [options]
11
+ * @param {boolean} [options.displayed=true]
12
+ * @param {Array} [calls] - Shared call log
13
+ * @returns {Object}
14
+ */
15
+ export function createMockWebElement(
16
+ { displayed = true, id = 'el' } = {},
17
+ calls = []
18
+ ) {
19
+ return {
20
+ id,
21
+ displayed,
22
+ click: async () => {
23
+ calls.push(['element.click', id]);
24
+ },
25
+ sendKeys: async (...keys) => {
26
+ calls.push(['element.sendKeys', id, ...keys]);
27
+ },
28
+ clear: async () => {
29
+ calls.push(['element.clear', id]);
30
+ },
31
+ async isDisplayed() {
32
+ return this.displayed;
33
+ },
34
+ };
35
+ }
36
+
37
+ function createMockActions(calls) {
38
+ const steps = [];
39
+ const chain = {};
40
+ for (const name of [
41
+ 'move',
42
+ 'press',
43
+ 'release',
44
+ 'keyDown',
45
+ 'keyUp',
46
+ 'sendKeys',
47
+ 'pause',
48
+ 'scroll',
49
+ 'click',
50
+ ]) {
51
+ chain[name] = (...args) => {
52
+ steps.push([name, ...args]);
53
+ return chain;
54
+ };
55
+ }
56
+ chain.perform = async () => {
57
+ calls.push(['actions', steps]);
58
+ };
59
+ return chain;
60
+ }
61
+
62
+ /**
63
+ * A mock WebDriver.
64
+ *
65
+ * @param {Object} [options]
66
+ * @param {string} [options.url='about:blank']
67
+ * @param {Object} [options.capabilities] - e.g. {webSocketUrl: 'ws://...'}
68
+ * @param {Object<string, Object[]>} [options.elements] - CSS selector -> elements
69
+ * @param {Function} [options.script] - (script, args) => value returned by the page function
70
+ * @returns {Object}
71
+ */
72
+ export function createMockDriver({
73
+ url = 'about:blank',
74
+ capabilities = {},
75
+ elements = {},
76
+ script = () => undefined,
77
+ cookies = [],
78
+ } = {}) {
79
+ const calls = [];
80
+ const state = { url, cookies: [...cookies] };
81
+ const driver = {
82
+ calls,
83
+ state,
84
+ getCurrentUrl: async () => state.url,
85
+ get: async (target) => {
86
+ calls.push(['get', target]);
87
+ state.url = target;
88
+ },
89
+ getTitle: async () => 'Mock title',
90
+ getPageSource: async () => '<html></html>',
91
+ getWindowHandle: async () => 'context-1',
92
+ getCapabilities: async () => ({ get: (name) => capabilities[name] }),
93
+ findElements: async (locator) => {
94
+ calls.push(['findElements', locator]);
95
+ return elements[locator.css] ?? [];
96
+ },
97
+ executeScript: async (source, ...args) => {
98
+ calls.push(['executeScript', source, args]);
99
+ return [await script(source, args), state.url];
100
+ },
101
+ manage: () => ({
102
+ getCookies: async () => state.cookies,
103
+ addCookie: async (cookie) => {
104
+ calls.push(['addCookie', cookie]);
105
+ state.cookies.push(cookie);
106
+ },
107
+ deleteCookie: async (name) => {
108
+ calls.push(['deleteCookie', name]);
109
+ state.cookies = state.cookies.filter((c) => c.name !== name);
110
+ },
111
+ setTimeouts: async (timeouts) => {
112
+ calls.push(['setTimeouts', timeouts]);
113
+ },
114
+ }),
115
+ navigate: () => ({
116
+ back: async () => calls.push(['back']),
117
+ forward: async () => calls.push(['forward']),
118
+ refresh: async () => calls.push(['refresh']),
119
+ }),
120
+ switchTo: () => ({
121
+ window: async (handle) => calls.push(['switchTo.window', handle]),
122
+ }),
123
+ actions: (options) => {
124
+ calls.push(['actions.new', options]);
125
+ return createMockActions(calls);
126
+ },
127
+ takeScreenshot: async () => Buffer.from('png').toString('base64'),
128
+ printPage: async (options) => {
129
+ calls.push(['printPage', options]);
130
+ return Buffer.from('%PDF-mock').toString('base64');
131
+ },
132
+ close: async () => calls.push(['close']),
133
+ quit: async () => calls.push(['quit']),
134
+ };
135
+ return driver;
136
+ }
137
+
138
+ /**
139
+ * A mock BiDi connection: `send` answers from `responses`, `addCallback`
140
+ * records handlers that `fire(method, params)` invokes.
141
+ *
142
+ * @param {Object} [options]
143
+ * @param {Object<string, Object|Function>} [options.responses] - method -> result (or params => result)
144
+ * @param {string[]} [options.unsupported] - Event methods that fail to subscribe
145
+ * @returns {Object}
146
+ */
147
+ export function createMockBidi({ responses = {}, unsupported = [] } = {}) {
148
+ const sent = [];
149
+ const callbacks = new Map();
150
+ const unsubscribed = [];
151
+ return {
152
+ sent,
153
+ callbacks,
154
+ unsubscribed,
155
+ send: async ({ method, params }) => {
156
+ sent.push({ method, params });
157
+ const response = responses[method];
158
+ if (response?.error) {
159
+ return response;
160
+ }
161
+ return {
162
+ type: 'success',
163
+ result:
164
+ typeof response === 'function' ? response(params) : (response ?? {}),
165
+ };
166
+ },
167
+ addCallback: async (method, handler) => {
168
+ if (unsupported.includes(method)) {
169
+ throw new Error(`invalid argument: ${method} is not a known event`);
170
+ }
171
+ callbacks.set(method, handler);
172
+ return {
173
+ unsubscribe: async () => {
174
+ unsubscribed.push(method);
175
+ callbacks.delete(method);
176
+ },
177
+ };
178
+ },
179
+ fire(method, params) {
180
+ callbacks.get(method)?.(params);
181
+ },
182
+ };
183
+ }