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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.github/workflows/release.yml +296 -0
- package/.husky/pre-commit +1 -0
- package/.jscpd.json +20 -0
- package/.prettierignore +7 -0
- package/.prettierrc +10 -0
- package/CHANGELOG.md +32 -0
- package/LICENSE +24 -0
- package/README.md +320 -0
- package/bunfig.toml +3 -0
- package/deno.json +7 -0
- package/eslint.config.js +125 -0
- package/examples/react-test-app/index.html +25 -0
- package/examples/react-test-app/package.json +19 -0
- package/examples/react-test-app/src/App.jsx +473 -0
- package/examples/react-test-app/src/main.jsx +10 -0
- package/examples/react-test-app/src/styles.css +323 -0
- package/examples/react-test-app/vite.config.js +9 -0
- package/package.json +89 -0
- package/scripts/changeset-version.mjs +38 -0
- package/scripts/create-github-release.mjs +93 -0
- package/scripts/create-manual-changeset.mjs +86 -0
- package/scripts/format-github-release.mjs +83 -0
- package/scripts/format-release-notes.mjs +216 -0
- package/scripts/instant-version-bump.mjs +121 -0
- package/scripts/merge-changesets.mjs +260 -0
- package/scripts/publish-to-npm.mjs +126 -0
- package/scripts/setup-npm.mjs +37 -0
- package/scripts/validate-changeset.mjs +262 -0
- package/scripts/version-and-commit.mjs +237 -0
- package/src/ARCHITECTURE.md +270 -0
- package/src/README.md +517 -0
- package/src/bindings.js +298 -0
- package/src/browser/launcher.js +93 -0
- package/src/browser/navigation.js +513 -0
- package/src/core/constants.js +24 -0
- package/src/core/engine-adapter.js +466 -0
- package/src/core/engine-detection.js +49 -0
- package/src/core/logger.js +21 -0
- package/src/core/navigation-manager.js +503 -0
- package/src/core/navigation-safety.js +160 -0
- package/src/core/network-tracker.js +373 -0
- package/src/core/page-session.js +299 -0
- package/src/core/page-trigger-manager.js +564 -0
- package/src/core/preferences.js +46 -0
- package/src/elements/content.js +197 -0
- package/src/elements/locators.js +243 -0
- package/src/elements/selectors.js +360 -0
- package/src/elements/visibility.js +166 -0
- package/src/exports.js +121 -0
- package/src/factory.js +192 -0
- package/src/high-level/universal-logic.js +206 -0
- package/src/index.js +17 -0
- package/src/interactions/click.js +684 -0
- package/src/interactions/fill.js +383 -0
- package/src/interactions/scroll.js +341 -0
- package/src/utilities/url.js +33 -0
- package/src/utilities/wait.js +135 -0
- package/tests/e2e/playwright.e2e.test.js +442 -0
- package/tests/e2e/puppeteer.e2e.test.js +408 -0
- package/tests/helpers/mocks.js +542 -0
- package/tests/unit/bindings.test.js +218 -0
- package/tests/unit/browser/navigation.test.js +345 -0
- package/tests/unit/core/constants.test.js +72 -0
- package/tests/unit/core/engine-adapter.test.js +170 -0
- package/tests/unit/core/engine-detection.test.js +81 -0
- package/tests/unit/core/logger.test.js +80 -0
- package/tests/unit/core/navigation-safety.test.js +202 -0
- package/tests/unit/core/network-tracker.test.js +198 -0
- package/tests/unit/core/page-trigger-manager.test.js +358 -0
- package/tests/unit/elements/content.test.js +318 -0
- package/tests/unit/elements/locators.test.js +236 -0
- package/tests/unit/elements/selectors.test.js +302 -0
- package/tests/unit/elements/visibility.test.js +234 -0
- package/tests/unit/factory.test.js +174 -0
- package/tests/unit/high-level/universal-logic.test.js +299 -0
- package/tests/unit/interactions/click.test.js +340 -0
- package/tests/unit/interactions/fill.test.js +378 -0
- package/tests/unit/interactions/scroll.test.js +330 -0
- package/tests/unit/utilities/url.test.js +63 -0
- 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
package/deno.json
ADDED
package/eslint.config.js
ADDED
|
@@ -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
|
+
}
|