browser-commander 0.25.0 → 0.26.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 (52) hide show
  1. package/.jscpd-baseline.json +6 -4
  2. package/CHANGELOG.md +32 -0
  3. package/package.json +2 -2
  4. package/scripts/check-duplication.mjs +176 -0
  5. package/scripts/create-manual-changeset.mjs +3 -1
  6. package/scripts/format-release-notes.mjs +8 -3
  7. package/scripts/merge-changesets.mjs +4 -1
  8. package/scripts/validate-changeset.mjs +23 -10
  9. package/scripts/version-and-commit.mjs +8 -10
  10. package/src/browser/browser-sources.json +10 -2
  11. package/src/browser/launcher.js +14 -0
  12. package/src/browser/real-browser.js +5 -0
  13. package/src/browser/safari-launch.js +42 -0
  14. package/src/browser/safari-support.js +98 -0
  15. package/src/browser/storage-state.js +28 -8
  16. package/src/browser/webdriver.js +132 -21
  17. package/src/cli/sessions.js +5 -0
  18. package/src/core/webdriver-page.js +62 -0
  19. package/src/elements/selectors.js +31 -14
  20. package/src/exports.js +6 -0
  21. package/src/parity/harness.js +4 -1
  22. package/src/tests/index.js +12 -1
  23. package/src/traces/recorder.js +5 -0
  24. package/tests/e2e/engine-matrix.e2e.test.js +2 -1
  25. package/tests/e2e/safari.e2e.test.js +105 -0
  26. package/tests/e2e/webdriver.e2e.test.js +2 -1
  27. package/tests/helpers/fixture-server.js +18 -1
  28. package/tests/unit/browser/real-browser.test.js +1 -1
  29. package/tests/unit/browser/safari.test.js +192 -0
  30. package/tests/unit/browser/webdriver.test.js +148 -3
  31. package/tests/unit/cli/dispatcher.test.js +1 -1
  32. package/tests/unit/elements/selectors.test.js +75 -0
  33. package/tests/unit/helpers/fixture-server.test.js +60 -0
  34. package/tests/unit/playwright-protocol-coverage.test.js +9 -6
  35. package/tests/unit/puppeteer-bindings-coverage.test.js +28 -7
  36. package/tests/unit/scripts/bot-commit-attribution.test.js +70 -0
  37. package/tests/unit/scripts/check-duplication.test.js +139 -0
  38. package/tests/unit/scripts/check-pipeline-status.test.js +264 -52
  39. package/tests/unit/scripts/check-version-modification.test.js +107 -1
  40. package/tests/unit/scripts/ci-timeout-budgets.test.js +15 -2
  41. package/tests/unit/scripts/ci-workflow-policy.test.js +91 -7
  42. package/tests/unit/scripts/command-stream-errexit.test.js +83 -41
  43. package/tests/unit/scripts/e2e-suite-coverage.test.js +174 -0
  44. package/tests/unit/scripts/github-actions-log.test.js +99 -0
  45. package/tests/unit/scripts/links-workflow.test.js +42 -0
  46. package/tests/unit/scripts/preflight-credentials.test.js +527 -0
  47. package/tests/unit/scripts/run-with-budget-warning.test.js +212 -6
  48. package/tests/unit/scripts/use-module-integration.test.js +2 -9
  49. package/tests/unit/scripts/use-module.test.js +20 -0
  50. package/tests/unit/scripts/validate-changeset.test.js +127 -0
  51. package/tests/unit/scripts/zizmor-version.test.js +82 -0
  52. package/tests/unit/tests/index.test.js +43 -0
@@ -3,11 +3,12 @@
3
3
  "fingerprints": {
4
4
  "00d381dd611ae8ac": 1,
5
5
  "029a62ef1f48e856": 1,
6
- "03241c1f33f40d14": 1,
7
6
  "038170e72e0af1f5": 1,
7
+ "0413f601c8911a92": 1,
8
8
  "04b28d01d0bb47dc": 1,
9
9
  "04f605be138bc350": 1,
10
10
  "0796d358ad2562c3": 1,
11
+ "080e47b73f48a044": 1,
11
12
  "08e28c7706cdddd6": 1,
12
13
  "090a69d9528e4cdb": 1,
13
14
  "09694dc8fb9d683c": 1,
@@ -30,6 +31,7 @@
30
31
  "17c26c0db2d35ad1": 1,
31
32
  "193b46c0e7f72053": 1,
32
33
  "19ff70fc80fad524": 1,
34
+ "1a9f0f0f68ed445c": 1,
33
35
  "1ad4dc7e19a77f68": 1,
34
36
  "1c1baf1bc8ea0eae": 1,
35
37
  "1d61f5c49f12d2dc": 1,
@@ -79,7 +81,6 @@
79
81
  "4715a17d690865ee": 1,
80
82
  "4b0d3cc5c27227e7": 1,
81
83
  "4c3b94caa28045d0": 1,
82
- "4e090d6bb30e6551": 1,
83
84
  "4f3c541b3f3e3f23": 1,
84
85
  "4f93b074bc2ac9e8": 1,
85
86
  "5085765e1f103f3e": 1,
@@ -114,6 +115,7 @@
114
115
  "6880d620e0e67229": 1,
115
116
  "69d50e6f6a5f2ee9": 1,
116
117
  "6a7ed9ad3fc00a39": 1,
118
+ "6a9854bcdc9f71d7": 1,
117
119
  "6af3c307939ef75e": 1,
118
120
  "6b35b48512756192": 1,
119
121
  "6cd00d6b6a039022": 1,
@@ -182,7 +184,7 @@
182
184
  "acc95d694eb2f581": 1,
183
185
  "ad4cfa76daa1b6fc": 1,
184
186
  "ad50b48938af8aef": 1,
185
- "ae477ba64bafba3f": 1,
187
+ "add45ff3644fbeeb": 1,
186
188
  "aee1e5c65f783848": 1,
187
189
  "aee45e8b58c2d87f": 1,
188
190
  "af702b20ebd45ef2": 1,
@@ -246,13 +248,13 @@
246
248
  "e5feedc0c24e103e": 1,
247
249
  "e67726a913675120": 1,
248
250
  "e686288f2e9b8a6d": 1,
251
+ "e7d7bb7e163d2327": 1,
249
252
  "ea1195048ded896c": 1,
250
253
  "ea88711c5deb1fe5": 1,
251
254
  "eb9a70c2a21094ff": 1,
252
255
  "ebe03c5f8c2f5147": 1,
253
256
  "ee19c8b9b332ee98": 1,
254
257
  "eeca0ccf8d821f21": 1,
255
- "f022b763ff342b2d": 1,
256
258
  "f07f979f8c2706e8": 1,
257
259
  "f186f73c118b8e8a": 1,
258
260
  "f38f72095a67c95e": 1,
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.26.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f2988a3: Close the parity probe server only after it stops listening, so a connection
8
+ that arrives during shutdown can no longer keep `close()` waiting. Test fixture
9
+ servers now drop idle browser preconnections too, which stopped the Safari
10
+ smoke test from timing out during cleanup on Node 24.
11
+
12
+ Parse Playwright `:has-text("…")` and `:text-is("…")` selectors with string
13
+ operations instead of a backtracking regular expression. A long selector that
14
+ did not match took quadratic time (16 seconds for 550,000 characters); it is
15
+ now rejected in linear time. CodeQL reported it as `js/polynomial-redos`.
16
+
17
+ Failure artifact names are trimmed of leading and trailing dashes by index, so
18
+ a test name with a long run of dashes no longer takes quadratic time.
19
+
20
+ A Safari launch right after a previous Safari session closed starts a fresh
21
+ `safaridriver`, up to three attempts, when the driver exits before it is ready
22
+ or refuses the connection. safaridriver serves one automation session at a
23
+ time, and CI saw both failures on back-to-back launches. A refused connection
24
+ now also names the driver server and whether it had exited. Authorization
25
+ errors are still reported at once. Set `VERBOSE=1` to log each retry.
26
+
27
+ ## 0.26.0
28
+
29
+ ### Minor Changes
30
+
31
+ - 8f9ef1c: Add native Safari and Technology Preview W3C WebDriver control through real/common launchers and CLI, with isolated cookie seeding, setup diagnostics and typed unsupported feature errors.
32
+
33
+ Update the transitive shell-quote lock entry to fix GHSA-pqg4-j6r4-53mv and unblock npm audit.
34
+
3
35
  ## 0.25.0
4
36
 
5
37
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "description": "Universal browser automation library that supports Playwright, Puppeteer, and Selenium with a unified API",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  "format": "prettier --write .",
33
33
  "format:check": "prettier --check .",
34
34
  "docs:api": "jsdoc -c jsdoc.json",
35
- "check:duplication": "jscpd . --baseline .jscpd-baseline.json --fail-on-new-clones",
35
+ "check:duplication": "node scripts/check-duplication.mjs",
36
36
  "check": "npm run lint && npm run format:check && npm run check:duplication",
37
37
  "changeset": "changeset",
38
38
  "changeset:version": "node scripts/changeset-version.mjs",
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Duplication gate that prints the clones it fails on, and only those.
5
+ *
6
+ * With the console reporter, `jscpd --baseline … --fail-on-new-clones` lists
7
+ * every clone it finds, accepted or not. A passing run printed 272 "Clone
8
+ * found" blocks, about 840 lines, for clones the baseline already accepts. The
9
+ * one clone that failed a run was marked only by a `[NEW]` tag somewhere in
10
+ * that list (issue #128). This runs jscpd with the JSON reporter instead. It
11
+ * prints the new clones from the report (as error annotations in GitHub
12
+ * Actions) and a one-line total.
13
+ *
14
+ * `npx jscpd .` still prints every clone, and `.jscpd.json` keeps the console
15
+ * reporter for that.
16
+ *
17
+ * Usage (from js/): node scripts/check-duplication.mjs
18
+ */
19
+
20
+ import { spawnSync } from 'node:child_process';
21
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
22
+ import { createRequire } from 'node:module';
23
+ import { tmpdir } from 'node:os';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const BASELINE = '.jscpd-baseline.json';
28
+
29
+ /**
30
+ * Path to jscpd's CLI entry point, or null when it is not installed. Running
31
+ * it through `process.execPath` avoids the `.bin` shims, which Windows cannot
32
+ * execute directly.
33
+ */
34
+ export function resolveJscpdCli() {
35
+ const require = createRequire(import.meta.url);
36
+ let manifestPath;
37
+ try {
38
+ manifestPath = require.resolve('jscpd/package.json');
39
+ } catch {
40
+ return null;
41
+ }
42
+ const { bin } = JSON.parse(readFileSync(manifestPath, 'utf8'));
43
+ const entry = typeof bin === 'string' ? bin : bin?.jscpd;
44
+ const cli = entry && path.join(path.dirname(manifestPath), entry);
45
+ return cli && existsSync(cli) ? cli : null;
46
+ }
47
+
48
+ function escapeProperty(value) {
49
+ return String(value)
50
+ .replaceAll('%', '%25')
51
+ .replaceAll('\r', '%0D')
52
+ .replaceAll('\n', '%0A')
53
+ .replaceAll(':', '%3A')
54
+ .replaceAll(',', '%2C');
55
+ }
56
+
57
+ function escapeMessage(value) {
58
+ return String(value)
59
+ .replaceAll('%', '%25')
60
+ .replaceAll('\r', '%0D')
61
+ .replaceAll('\n', '%0A');
62
+ }
63
+
64
+ /**
65
+ * The new clones in a jscpd JSON report, one line each.
66
+ *
67
+ * jscpd names code embedded in Markdown `README.md:javascript`, so the
68
+ * language suffix is dropped before the name is used as a path. In GitHub
69
+ * Actions each clone becomes an `::error` annotation on the
70
+ * repository-relative path. `cwd` is where jscpd ran, and `workspace` is the
71
+ * checkout root.
72
+ *
73
+ * @param {{duplicates: Array<object>}} report
74
+ * @param {{githubActions?: boolean, cwd?: string, workspace?: string}} [options]
75
+ */
76
+ export function formatNewClones(report, options = {}) {
77
+ const {
78
+ githubActions = false,
79
+ cwd = process.cwd(),
80
+ workspace = cwd,
81
+ } = options;
82
+ const fileOf = (side) => {
83
+ const name = side.name.replace(/:[a-z]+$/, '');
84
+ return githubActions
85
+ ? path
86
+ .relative(workspace, path.resolve(cwd, name))
87
+ .split(path.sep)
88
+ .join('/')
89
+ : name;
90
+ };
91
+
92
+ return report.duplicates
93
+ .filter((duplicate) => duplicate.isNew)
94
+ .map(({ firstFile, secondFile, lines }) => {
95
+ const first = fileOf(firstFile);
96
+ const second = `${fileOf(secondFile)}:${secondFile.start}-${secondFile.end}`;
97
+ if (!githubActions) {
98
+ return `New clone: ${first}:${firstFile.start}-${firstFile.end} ~ ${second} (${lines} lines)\n`;
99
+ }
100
+ const properties = [
101
+ `file=${escapeProperty(first)}`,
102
+ `line=${firstFile.start}`,
103
+ `endLine=${firstFile.end}`,
104
+ 'title=New duplicated code',
105
+ ].join(',');
106
+ const message =
107
+ `${lines} lines duplicate ${second}. Extract the shared code, or ` +
108
+ 'accept the clone with `npm run check:duplication:update`.';
109
+ return `::error ${properties}::${escapeMessage(message)}\n`;
110
+ })
111
+ .join('');
112
+ }
113
+
114
+ function main() {
115
+ const cli = resolveJscpdCli();
116
+ if (!cli) {
117
+ console.error('jscpd is not installed; run npm ci in js/.');
118
+ return 2;
119
+ }
120
+
121
+ const output = mkdtempSync(path.join(tmpdir(), 'jscpd-report-'));
122
+ try {
123
+ const jscpd = spawnSync(
124
+ process.execPath,
125
+ [
126
+ cli,
127
+ '.',
128
+ '--baseline',
129
+ BASELINE,
130
+ '--fail-on-new-clones',
131
+ '--reporters',
132
+ 'json',
133
+ '--output',
134
+ output,
135
+ '--no-colors',
136
+ ],
137
+ { encoding: 'utf8' }
138
+ );
139
+ const reportPath = path.join(output, 'jscpd-report.json');
140
+ if (jscpd.error || !existsSync(reportPath)) {
141
+ // Fail closed: a run that produced no report proved nothing.
142
+ process.stdout.write(jscpd.stdout ?? '');
143
+ process.stderr.write(jscpd.stderr ?? '');
144
+ console.error(
145
+ `jscpd wrote no report (exit ${jscpd.status}): ${jscpd.error?.message ?? reportPath}`
146
+ );
147
+ return jscpd.status || 2;
148
+ }
149
+
150
+ const report = JSON.parse(readFileSync(reportPath, 'utf8'));
151
+ const newCount = report.duplicates.filter((d) => d.isNew).length;
152
+ process.stdout.write(
153
+ formatNewClones(report, {
154
+ githubActions: process.env.GITHUB_ACTIONS === 'true',
155
+ workspace: process.env.GITHUB_WORKSPACE || process.cwd(),
156
+ })
157
+ );
158
+ const total = report.duplicates.length;
159
+ console.log(
160
+ `jscpd: ${total} clone${total === 1 ? '' : 's'}, ${newCount} new ` +
161
+ `(the rest are accepted in ${BASELINE}).`
162
+ );
163
+ if (jscpd.status !== 0 && newCount === 0) {
164
+ // jscpd failed for a reason other than new clones; show what it said.
165
+ process.stdout.write(jscpd.stdout);
166
+ process.stderr.write(jscpd.stderr);
167
+ }
168
+ return jscpd.status ?? 2;
169
+ } finally {
170
+ rmSync(output, { recursive: true, force: true });
171
+ }
172
+ }
173
+
174
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
175
+ process.exitCode = main();
176
+ }
@@ -13,6 +13,8 @@
13
13
  import { writeFileSync } from 'fs';
14
14
  import { randomBytes } from 'crypto';
15
15
 
16
+ import { printUntrusted } from '../../scripts/github-actions-log.mjs';
17
+
16
18
  const PACKAGE_NAME = 'browser-commander';
17
19
 
18
20
  import {
@@ -69,7 +71,7 @@ ${description}
69
71
 
70
72
  console.log(`Created changeset: ${changesetFile}`);
71
73
  console.log('Content:');
72
- console.log(content);
74
+ printUntrusted(content);
73
75
 
74
76
  // Format with Prettier
75
77
  console.log('\nFormatting with Prettier...');
@@ -70,6 +70,9 @@ if (!releaseId || !version || !repository) {
70
70
  process.exit(1);
71
71
  }
72
72
 
73
+ /** Start of the npm badge appended below, which marks a formatted body. */
74
+ const NPM_BADGE_PREFIX = '[![npm version](';
75
+
73
76
  try {
74
77
  // Get current release body
75
78
  const result = await $`gh api repos/${repository}/releases/${releaseId}`.run({
@@ -79,8 +82,10 @@ try {
79
82
 
80
83
  const currentBody = releaseData.body || '';
81
84
 
82
- // Skip if already formatted (has shields.io badge image)
83
- if (currentBody.includes('img.shields.io')) {
85
+ // Skip if already formatted (has the npm badge this script appends). The
86
+ // badge's alt text marks it: matching the image host would also skip a
87
+ // body whose changelog text mentions that host.
88
+ if (currentBody.includes(NPM_BADGE_PREFIX)) {
84
89
  console.log('ℹ️ Release notes already formatted');
85
90
  process.exit(0);
86
91
  }
@@ -186,7 +191,7 @@ try {
186
191
 
187
192
  // Build formatted release notes
188
193
  const versionWithoutV = version.replace(/^v/, '');
189
- const npmBadge = `[![npm version](https://img.shields.io/badge/npm-${versionWithoutV}-blue.svg)](https://www.npmjs.com/package/${PACKAGE_NAME}/v/${versionWithoutV})`;
194
+ const npmBadge = `${NPM_BADGE_PREFIX}https://img.shields.io/badge/npm-${versionWithoutV}-blue.svg)](https://www.npmjs.com/package/${PACKAGE_NAME}/v/${versionWithoutV})`;
190
195
 
191
196
  let formattedBody = `${cleanDescription}`;
192
197
 
@@ -23,6 +23,8 @@ import {
23
23
  } from 'fs';
24
24
  import { join } from 'path';
25
25
 
26
+ import { printUntrusted } from '../../scripts/github-actions-log.mjs';
27
+
26
28
  const PACKAGE_NAME = 'browser-commander';
27
29
  const CHANGESET_DIR = '.changeset';
28
30
 
@@ -254,7 +256,8 @@ function main() {
254
256
  }
255
257
 
256
258
  console.log('\nChangeset merge completed successfully');
257
- console.log(`\nMerged changeset content:\n${mergedContent}`);
259
+ console.log('\nMerged changeset content:');
260
+ printUntrusted(mergedContent);
258
261
  }
259
262
 
260
263
  main();
@@ -7,13 +7,17 @@
7
7
  * - Only checks changeset files ADDED by the current PR (not pre-existing ones)
8
8
  * - Uses git diff to compare PR head against base branch
9
9
  * - Validates that the PR adds exactly one changeset with proper format
10
- * - Falls back to checking all changesets for local development
10
+ * - Fails when a pull request's diff cannot be computed (issue #128): checking
11
+ * every changeset on disk instead would let a stale one satisfy the check
12
+ * - Outside a pull request (local runs), checks the changesets on disk
11
13
  */
12
14
 
13
15
  import { execSync } from 'child_process';
14
16
  import { readFileSync, readdirSync, existsSync } from 'fs';
15
17
  import { join } from 'path';
16
18
 
19
+ import { printUntrusted } from '../../scripts/github-actions-log.mjs';
20
+
17
21
  const PACKAGE_NAME = 'browser-commander';
18
22
  // When running from js/ directory, the changeset dir is local
19
23
  const CHANGESET_DIR = '.changeset';
@@ -113,13 +117,11 @@ function tryBaseBranchComparison(prBase) {
113
117
  }
114
118
 
115
119
  /**
116
- * Fallback: get all changesets in directory
120
+ * Outside a pull request: get all changesets in directory
117
121
  * @returns {string[]} Array of all changeset file names
118
122
  */
119
123
  function getAllChangesets() {
120
- console.log(
121
- 'Warning: Could not determine PR diff, checking all changesets in directory'
122
- );
124
+ console.log('No pull request to compare; checking all changesets on disk');
123
125
  if (!existsSync(CHANGESET_DIR)) {
124
126
  return [];
125
127
  }
@@ -136,6 +138,8 @@ function getAddedChangesetFiles() {
136
138
  const baseSha = process.env.GITHUB_BASE_SHA || process.env.BASE_SHA;
137
139
  const headSha = process.env.GITHUB_HEAD_SHA || process.env.HEAD_SHA;
138
140
 
141
+ const prBase = process.env.GITHUB_BASE_REF;
142
+
139
143
  // Try explicit SHAs first
140
144
  if (baseSha && headSha) {
141
145
  const result = tryExplicitShaComparison(baseSha, headSha);
@@ -145,7 +149,6 @@ function getAddedChangesetFiles() {
145
149
  }
146
150
 
147
151
  // Try base branch comparison
148
- const prBase = process.env.GITHUB_BASE_REF;
149
152
  if (prBase) {
150
153
  const result = tryBaseBranchComparison(prBase);
151
154
  if (result !== null) {
@@ -153,7 +156,14 @@ function getAddedChangesetFiles() {
153
156
  }
154
157
  }
155
158
 
156
- // Fallback to checking all changesets
159
+ // In a pull request, every changeset on disk includes ones already on the
160
+ // base branch, so falling back to them would pass a PR that adds none.
161
+ if (prBase || baseSha || headSha) {
162
+ throw new Error(
163
+ 'Could not determine which changesets this pull request adds, because git could not diff it against its base'
164
+ );
165
+ }
166
+
157
167
  return getAllChangesets();
158
168
  }
159
169
 
@@ -252,7 +262,9 @@ try {
252
262
  console.error(`::error::${validation.error}`);
253
263
  console.error(`\nFile content of ${changesetFile}:`);
254
264
  try {
255
- console.error(readFileSync(changesetFile, 'utf-8'));
265
+ printUntrusted(readFileSync(changesetFile, 'utf-8'), {
266
+ stream: process.stderr,
267
+ });
256
268
  } catch {
257
269
  console.error('(could not read file)');
258
270
  }
@@ -261,9 +273,10 @@ try {
261
273
 
262
274
  console.log('Changeset validation passed');
263
275
  console.log(` Type: ${validation.type}`);
264
- console.log(` Description: ${validation.description}`);
276
+ console.log(' Description:');
277
+ printUntrusted(validation.description);
265
278
  } catch (error) {
266
- console.error('Error during changeset validation:', error.message);
279
+ console.error(`::error::${error.message}`);
267
280
  if (process.env.DEBUG) {
268
281
  console.error('Stack trace:', error.stack);
269
282
  }
@@ -24,6 +24,7 @@ import {
24
24
  loadCommandStream,
25
25
  loadLinoArguments,
26
26
  } from '../../scripts/use-module.mjs';
27
+ import { printUntrusted } from '../../scripts/github-actions-log.mjs';
27
28
 
28
29
  const { $ } = await loadCommandStream();
29
30
  const { makeConfig } = await loadLinoArguments();
@@ -53,11 +54,9 @@ const config = makeConfig({
53
54
  const { mode, bumpType, description } = config;
54
55
 
55
56
  // Debug: Log parsed configuration
56
- console.log('Parsed configuration:', {
57
- mode,
58
- bumpType,
59
- description: description || '(none)',
60
- });
57
+ console.log('Parsed configuration:', { mode, bumpType });
58
+ console.log('Description:');
59
+ printUntrusted(description || '(none)');
61
60
 
62
61
  // Detect if positional arguments were used (common mistake)
63
62
  const args = process.argv.slice(2);
@@ -145,7 +144,7 @@ async function main() {
145
144
  try {
146
145
  // Configure git
147
146
  await $`git config user.name "github-actions[bot]"`;
148
- await $`git config user.email "github-actions[bot]@users.noreply.github.com"`;
147
+ await $`git config user.email "41898282+github-actions[bot]@users.noreply.github.com"`;
149
148
 
150
149
  // Check if remote main has advanced (handles re-runs after partial success)
151
150
  console.log('Checking for remote changes...');
@@ -231,10 +230,9 @@ async function main() {
231
230
  // Stage all changes (package.json, package-lock.json, CHANGELOG.md, deleted changesets)
232
231
  await $`git add -A`;
233
232
 
234
- // Commit with version number as message
235
- const commitMessage = newVersion;
236
- const escapedMessage = commitMessage.replace(/"/g, '\\"');
237
- await $`git commit -m "${escapedMessage}"`;
233
+ // Commit with version number as message. command-stream quotes the
234
+ // interpolated value itself; escaping it here would add backslashes.
235
+ await $`git commit -m ${newVersion}`;
238
236
 
239
237
  // Push directly to main.
240
238
  //
@@ -766,7 +766,9 @@
766
766
  },
767
767
  "default": {
768
768
  "darwin": ["com.apple.safari"]
769
- }
769
+ },
770
+ "executables": { "darwin": ["/usr/bin/safaridriver"] },
771
+ "controlProtocol": "webdriver"
770
772
  },
771
773
  {
772
774
  "id": "safari-technology-preview",
@@ -781,7 +783,13 @@
781
783
  },
782
784
  "default": {
783
785
  "darwin": ["com.apple.safaritechnologypreview"]
784
- }
786
+ },
787
+ "executables": {
788
+ "darwin": [
789
+ "/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver"
790
+ ]
791
+ },
792
+ "controlProtocol": "webdriver"
785
793
  },
786
794
  {
787
795
  "id": "whale",
@@ -17,6 +17,8 @@ import { resolveSystemBrowserExecutable } from './system-browser.js';
17
17
  import { applyFingerprint } from '../fingerprint/apply.js';
18
18
  import { attachDownloads } from '../downloads/attach.js';
19
19
  import { launchWebDriver } from './webdriver.js';
20
+ import { launchSafari } from './safari-launch.js';
21
+ import { isSafariChannel } from './safari-support.js';
20
22
  import {
21
23
  loadStorageState,
22
24
  restorePlaywrightStorageState,
@@ -300,6 +302,18 @@ export async function launchBrowserWithDependencies(
300
302
  options = {},
301
303
  dependencies = {}
302
304
  ) {
305
+ if (isSafariChannel(options.channel ?? options.browser)) {
306
+ validateLaunchMode({
307
+ engine: options.engine ?? 'playwright',
308
+ launch: options.launch ?? 'real',
309
+ attach: options.attach,
310
+ });
311
+ return await launchSafari(options, dependencies);
312
+ }
313
+ return await launchNonSafariBrowser(options, dependencies);
314
+ }
315
+
316
+ async function launchNonSafariBrowser(options, dependencies) {
303
317
  const {
304
318
  engine = 'playwright',
305
319
  launch = 'real',
@@ -1,6 +1,8 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { findBrowserSource } from './browser-sources.js';
4
+ import { launchSafari } from './safari-launch.js';
5
+ import { isSafariChannel } from './safari-support.js';
4
6
 
5
7
  import { startProcess } from '../utilities/subprocess.js';
6
8
  import {
@@ -563,6 +565,9 @@ export async function launchAndConnectRealBrowserWithDependencies(
563
565
  options = {},
564
566
  dependencies = {}
565
567
  ) {
568
+ if (isSafariChannel(options.channel ?? options.browser)) {
569
+ return launchSafari(options, dependencies);
570
+ }
566
571
  const {
567
572
  engine = 'playwright',
568
573
  channel = 'chrome',
@@ -0,0 +1,42 @@
1
+ import { launchWebDriver } from './webdriver.js';
2
+ import { findBrowserSource } from './browser-sources.js';
3
+ import { validateSafariOptions } from './safari-support.js';
4
+ import { restoreWebDriverStorageState } from './storage-state.js';
5
+
6
+ /** Launch installed Safari through its native W3C WebDriver server. */
7
+ export async function launchSafari(options = {}, dependencies = {}) {
8
+ validateSafariOptions(options);
9
+ const channel = findBrowserSource(
10
+ options.channel ?? options.browser ?? 'safari'
11
+ ).id;
12
+ const launched = await (dependencies.launchWebDriver ?? launchWebDriver)(
13
+ {
14
+ ...options,
15
+ browser: channel,
16
+ driverPath: options.driverPath ?? options.executablePath,
17
+ executablePath: undefined,
18
+ },
19
+ dependencies
20
+ );
21
+ try {
22
+ const state = options.storageState;
23
+ await restoreWebDriverStorageState({
24
+ page: launched.page,
25
+ storageState: state,
26
+ seedCookies: options.seedCookies,
27
+ });
28
+ } catch (error) {
29
+ await launched.close();
30
+ throw error;
31
+ }
32
+ return {
33
+ ...launched,
34
+ browser: launched.driver,
35
+ engine: 'selenium',
36
+ controlProtocol: 'webdriver',
37
+ executablePath: launched.driverPath,
38
+ browserProcess: launched.driverProcess,
39
+ downloads: null,
40
+ launch: options.launch ?? 'real',
41
+ };
42
+ }