browser-commander 0.9.0 → 0.9.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.1
4
+
5
+ ### Patch Changes
6
+
7
+ - cbd4fe1: Remove CI warning noise from workflow actions, release logging, and lint output.
8
+
3
9
  ## 0.9.0
4
10
 
5
11
  ### Minor Changes
package/eslint.config.js CHANGED
@@ -88,13 +88,13 @@ export default [
88
88
  // Comments and documentation
89
89
  'spaced-comment': ['error', 'always', { markers: ['/'] }],
90
90
 
91
- // Complexity rules - reasonable thresholds for maintainability
92
- complexity: ['warn', 15], // Cyclomatic complexity - allow more complex logic than strict 8
91
+ // Complexity rules - tuned for browser-automation orchestration code
92
+ complexity: ['warn', 30], // Cyclomatic complexity guardrail for unusually branchy functions
93
93
  'max-depth': ['warn', 5], // Maximum nesting depth - slightly more lenient than strict 4
94
94
  'max-lines-per-function': [
95
95
  'warn',
96
96
  {
97
- max: 150, // More reasonable than strict 50 lines per function
97
+ max: 300, // Allows orchestration functions while still catching very large functions
98
98
  skipBlankLines: true,
99
99
  skipComments: true,
100
100
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Universal browser automation library that supports both Playwright and Puppeteer with a unified API",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -9,12 +9,12 @@
9
9
  "./tests": "./src/tests/index.js"
10
10
  },
11
11
  "scripts": {
12
- "test": "node --test --test-reporter spec tests/unit/",
13
- "test:unit": "node --test --test-reporter spec tests/unit/",
14
- "test:coverage": "node --test --experimental-test-coverage tests/unit/",
15
- "test:e2e": "RUN_E2E=true node --test tests/e2e/",
16
- "test:e2e:playwright": "RUN_E2E=true node --test tests/e2e/playwright.e2e.test.js",
17
- "test:e2e:puppeteer": "RUN_E2E=true node --test tests/e2e/puppeteer.e2e.test.js",
12
+ "test": "node scripts/run-tests.mjs --reporter spec tests/unit",
13
+ "test:unit": "node scripts/run-tests.mjs --reporter spec tests/unit",
14
+ "test:coverage": "node scripts/run-tests.mjs --coverage tests/unit",
15
+ "test:e2e": "node scripts/run-tests.mjs --env RUN_E2E=true tests/e2e",
16
+ "test:e2e:playwright": "node scripts/run-tests.mjs --env RUN_E2E=true tests/e2e/playwright.e2e.test.js",
17
+ "test:e2e:puppeteer": "node scripts/run-tests.mjs --env RUN_E2E=true tests/e2e/puppeteer.e2e.test.js",
18
18
  "test:all": "npm run test:unit && npm run test:e2e",
19
19
  "lint": "eslint .",
20
20
  "lint:fix": "eslint . --fix",
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ export function removeDeprecatedAlwaysAuth(configPath) {
7
+ if (!configPath || !existsSync(configPath)) {
8
+ return false;
9
+ }
10
+
11
+ const config = readFileSync(configPath, 'utf8');
12
+ const lines = config.split(/\r?\n/);
13
+ const filteredLines = lines.filter(
14
+ (line) => !/^\s*always-auth\s*=/.test(line)
15
+ );
16
+
17
+ if (filteredLines.length === lines.length) {
18
+ return false;
19
+ }
20
+
21
+ writeFileSync(configPath, filteredLines.join('\n'));
22
+ return true;
23
+ }
24
+
25
+ function main() {
26
+ const removed = removeDeprecatedAlwaysAuth(process.env.NPM_CONFIG_USERCONFIG);
27
+ if (removed) {
28
+ console.log('Removed deprecated always-auth from npm user config.');
29
+ } else {
30
+ console.log('No deprecated npm always-auth config found.');
31
+ }
32
+ }
33
+
34
+ if (
35
+ process.argv[1] &&
36
+ import.meta.url === pathToFileURL(process.argv[1]).href
37
+ ) {
38
+ main();
39
+ }
@@ -0,0 +1,31 @@
1
+ export function formatRegistryPackagePath(packageName) {
2
+ return encodeURIComponent(packageName);
3
+ }
4
+
5
+ export async function isVersionPublished(
6
+ packageName,
7
+ version,
8
+ fetchFn = fetch
9
+ ) {
10
+ const packagePath = formatRegistryPackagePath(packageName);
11
+ const versionPath = encodeURIComponent(version);
12
+ const response = await fetchFn(
13
+ `https://registry.npmjs.org/${packagePath}/${versionPath}`,
14
+ {
15
+ headers: { accept: 'application/json' },
16
+ }
17
+ );
18
+
19
+ if (response.status === 404) {
20
+ return false;
21
+ }
22
+
23
+ if (!response.ok) {
24
+ throw new Error(
25
+ `Failed to check npm registry for ${packageName}@${version}: ${response.status} ${response.statusText}`
26
+ );
27
+ }
28
+
29
+ const metadata = await response.json();
30
+ return metadata?.version === version;
31
+ }
@@ -13,6 +13,8 @@
13
13
 
14
14
  import { readFileSync, appendFileSync } from 'fs';
15
15
 
16
+ import { isVersionPublished } from './npm-registry.mjs';
17
+
16
18
  const PACKAGE_NAME = 'browser-commander';
17
19
 
18
20
  // Load use-m dynamically
@@ -74,26 +76,23 @@ async function main() {
74
76
  console.log(
75
77
  `Checking if version ${currentVersion} is already published...`
76
78
  );
77
- const checkResult =
78
- await $`npm view "${PACKAGE_NAME}@${currentVersion}" version`.run({
79
- capture: true,
80
- });
81
-
82
- // command-stream returns { code: 0 } on success, { code: 1 } on failure (e.g., E404)
83
- // Exit code 0 means version exists, non-zero means version not found
84
- if (checkResult.code === 0) {
79
+ const versionIsPublished = await isVersionPublished(
80
+ PACKAGE_NAME,
81
+ currentVersion
82
+ );
83
+
84
+ if (versionIsPublished) {
85
85
  console.log(`Version ${currentVersion} is already published to npm`);
86
86
  setOutput('published', 'true');
87
87
  setOutput('published_version', currentVersion);
88
88
  setOutput('already_published', 'true');
89
89
  return;
90
- } else {
91
- // Version not found on npm (E404), proceed with publish
92
- console.log(
93
- `Version ${currentVersion} not found on npm, proceeding with publish...`
94
- );
95
90
  }
96
91
 
92
+ console.log(
93
+ `Version ${currentVersion} not found on npm, proceeding with publish...`
94
+ );
95
+
97
96
  // Publish to npm using OIDC trusted publishing with retry logic
98
97
  for (let i = 1; i <= MAX_RETRIES; i++) {
99
98
  console.log(`Publish attempt ${i} of ${MAX_RETRIES}...`);
@@ -0,0 +1,192 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { resolve, relative, sep } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ const TEST_FILE_PATTERN = /\.test\.[cm]?js$/u;
7
+
8
+ function readOptionValue(argv, index, optionName) {
9
+ const value = argv[index + 1];
10
+
11
+ if (!value || value.startsWith('--')) {
12
+ throw new Error(`${optionName} requires a value`);
13
+ }
14
+
15
+ return value;
16
+ }
17
+
18
+ function parseEnvAssignment(assignment) {
19
+ const separatorIndex = assignment.indexOf('=');
20
+
21
+ if (separatorIndex <= 0) {
22
+ throw new Error(`Invalid environment override: ${assignment}`);
23
+ }
24
+
25
+ return {
26
+ name: assignment.slice(0, separatorIndex),
27
+ value: assignment.slice(separatorIndex + 1),
28
+ };
29
+ }
30
+
31
+ export function parseTestRunnerArgs(argv) {
32
+ const options = {
33
+ coverage: false,
34
+ env: {},
35
+ reporter: null,
36
+ targets: [],
37
+ };
38
+
39
+ for (let index = 0; index < argv.length; index += 1) {
40
+ const arg = argv[index];
41
+
42
+ if (arg === '--coverage') {
43
+ options.coverage = true;
44
+ continue;
45
+ }
46
+
47
+ if (arg === '--reporter') {
48
+ options.reporter = readOptionValue(argv, index, '--reporter');
49
+ index += 1;
50
+ continue;
51
+ }
52
+
53
+ if (arg === '--env') {
54
+ const { name, value } = parseEnvAssignment(
55
+ readOptionValue(argv, index, '--env')
56
+ );
57
+ options.env[name] = value;
58
+ index += 1;
59
+ continue;
60
+ }
61
+
62
+ if (arg.startsWith('--env=')) {
63
+ const { name, value } = parseEnvAssignment(arg.slice('--env='.length));
64
+ options.env[name] = value;
65
+ continue;
66
+ }
67
+
68
+ options.targets.push(arg);
69
+ }
70
+
71
+ if (options.targets.length === 0) {
72
+ throw new Error('At least one test file or directory is required');
73
+ }
74
+
75
+ return options;
76
+ }
77
+
78
+ function normalizeForSort(filePath) {
79
+ return filePath.split(sep).join('/');
80
+ }
81
+
82
+ function isTestFile(filePath) {
83
+ return TEST_FILE_PATTERN.test(filePath);
84
+ }
85
+
86
+ function listFilesRecursively(directory) {
87
+ const entries = readdirSync(directory, { withFileTypes: true });
88
+ const files = [];
89
+
90
+ for (const entry of entries) {
91
+ const entryPath = resolve(directory, entry.name);
92
+
93
+ if (entry.isDirectory()) {
94
+ files.push(...listFilesRecursively(entryPath));
95
+ continue;
96
+ }
97
+
98
+ if (entry.isFile() && isTestFile(entryPath)) {
99
+ files.push(entryPath);
100
+ }
101
+ }
102
+
103
+ return files;
104
+ }
105
+
106
+ export function resolveTestFiles(targets, cwd = process.cwd()) {
107
+ const files = [];
108
+
109
+ for (const target of targets) {
110
+ const absoluteTarget = resolve(cwd, target);
111
+
112
+ if (!existsSync(absoluteTarget)) {
113
+ throw new Error(`Test target does not exist: ${target}`);
114
+ }
115
+
116
+ const targetStats = statSync(absoluteTarget);
117
+
118
+ if (targetStats.isDirectory()) {
119
+ files.push(...listFilesRecursively(absoluteTarget));
120
+ continue;
121
+ }
122
+
123
+ if (targetStats.isFile() && isTestFile(absoluteTarget)) {
124
+ files.push(absoluteTarget);
125
+ }
126
+ }
127
+
128
+ const uniqueFiles = [...new Set(files)];
129
+ uniqueFiles.sort((left, right) =>
130
+ normalizeForSort(left).localeCompare(normalizeForSort(right))
131
+ );
132
+
133
+ if (uniqueFiles.length === 0) {
134
+ throw new Error(`No test files found in: ${targets.join(', ')}`);
135
+ }
136
+
137
+ return uniqueFiles.map((file) => relative(cwd, file));
138
+ }
139
+
140
+ export function buildNodeTestArgs(options, testFiles) {
141
+ const args = ['--test'];
142
+
143
+ if (options.coverage) {
144
+ args.push('--experimental-test-coverage');
145
+ }
146
+
147
+ if (options.reporter) {
148
+ args.push('--test-reporter', options.reporter);
149
+ }
150
+
151
+ args.push(...testFiles);
152
+ return args;
153
+ }
154
+
155
+ function runNodeTests(options, testFiles) {
156
+ const result = spawnSync(
157
+ process.execPath,
158
+ buildNodeTestArgs(options, testFiles),
159
+ {
160
+ env: { ...process.env, ...options.env },
161
+ stdio: 'inherit',
162
+ }
163
+ );
164
+
165
+ if (result.error) {
166
+ throw result.error;
167
+ }
168
+
169
+ if (result.signal) {
170
+ throw new Error(`Node test runner terminated by signal ${result.signal}`);
171
+ }
172
+
173
+ return result.status ?? 1;
174
+ }
175
+
176
+ export function main(argv = process.argv.slice(2)) {
177
+ const options = parseTestRunnerArgs(argv);
178
+ const testFiles = resolveTestFiles(options.targets);
179
+ return runNodeTests(options, testFiles);
180
+ }
181
+
182
+ if (
183
+ process.argv[1] &&
184
+ import.meta.url === pathToFileURL(process.argv[1]).href
185
+ ) {
186
+ try {
187
+ process.exitCode = main();
188
+ } catch (error) {
189
+ console.error(error.message);
190
+ process.exitCode = 1;
191
+ }
192
+ }
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * Update npm for OIDC trusted publishing
5
5
  * npm trusted publishing requires npm >= 11.5.1
6
- * Node.js 20.x ships with npm 10.x, so we need to update
6
+ * Keep the runner npm version current enough for OIDC publishing
7
7
  *
8
8
  * Uses link-foundation libraries:
9
9
  * - use-m: Dynamic package loading without package.json dependencies
@@ -25,7 +25,7 @@ export async function defaultNavigationVerification(options = {}) {
25
25
  const { page, expectedUrl, startUrl } = options;
26
26
 
27
27
  try {
28
- const actualUrl = page.url();
28
+ const actualUrl = await Promise.resolve(page.url());
29
29
 
30
30
  // If expected URL is provided, verify it matches
31
31
  if (expectedUrl) {
@@ -232,7 +232,7 @@ export async function goto(options = {}) {
232
232
  waitUntil = 'domcontentloaded',
233
233
  waitForStableUrlBefore = true,
234
234
  waitForStableUrlAfter = true,
235
- waitForNetworkIdle = true,
235
+ waitForNetworkIdle: _waitForNetworkIdle = true,
236
236
  stableChecks = 3,
237
237
  checkInterval = 1000,
238
238
  timeout = 240000,
@@ -395,7 +395,6 @@ export async function waitForNavigation(options = {}) {
395
395
  */
396
396
  export async function waitForPageReady(options = {}) {
397
397
  const {
398
- page,
399
398
  navigationManager,
400
399
  networkTracker,
401
400
  log,
@@ -19,7 +19,7 @@
19
19
  * @returns {Object} - DialogManager API
20
20
  */
21
21
  export function createDialogManager(options = {}) {
22
- const { page, engine, log } = options;
22
+ const { page, log } = options;
23
23
 
24
24
  if (!page) {
25
25
  throw new Error('page is required in options');
@@ -13,6 +13,12 @@
13
13
 
14
14
  import { TIMING } from './constants.js';
15
15
 
16
+ function notImplemented(methodName) {
17
+ return Promise.reject(
18
+ new Error(`${methodName}() must be implemented by subclass`)
19
+ );
20
+ }
21
+
16
22
  /**
17
23
  * Base class defining the engine adapter interface
18
24
  * All engine-specific operations should be defined here
@@ -39,7 +45,7 @@ export class EngineAdapter {
39
45
  * @param {string} selector - CSS selector
40
46
  * @returns {Object} - Locator (Playwright) or ElementHandle (Puppeteer)
41
47
  */
42
- createLocator(selector) {
48
+ createLocator(_selector) {
43
49
  throw new Error('createLocator() must be implemented by subclass');
44
50
  }
45
51
 
@@ -48,8 +54,8 @@ export class EngineAdapter {
48
54
  * @param {string} selector - CSS selector
49
55
  * @returns {Promise<Object|null>} - Locator/Element or null
50
56
  */
51
- async querySelector(selector) {
52
- throw new Error('querySelector() must be implemented by subclass');
57
+ querySelector(_selector) {
58
+ return notImplemented('querySelector');
53
59
  }
54
60
 
55
61
  /**
@@ -57,8 +63,8 @@ export class EngineAdapter {
57
63
  * @param {string} selector - CSS selector
58
64
  * @returns {Promise<Array>} - Array of locators/elements
59
65
  */
60
- async querySelectorAll(selector) {
61
- throw new Error('querySelectorAll() must be implemented by subclass');
66
+ querySelectorAll(_selector) {
67
+ return notImplemented('querySelectorAll');
62
68
  }
63
69
 
64
70
  /**
@@ -67,8 +73,8 @@ export class EngineAdapter {
67
73
  * @param {Object} options - Wait options {visible, timeout}
68
74
  * @returns {Promise<void>}
69
75
  */
70
- async waitForSelector(selector, options = {}) {
71
- throw new Error('waitForSelector() must be implemented by subclass');
76
+ waitForSelector(_selector, _options = {}) {
77
+ return notImplemented('waitForSelector');
72
78
  }
73
79
 
74
80
  /**
@@ -77,8 +83,8 @@ export class EngineAdapter {
77
83
  * @param {number} timeout - Timeout in ms
78
84
  * @returns {Promise<Object>} - The locator/element
79
85
  */
80
- async waitForVisible(locatorOrElement, timeout = TIMING.DEFAULT_TIMEOUT) {
81
- throw new Error('waitForVisible() must be implemented by subclass');
86
+ waitForVisible(_locatorOrElement, _timeout = TIMING.DEFAULT_TIMEOUT) {
87
+ return notImplemented('waitForVisible');
82
88
  }
83
89
 
84
90
  /**
@@ -86,8 +92,8 @@ export class EngineAdapter {
86
92
  * @param {string} selector - CSS selector
87
93
  * @returns {Promise<number>} - Number of matching elements
88
94
  */
89
- async count(selector) {
90
- throw new Error('count() must be implemented by subclass');
95
+ count(_selector) {
96
+ return notImplemented('count');
91
97
  }
92
98
 
93
99
  // ============================================================================
@@ -101,8 +107,8 @@ export class EngineAdapter {
101
107
  * @param {any} args - Arguments to pass (optional)
102
108
  * @returns {Promise<any>} - Result of evaluation
103
109
  */
104
- async evaluateOnElement(locatorOrElement, fn, args) {
105
- throw new Error('evaluateOnElement() must be implemented by subclass');
110
+ evaluateOnElement(_locatorOrElement, _fn, _args) {
111
+ return notImplemented('evaluateOnElement');
106
112
  }
107
113
 
108
114
  /**
@@ -110,8 +116,8 @@ export class EngineAdapter {
110
116
  * @param {Object} locatorOrElement - Locator or element
111
117
  * @returns {Promise<string|null>} - Text content
112
118
  */
113
- async getTextContent(locatorOrElement) {
114
- throw new Error('getTextContent() must be implemented by subclass');
119
+ getTextContent(_locatorOrElement) {
120
+ return notImplemented('getTextContent');
115
121
  }
116
122
 
117
123
  /**
@@ -119,8 +125,8 @@ export class EngineAdapter {
119
125
  * @param {Object} locatorOrElement - Locator or element
120
126
  * @returns {Promise<string>} - Input value
121
127
  */
122
- async getInputValue(locatorOrElement) {
123
- throw new Error('getInputValue() must be implemented by subclass');
128
+ getInputValue(_locatorOrElement) {
129
+ return notImplemented('getInputValue');
124
130
  }
125
131
 
126
132
  /**
@@ -129,8 +135,8 @@ export class EngineAdapter {
129
135
  * @param {string} attribute - Attribute name
130
136
  * @returns {Promise<string|null>} - Attribute value
131
137
  */
132
- async getAttribute(locatorOrElement, attribute) {
133
- throw new Error('getAttribute() must be implemented by subclass');
138
+ getAttribute(_locatorOrElement, _attribute) {
139
+ return notImplemented('getAttribute');
134
140
  }
135
141
 
136
142
  // ============================================================================
@@ -143,8 +149,8 @@ export class EngineAdapter {
143
149
  * @param {Object} options - Click options {force, etc.}
144
150
  * @returns {Promise<void>}
145
151
  */
146
- async click(locatorOrElement, options = {}) {
147
- throw new Error('click() must be implemented by subclass');
152
+ click(_locatorOrElement, _options = {}) {
153
+ return notImplemented('click');
148
154
  }
149
155
 
150
156
  /**
@@ -153,8 +159,8 @@ export class EngineAdapter {
153
159
  * @param {string} text - Text to type
154
160
  * @returns {Promise<void>}
155
161
  */
156
- async type(locatorOrElement, text) {
157
- throw new Error('type() must be implemented by subclass');
162
+ type(_locatorOrElement, _text) {
163
+ return notImplemented('type');
158
164
  }
159
165
 
160
166
  /**
@@ -163,8 +169,8 @@ export class EngineAdapter {
163
169
  * @param {string} text - Text to fill
164
170
  * @returns {Promise<void>}
165
171
  */
166
- async fill(locatorOrElement, text) {
167
- throw new Error('fill() must be implemented by subclass');
172
+ fill(_locatorOrElement, _text) {
173
+ return notImplemented('fill');
168
174
  }
169
175
 
170
176
  /**
@@ -172,8 +178,8 @@ export class EngineAdapter {
172
178
  * @param {Object} locatorOrElement - Locator or element
173
179
  * @returns {Promise<void>}
174
180
  */
175
- async focus(locatorOrElement) {
176
- throw new Error('focus() must be implemented by subclass');
181
+ focus(_locatorOrElement) {
182
+ return notImplemented('focus');
177
183
  }
178
184
 
179
185
  // ============================================================================
@@ -185,8 +191,8 @@ export class EngineAdapter {
185
191
  * @param {string} key - Key name (Playwright/Puppeteer key format)
186
192
  * @returns {Promise<void>}
187
193
  */
188
- async keyboardPress(key) {
189
- throw new Error('keyboardPress() must be implemented by subclass');
194
+ keyboardPress(_key) {
195
+ return notImplemented('keyboardPress');
190
196
  }
191
197
 
192
198
  /**
@@ -194,8 +200,8 @@ export class EngineAdapter {
194
200
  * @param {string} text - Text to type
195
201
  * @returns {Promise<void>}
196
202
  */
197
- async keyboardType(text) {
198
- throw new Error('keyboardType() must be implemented by subclass');
203
+ keyboardType(_text) {
204
+ return notImplemented('keyboardType');
199
205
  }
200
206
 
201
207
  /**
@@ -203,8 +209,8 @@ export class EngineAdapter {
203
209
  * @param {string} key - Key name
204
210
  * @returns {Promise<void>}
205
211
  */
206
- async keyboardDown(key) {
207
- throw new Error('keyboardDown() must be implemented by subclass');
212
+ keyboardDown(_key) {
213
+ return notImplemented('keyboardDown');
208
214
  }
209
215
 
210
216
  /**
@@ -212,8 +218,8 @@ export class EngineAdapter {
212
218
  * @param {string} key - Key name
213
219
  * @returns {Promise<void>}
214
220
  */
215
- async keyboardUp(key) {
216
- throw new Error('keyboardUp() must be implemented by subclass');
221
+ keyboardUp(_key) {
222
+ return notImplemented('keyboardUp');
217
223
  }
218
224
 
219
225
  // ============================================================================
@@ -226,8 +232,8 @@ export class EngineAdapter {
226
232
  * @param {Array} args - Arguments to pass
227
233
  * @returns {Promise<any>} - Result of evaluation
228
234
  */
229
- async evaluateOnPage(fn, args = []) {
230
- throw new Error('evaluateOnPage() must be implemented by subclass');
235
+ evaluateOnPage(_fn, _args = []) {
236
+ return notImplemented('evaluateOnPage');
231
237
  }
232
238
 
233
239
  /**
@@ -243,8 +249,8 @@ export class EngineAdapter {
243
249
  * @param {Object} options - PDF options (format, printBackground, margin, etc.)
244
250
  * @returns {Promise<Buffer>} - PDF buffer
245
251
  */
246
- async pdf(options = {}) {
247
- throw new Error('pdf() must be implemented by subclass');
252
+ pdf(_options = {}) {
253
+ return notImplemented('pdf');
248
254
  }
249
255
  }
250
256
 
@@ -435,7 +441,7 @@ export class PuppeteerAdapter extends EngineAdapter {
435
441
  await this.page.waitForSelector(selector, { visible, timeout });
436
442
  }
437
443
 
438
- async waitForVisible(locatorOrElement, timeout = TIMING.DEFAULT_TIMEOUT) {
444
+ waitForVisible(locatorOrElement, _timeout = TIMING.DEFAULT_TIMEOUT) {
439
445
  // For Puppeteer, locatorOrElement is already an ElementHandle
440
446
  // We can't wait on it directly, so we just return it
441
447
  // The caller should have already used waitForSelector
@@ -370,7 +370,7 @@ export function createNavigationManager(options = {}) {
370
370
  });
371
371
  }
372
372
 
373
- return navigationPromise;
373
+ return await navigationPromise;
374
374
  }
375
375
 
376
376
  /**
@@ -209,7 +209,6 @@ export function createNetworkTracker(options = {}) {
209
209
  async function waitForNetworkIdle(opts = {}) {
210
210
  const { timeout = 30000, idleTime = idleTimeout } = opts;
211
211
 
212
- const startTime = Date.now();
213
212
  const currentNavId = navigationId;
214
213
 
215
214
  // If already idle, wait for idle time
@@ -317,7 +316,7 @@ export function createNetworkTracker(options = {}) {
317
316
  */
318
317
  function getPendingUrls() {
319
318
  const urls = [];
320
- for (const [key, req] of pendingRequests) {
319
+ for (const req of pendingRequests.values()) {
321
320
  const url = typeof req.url === 'function' ? req.url() : req.url;
322
321
  urls.push(url);
323
322
  }
@@ -65,7 +65,7 @@ export function createPageSessionFactory(options = {}) {
65
65
  /**
66
66
  * Handle navigation - deactivate session if URL no longer matches
67
67
  */
68
- function handleUrlChange({ previousUrl, newUrl }) {
68
+ function handleUrlChange({ newUrl }) {
69
69
  if (!isActive) {
70
70
  return;
71
71
  }
@@ -115,7 +115,7 @@ export function createPageSessionFactory(options = {}) {
115
115
  for (const { target, event, handler } of eventListeners) {
116
116
  try {
117
117
  target.off(event, handler);
118
- } catch (e) {
118
+ } catch {
119
119
  // Ignore removal errors
120
120
  }
121
121
  }
@@ -166,7 +166,7 @@ export function createPageSessionFactory(options = {}) {
166
166
  log.debug(() => `⏭️ Skipping action - session "${name}" is inactive`);
167
167
  return null;
168
168
  }
169
- return fn();
169
+ return await fn();
170
170
  }
171
171
 
172
172
  /**
@@ -214,7 +214,7 @@ export function createPageSessionFactory(options = {}) {
214
214
  if (!networkTracker) {
215
215
  return true;
216
216
  }
217
- return networkTracker.waitForNetworkIdle(opts);
217
+ return await networkTracker.waitForNetworkIdle(opts);
218
218
  }
219
219
 
220
220
  /**
@@ -224,7 +224,7 @@ export function createPageSessionFactory(options = {}) {
224
224
  if (!isActive) {
225
225
  return false;
226
226
  }
227
- return navigationManager.waitForPageReady(opts);
227
+ return await navigationManager.waitForPageReady(opts);
228
228
  }
229
229
 
230
230
  /**
@@ -310,7 +310,7 @@ export function createPageTriggerManager(options = {}) {
310
310
  * @param {string} url - URL to start action for
311
311
  * @param {Object} commander - BrowserCommander instance
312
312
  */
313
- async function startAction(url, commander) {
313
+ function startAction(url, commander) {
314
314
  // Create context for condition checking
315
315
  const conditionCtx = {
316
316
  url,
@@ -321,7 +321,7 @@ export function createPageTriggerManager(options = {}) {
321
321
  const matchingTrigger = findMatchingTrigger(conditionCtx);
322
322
  if (!matchingTrigger) {
323
323
  log.debug(() => `📋 No trigger registered for: ${url}`);
324
- return;
324
+ return Promise.resolve();
325
325
  }
326
326
 
327
327
  log.debug(() => `🚀 Starting action "${matchingTrigger.name}" for: ${url}`);
@@ -369,6 +369,8 @@ export function createPageTriggerManager(options = {}) {
369
369
  }
370
370
  }
371
371
  })();
372
+
373
+ return Promise.resolve();
372
374
  }
373
375
 
374
376
  /**
@@ -92,16 +92,16 @@ export async function findByText(options = {}) {
92
92
  if (engine === 'playwright') {
93
93
  // Playwright supports :has-text() natively
94
94
  const textSelector = exact ? `:text-is("${text}")` : `:has-text("${text}")`;
95
- return `${selector}${textSelector}`;
95
+ return await Promise.resolve(`${selector}${textSelector}`);
96
96
  } else {
97
97
  // For Puppeteer, we need to use XPath or evaluate
98
98
  // Return a special selector marker that will be handled by other methods
99
- return {
99
+ return await Promise.resolve({
100
100
  _isPuppeteerTextSelector: true,
101
101
  baseSelector: selector,
102
102
  text,
103
103
  exact,
104
- };
104
+ });
105
105
  }
106
106
  }
107
107
 
@@ -25,7 +25,6 @@ export async function waitForUrlCondition(options = {}) {
25
25
  const {
26
26
  getUrl,
27
27
  wait,
28
- evaluate,
29
28
  targetUrl,
30
29
  description,
31
30
  customCheck,
@@ -257,7 +257,7 @@ export async function clickElement(options = {}) {
257
257
  * @param {Function} options.log - Logger instance
258
258
  * @returns {Promise<{navigated: boolean, newUrl: string}>}
259
259
  */
260
- async function detectNavigation(options = {}) {
260
+ function detectNavigation(options = {}) {
261
261
  const { page, navigationManager, startUrl, log } = options;
262
262
 
263
263
  const currentUrl = page.url();
@@ -0,0 +1,138 @@
1
+ import {
2
+ mkdirSync,
3
+ mkdtempSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import assert from 'node:assert/strict';
11
+ import { describe, it } from 'node:test';
12
+
13
+ import { removeDeprecatedAlwaysAuth } from '../../../scripts/clean-npm-config.mjs';
14
+ import {
15
+ formatRegistryPackagePath,
16
+ isVersionPublished,
17
+ } from '../../../scripts/npm-registry.mjs';
18
+ import {
19
+ buildNodeTestArgs,
20
+ parseTestRunnerArgs,
21
+ resolveTestFiles,
22
+ } from '../../../scripts/run-tests.mjs';
23
+
24
+ describe('npm release helpers', () => {
25
+ describe('removeDeprecatedAlwaysAuth', () => {
26
+ it('removes always-auth while preserving other config', () => {
27
+ const dir = mkdtempSync(join(tmpdir(), 'browser-commander-npmrc-'));
28
+ const npmrc = join(dir, '.npmrc');
29
+
30
+ try {
31
+ writeFileSync(
32
+ npmrc,
33
+ [
34
+ '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}',
35
+ 'always-auth=true',
36
+ 'registry=https://registry.npmjs.org/',
37
+ '',
38
+ ].join('\n')
39
+ );
40
+
41
+ assert.equal(removeDeprecatedAlwaysAuth(npmrc), true);
42
+ assert.equal(
43
+ readFileSync(npmrc, 'utf8'),
44
+ [
45
+ '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}',
46
+ 'registry=https://registry.npmjs.org/',
47
+ '',
48
+ ].join('\n')
49
+ );
50
+ } finally {
51
+ rmSync(dir, { force: true, recursive: true });
52
+ }
53
+ });
54
+
55
+ it('returns false when no config path exists', () => {
56
+ assert.equal(removeDeprecatedAlwaysAuth(''), false);
57
+ });
58
+ });
59
+
60
+ describe('isVersionPublished', () => {
61
+ it('returns false for npm registry 404 responses without logging npm errors', async () => {
62
+ const fetchCalls = [];
63
+ const fetchFn = async (url) => {
64
+ fetchCalls.push(url);
65
+ return {
66
+ ok: false,
67
+ status: 404,
68
+ statusText: 'Not Found',
69
+ };
70
+ };
71
+
72
+ const published = await isVersionPublished(
73
+ 'browser-commander',
74
+ '0.9.0',
75
+ fetchFn
76
+ );
77
+
78
+ assert.equal(published, false);
79
+ assert.deepEqual(fetchCalls, [
80
+ 'https://registry.npmjs.org/browser-commander/0.9.0',
81
+ ]);
82
+ });
83
+
84
+ it('handles scoped package names', () => {
85
+ assert.equal(
86
+ formatRegistryPackagePath('@scope/package'),
87
+ '%40scope%2Fpackage'
88
+ );
89
+ });
90
+ });
91
+ });
92
+
93
+ describe('test runner helper', () => {
94
+ it('expands test directories into stable explicit test files', () => {
95
+ const dir = mkdtempSync(join(tmpdir(), 'browser-commander-tests-'));
96
+
97
+ try {
98
+ writeFileSync(join(dir, 'root.test.js'), '');
99
+ writeFileSync(join(dir, 'ignored.js'), '');
100
+
101
+ const nestedDir = join(dir, 'nested');
102
+ mkdirSync(nestedDir);
103
+ writeFileSync(join(nestedDir, 'nested.test.js'), '');
104
+
105
+ assert.deepEqual(resolveTestFiles([dir], dir), [
106
+ join('nested', 'nested.test.js'),
107
+ 'root.test.js',
108
+ ]);
109
+ } finally {
110
+ rmSync(dir, { force: true, recursive: true });
111
+ }
112
+ });
113
+
114
+ it('builds node --test arguments with reporter and coverage options', () => {
115
+ const options = parseTestRunnerArgs([
116
+ '--coverage',
117
+ '--reporter',
118
+ 'spec',
119
+ '--env',
120
+ 'RUN_E2E=true',
121
+ 'tests/unit',
122
+ ]);
123
+
124
+ assert.deepEqual(options, {
125
+ coverage: true,
126
+ env: { RUN_E2E: 'true' },
127
+ reporter: 'spec',
128
+ targets: ['tests/unit'],
129
+ });
130
+ assert.deepEqual(buildNodeTestArgs(options, ['tests/unit/a.test.js']), [
131
+ '--test',
132
+ '--experimental-test-coverage',
133
+ '--test-reporter',
134
+ 'spec',
135
+ 'tests/unit/a.test.js',
136
+ ]);
137
+ });
138
+ });