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,186 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { createDispatcher } from '../../../src/cli/dispatcher.js';
4
+ import { buildLaunchOptions } from '../../../src/cli/sessions.js';
5
+ import { launchBrowserWithDependencies } from '../../../src/browser/launcher.js';
6
+ import { connectBrowserWithDependencies } from '../../../src/browser/connector.js';
7
+
8
+ // feature-parity: engines.webdriver@native-typed engines.cli-matrix@native-typed
9
+
10
+ describe('Selenium uses the common engine entry points', () => {
11
+ it('restores portable cookies and origin state through the common launcher', async () => {
12
+ const visited = [];
13
+ const seeded = [];
14
+ const page = {
15
+ url: () => 'about:blank',
16
+ goto: async (url) => {
17
+ visited.push(url);
18
+ },
19
+ setCookie: async (...cookies) => {
20
+ seeded.push(...cookies);
21
+ },
22
+ evaluateOnNewDocument: async () => {},
23
+ evaluate: async () => {},
24
+ bringToFront: async () => {},
25
+ };
26
+ const cookie = {
27
+ name: 'session',
28
+ value: 'value',
29
+ domain: 'example.com',
30
+ path: '/',
31
+ secure: true,
32
+ };
33
+ const result = await launchBrowserWithDependencies(
34
+ {
35
+ engine: 'selenium',
36
+ launch: 'engine',
37
+ storageState: {
38
+ cookies: [cookie],
39
+ origins: [
40
+ {
41
+ origin: 'https://example.com',
42
+ localStorage: [{ name: 'theme', value: 'dark' }],
43
+ },
44
+ ],
45
+ },
46
+ },
47
+ {
48
+ launchWebDriver: async () => ({
49
+ driver: {},
50
+ page,
51
+ close: async () => {},
52
+ }),
53
+ settleMs: 0,
54
+ }
55
+ );
56
+ assert.deepEqual(seeded, [cookie]);
57
+ assert.deepEqual(visited, ['https://example.com', 'about:blank']);
58
+ await result.close();
59
+ });
60
+ it('preserves WebDriver browser and driver options in the CLI', () => {
61
+ assert.deepEqual(
62
+ buildLaunchOptions({
63
+ engine: 'selenium',
64
+ browser: 'firefox',
65
+ driverPath: '/driver',
66
+ bidi: true,
67
+ }),
68
+ {
69
+ engine: 'selenium',
70
+ headless: false,
71
+ browser: 'firefox',
72
+ driverPath: '/driver',
73
+ bidi: true,
74
+ }
75
+ );
76
+ });
77
+
78
+ it('launches through the native driver without loading a CDP engine', async () => {
79
+ const page = { bringToFront: async () => {} };
80
+ const driver = {};
81
+ let options;
82
+ let closed = false;
83
+ const result = await launchBrowserWithDependencies(
84
+ { engine: 'selenium', launch: 'engine', extraArgs: ['--no-sandbox'] },
85
+ {
86
+ launchWebDriver: async (value) => {
87
+ options = value;
88
+ return {
89
+ driver,
90
+ page,
91
+ close: async () => {
92
+ closed = true;
93
+ },
94
+ };
95
+ },
96
+ settleMs: 0,
97
+ }
98
+ );
99
+ assert.equal(result.browser, driver);
100
+ assert.equal(result.page, page);
101
+ assert.deepEqual(options.args, ['--no-sandbox']);
102
+ await result.close();
103
+ assert.equal(closed, true);
104
+ });
105
+
106
+ it('connects to a Selenium Grid through the common connector', async () => {
107
+ const driver = {};
108
+ const page = {};
109
+ let options;
110
+ const result = await connectBrowserWithDependencies(
111
+ {
112
+ engine: 'selenium',
113
+ serverUrl: 'http://grid:4444',
114
+ capabilities: { browserName: 'firefox' },
115
+ },
116
+ {
117
+ connectWebDriver: async (value) => {
118
+ options = value;
119
+ return { driver, page, close: async () => {} };
120
+ },
121
+ }
122
+ );
123
+ assert.equal(result.browser, driver);
124
+ assert.equal(options.capabilities.browserName, 'firefox');
125
+ });
126
+
127
+ it('exposes the raw Selenium module and driver over generic handles', async () => {
128
+ const driver = { getTitle: async () => 'native driver' };
129
+ const page = {
130
+ waitForSelector: async () => ({
131
+ clear: async () => {},
132
+ sendKeys: async () => {},
133
+ }),
134
+ url: () => 'about:blank',
135
+ };
136
+ const dispatcher = createDispatcher({
137
+ dependencies: {
138
+ loadEngine: async (name) => {
139
+ assert.equal(name, 'selenium');
140
+ return { Builder: class Builder {} };
141
+ },
142
+ launchBrowser: async () => ({
143
+ browser: driver,
144
+ driver,
145
+ page,
146
+ close: async () => {},
147
+ }),
148
+ },
149
+ });
150
+ try {
151
+ const root = await dispatcher.dispatch('handle.root', {
152
+ name: 'selenium',
153
+ });
154
+ assert.ok(root.$handle);
155
+ const builder = await dispatcher.dispatch('handle.get', {
156
+ handle: root,
157
+ property: 'Builder',
158
+ });
159
+ const instance = await dispatcher.dispatch('handle.construct', {
160
+ handle: builder,
161
+ });
162
+ assert.equal(
163
+ (await dispatcher.dispatch('handle.describe', { handle: instance }))
164
+ .type,
165
+ 'Builder'
166
+ );
167
+ await dispatcher.dispatch('session.launch', { engine: 'selenium' });
168
+ await dispatcher.dispatch('page.fill', {
169
+ selector: '#name',
170
+ value: 'Ada',
171
+ });
172
+ const handles = await dispatcher.dispatch('handle.root', {
173
+ name: 'session:s1',
174
+ });
175
+ assert.equal(
176
+ await dispatcher.dispatch('handle.call', {
177
+ handle: handles.browser,
178
+ method: 'getTitle',
179
+ }),
180
+ 'native driver'
181
+ );
182
+ } finally {
183
+ await dispatcher.close();
184
+ }
185
+ });
186
+ });
@@ -16,12 +16,13 @@ import { pathToFileURL } from 'node:url';
16
16
 
17
17
  import { repoPath } from '../../helpers/repo.js';
18
18
 
19
- const { BEGIN_MARKER, END_MARKER, readCatalogue, renderBrowserSupport } =
20
- await import(pathToFileURL(repoPath('scripts/generate-browser-support.mjs')));
19
+ const generator = await import(
20
+ pathToFileURL(repoPath('scripts/generate-browser-support.mjs'))
21
+ );
21
22
 
22
23
  describe('generate-browser-support', () => {
23
24
  it('renders a row per browser with per-platform availability', () => {
24
- const table = renderBrowserSupport([
25
+ const table = generator.renderBrowserSupport([
25
26
  {
26
27
  id: 'chrome',
27
28
  family: 'chromium',
@@ -47,10 +48,16 @@ describe('generate-browser-support', () => {
47
48
  const readme = fs
48
49
  .readFileSync(repoPath('README.md'), 'utf8')
49
50
  .replaceAll('\r\n', '\n');
50
- const begin = readme.indexOf(BEGIN_MARKER);
51
- const end = readme.indexOf(END_MARKER);
51
+ const begin = readme.indexOf(generator.BEGIN_MARKER);
52
+ const end = readme.indexOf(generator.END_MARKER);
52
53
  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('.'))));
54
+ const between = readme
55
+ .slice(begin + generator.BEGIN_MARKER.length, end)
56
+ .trim();
57
+ const browsers = generator.readCatalogue(repoPath('.'));
58
+ assert.equal(
59
+ between,
60
+ `${generator.renderBrowserSupport(browsers)}\n\n${generator.renderBrowserControls(browsers)}`
61
+ );
55
62
  });
56
63
  });
@@ -5,10 +5,12 @@
5
5
  * *failed*, and a run whose only casualty is a cancelled job is filed under
6
6
  * `cancelled` as well - run 24045269874 of this repository is one such run on
7
7
  * `main`. scripts/check-pipeline-status.sh is the only thing that looks at
8
- * that, so these tests pin the four readings it has to get right: a failure is
8
+ * that, so these tests pin the readings it has to get right: a failure is
9
9
  * always an error, a cancellation off the default branch is a warning, a
10
10
  * cancellation on the default branch is an error unless a newer run has already
11
11
  * taken over the branch head.
12
+ * Hosted-runner failures can report an undocumented abandoned result; it and
13
+ * other unexpected/missing results must fail on every branch.
12
14
  *
13
15
  * See docs/CI-TIMEOUT-BUDGETS.md.
14
16
  */
@@ -66,6 +68,36 @@ describe('check-pipeline-status.sh', { skip: !BASH_AVAILABLE }, () => {
66
68
  assert.match(output, /::error::Pipeline failed\. Failing jobs: lint/);
67
69
  });
68
70
 
71
+ for (const env of [
72
+ { IS_MAIN: 'false' },
73
+ { IS_MAIN: 'true', RUN_SHA: 'abc', BRANCH_HEAD_SHA: 'def' },
74
+ ]) {
75
+ it(`fails on an abandoned runner job with IS_MAIN=${env.IS_MAIN}`, () => {
76
+ // Hosted-runner acquisition failure in run 37362314527 left the macOS
77
+ // check cancelled, but GitHub sent "abandoned" to the needs context.
78
+ const { status, output } = runGate(
79
+ results({ lint: 'success', test: 'abandoned', release: 'skipped' }),
80
+ env
81
+ );
82
+
83
+ assert.equal(status, 1);
84
+ assert.match(output, /::error::Pipeline has unexpected job results/);
85
+ assert.match(output, /test \(abandoned\)/);
86
+ assert.doesNotMatch(output, /All required jobs succeeded/);
87
+ });
88
+ }
89
+
90
+ for (const result of ['timed_out', null, undefined]) {
91
+ it(`rejects an unexpected result of ${result}`, () => {
92
+ const { status, output } = runGate(results({ test: result }));
93
+
94
+ assert.equal(status, 1);
95
+ assert.match(output, /::error::Pipeline has unexpected job results/);
96
+ assert.match(output, /test \(/);
97
+ assert.doesNotMatch(output, /All required jobs succeeded/);
98
+ });
99
+ }
100
+
69
101
  it('only warns about a cancellation off the default branch', () => {
70
102
  const { status, output } = runGate(results({ test: 'cancelled' }), {
71
103
  IS_MAIN: 'false',
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readFileSync } from 'node:fs';
3
+ import { it } from 'node:test';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { repoPath } from '../../helpers/repo.js';
6
+ import { ALL_DATA_CLASSES } from '../../../src/browser/migration/index.js';
7
+
8
+ const { BEGIN_MARKER, END_MARKER, readCapabilities, renderMigrationSupport } =
9
+ await import(
10
+ pathToFileURL(repoPath('scripts/generate-migration-support.mjs'))
11
+ );
12
+ it('generates each source/target/class cell and declares unsupported target writers', () => {
13
+ const capabilities = readCapabilities(repoPath('.'));
14
+ assert.deepEqual(ALL_DATA_CLASSES, capabilities.classes);
15
+ const table = renderMigrationSupport(capabilities);
16
+ assert.equal(table.split('\n').length, 11);
17
+ assert.match(table, /safari\s+\| chromium\s+\| seed\s+\| translate/u);
18
+ const doc = readFileSync(
19
+ repoPath('docs/profile-migration.md'),
20
+ 'utf8'
21
+ ).replaceAll('\r\n', '\n');
22
+ assert.equal(
23
+ doc
24
+ .slice(
25
+ doc.indexOf(BEGIN_MARKER) + BEGIN_MARKER.length,
26
+ doc.indexOf(END_MARKER)
27
+ )
28
+ .trim(),
29
+ table
30
+ );
31
+ });
@@ -26,12 +26,13 @@ describe('browser-commander/tests', () => {
26
26
  assert.deepStrictEqual(normalizeBrowserTestEngines(), [
27
27
  'playwright',
28
28
  'puppeteer',
29
+ 'selenium',
29
30
  ]);
30
31
  assert.deepStrictEqual(normalizeBrowserTestEngines('Playwright'), [
31
32
  'playwright',
32
33
  ]);
33
34
  assert.throws(
34
- () => normalizeBrowserTestEngines('selenium'),
35
+ () => normalizeBrowserTestEngines('unknown'),
35
36
  /Unsupported browser test engine/
36
37
  );
37
38
  });
@@ -544,8 +544,8 @@ describe('trace links export (issue #94)', () => {
544
544
  // The notation's formatter and parser decide whether an export can be read
545
545
  // back, so the range this package depends on and the version the tests
546
546
  // proved are the same range.
547
- assert.strictEqual(manifest.dependencies['links-notation'], '^0.20.0');
548
- assert.match(installed.version, /^0\.20\./);
547
+ assert.strictEqual(manifest.dependencies['links-notation'], '^0.22.0');
548
+ assert.match(installed.version, /^0\.22\./);
549
549
  });
550
550
 
551
551
  it('should export a bundle named by path as readily as an open one', async () => {