browser-commander 0.2.1 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5af2479: Add support for custom Chrome args in launchBrowser
8
+
9
+ Adds a new `args` option to the `launchBrowser` function that allows passing custom Chrome arguments to append to the default `CHROME_ARGS`. This is useful for headless server environments (Docker, CI/CD) that require additional flags like `--no-sandbox`, `--disable-setuid-sandbox`, or `--disable-dev-shm-usage`.
10
+
11
+ Usage example:
12
+
13
+ ```javascript
14
+ import { launchBrowser } from 'browser-commander';
15
+
16
+ const { browser, page } = await launchBrowser({
17
+ engine: 'puppeteer',
18
+ headless: true,
19
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
20
+ });
21
+ ```
22
+
23
+ Fixes #11
24
+
25
+ ## 0.3.0
26
+
27
+ ### Minor Changes
28
+
29
+ - 03e9ccb: Add isTimeoutError function for detecting timeout errors
30
+
31
+ Adds a new `isTimeoutError` function exported from the library that helps detect timeout errors from selector waiting operations. This function is complementary to `isNavigationError` and allows automation loops to handle timeout errors gracefully without crashing.
32
+
33
+ Usage example:
34
+
35
+ ```javascript
36
+ import { isTimeoutError } from 'browser-commander';
37
+
38
+ try {
39
+ await page.waitForSelector('.button');
40
+ } catch (error) {
41
+ if (isTimeoutError(error)) {
42
+ console.log('Timeout occurred, continuing with next item...');
43
+ }
44
+ }
45
+ ```
46
+
3
47
  ## 0.2.1
4
48
 
5
49
  ### Patch Changes
package/README.md CHANGED
@@ -191,6 +191,21 @@ commander.pageTrigger({
191
191
 
192
192
  ## API Reference
193
193
 
194
+ ### launchBrowser(options)
195
+
196
+ ```javascript
197
+ const { browser, page } = await launchBrowser({
198
+ engine: 'playwright', // 'playwright' or 'puppeteer'
199
+ headless: false, // Run in headless mode
200
+ userDataDir: '~/.hh-apply/playwright-data', // Browser profile directory
201
+ slowMo: 150, // Slow down operations (ms)
202
+ verbose: false, // Enable debug logging
203
+ args: ['--no-sandbox', '--disable-setuid-sandbox'], // Custom Chrome args to append
204
+ });
205
+ ```
206
+
207
+ The `args` option allows passing custom Chrome arguments, which is useful for headless server environments (Docker, CI/CD) that require flags like `--no-sandbox`.
208
+
194
209
  ### makeBrowserCommander(options)
195
210
 
196
211
  ```javascript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-commander",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
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",
@@ -11,6 +11,7 @@ import { disableTranslateInPreferences } from '../core/preferences.js';
11
11
  * @param {boolean} options.headless - Run in headless mode (default: false)
12
12
  * @param {number} options.slowMo - Slow down operations by ms (default: 150 for Playwright, 0 for Puppeteer)
13
13
  * @param {boolean} options.verbose - Enable verbose logging (default: false)
14
+ * @param {string[]} options.args - Custom Chrome arguments to append to the default CHROME_ARGS
14
15
  * @returns {Promise<Object>} - Object with browser and page
15
16
  */
16
17
  export async function launchBrowser(options = {}) {
@@ -20,8 +21,12 @@ export async function launchBrowser(options = {}) {
20
21
  headless = false,
21
22
  slowMo = engine === 'playwright' ? 150 : 0,
22
23
  verbose = false,
24
+ args = [],
23
25
  } = options;
24
26
 
27
+ // Combine default CHROME_ARGS with custom args
28
+ const chromeArgs = [...CHROME_ARGS, ...args];
29
+
25
30
  if (!['playwright', 'puppeteer'].includes(engine)) {
26
31
  throw new Error(
27
32
  `Invalid engine: ${engine}. Expected 'playwright' or 'puppeteer'`
@@ -50,7 +55,7 @@ export async function launchBrowser(options = {}) {
50
55
  slowMo,
51
56
  chromiumSandbox: true,
52
57
  viewport: null,
53
- args: CHROME_ARGS,
58
+ args: chromeArgs,
54
59
  ignoreDefaultArgs: ['--enable-automation'],
55
60
  });
56
61
  page = browser.pages()[0];
@@ -59,7 +64,7 @@ export async function launchBrowser(options = {}) {
59
64
  browser = await puppeteer.default.launch({
60
65
  headless,
61
66
  defaultViewport: null,
62
- args: ['--start-maximized', ...CHROME_ARGS],
67
+ args: ['--start-maximized', ...chromeArgs],
63
68
  userDataDir,
64
69
  });
65
70
  const pages = await browser.pages();
@@ -158,3 +158,32 @@ export function withNavigationSafety(fn, options = {}) {
158
158
  }
159
159
  };
160
160
  }
161
+
162
+ /**
163
+ * Check if an error is a timeout error from selector waiting
164
+ * These errors should be treated as non-fatal in automation loops
165
+ * @param {Error} error - The error to check
166
+ * @returns {boolean} - True if this is a timeout error
167
+ */
168
+ export function isTimeoutError(error) {
169
+ if (!error) {
170
+ return false;
171
+ }
172
+
173
+ // Check error name first (most reliable)
174
+ if (error.name === 'TimeoutError') {
175
+ return true;
176
+ }
177
+
178
+ // Check error message patterns (case-insensitive)
179
+ const message = (error.message || '').toLowerCase();
180
+ const timeoutErrorPatterns = [
181
+ 'waiting for selector',
182
+ 'timeout',
183
+ 'timeouterror',
184
+ 'timeout exceeded',
185
+ 'timed out',
186
+ ];
187
+
188
+ return timeoutErrorPatterns.some((pattern) => message.includes(pattern));
189
+ }
package/src/exports.js CHANGED
@@ -10,6 +10,7 @@ export { disableTranslateInPreferences } from './core/preferences.js';
10
10
  export { detectEngine } from './core/engine-detection.js';
11
11
  export {
12
12
  isNavigationError,
13
+ isTimeoutError,
13
14
  safeOperation,
14
15
  makeNavigationSafe,
15
16
  withNavigationSafety,
@@ -0,0 +1,126 @@
1
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
+ import assert from 'node:assert';
3
+
4
+ describe('launcher', () => {
5
+ describe('launchBrowser args option', () => {
6
+ let originalEnv;
7
+ let mockChromium;
8
+ let mockPuppeteer;
9
+ let launchBrowser;
10
+ let capturedPlaywrightArgs;
11
+ let capturedPuppeteerArgs;
12
+
13
+ beforeEach(async () => {
14
+ // Save original environment
15
+ originalEnv = { ...process.env };
16
+
17
+ // Reset captured args
18
+ capturedPlaywrightArgs = null;
19
+ capturedPuppeteerArgs = null;
20
+
21
+ // Create mock browser context for Playwright
22
+ const mockBrowserContext = {
23
+ pages: () => [
24
+ {
25
+ bringToFront: async () => {},
26
+ },
27
+ ],
28
+ close: async () => {},
29
+ };
30
+
31
+ // Create mock browser for Puppeteer
32
+ const mockBrowser = {
33
+ pages: async () => [
34
+ {
35
+ bringToFront: async () => {},
36
+ },
37
+ ],
38
+ close: async () => {},
39
+ };
40
+
41
+ // Mock Playwright's chromium
42
+ mockChromium = {
43
+ launchPersistentContext: async (userDataDir, options) => {
44
+ capturedPlaywrightArgs = options.args;
45
+ return mockBrowserContext;
46
+ },
47
+ };
48
+
49
+ // Mock Puppeteer
50
+ mockPuppeteer = {
51
+ default: {
52
+ launch: async (options) => {
53
+ capturedPuppeteerArgs = options.args;
54
+ return mockBrowser;
55
+ },
56
+ },
57
+ };
58
+
59
+ // Use mock.module to mock the dynamic imports
60
+ // Since we can't easily mock dynamic imports in Node.js test runner,
61
+ // we'll test the CHROME_ARGS constant usage directly
62
+ });
63
+
64
+ afterEach(() => {
65
+ // Restore original environment
66
+ process.env = originalEnv;
67
+ });
68
+
69
+ it('should export CHROME_ARGS constant', async () => {
70
+ const { CHROME_ARGS } = await import('../../../src/core/constants.js');
71
+ assert.ok(Array.isArray(CHROME_ARGS));
72
+ assert.ok(CHROME_ARGS.length > 0);
73
+ });
74
+
75
+ it('should include expected default Chrome args', async () => {
76
+ const { CHROME_ARGS } = await import('../../../src/core/constants.js');
77
+ assert.ok(CHROME_ARGS.includes('--disable-session-crashed-bubble'));
78
+ assert.ok(CHROME_ARGS.includes('--no-first-run'));
79
+ assert.ok(CHROME_ARGS.includes('--no-default-browser-check'));
80
+ });
81
+
82
+ it('launchBrowser function should accept args option', async () => {
83
+ // We can verify the function signature by checking that it
84
+ // destructures args from options without throwing
85
+ const { launchBrowser } =
86
+ await import('../../../src/browser/launcher.js');
87
+ assert.ok(typeof launchBrowser === 'function');
88
+
89
+ // The function should accept options object with args array
90
+ // We can't fully test the launch without actual browsers,
91
+ // but we can verify the function exists and is callable
92
+ });
93
+
94
+ it('launchBrowser should throw for invalid engine', async () => {
95
+ const { launchBrowser } =
96
+ await import('../../../src/browser/launcher.js');
97
+
98
+ await assert.rejects(
99
+ () => launchBrowser({ engine: 'invalid-engine' }),
100
+ (error) => {
101
+ assert.ok(error.message.includes('Invalid engine'));
102
+ assert.ok(error.message.includes('invalid-engine'));
103
+ return true;
104
+ }
105
+ );
106
+ });
107
+
108
+ it('launchBrowser should accept args in options', async () => {
109
+ const { launchBrowser } =
110
+ await import('../../../src/browser/launcher.js');
111
+
112
+ // Verify the function signature accepts args
113
+ // This test validates that the args parameter is correctly destructured
114
+ // by attempting to call with an invalid engine (which fails before browser launch)
115
+ // but proves args is accepted without error during options parsing
116
+ const customArgs = ['--no-sandbox', '--disable-setuid-sandbox'];
117
+
118
+ await assert.rejects(
119
+ () => launchBrowser({ engine: 'invalid', args: customArgs }),
120
+ /Invalid engine/
121
+ );
122
+
123
+ // If we got here, the args option was accepted without error
124
+ });
125
+ });
126
+ });
@@ -2,6 +2,7 @@ import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert';
3
3
  import {
4
4
  isNavigationError,
5
+ isTimeoutError,
5
6
  safeOperation,
6
7
  makeNavigationSafe,
7
8
  withNavigationSafety,
@@ -67,6 +68,80 @@ describe('navigation-safety', () => {
67
68
  });
68
69
  });
69
70
 
71
+ describe('isTimeoutError', () => {
72
+ it('should return false for null', () => {
73
+ assert.strictEqual(isTimeoutError(null), false);
74
+ });
75
+
76
+ it('should return false for undefined', () => {
77
+ assert.strictEqual(isTimeoutError(undefined), false);
78
+ });
79
+
80
+ it('should return false for error without message or name', () => {
81
+ assert.strictEqual(isTimeoutError({}), false);
82
+ });
83
+
84
+ it('should return false for regular error', () => {
85
+ const error = new Error('Some regular error');
86
+ assert.strictEqual(isTimeoutError(error), false);
87
+ });
88
+
89
+ it('should return true for error with name "TimeoutError"', () => {
90
+ const error = new Error('Something failed');
91
+ error.name = 'TimeoutError';
92
+ assert.strictEqual(isTimeoutError(error), true);
93
+ });
94
+
95
+ it('should return true for "waiting for selector" error', () => {
96
+ const error = new Error('Waiting for selector ".button" timed out');
97
+ assert.strictEqual(isTimeoutError(error), true);
98
+ });
99
+
100
+ it('should return true for "timeout" error (case-insensitive)', () => {
101
+ const error = new Error('Timeout while waiting for element');
102
+ assert.strictEqual(isTimeoutError(error), true);
103
+ });
104
+
105
+ it('should return true for "timeout exceeded" error', () => {
106
+ const error = new Error('timeout exceeded');
107
+ assert.strictEqual(isTimeoutError(error), true);
108
+ });
109
+
110
+ it('should return true for "timed out" error', () => {
111
+ const error = new Error('Operation timed out after 30000ms');
112
+ assert.strictEqual(isTimeoutError(error), true);
113
+ });
114
+
115
+ it('should return true for Playwright locator timeout error', () => {
116
+ const error = new Error(
117
+ 'locator.click: Timeout 30000ms exceeded. Waiting for selector "button.submit"'
118
+ );
119
+ assert.strictEqual(isTimeoutError(error), true);
120
+ });
121
+
122
+ it('should return true for Puppeteer waitForSelector timeout', () => {
123
+ const error = new Error(
124
+ 'waiting for selector `#myElement` failed: timeout 30000ms exceeded'
125
+ );
126
+ assert.strictEqual(isTimeoutError(error), true);
127
+ });
128
+
129
+ it('should handle uppercase TIMEOUT in message', () => {
130
+ const error = new Error('TIMEOUT ERROR occurred');
131
+ assert.strictEqual(isTimeoutError(error), true);
132
+ });
133
+
134
+ it('should not match navigation errors as timeout errors', () => {
135
+ const error = new Error('Execution context was destroyed');
136
+ assert.strictEqual(isTimeoutError(error), false);
137
+ });
138
+
139
+ it('should not match frame detached errors as timeout errors', () => {
140
+ const error = new Error('frame was detached');
141
+ assert.strictEqual(isTimeoutError(error), false);
142
+ });
143
+ });
144
+
70
145
  describe('safeOperation', () => {
71
146
  it('should return success result on successful operation', async () => {
72
147
  const result = await safeOperation(async () => 'success');