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 +44 -0
- package/README.md +15 -0
- package/package.json +1 -1
- package/src/browser/launcher.js +7 -2
- package/src/core/navigation-safety.js +29 -0
- package/src/exports.js +1 -0
- package/tests/unit/browser/launcher.test.js +126 -0
- package/tests/unit/core/navigation-safety.test.js +75 -0
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
package/src/browser/launcher.js
CHANGED
|
@@ -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:
|
|
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', ...
|
|
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
|
@@ -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');
|