browser-commander 0.24.0 → 0.26.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 (72) hide show
  1. package/.jscpd-baseline.json +1 -1
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +7 -4
  4. package/package.json +4 -1
  5. package/src/browser/attach/snapshot.js +4 -28
  6. package/src/browser/browser-cookie-credentials.js +15 -8
  7. package/src/browser/browser-cookies.js +36 -30
  8. package/src/browser/browser-profile-files.js +31 -0
  9. package/src/browser/browser-profiles.js +11 -13
  10. package/src/browser/browser-sources.js +39 -9
  11. package/src/browser/browser-sources.json +610 -25
  12. package/src/browser/launcher.js +14 -0
  13. package/src/browser/migration/capabilities.json +117 -0
  14. package/src/browser/migration/chromium-history.sql +22 -0
  15. package/src/browser/migration/chromium-writers.js +85 -0
  16. package/src/browser/migration/cookies.js +4 -1
  17. package/src/browser/migration/data-classes.js +68 -0
  18. package/src/browser/migration/domains.js +32 -0
  19. package/src/browser/migration/firefox-history.js +106 -0
  20. package/src/browser/migration/firefox.js +39 -25
  21. package/src/browser/migration/history.js +161 -2
  22. package/src/browser/migration/index.js +229 -83
  23. package/src/browser/migration/os-crypt-keys.js +1 -5
  24. package/src/browser/migration/password-metadata.js +95 -0
  25. package/src/browser/migration/passwords.js +78 -41
  26. package/src/browser/migration/safari-import.js +100 -0
  27. package/src/browser/migration/safari.js +110 -0
  28. package/src/browser/migration/sqlite-snapshot.js +31 -65
  29. package/src/browser/migration/validation.js +48 -0
  30. package/src/browser/real-browser.js +18 -1
  31. package/src/browser/safari-access.js +45 -0
  32. package/src/browser/safari-cookies.js +39 -36
  33. package/src/browser/safari-launch.js +42 -0
  34. package/src/browser/safari-profiles.js +83 -0
  35. package/src/browser/safari-support.js +98 -0
  36. package/src/browser/storage-state.js +28 -8
  37. package/src/browser/system-browser.js +31 -143
  38. package/src/browser/webdriver.js +64 -19
  39. package/src/cli/args.js +2 -0
  40. package/src/cli/commands.js +2 -0
  41. package/src/cli/high-level-methods.js +2 -0
  42. package/src/cli/sessions.js +5 -0
  43. package/src/core/webdriver-page.js +62 -0
  44. package/src/exports.js +6 -0
  45. package/src/traces/recorder.js +5 -0
  46. package/tests/e2e/safari.e2e.test.js +104 -0
  47. package/tests/unit/browser/attach/snapshot.test.js +30 -0
  48. package/tests/unit/browser/browser-cookies.test.js +35 -0
  49. package/tests/unit/browser/catalogue-launch.test.js +93 -0
  50. package/tests/unit/browser/cookie-sources.test.js +92 -17
  51. package/tests/unit/browser/credential-catalogue.test.js +50 -0
  52. package/tests/unit/browser/firefox-cookie-expiry.test.js +79 -0
  53. package/tests/unit/browser/migration/data-classes.test.js +89 -0
  54. package/tests/unit/browser/migration/domain-isolation.test.js +212 -0
  55. package/tests/unit/browser/migration/firefox-history.test.js +130 -0
  56. package/tests/unit/browser/migration/history-metadata.test.js +74 -0
  57. package/tests/unit/browser/migration/index.test.js +16 -1
  58. package/tests/unit/browser/migration/opera-keys.test.js +22 -0
  59. package/tests/unit/browser/migration/safari-data.test.js +143 -0
  60. package/tests/unit/browser/migration/sqlite-snapshot.test.js +56 -1
  61. package/tests/unit/browser/migration/validation.test.js +63 -0
  62. package/tests/unit/browser/migration/yandex.test.js +84 -0
  63. package/tests/unit/browser/real-browser.test.js +19 -0
  64. package/tests/unit/browser/safari-cookies.test.js +26 -1
  65. package/tests/unit/browser/safari-profiles.test.js +111 -0
  66. package/tests/unit/browser/safari.test.js +192 -0
  67. package/tests/unit/browser/webdriver.test.js +48 -3
  68. package/tests/unit/cli/dispatcher.test.js +5 -1
  69. package/tests/unit/cli/main.test.js +5 -0
  70. package/tests/unit/scripts/browser-support.test.js +14 -7
  71. package/tests/unit/scripts/check-pipeline-status.test.js +33 -1
  72. package/tests/unit/scripts/migration-support.test.js +31 -0
@@ -49,6 +49,22 @@ async function installed(browser = 'safari', legacy = false) {
49
49
  }
50
50
 
51
51
  describe('Safari cookies', { timeout: 10000 }, () => {
52
+ it('uses whole domains for discovery while retaining reader substring filters', async () => {
53
+ const options = await installed();
54
+ const sources = await listCookieSources({
55
+ ...options,
56
+ domains: ['hub.com', 'github.co'],
57
+ });
58
+ assert.deepEqual(sources, []);
59
+ assert.deepEqual(
60
+ await readBrowserCookiesWithDependencies(
61
+ { browser: 'safari', domainFilter: 'hub.com', cache: false },
62
+ options
63
+ ),
64
+ expected.slice(0, 2)
65
+ );
66
+ });
67
+
52
68
  it('decodes multiple pages, flags, UTF-8 and Apple epoch without guessing session cookies', () => {
53
69
  assert.deepEqual(parseSafariCookies(fixture), expected);
54
70
  });
@@ -133,7 +149,16 @@ describe('Safari cookies', { timeout: 10000 }, () => {
133
149
  });
134
150
  assert.deepEqual(report.cookies, expected.slice(0, 2));
135
151
  assert.equal(report.migrated.cookies, 2);
136
- assert.equal(report.skipped.length, 5);
152
+ const classes = JSON.parse(
153
+ await readFile(
154
+ repoPath('tests/fixtures/migration-data-classes.json'),
155
+ 'utf8'
156
+ )
157
+ );
158
+ assert.deepEqual(
159
+ new Set(report.skipped.map((entry) => entry.type)),
160
+ new Set(classes.filter((type) => type !== 'cookies'))
161
+ );
137
162
  assert.equal(
138
163
  report.skipped.find((entry) => entry.type === 'passwords').reason,
139
164
  'safari-password-export-required'
@@ -0,0 +1,111 @@
1
+ import assert from 'node:assert/strict';
2
+ import { copyFile, mkdir, readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { it } from 'node:test';
5
+ import Database from 'better-sqlite3';
6
+ import { listBrowserProfiles } from '../../../src/browser/browser-profiles.js';
7
+ import { listCookieSources } from '../../../src/browser/browser-cookies.js';
8
+ import { findSafariCookieFile } from '../../../src/browser/safari-cookies.js';
9
+ import { repoPath } from '../../helpers/repo.js';
10
+ import { useTempDirectories } from '../../helpers/temp-directory.js';
11
+
12
+ const temporary = useTempDirectories('bc-safari-stores-');
13
+ const uuid = '11111111-2222-3333-4444-555555555555';
14
+
15
+ it('discovers Safari named profiles and prefers WebKit stores to stale legacy cookies', async () => {
16
+ const homeDir = await temporary();
17
+ const root = path.join(
18
+ homeDir,
19
+ 'Library/Containers/com.apple.Safari/Data/Library'
20
+ );
21
+ const named = path.join(root, 'Safari/Profiles', uuid);
22
+ await mkdir(named, { recursive: true });
23
+ const db = new Database(path.join(root, 'Safari/SafariTabs.db'));
24
+ db.exec(
25
+ await readFile(
26
+ repoPath('tests/fixtures/safari-data/SafariTabs.sql'),
27
+ 'utf8'
28
+ )
29
+ );
30
+ db.close();
31
+ const locations = [
32
+ 'Cookies',
33
+ 'WebKit/WebsiteData/Default/Cookies',
34
+ `WebKit/WebsiteDataStore/${uuid}/Cookies`,
35
+ ];
36
+ for (const location of locations) {
37
+ await mkdir(path.join(root, location), { recursive: true });
38
+ await copyFile(
39
+ repoPath('tests/fixtures/safari/Cookies.binarycookies'),
40
+ path.join(root, location, 'Cookies.binarycookies')
41
+ );
42
+ }
43
+ const profiles = await listBrowserProfiles({
44
+ browser: 'safari',
45
+ platform: 'darwin',
46
+ homeDir,
47
+ environment: {},
48
+ });
49
+ assert.deepEqual(
50
+ profiles.map(({ name }) => name),
51
+ ['Default', uuid]
52
+ );
53
+ assert.equal(profiles[1].displayName, 'Work');
54
+ assert.equal(profiles[1].path, named);
55
+ assert.equal(
56
+ await findSafariCookieFile(root),
57
+ path.join(root, locations[1], 'Cookies.binarycookies')
58
+ );
59
+ assert.equal(
60
+ await findSafariCookieFile(named),
61
+ path.join(root, locations[2], 'Cookies.binarycookies')
62
+ );
63
+ });
64
+
65
+ it('preserves a Safari catalogue error while listing its readable cookies and other browsers', async () => {
66
+ const homeDir = await temporary();
67
+ const root = path.join(
68
+ homeDir,
69
+ 'Library/Containers/com.apple.Safari/Data/Library'
70
+ );
71
+ await mkdir(path.join(root, 'Safari'), { recursive: true });
72
+ await mkdir(path.join(root, 'Cookies'));
73
+ await copyFile(
74
+ repoPath('tests/fixtures/safari/Cookies.binarycookies'),
75
+ path.join(root, 'Cookies/Cookies.binarycookies')
76
+ );
77
+ const tabs = path.join(root, 'Safari/SafariTabs.db');
78
+ new Database(tabs).close();
79
+ const chromium = path.join(
80
+ homeDir,
81
+ 'Library/Application Support/Google/Chrome/Default'
82
+ );
83
+ await mkdir(chromium, { recursive: true });
84
+ const db = new Database(path.join(chromium, 'Cookies'));
85
+ db.exec(
86
+ "CREATE TABLE cookies(host_key TEXT); INSERT INTO cookies VALUES ('.github.com')"
87
+ );
88
+ db.close();
89
+ const before = await readFile(tabs);
90
+ const options = { platform: 'darwin', homeDir, environment: {} };
91
+ const profiles = await listBrowserProfiles(options);
92
+ assert.equal(
93
+ profiles.filter(({ browser }) => browser === 'safari').length,
94
+ 2
95
+ );
96
+ const sources = await listCookieSources({
97
+ ...options,
98
+ domains: ['github.com'],
99
+ });
100
+ assert.equal(sources.find(({ browser }) => browser === 'chrome').cookies, 1);
101
+ assert.equal(
102
+ sources.find(({ browser, error }) => browser === 'safari' && !error)
103
+ .cookies,
104
+ 4
105
+ );
106
+ assert.match(
107
+ sources.find(({ browser, error }) => browser === 'safari' && error).error,
108
+ /no such table/u
109
+ );
110
+ assert.deepEqual(await readFile(tabs), before);
111
+ });
@@ -0,0 +1,192 @@
1
+ // feature-parity: safari.control@native-typed safari.setup@native-typed safari.seed@native-typed safari.unsupported@native-typed
2
+ import { it } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import vm from 'node:vm';
5
+ import { findBrowserSource } from '../../../src/browser/browser-sources.js';
6
+ import {
7
+ buildDriverServerArgs,
8
+ resolveWebDriverExecutable,
9
+ } from '../../../src/browser/webdriver.js';
10
+ import {
11
+ WebDriverPage,
12
+ createWebDriverPage,
13
+ } from '../../../src/core/webdriver-page.js';
14
+ import { createMockDriver } from '../../helpers/webdriver-mocks.js';
15
+ import { launchBrowserWithDependencies } from '../../../src/browser/launcher.js';
16
+ import { launchAndConnectRealBrowserWithDependencies } from '../../../src/browser/real-browser.js';
17
+ import {
18
+ SafariSetupError,
19
+ SafariUnsupportedError,
20
+ safariLaunchError,
21
+ openSafariSettings,
22
+ } from '../../../src/browser/safari-support.js';
23
+ import { startTrace } from '../../../src/traces/recorder.js';
24
+ import {
25
+ buildLaunchOptions,
26
+ sessionFromLaunch,
27
+ } from '../../../src/cli/sessions.js';
28
+
29
+ it('catalogue exposes both Safari WebDriver executables', () => {
30
+ for (const channel of ['safari', 'safari-tp']) {
31
+ const source = findBrowserSource(channel);
32
+ assert.equal(source.controlProtocol, 'webdriver');
33
+ assert.ok(source.executables.darwin[0].endsWith('/safaridriver'));
34
+ }
35
+ });
36
+
37
+ it('Safari channels route real/common launch and CLI to Selenium and seed cookies', async () => {
38
+ for (const launch of [
39
+ launchBrowserWithDependencies,
40
+ launchAndConnectRealBrowserWithDependencies,
41
+ ]) {
42
+ const driver = createMockDriver({
43
+ capabilities: { browserName: 'safari' },
44
+ });
45
+ const page = await createWebDriverPage(driver);
46
+ let calls = 0;
47
+ const launched = await launch(
48
+ {
49
+ channel: 'safari-tp',
50
+ seedCookies: [
51
+ {
52
+ name: 'auth',
53
+ value: 'yes',
54
+ domain: '.example.test',
55
+ secure: true,
56
+ httpOnly: true,
57
+ expires: -1,
58
+ },
59
+ ],
60
+ },
61
+ {
62
+ launchWebDriver: async (options) => {
63
+ calls++;
64
+ assert.equal(options.browser, 'safari-technology-preview');
65
+ return {
66
+ driver,
67
+ page,
68
+ close: () => driver.quit(),
69
+ driverPath: '/Safari TP/safaridriver',
70
+ };
71
+ },
72
+ resolveExecutable: () => assert.fail('Safari must bypass CDP'),
73
+ resolveSystem: () =>
74
+ assert.fail('Safari must bypass Chromium resolution'),
75
+ }
76
+ );
77
+ assert.equal(calls, 1);
78
+ assert.equal(launched.engine, 'selenium');
79
+ assert.equal(launched.browser, driver);
80
+ assert.equal(driver.state.url, 'about:blank');
81
+ assert.ok(
82
+ driver.calls.some(
83
+ ([method, url]) => method === 'get' && url === 'https://example.test'
84
+ )
85
+ );
86
+ assert.ok(
87
+ driver.calls.some(
88
+ ([method, cookie]) => method === 'addCookie' && cookie.httpOnly
89
+ )
90
+ );
91
+ assert.equal(sessionFromLaunch('playwright', launched).engine, 'selenium');
92
+ await launched.close();
93
+ }
94
+ assert.equal(buildLaunchOptions({ browser: 'safari' }).channel, 'safari');
95
+ });
96
+
97
+ it('Safari fails unsupported launch options before starting and page features before writes', async () => {
98
+ for (const options of [
99
+ { headless: true },
100
+ { userDataDir: '/tmp/profile' },
101
+ { args: ['--flag'] },
102
+ { fingerprint: {} },
103
+ { downloads: true },
104
+ ]) {
105
+ await assert.rejects(
106
+ () =>
107
+ launchBrowserWithDependencies(
108
+ { channel: 'safari', ...options },
109
+ { launchWebDriver: () => assert.fail('must reject before starting') }
110
+ ),
111
+ SafariUnsupportedError
112
+ );
113
+ }
114
+ const page = new WebDriverPage(createMockDriver(), { browserName: 'safari' });
115
+ await assert.rejects(
116
+ () => page.pdf(),
117
+ (error) =>
118
+ error instanceof SafariUnsupportedError && error.feature === 'PDF'
119
+ );
120
+ await assert.rejects(
121
+ () => page.setRequestInterception(true),
122
+ SafariUnsupportedError
123
+ );
124
+ await assert.rejects(
125
+ () => startTrace({ page, output: '/must-not-create' }),
126
+ SafariUnsupportedError
127
+ );
128
+ });
129
+
130
+ it('Safari async evaluation uses execute/async and returns rejected promises as errors', async () => {
131
+ const driver = createMockDriver();
132
+ let mode = 'success';
133
+ driver.executeAsyncScript = (source, ...args) =>
134
+ new Promise((resolve) => {
135
+ vm.runInNewContext(`(function(){${source}}).apply(null, args)`, {
136
+ args: [...args, resolve],
137
+ location: { href: 'https://example.test' },
138
+ setTimeout,
139
+ mode,
140
+ });
141
+ });
142
+ const page = new WebDriverPage(driver, { browserName: 'safari' });
143
+ assert.equal(await page.evaluateAsync(async (value) => value + 1, 4), 5);
144
+ mode = 'error';
145
+ await assert.rejects(
146
+ () =>
147
+ page.evaluateAsync(async () => {
148
+ throw new Error('rejected');
149
+ }),
150
+ /rejected/
151
+ );
152
+ });
153
+
154
+ it('setup errors identify both authorization steps and offer an explicit settings opener', async () => {
155
+ for (const message of [
156
+ "You must enable the 'Allow Remote Automation' option",
157
+ 'safaridriver requires --enable authorization',
158
+ ]) {
159
+ const error = safariLaunchError(
160
+ new Error(message),
161
+ 'safari-technology-preview'
162
+ );
163
+ assert.ok(error instanceof SafariSetupError);
164
+ assert.match(error.message, /Show features for web developers/);
165
+ assert.match(error.message, /admin password/);
166
+ assert.match(error.message, /Safari Technology Preview.app/);
167
+ assert.equal(typeof error.openSettings, 'function');
168
+ }
169
+ const other = new Error('port already in use');
170
+ assert.equal(safariLaunchError(other, 'safari'), other);
171
+ let argv;
172
+ await openSafariSettings({
173
+ channel: 'safari-tp',
174
+ platform: 'darwin',
175
+ run: async (file, args) => {
176
+ assert.equal(file, 'osascript');
177
+ argv = args;
178
+ },
179
+ });
180
+ assert.ok(argv.some((arg) => arg.includes('Safari Technology Preview')));
181
+ });
182
+
183
+ it('Safari uses its bundled driver and the native --port argument', async () => {
184
+ assert.deepEqual(buildDriverServerArgs('safari', 12345), ['--port', '12345']);
185
+ const result = await resolveWebDriverExecutable({
186
+ browser: 'safari',
187
+ platform: 'darwin',
188
+ checkAccess: async () => {},
189
+ runCommand: () => assert.fail('Safari needs no Selenium Manager'),
190
+ });
191
+ assert.equal(result.driverPath, '/usr/bin/safaridriver');
192
+ });
@@ -92,7 +92,7 @@ function createMockSelenium(driver = createMockDriver()) {
92
92
  record,
93
93
  driver,
94
94
  selenium: {
95
- webdriver: { Builder },
95
+ webdriver: { Builder, Capabilities: Options },
96
96
  chrome: {
97
97
  Options: class extends Options {
98
98
  constructor() {
@@ -265,8 +265,8 @@ describe('resolveWebDriverExecutable', () => {
265
265
 
266
266
  it('rejects browsers it has no driver for', async () => {
267
267
  await assert.rejects(
268
- () => resolveWebDriverExecutable({ browser: 'safari' }),
269
- /Unsupported WebDriver browser: safari/
268
+ () => resolveWebDriverExecutable({ browser: 'unknown-browser' }),
269
+ /Unsupported WebDriver browser: unknown-browser/
270
270
  );
271
271
  });
272
272
  });
@@ -446,6 +446,51 @@ describe('launchWebDriver', () => {
446
446
  });
447
447
  });
448
448
 
449
+ function safariDependencies() {
450
+ return launchDependencies({
451
+ platform: 'darwin',
452
+ checkAccess: async () => {},
453
+ });
454
+ }
455
+
456
+ describe('Safari driver lifecycle', () => {
457
+ it('uses no disk profile or Chromium/BiDi capabilities and closes idempotently', async () => {
458
+ const { dependencies, started, mock } = safariDependencies();
459
+ mock.driver.state.url = '';
460
+ const result = await launchWebDriver(
461
+ { browser: 'safari-technology-preview' },
462
+ dependencies
463
+ );
464
+ assert.equal(result.page.url(), 'about:blank');
465
+ assert.equal(result.userDataDir, undefined);
466
+ assert.equal(result.temporaryProfile, false);
467
+ assert.equal(
468
+ mock.record.builders[0].capabilities.capabilities.browserName,
469
+ 'Safari Technology Preview'
470
+ );
471
+ assert.deepEqual(started[0].args, ['--port', '41001']);
472
+ assert.match(started[0].file, /Safari Technology Preview.app/);
473
+ await Promise.all([result.close(), result.close()]);
474
+ assert.equal(
475
+ mock.driver.calls.filter(([name]) => name === 'quit').length,
476
+ 1
477
+ );
478
+ assert.ok(started[0].child.killed);
479
+ });
480
+
481
+ it('cleans up the driver if Safari authorization fails', async () => {
482
+ const { dependencies, started, mock } = safariDependencies();
483
+ mock.selenium.webdriver.Builder.prototype.build = () => {
484
+ throw new Error("You must enable the 'Allow Remote Automation' option");
485
+ };
486
+ await assert.rejects(
487
+ () => launchWebDriver({ browser: 'safari' }, dependencies),
488
+ (error) => error.code === 'SAFARI_SETUP_REQUIRED'
489
+ );
490
+ assert.ok(started[0].child.killed);
491
+ });
492
+ });
493
+
449
494
  describe('connectWebDriver', () => {
450
495
  it('opens a session on a running server with the given capabilities', async () => {
451
496
  const mock = createMockSelenium();
@@ -182,8 +182,12 @@ describe('dispatcher: high-level methods', () => {
182
182
  const migrated = await dispatch('profile.migrate', {
183
183
  from: 'chrome',
184
184
  include: ['cookies'],
185
+ passwordCsv: '/tmp/export.csv',
186
+ includePaymentCards: true,
185
187
  });
186
188
  assert.deepEqual(migrated.options.include, ['cookies']);
189
+ assert.equal(migrated.options.passwordCsv, '/tmp/export.csv');
190
+ assert.equal(migrated.options.includePaymentCards, true);
187
191
  assert.deepEqual(await dispatch('doctor', {}), { ok: true, unlisted: [] });
188
192
  assert.equal((await dispatch('version')).language, 'js');
189
193
  });
@@ -276,7 +280,7 @@ describe('dispatcher: high-level methods', () => {
276
280
  { code: -32602 }
277
281
  );
278
282
  await assert.rejects(
279
- dispatcher.dispatch('session.launch', { browser: 'safari' }),
283
+ dispatcher.dispatch('session.launch', { browser: 'unknown-browser' }),
280
284
  { code: -32602 }
281
285
  );
282
286
  await assert.rejects(dispatcher.dispatch('page.goto', { url: 'x' }), {
@@ -247,6 +247,9 @@ describe('runCli', () => {
247
247
  'cookies, bookmarks',
248
248
  '--domain',
249
249
  'a.test',
250
+ '--password-csv',
251
+ '/tmp/export.csv',
252
+ '--include-payment-cards',
250
253
  ]);
251
254
 
252
255
  // The source browser is grouped under `from` so migrateProfile receives
@@ -254,6 +257,8 @@ describe('runCli', () => {
254
257
  assert.equal(document.options.from.browser, 'chrome');
255
258
  assert.deepEqual(document.options.include, ['cookies', 'bookmarks']);
256
259
  assert.deepEqual(document.options.domains, ['a.test']);
260
+ assert.equal(document.options.passwordCsv, '/tmp/export.csv');
261
+ assert.equal(document.options.includePaymentCards, true);
257
262
  });
258
263
 
259
264
  it('lists cookie sources with per-domain counts', async () => {
@@ -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
+ });