browser-commander 0.2.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 (82) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.github/workflows/release.yml +296 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.jscpd.json +20 -0
  6. package/.prettierignore +7 -0
  7. package/.prettierrc +10 -0
  8. package/CHANGELOG.md +32 -0
  9. package/LICENSE +24 -0
  10. package/README.md +320 -0
  11. package/bunfig.toml +3 -0
  12. package/deno.json +7 -0
  13. package/eslint.config.js +125 -0
  14. package/examples/react-test-app/index.html +25 -0
  15. package/examples/react-test-app/package.json +19 -0
  16. package/examples/react-test-app/src/App.jsx +473 -0
  17. package/examples/react-test-app/src/main.jsx +10 -0
  18. package/examples/react-test-app/src/styles.css +323 -0
  19. package/examples/react-test-app/vite.config.js +9 -0
  20. package/package.json +89 -0
  21. package/scripts/changeset-version.mjs +38 -0
  22. package/scripts/create-github-release.mjs +93 -0
  23. package/scripts/create-manual-changeset.mjs +86 -0
  24. package/scripts/format-github-release.mjs +83 -0
  25. package/scripts/format-release-notes.mjs +216 -0
  26. package/scripts/instant-version-bump.mjs +121 -0
  27. package/scripts/merge-changesets.mjs +260 -0
  28. package/scripts/publish-to-npm.mjs +126 -0
  29. package/scripts/setup-npm.mjs +37 -0
  30. package/scripts/validate-changeset.mjs +262 -0
  31. package/scripts/version-and-commit.mjs +237 -0
  32. package/src/ARCHITECTURE.md +270 -0
  33. package/src/README.md +517 -0
  34. package/src/bindings.js +298 -0
  35. package/src/browser/launcher.js +93 -0
  36. package/src/browser/navigation.js +513 -0
  37. package/src/core/constants.js +24 -0
  38. package/src/core/engine-adapter.js +466 -0
  39. package/src/core/engine-detection.js +49 -0
  40. package/src/core/logger.js +21 -0
  41. package/src/core/navigation-manager.js +503 -0
  42. package/src/core/navigation-safety.js +160 -0
  43. package/src/core/network-tracker.js +373 -0
  44. package/src/core/page-session.js +299 -0
  45. package/src/core/page-trigger-manager.js +564 -0
  46. package/src/core/preferences.js +46 -0
  47. package/src/elements/content.js +197 -0
  48. package/src/elements/locators.js +243 -0
  49. package/src/elements/selectors.js +360 -0
  50. package/src/elements/visibility.js +166 -0
  51. package/src/exports.js +121 -0
  52. package/src/factory.js +192 -0
  53. package/src/high-level/universal-logic.js +206 -0
  54. package/src/index.js +17 -0
  55. package/src/interactions/click.js +684 -0
  56. package/src/interactions/fill.js +383 -0
  57. package/src/interactions/scroll.js +341 -0
  58. package/src/utilities/url.js +33 -0
  59. package/src/utilities/wait.js +135 -0
  60. package/tests/e2e/playwright.e2e.test.js +442 -0
  61. package/tests/e2e/puppeteer.e2e.test.js +408 -0
  62. package/tests/helpers/mocks.js +542 -0
  63. package/tests/unit/bindings.test.js +218 -0
  64. package/tests/unit/browser/navigation.test.js +345 -0
  65. package/tests/unit/core/constants.test.js +72 -0
  66. package/tests/unit/core/engine-adapter.test.js +170 -0
  67. package/tests/unit/core/engine-detection.test.js +81 -0
  68. package/tests/unit/core/logger.test.js +80 -0
  69. package/tests/unit/core/navigation-safety.test.js +202 -0
  70. package/tests/unit/core/network-tracker.test.js +198 -0
  71. package/tests/unit/core/page-trigger-manager.test.js +358 -0
  72. package/tests/unit/elements/content.test.js +318 -0
  73. package/tests/unit/elements/locators.test.js +236 -0
  74. package/tests/unit/elements/selectors.test.js +302 -0
  75. package/tests/unit/elements/visibility.test.js +234 -0
  76. package/tests/unit/factory.test.js +174 -0
  77. package/tests/unit/high-level/universal-logic.test.js +299 -0
  78. package/tests/unit/interactions/click.test.js +340 -0
  79. package/tests/unit/interactions/fill.test.js +378 -0
  80. package/tests/unit/interactions/scroll.test.js +330 -0
  81. package/tests/unit/utilities/url.test.js +63 -0
  82. package/tests/unit/utilities/wait.test.js +207 -0
package/README.md ADDED
@@ -0,0 +1,320 @@
1
+ # Browser Commander
2
+
3
+ A universal browser automation library that supports both Playwright and Puppeteer with a unified API. The key focus is on **stoppable page triggers** - ensuring automation logic is properly mounted/unmounted during page navigation.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install browser-commander
9
+ ```
10
+
11
+ You'll also need either Playwright or Puppeteer:
12
+
13
+ ```bash
14
+ # With Playwright
15
+ npm install playwright
16
+
17
+ # Or with Puppeteer
18
+ npm install puppeteer
19
+ ```
20
+
21
+ ## Core Concept: Page State Machine
22
+
23
+ Browser Commander manages the browser as a state machine with two states:
24
+
25
+ ```
26
+ +------------------+ +------------------+
27
+ | | navigation start | |
28
+ | WORKING STATE | -------------------> | LOADING STATE |
29
+ | (action runs) | | (wait only) |
30
+ | | <----------------- | |
31
+ +------------------+ page ready +------------------+
32
+ ```
33
+
34
+ **LOADING STATE**: Page is loading. Only waiting/tracking operations are allowed. No automation logic runs.
35
+
36
+ **WORKING STATE**: Page is fully loaded (30 seconds of network idle). Page triggers can safely interact with DOM.
37
+
38
+ ## Quick Start
39
+
40
+ ```javascript
41
+ import {
42
+ launchBrowser,
43
+ makeBrowserCommander,
44
+ makeUrlCondition,
45
+ } from 'browser-commander';
46
+
47
+ // 1. Launch browser
48
+ const { browser, page } = await launchBrowser({ engine: 'playwright' });
49
+
50
+ // 2. Create commander
51
+ const commander = makeBrowserCommander({ page, verbose: true });
52
+
53
+ // 3. Register page trigger with condition and action
54
+ commander.pageTrigger({
55
+ name: 'example-trigger',
56
+ condition: makeUrlCondition('*example.com*'), // matches URLs containing 'example.com'
57
+ action: async (ctx) => {
58
+ // ctx.commander has all methods, but they throw ActionStoppedError if navigation happens
59
+ // ctx.checkStopped() - call in loops to check if should stop
60
+ // ctx.abortSignal - use with fetch() for cancellation
61
+ // ctx.onCleanup(fn) - register cleanup when action stops
62
+
63
+ console.log(`Processing: ${ctx.url}`);
64
+
65
+ // Safe iteration - stops if navigation detected
66
+ await ctx.forEach(['item1', 'item2'], async (item) => {
67
+ await ctx.commander.clickButton({ selector: `[data-id="${item}"]` });
68
+ });
69
+ },
70
+ });
71
+
72
+ // 4. Navigate - action auto-starts when page is ready
73
+ await commander.goto({ url: 'https://example.com' });
74
+
75
+ // 5. Cleanup
76
+ await commander.destroy();
77
+ await browser.close();
78
+ ```
79
+
80
+ ## URL Condition Helpers
81
+
82
+ The `makeUrlCondition` helper makes it easy to create URL matching conditions:
83
+
84
+ ```javascript
85
+ import {
86
+ makeUrlCondition,
87
+ allConditions,
88
+ anyCondition,
89
+ notCondition,
90
+ } from 'browser-commander';
91
+
92
+ // Exact URL match
93
+ makeUrlCondition('https://example.com/page');
94
+
95
+ // Contains substring (use * wildcards)
96
+ makeUrlCondition('*checkout*'); // URL contains 'checkout'
97
+ makeUrlCondition('*example.com*'); // URL contains 'example.com'
98
+
99
+ // Starts with / ends with
100
+ makeUrlCondition('/api/*'); // starts with '/api/'
101
+ makeUrlCondition('*.json'); // ends with '.json'
102
+
103
+ // Express-style route patterns
104
+ makeUrlCondition('/vacancy/:id'); // matches /vacancy/123
105
+ makeUrlCondition('https://hh.ru/vacancy/:vacancyId'); // matches specific domain + path
106
+ makeUrlCondition('/user/:userId/profile'); // multiple segments
107
+
108
+ // RegExp
109
+ makeUrlCondition(/\/product\/\d+/);
110
+
111
+ // Custom function (receives full context)
112
+ makeUrlCondition((url, ctx) => {
113
+ const parsed = new URL(url);
114
+ return (
115
+ parsed.pathname.startsWith('/admin') && parsed.searchParams.has('edit')
116
+ );
117
+ });
118
+
119
+ // Combine conditions
120
+ allConditions(
121
+ makeUrlCondition('*example.com*'),
122
+ makeUrlCondition('*/checkout*')
123
+ ); // Both must match
124
+
125
+ anyCondition(makeUrlCondition('*/cart*'), makeUrlCondition('*/checkout*')); // Either matches
126
+
127
+ notCondition(makeUrlCondition('*/admin*')); // Negation
128
+ ```
129
+
130
+ ## Page Trigger Lifecycle
131
+
132
+ ### The Guarantee
133
+
134
+ When navigation is detected:
135
+
136
+ 1. **Action is signaled to stop** (AbortController.abort())
137
+ 2. **Wait for action to finish** (up to 10 seconds for graceful cleanup)
138
+ 3. **Only then start waiting for page load**
139
+
140
+ This ensures:
141
+
142
+ - No DOM operations on stale/loading pages
143
+ - Actions can do proper cleanup (clear intervals, save state)
144
+ - No race conditions between action and navigation
145
+
146
+ ## Action Context API
147
+
148
+ When your action is called, it receives a context object with these properties:
149
+
150
+ ```javascript
151
+ commander.pageTrigger({
152
+ name: 'my-trigger',
153
+ condition: makeUrlCondition('*/checkout*'),
154
+ action: async (ctx) => {
155
+ // Current URL
156
+ ctx.url; // 'https://example.com/checkout'
157
+
158
+ // Trigger name (for debugging)
159
+ ctx.triggerName; // 'my-trigger'
160
+
161
+ // Check if action should stop
162
+ ctx.isStopped(); // Returns true if navigation detected
163
+
164
+ // Throw ActionStoppedError if stopped (use in manual loops)
165
+ ctx.checkStopped();
166
+
167
+ // AbortSignal - use with fetch() or other cancellable APIs
168
+ ctx.abortSignal;
169
+
170
+ // Safe wait (throws if stopped during wait)
171
+ await ctx.wait(1000);
172
+
173
+ // Safe iteration (checks stopped between items)
174
+ await ctx.forEach(items, async (item) => {
175
+ await ctx.commander.clickButton({ selector: item.selector });
176
+ });
177
+
178
+ // Register cleanup (runs when action stops)
179
+ ctx.onCleanup(() => {
180
+ console.log('Cleaning up...');
181
+ });
182
+
183
+ // Commander with all methods wrapped to throw on stop
184
+ await ctx.commander.fillTextArea({ selector: 'input', text: 'hello' });
185
+
186
+ // Raw commander (use carefully - does not auto-throw)
187
+ ctx.rawCommander;
188
+ },
189
+ });
190
+ ```
191
+
192
+ ## API Reference
193
+
194
+ ### makeBrowserCommander(options)
195
+
196
+ ```javascript
197
+ const commander = makeBrowserCommander({
198
+ page, // Required: Playwright/Puppeteer page
199
+ verbose: false, // Enable debug logging
200
+ enableNetworkTracking: true, // Track HTTP requests
201
+ enableNavigationManager: true, // Enable navigation events
202
+ });
203
+ ```
204
+
205
+ ### commander.pageTrigger(config)
206
+
207
+ ```javascript
208
+ const unregister = commander.pageTrigger({
209
+ name: 'trigger-name', // For debugging
210
+ condition: (ctx) => boolean, // When to run (receives {url, commander})
211
+ action: async (ctx) => void, // What to do
212
+ priority: 0, // Higher runs first
213
+ });
214
+ ```
215
+
216
+ ### commander.goto(options)
217
+
218
+ ```javascript
219
+ await commander.goto({
220
+ url: 'https://example.com',
221
+ waitUntil: 'domcontentloaded', // Playwright/Puppeteer option
222
+ timeout: 60000,
223
+ });
224
+ ```
225
+
226
+ ### commander.clickButton(options)
227
+
228
+ ```javascript
229
+ await commander.clickButton({
230
+ selector: 'button.submit',
231
+ scrollIntoView: true,
232
+ waitForNavigation: true,
233
+ });
234
+ ```
235
+
236
+ ### commander.fillTextArea(options)
237
+
238
+ ```javascript
239
+ await commander.fillTextArea({
240
+ selector: 'textarea.message',
241
+ text: 'Hello world',
242
+ checkEmpty: true,
243
+ });
244
+ ```
245
+
246
+ ### commander.destroy()
247
+
248
+ ```javascript
249
+ await commander.destroy(); // Stop actions, cleanup
250
+ ```
251
+
252
+ ## Best Practices
253
+
254
+ ### 1. Use ctx.forEach for Loops
255
+
256
+ ```javascript
257
+ // BAD: Won't stop on navigation
258
+ for (const item of items) {
259
+ await ctx.commander.click({ selector: item });
260
+ }
261
+
262
+ // GOOD: Stops immediately on navigation
263
+ await ctx.forEach(items, async (item) => {
264
+ await ctx.commander.click({ selector: item });
265
+ });
266
+ ```
267
+
268
+ ### 2. Use ctx.checkStopped for Complex Logic
269
+
270
+ ```javascript
271
+ action: async (ctx) => {
272
+ while (hasMorePages) {
273
+ ctx.checkStopped(); // Throws if navigation detected
274
+
275
+ await processPage(ctx);
276
+ hasMorePages = await ctx.commander.isVisible({ selector: '.next' });
277
+ }
278
+ };
279
+ ```
280
+
281
+ ### 3. Register Cleanup for Resources
282
+
283
+ ```javascript
284
+ action: async (ctx) => {
285
+ const intervalId = setInterval(updateStatus, 1000);
286
+
287
+ ctx.onCleanup(() => {
288
+ clearInterval(intervalId);
289
+ console.log('Interval cleared');
290
+ });
291
+
292
+ // ... rest of action
293
+ };
294
+ ```
295
+
296
+ ### 4. Use ctx.abortSignal with Fetch
297
+
298
+ ```javascript
299
+ action: async (ctx) => {
300
+ const response = await fetch(url, {
301
+ signal: ctx.abortSignal, // Cancels on navigation
302
+ });
303
+ };
304
+ ```
305
+
306
+ ## Debugging
307
+
308
+ Enable verbose mode for detailed logs:
309
+
310
+ ```javascript
311
+ const commander = makeBrowserCommander({ page, verbose: true });
312
+ ```
313
+
314
+ ## Architecture
315
+
316
+ See [src/ARCHITECTURE.md](src/ARCHITECTURE.md) for detailed architecture documentation.
317
+
318
+ ## License
319
+
320
+ [UNLICENSE](LICENSE)
package/bunfig.toml ADDED
@@ -0,0 +1,3 @@
1
+ # Bun configuration
2
+ # Note: Bun doesn't support excluding test files directly in config
3
+ # Use CLI flags or file naming patterns instead
package/deno.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "nodeModulesDir": "auto",
3
+ "test": {
4
+ "include": ["tests/"],
5
+ "exclude": ["examples/", "node_modules/"]
6
+ }
7
+ }
@@ -0,0 +1,125 @@
1
+ import js from '@eslint/js';
2
+ import prettierConfig from 'eslint-config-prettier';
3
+ import prettierPlugin from 'eslint-plugin-prettier';
4
+
5
+ export default [
6
+ js.configs.recommended,
7
+ prettierConfig,
8
+ {
9
+ files: ['**/*.js', '**/*.mjs'],
10
+ plugins: {
11
+ prettier: prettierPlugin,
12
+ },
13
+ languageOptions: {
14
+ ecmaVersion: 'latest',
15
+ sourceType: 'module',
16
+ globals: {
17
+ // Node.js globals
18
+ console: 'readonly',
19
+ process: 'readonly',
20
+ Buffer: 'readonly',
21
+ __dirname: 'readonly',
22
+ __filename: 'readonly',
23
+ // Node.js 18+ globals
24
+ fetch: 'readonly',
25
+ // Web/Node.js shared globals
26
+ setTimeout: 'readonly',
27
+ clearTimeout: 'readonly',
28
+ setInterval: 'readonly',
29
+ clearInterval: 'readonly',
30
+ AbortController: 'readonly',
31
+ AbortSignal: 'readonly',
32
+ Event: 'readonly',
33
+ URL: 'readonly',
34
+ URLSearchParams: 'readonly',
35
+ // Testing globals
36
+ describe: 'readonly',
37
+ it: 'readonly',
38
+ test: 'readonly',
39
+ expect: 'readonly',
40
+ beforeEach: 'readonly',
41
+ afterEach: 'readonly',
42
+ beforeAll: 'readonly',
43
+ afterAll: 'readonly',
44
+ // Runtime-specific globals
45
+ Bun: 'readonly',
46
+ Deno: 'readonly',
47
+ globalThis: 'readonly',
48
+ // Browser globals (used in evaluate functions)
49
+ document: 'readonly',
50
+ window: 'readonly',
51
+ },
52
+ },
53
+ rules: {
54
+ // Prettier integration
55
+ 'prettier/prettier': 'error',
56
+
57
+ // Code quality rules
58
+ // Note: Using 'warn' instead of 'error' for unused-vars since this codebase
59
+ // has intentionally unused parameters in abstract base classes and mocks
60
+ 'no-unused-vars': [
61
+ 'warn',
62
+ {
63
+ argsIgnorePattern: '^_',
64
+ varsIgnorePattern: '^_',
65
+ destructuredArrayIgnorePattern: '^_',
66
+ },
67
+ ],
68
+ 'no-console': 'off', // Allow console in this project
69
+ 'no-debugger': 'error',
70
+
71
+ // Best practices
72
+ eqeqeq: ['error', 'always'],
73
+ curly: ['error', 'all'],
74
+ 'no-var': 'error',
75
+ 'prefer-const': 'error',
76
+ 'prefer-arrow-callback': 'error',
77
+ 'no-duplicate-imports': 'error',
78
+
79
+ // ES6+ features
80
+ 'arrow-body-style': ['error', 'as-needed'],
81
+ 'object-shorthand': ['error', 'always'],
82
+ 'prefer-template': 'error',
83
+
84
+ // Async/await
85
+ 'no-async-promise-executor': 'error',
86
+ 'require-await': 'warn',
87
+
88
+ // Comments and documentation
89
+ 'spaced-comment': ['error', 'always', { markers: ['/'] }],
90
+
91
+ // Complexity rules - reasonable thresholds for maintainability
92
+ complexity: ['warn', 15], // Cyclomatic complexity - allow more complex logic than strict 8
93
+ 'max-depth': ['warn', 5], // Maximum nesting depth - slightly more lenient than strict 4
94
+ 'max-lines-per-function': [
95
+ 'warn',
96
+ {
97
+ max: 150, // More reasonable than strict 50 lines per function
98
+ skipBlankLines: true,
99
+ skipComments: true,
100
+ },
101
+ ],
102
+ 'max-params': ['warn', 6], // Maximum function parameters - slightly more lenient than strict 5
103
+ 'max-statements': ['warn', 60], // Maximum statements per function - reasonable limit for orchestration functions
104
+ 'max-lines': ['error', 1500], // Maximum lines per file - counts all lines including blank lines and comments
105
+ },
106
+ },
107
+ {
108
+ // Test files have different requirements
109
+ files: ['tests/**/*.js', '**/*.test.js'],
110
+ rules: {
111
+ 'require-await': 'off', // Async functions without await are common in tests
112
+ 'no-unused-vars': 'off', // Mock functions often have unused parameters for API compatibility
113
+ 'max-lines-per-function': 'off', // Tests can be long
114
+ },
115
+ },
116
+ {
117
+ ignores: [
118
+ 'node_modules/**',
119
+ 'coverage/**',
120
+ 'dist/**',
121
+ '*.min.js',
122
+ '.eslintcache',
123
+ ],
124
+ },
125
+ ];
@@ -0,0 +1,25 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>React Test App - Browser Commander E2E Tests</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ }
11
+ body {
12
+ font-family:
13
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
14
+ sans-serif;
15
+ margin: 0;
16
+ padding: 20px;
17
+ background: #f5f5f5;
18
+ }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <div id="root"></div>
23
+ <script type="module" src="/src/main.jsx"></script>
24
+ </body>
25
+ </html>
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "react-test-app",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.2.0",
13
+ "react-dom": "^18.2.0"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-react": "^4.2.0",
17
+ "vite": "^5.0.0"
18
+ }
19
+ }