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/src/README.md
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
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
|
+
## Core Concept: Page State Machine
|
|
6
|
+
|
|
7
|
+
Browser Commander manages the browser as a state machine with two states:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
11
|
+
│ │ navigation start │ │
|
|
12
|
+
│ WORKING STATE │ ─────────────────► │ LOADING STATE │
|
|
13
|
+
│ (action runs) │ │ (wait only) │
|
|
14
|
+
│ │ ◄───────────────── │ │
|
|
15
|
+
└─────────────────┘ page ready └─────────────────┘
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**LOADING STATE**: Page is loading. Only waiting/tracking operations are allowed. No automation logic runs.
|
|
19
|
+
|
|
20
|
+
**WORKING STATE**: Page is fully loaded (30 seconds of network idle). Page triggers can safely interact with DOM.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
import {
|
|
26
|
+
launchBrowser,
|
|
27
|
+
makeBrowserCommander,
|
|
28
|
+
makeUrlCondition,
|
|
29
|
+
} from './browser-commander/index.js';
|
|
30
|
+
|
|
31
|
+
// 1. Launch browser
|
|
32
|
+
const { browser, page } = await launchBrowser({ engine: 'playwright' });
|
|
33
|
+
|
|
34
|
+
// 2. Create commander
|
|
35
|
+
const commander = makeBrowserCommander({ page, verbose: true });
|
|
36
|
+
|
|
37
|
+
// 3. Register page trigger with condition and action
|
|
38
|
+
commander.pageTrigger({
|
|
39
|
+
name: 'example-trigger',
|
|
40
|
+
condition: makeUrlCondition('*example.com*'), // matches URLs containing 'example.com'
|
|
41
|
+
action: async (ctx) => {
|
|
42
|
+
// ctx.commander has all methods, but they throw ActionStoppedError if navigation happens
|
|
43
|
+
// ctx.checkStopped() - call in loops to check if should stop
|
|
44
|
+
// ctx.abortSignal - use with fetch() for cancellation
|
|
45
|
+
// ctx.onCleanup(fn) - register cleanup when action stops
|
|
46
|
+
|
|
47
|
+
console.log(`Processing: ${ctx.url}`);
|
|
48
|
+
|
|
49
|
+
// Safe iteration - stops if navigation detected
|
|
50
|
+
await ctx.forEach(['item1', 'item2'], async (item) => {
|
|
51
|
+
await ctx.commander.clickButton({ selector: `[data-id="${item}"]` });
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 4. Navigate - action auto-starts when page is ready
|
|
57
|
+
await commander.goto({ url: 'https://example.com' });
|
|
58
|
+
|
|
59
|
+
// 5. Cleanup
|
|
60
|
+
await commander.destroy();
|
|
61
|
+
await browser.close();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## URL Condition Helpers
|
|
65
|
+
|
|
66
|
+
The `makeUrlCondition` helper makes it easy to create URL matching conditions:
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
import {
|
|
70
|
+
makeUrlCondition,
|
|
71
|
+
allConditions,
|
|
72
|
+
anyCondition,
|
|
73
|
+
notCondition,
|
|
74
|
+
} from './browser-commander/index.js';
|
|
75
|
+
|
|
76
|
+
// Exact URL match
|
|
77
|
+
makeUrlCondition('https://example.com/page');
|
|
78
|
+
|
|
79
|
+
// Contains substring (use * wildcards)
|
|
80
|
+
makeUrlCondition('*checkout*'); // URL contains 'checkout'
|
|
81
|
+
makeUrlCondition('*example.com*'); // URL contains 'example.com'
|
|
82
|
+
|
|
83
|
+
// Starts with / ends with
|
|
84
|
+
makeUrlCondition('/api/*'); // starts with '/api/'
|
|
85
|
+
makeUrlCondition('*.json'); // ends with '.json'
|
|
86
|
+
|
|
87
|
+
// Express-style route patterns
|
|
88
|
+
makeUrlCondition('/vacancy/:id'); // matches /vacancy/123
|
|
89
|
+
makeUrlCondition('https://hh.ru/vacancy/:vacancyId'); // matches specific domain + path
|
|
90
|
+
makeUrlCondition('/user/:userId/profile'); // multiple segments
|
|
91
|
+
|
|
92
|
+
// RegExp
|
|
93
|
+
makeUrlCondition(/\/product\/\d+/);
|
|
94
|
+
|
|
95
|
+
// Custom function (receives full context)
|
|
96
|
+
makeUrlCondition((url, ctx) => {
|
|
97
|
+
const parsed = new URL(url);
|
|
98
|
+
return (
|
|
99
|
+
parsed.pathname.startsWith('/admin') && parsed.searchParams.has('edit')
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Combine conditions
|
|
104
|
+
allConditions(
|
|
105
|
+
makeUrlCondition('*example.com*'),
|
|
106
|
+
makeUrlCondition('*/checkout*')
|
|
107
|
+
); // Both must match
|
|
108
|
+
|
|
109
|
+
anyCondition(makeUrlCondition('*/cart*'), makeUrlCondition('*/checkout*')); // Either matches
|
|
110
|
+
|
|
111
|
+
notCondition(makeUrlCondition('*/admin*')); // Negation
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Page Trigger Lifecycle
|
|
115
|
+
|
|
116
|
+
### The Guarantee
|
|
117
|
+
|
|
118
|
+
When navigation is detected:
|
|
119
|
+
|
|
120
|
+
1. **Action is signaled to stop** (AbortController.abort())
|
|
121
|
+
2. **Wait for action to finish** (up to 10 seconds for graceful cleanup)
|
|
122
|
+
3. **Only then start waiting for page load**
|
|
123
|
+
|
|
124
|
+
This ensures:
|
|
125
|
+
|
|
126
|
+
- No DOM operations on stale/loading pages
|
|
127
|
+
- Actions can do proper cleanup (clear intervals, save state)
|
|
128
|
+
- No race conditions between action and navigation
|
|
129
|
+
|
|
130
|
+
### Lifecycle Flow
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
URL Change Detected
|
|
134
|
+
│
|
|
135
|
+
▼
|
|
136
|
+
┌──────────────────────────────────┐
|
|
137
|
+
│ 1. Signal action to stop │ ◄── AbortController.abort()
|
|
138
|
+
│ 2. Wait for action to finish │ ◄── Max 10 seconds
|
|
139
|
+
│ 3. Run cleanup callbacks │ ◄── ctx.onCleanup()
|
|
140
|
+
└──────────────────────────────────┘
|
|
141
|
+
│
|
|
142
|
+
▼
|
|
143
|
+
LOADING STATE
|
|
144
|
+
│
|
|
145
|
+
▼
|
|
146
|
+
┌──────────────────────────────────┐
|
|
147
|
+
│ 1. Wait for URL stabilization │ ◄── No more redirects (1s)
|
|
148
|
+
│ 2. Wait for network idle │ ◄── 30 seconds no requests
|
|
149
|
+
└──────────────────────────────────┘
|
|
150
|
+
│
|
|
151
|
+
▼
|
|
152
|
+
WORKING STATE
|
|
153
|
+
│
|
|
154
|
+
▼
|
|
155
|
+
┌──────────────────────────────────┐
|
|
156
|
+
│ 1. Find matching trigger │ ◄── condition(ctx)
|
|
157
|
+
│ 2. Start action │ ◄── action(ctx)
|
|
158
|
+
└──────────────────────────────────┘
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Action Context API
|
|
162
|
+
|
|
163
|
+
When your action is called, it receives a context object with these properties:
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
commander.pageTrigger({
|
|
167
|
+
name: 'my-trigger',
|
|
168
|
+
condition: makeUrlCondition('*/checkout*'),
|
|
169
|
+
action: async (ctx) => {
|
|
170
|
+
// Current URL
|
|
171
|
+
ctx.url; // 'https://example.com/checkout'
|
|
172
|
+
|
|
173
|
+
// Trigger name (for debugging)
|
|
174
|
+
ctx.triggerName; // 'my-trigger'
|
|
175
|
+
|
|
176
|
+
// Check if action should stop
|
|
177
|
+
ctx.isStopped(); // Returns true if navigation detected
|
|
178
|
+
|
|
179
|
+
// Throw ActionStoppedError if stopped (use in manual loops)
|
|
180
|
+
ctx.checkStopped();
|
|
181
|
+
|
|
182
|
+
// AbortSignal - use with fetch() or other cancellable APIs
|
|
183
|
+
ctx.abortSignal;
|
|
184
|
+
|
|
185
|
+
// Safe wait (throws if stopped during wait)
|
|
186
|
+
await ctx.wait(1000);
|
|
187
|
+
|
|
188
|
+
// Safe iteration (checks stopped between items)
|
|
189
|
+
await ctx.forEach(items, async (item) => {
|
|
190
|
+
await ctx.commander.clickButton({ selector: item.selector });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Register cleanup (runs when action stops)
|
|
194
|
+
ctx.onCleanup(() => {
|
|
195
|
+
console.log('Cleaning up...');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Commander with all methods wrapped to throw on stop
|
|
199
|
+
await ctx.commander.fillTextArea({ selector: 'input', text: 'hello' });
|
|
200
|
+
|
|
201
|
+
// Raw commander (use carefully - does not auto-throw)
|
|
202
|
+
ctx.rawCommander;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## ActionStoppedError
|
|
208
|
+
|
|
209
|
+
When navigation is detected, all `ctx.commander` methods throw `ActionStoppedError`:
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
action: async (ctx) => {
|
|
213
|
+
try {
|
|
214
|
+
await ctx.commander.clickButton({ selector: 'button' });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (commander.isActionStoppedError(error)) {
|
|
217
|
+
// Navigation happened - clean up and return
|
|
218
|
+
console.log('Navigation detected, stopping');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
throw error; // Re-throw other errors
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The error is automatically caught by the PageTriggerManager, so you usually don't need to catch it unless you need custom cleanup logic.
|
|
227
|
+
|
|
228
|
+
## Condition Function
|
|
229
|
+
|
|
230
|
+
The `condition` function determines when your trigger runs. It receives full context:
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
// Simple URL check
|
|
234
|
+
condition: (ctx) => ctx.url.includes('/checkout');
|
|
235
|
+
|
|
236
|
+
// Multiple pages
|
|
237
|
+
condition: (ctx) => ctx.url.includes('/cart') || ctx.url.includes('/checkout');
|
|
238
|
+
|
|
239
|
+
// Regex
|
|
240
|
+
condition: (ctx) => /\/product\/\d+/.test(ctx.url);
|
|
241
|
+
|
|
242
|
+
// Complex logic with commander access
|
|
243
|
+
condition: (ctx) => {
|
|
244
|
+
const parsed = new URL(ctx.url);
|
|
245
|
+
return (
|
|
246
|
+
parsed.pathname.startsWith('/admin') && parsed.searchParams.has('edit')
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Or use makeUrlCondition helper
|
|
251
|
+
condition: makeUrlCondition('/checkout/*');
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Trigger Priority
|
|
255
|
+
|
|
256
|
+
If multiple triggers match, the highest priority runs:
|
|
257
|
+
|
|
258
|
+
```javascript
|
|
259
|
+
// Higher priority runs first
|
|
260
|
+
commander.pageTrigger({
|
|
261
|
+
name: 'specific-checkout',
|
|
262
|
+
priority: 10, // Higher priority
|
|
263
|
+
condition: makeUrlCondition('*/checkout/payment*'),
|
|
264
|
+
action: handlePaymentPage,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
commander.pageTrigger({
|
|
268
|
+
name: 'general-checkout',
|
|
269
|
+
priority: 0, // Default priority
|
|
270
|
+
condition: makeUrlCondition('*/checkout*'),
|
|
271
|
+
action: handleCheckoutPage,
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Returning to a Page
|
|
276
|
+
|
|
277
|
+
If navigation brings you back to a matching URL, the action restarts:
|
|
278
|
+
|
|
279
|
+
```javascript
|
|
280
|
+
// Trigger registered for /search
|
|
281
|
+
commander.pageTrigger({
|
|
282
|
+
condition: makeUrlCondition('*/search*'),
|
|
283
|
+
action: async (ctx) => {
|
|
284
|
+
console.log('Search action started');
|
|
285
|
+
// ... do work
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Navigate to search -> action starts
|
|
290
|
+
await commander.goto({ url: '/search' });
|
|
291
|
+
|
|
292
|
+
// Navigate away -> action stops
|
|
293
|
+
await commander.goto({ url: '/product/123' });
|
|
294
|
+
|
|
295
|
+
// Navigate back -> action restarts (new instance)
|
|
296
|
+
await commander.goto({ url: '/search' });
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Unregistering Triggers
|
|
300
|
+
|
|
301
|
+
`pageTrigger` returns an unregister function:
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
const unregister = commander.pageTrigger({
|
|
305
|
+
name: 'temp-trigger',
|
|
306
|
+
condition: makeUrlCondition('*/temp*'),
|
|
307
|
+
action: async (ctx) => {
|
|
308
|
+
/* ... */
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Later: remove the trigger
|
|
313
|
+
unregister();
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Architecture
|
|
317
|
+
|
|
318
|
+
### File Structure
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
browser-commander/
|
|
322
|
+
├── index.js # Main entry, makeBrowserCommander()
|
|
323
|
+
├── core/
|
|
324
|
+
│ ├── page-trigger-manager.js # Trigger lifecycle management
|
|
325
|
+
│ ├── navigation-manager.js # URL changes, abort signals
|
|
326
|
+
│ ├── network-tracker.js # HTTP request tracking
|
|
327
|
+
│ ├── page-session.js # Per-page context (legacy)
|
|
328
|
+
│ ├── navigation-safety.js # Handle navigation errors
|
|
329
|
+
│ ├── constants.js # CHROME_ARGS, TIMING
|
|
330
|
+
│ ├── logger.js # Logging utilities
|
|
331
|
+
│ ├── engine-detection.js # Detect Playwright/Puppeteer
|
|
332
|
+
│ └── preferences.js # Chrome preferences
|
|
333
|
+
├── browser/
|
|
334
|
+
│ ├── launcher.js # Browser launch
|
|
335
|
+
│ └── navigation.js # goto, waitForNavigation
|
|
336
|
+
├── elements/
|
|
337
|
+
│ ├── locators.js # Element location
|
|
338
|
+
│ ├── selectors.js # querySelector, findByText
|
|
339
|
+
│ ├── visibility.js # isVisible, isEnabled
|
|
340
|
+
│ └── content.js # textContent, getAttribute
|
|
341
|
+
├── interactions/
|
|
342
|
+
│ ├── click.js # clickButton, clickElement
|
|
343
|
+
│ ├── fill.js # fillTextArea
|
|
344
|
+
│ └── scroll.js # scrollIntoView
|
|
345
|
+
├── utilities/
|
|
346
|
+
│ ├── wait.js # wait(), evaluate()
|
|
347
|
+
│ └── url.js # getUrl
|
|
348
|
+
└── high-level/
|
|
349
|
+
└── universal-logic.js # High-level utilities
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Component Relationships
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
356
|
+
│ BrowserCommander │
|
|
357
|
+
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │
|
|
358
|
+
│ │ NetworkTracker │ │NavigationManager│ │PageTriggerMgr │ │
|
|
359
|
+
│ │ │ │ │ │ │ │
|
|
360
|
+
│ │ - Track HTTP │◄─│ - URL changes │◄─│ - Register │ │
|
|
361
|
+
│ │ - Wait idle │ │ - Abort signals │ │ - Start/Stop │ │
|
|
362
|
+
│ │ - 30s threshold │ │ - Events │ │ - Lifecycle │ │
|
|
363
|
+
│ └─────────────────┘ └─────────────────┘ └────────────────┘ │
|
|
364
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Network Idle Detection
|
|
368
|
+
|
|
369
|
+
The library waits for **30 seconds of zero pending HTTP requests** before considering a page fully loaded:
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
// NetworkTracker created with 30s idle timeout
|
|
373
|
+
networkTracker = createNetworkTracker({
|
|
374
|
+
idleTimeout: 30000, // 30 seconds without requests = idle
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
This ensures:
|
|
379
|
+
|
|
380
|
+
- All lazy-loaded content is fetched
|
|
381
|
+
- All analytics scripts complete
|
|
382
|
+
- All async JavaScript executes
|
|
383
|
+
- SPAs fully hydrate
|
|
384
|
+
|
|
385
|
+
## API Reference
|
|
386
|
+
|
|
387
|
+
### makeBrowserCommander(options)
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
const commander = makeBrowserCommander({
|
|
391
|
+
page, // Required: Playwright/Puppeteer page
|
|
392
|
+
verbose: false, // Enable debug logging
|
|
393
|
+
enableNetworkTracking: true, // Track HTTP requests
|
|
394
|
+
enableNavigationManager: true, // Enable navigation events
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### commander.pageTrigger(config)
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
const unregister = commander.pageTrigger({
|
|
402
|
+
name: 'trigger-name', // For debugging
|
|
403
|
+
condition: (ctx) => boolean, // When to run (receives {url, commander})
|
|
404
|
+
action: async (ctx) => void, // What to do
|
|
405
|
+
priority: 0, // Higher runs first
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### commander.goto(options)
|
|
410
|
+
|
|
411
|
+
```javascript
|
|
412
|
+
await commander.goto({
|
|
413
|
+
url: 'https://example.com',
|
|
414
|
+
waitUntil: 'domcontentloaded', // Playwright/Puppeteer option
|
|
415
|
+
timeout: 60000,
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### commander.clickButton(options)
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
await commander.clickButton({
|
|
423
|
+
selector: 'button.submit',
|
|
424
|
+
scrollIntoView: true,
|
|
425
|
+
waitForNavigation: true,
|
|
426
|
+
});
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### commander.fillTextArea(options)
|
|
430
|
+
|
|
431
|
+
```javascript
|
|
432
|
+
await commander.fillTextArea({
|
|
433
|
+
selector: 'textarea.message',
|
|
434
|
+
text: 'Hello world',
|
|
435
|
+
checkEmpty: true,
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### commander.destroy()
|
|
440
|
+
|
|
441
|
+
```javascript
|
|
442
|
+
await commander.destroy(); // Stop actions, cleanup
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Best Practices
|
|
446
|
+
|
|
447
|
+
### 1. Use ctx.forEach for Loops
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
// BAD: Won't stop on navigation
|
|
451
|
+
for (const item of items) {
|
|
452
|
+
await ctx.commander.click({ selector: item });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// GOOD: Stops immediately on navigation
|
|
456
|
+
await ctx.forEach(items, async (item) => {
|
|
457
|
+
await ctx.commander.click({ selector: item });
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### 2. Use ctx.checkStopped for Complex Logic
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
action: async (ctx) => {
|
|
465
|
+
while (hasMorePages) {
|
|
466
|
+
ctx.checkStopped(); // Throws if navigation detected
|
|
467
|
+
|
|
468
|
+
await processPage(ctx);
|
|
469
|
+
hasMorePages = await ctx.commander.isVisible({ selector: '.next' });
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 3. Register Cleanup for Resources
|
|
475
|
+
|
|
476
|
+
```javascript
|
|
477
|
+
action: async (ctx) => {
|
|
478
|
+
const intervalId = setInterval(updateStatus, 1000);
|
|
479
|
+
|
|
480
|
+
ctx.onCleanup(() => {
|
|
481
|
+
clearInterval(intervalId);
|
|
482
|
+
console.log('Interval cleared');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// ... rest of action
|
|
486
|
+
};
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### 4. Use ctx.abortSignal with Fetch
|
|
490
|
+
|
|
491
|
+
```javascript
|
|
492
|
+
action: async (ctx) => {
|
|
493
|
+
const response = await fetch(url, {
|
|
494
|
+
signal: ctx.abortSignal, // Cancels on navigation
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## Debugging
|
|
500
|
+
|
|
501
|
+
Enable verbose mode for detailed logs:
|
|
502
|
+
|
|
503
|
+
```javascript
|
|
504
|
+
const commander = makeBrowserCommander({ page, verbose: true });
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Log symbols:
|
|
508
|
+
|
|
509
|
+
- `📋` Trigger registration/lifecycle
|
|
510
|
+
- `🚀` Action starting
|
|
511
|
+
- `🛑` Action stopping
|
|
512
|
+
- `✅` Action completed
|
|
513
|
+
- `❌` Action error
|
|
514
|
+
- `📤` Request started
|
|
515
|
+
- `📥` Request ended
|
|
516
|
+
- `🔗` URL change
|
|
517
|
+
- `🌐` Network idle
|