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
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Engine Adapter - Abstract away Playwright/Puppeteer differences
3
+ *
4
+ * This module implements the Adapter pattern to encapsulate engine-specific
5
+ * logic in a single place, following the "Protected Variations" principle.
6
+ *
7
+ * Benefits:
8
+ * - Eliminates scattered `if (engine === 'playwright')` checks
9
+ * - Easier to add new engines (e.g., Selenium)
10
+ * - Easier to test with mock adapters
11
+ * - Clearer separation of concerns
12
+ */
13
+
14
+ import { TIMING } from './constants.js';
15
+
16
+ /**
17
+ * Base class defining the engine adapter interface
18
+ * All engine-specific operations should be defined here
19
+ */
20
+ export class EngineAdapter {
21
+ constructor(page) {
22
+ this.page = page;
23
+ }
24
+
25
+ /**
26
+ * Get engine name
27
+ * @returns {string} - 'playwright' or 'puppeteer'
28
+ */
29
+ getEngineName() {
30
+ throw new Error('getEngineName() must be implemented by subclass');
31
+ }
32
+
33
+ // ============================================================================
34
+ // Element Selection and Locators
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Create a locator/element handle from a selector
39
+ * @param {string} selector - CSS selector
40
+ * @returns {Object} - Locator (Playwright) or ElementHandle (Puppeteer)
41
+ */
42
+ createLocator(selector) {
43
+ throw new Error('createLocator() must be implemented by subclass');
44
+ }
45
+
46
+ /**
47
+ * Query single element
48
+ * @param {string} selector - CSS selector
49
+ * @returns {Promise<Object|null>} - Locator/Element or null
50
+ */
51
+ async querySelector(selector) {
52
+ throw new Error('querySelector() must be implemented by subclass');
53
+ }
54
+
55
+ /**
56
+ * Query all elements
57
+ * @param {string} selector - CSS selector
58
+ * @returns {Promise<Array>} - Array of locators/elements
59
+ */
60
+ async querySelectorAll(selector) {
61
+ throw new Error('querySelectorAll() must be implemented by subclass');
62
+ }
63
+
64
+ /**
65
+ * Wait for selector to appear
66
+ * @param {string} selector - CSS selector
67
+ * @param {Object} options - Wait options {visible, timeout}
68
+ * @returns {Promise<void>}
69
+ */
70
+ async waitForSelector(selector, options = {}) {
71
+ throw new Error('waitForSelector() must be implemented by subclass');
72
+ }
73
+
74
+ /**
75
+ * Wait for element to be visible
76
+ * @param {Object} locatorOrElement - Locator or element
77
+ * @param {number} timeout - Timeout in ms
78
+ * @returns {Promise<Object>} - The locator/element
79
+ */
80
+ async waitForVisible(locatorOrElement, timeout = TIMING.DEFAULT_TIMEOUT) {
81
+ throw new Error('waitForVisible() must be implemented by subclass');
82
+ }
83
+
84
+ /**
85
+ * Count matching elements
86
+ * @param {string} selector - CSS selector
87
+ * @returns {Promise<number>} - Number of matching elements
88
+ */
89
+ async count(selector) {
90
+ throw new Error('count() must be implemented by subclass');
91
+ }
92
+
93
+ // ============================================================================
94
+ // Element Evaluation and Properties
95
+ // ============================================================================
96
+
97
+ /**
98
+ * Evaluate function on element
99
+ * @param {Object} locatorOrElement - Locator or element
100
+ * @param {Function} fn - Function to evaluate
101
+ * @param {any} args - Arguments to pass (optional)
102
+ * @returns {Promise<any>} - Result of evaluation
103
+ */
104
+ async evaluateOnElement(locatorOrElement, fn, args) {
105
+ throw new Error('evaluateOnElement() must be implemented by subclass');
106
+ }
107
+
108
+ /**
109
+ * Get element text content
110
+ * @param {Object} locatorOrElement - Locator or element
111
+ * @returns {Promise<string|null>} - Text content
112
+ */
113
+ async getTextContent(locatorOrElement) {
114
+ throw new Error('getTextContent() must be implemented by subclass');
115
+ }
116
+
117
+ /**
118
+ * Get input value
119
+ * @param {Object} locatorOrElement - Locator or element
120
+ * @returns {Promise<string>} - Input value
121
+ */
122
+ async getInputValue(locatorOrElement) {
123
+ throw new Error('getInputValue() must be implemented by subclass');
124
+ }
125
+
126
+ /**
127
+ * Get element attribute
128
+ * @param {Object} locatorOrElement - Locator or element
129
+ * @param {string} attribute - Attribute name
130
+ * @returns {Promise<string|null>} - Attribute value
131
+ */
132
+ async getAttribute(locatorOrElement, attribute) {
133
+ throw new Error('getAttribute() must be implemented by subclass');
134
+ }
135
+
136
+ // ============================================================================
137
+ // Element Interactions
138
+ // ============================================================================
139
+
140
+ /**
141
+ * Click element
142
+ * @param {Object} locatorOrElement - Locator or element
143
+ * @param {Object} options - Click options {force, etc.}
144
+ * @returns {Promise<void>}
145
+ */
146
+ async click(locatorOrElement, options = {}) {
147
+ throw new Error('click() must be implemented by subclass');
148
+ }
149
+
150
+ /**
151
+ * Type text into element (simulates typing)
152
+ * @param {Object} locatorOrElement - Locator or element
153
+ * @param {string} text - Text to type
154
+ * @returns {Promise<void>}
155
+ */
156
+ async type(locatorOrElement, text) {
157
+ throw new Error('type() must be implemented by subclass');
158
+ }
159
+
160
+ /**
161
+ * Fill element with text (direct value assignment)
162
+ * @param {Object} locatorOrElement - Locator or element
163
+ * @param {string} text - Text to fill
164
+ * @returns {Promise<void>}
165
+ */
166
+ async fill(locatorOrElement, text) {
167
+ throw new Error('fill() must be implemented by subclass');
168
+ }
169
+
170
+ /**
171
+ * Focus element
172
+ * @param {Object} locatorOrElement - Locator or element
173
+ * @returns {Promise<void>}
174
+ */
175
+ async focus(locatorOrElement) {
176
+ throw new Error('focus() must be implemented by subclass');
177
+ }
178
+
179
+ // ============================================================================
180
+ // Page-level Operations
181
+ // ============================================================================
182
+
183
+ /**
184
+ * Evaluate JavaScript in page context
185
+ * @param {Function} fn - Function to evaluate
186
+ * @param {Array} args - Arguments to pass
187
+ * @returns {Promise<any>} - Result of evaluation
188
+ */
189
+ async evaluateOnPage(fn, args = []) {
190
+ throw new Error('evaluateOnPage() must be implemented by subclass');
191
+ }
192
+
193
+ /**
194
+ * Get the main frame
195
+ * @returns {Object} - Main frame
196
+ */
197
+ getMainFrame() {
198
+ throw new Error('getMainFrame() must be implemented by subclass');
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Playwright adapter implementation
204
+ */
205
+ export class PlaywrightAdapter extends EngineAdapter {
206
+ getEngineName() {
207
+ return 'playwright';
208
+ }
209
+
210
+ // ============================================================================
211
+ // Element Selection and Locators
212
+ // ============================================================================
213
+
214
+ createLocator(selector) {
215
+ // Handle :nth-of-type() pseudo-selectors which don't work in Playwright locators
216
+ const nthOfTypeMatch = selector.match(/^(.+):nth-of-type\((\d+)\)$/);
217
+ if (nthOfTypeMatch) {
218
+ const baseSelector = nthOfTypeMatch[1];
219
+ const index = parseInt(nthOfTypeMatch[2], 10) - 1; // Convert to 0-based
220
+ return this.page.locator(baseSelector).nth(index);
221
+ }
222
+ return this.page.locator(selector);
223
+ }
224
+
225
+ async querySelector(selector) {
226
+ const locator = this.createLocator(selector).first();
227
+ const count = await locator.count();
228
+ return count > 0 ? locator : null;
229
+ }
230
+
231
+ async querySelectorAll(selector) {
232
+ const locator = this.createLocator(selector);
233
+ const count = await locator.count();
234
+ const elements = [];
235
+ for (let i = 0; i < count; i++) {
236
+ elements.push(locator.nth(i));
237
+ }
238
+ return elements;
239
+ }
240
+
241
+ async waitForSelector(selector, options = {}) {
242
+ const { visible = true, timeout = 5000 } = options;
243
+ const locator = this.createLocator(selector);
244
+ await locator.waitFor({ state: visible ? 'visible' : 'attached', timeout });
245
+ }
246
+
247
+ async waitForVisible(locatorOrElement, timeout = TIMING.DEFAULT_TIMEOUT) {
248
+ const firstLocator = locatorOrElement.first();
249
+ await firstLocator.waitFor({ state: 'visible', timeout });
250
+ return firstLocator;
251
+ }
252
+
253
+ async count(selector) {
254
+ return await this.page.locator(selector).count();
255
+ }
256
+
257
+ // ============================================================================
258
+ // Element Evaluation and Properties
259
+ // ============================================================================
260
+
261
+ async evaluateOnElement(locatorOrElement, fn, args) {
262
+ // Playwright only accepts a single argument
263
+ if (args === undefined) {
264
+ return await locatorOrElement.evaluate(fn);
265
+ }
266
+ return await locatorOrElement.evaluate(fn, args);
267
+ }
268
+
269
+ async getTextContent(locatorOrElement) {
270
+ return await locatorOrElement.textContent();
271
+ }
272
+
273
+ async getInputValue(locatorOrElement) {
274
+ return await locatorOrElement.inputValue();
275
+ }
276
+
277
+ async getAttribute(locatorOrElement, attribute) {
278
+ return await locatorOrElement.getAttribute(attribute);
279
+ }
280
+
281
+ // ============================================================================
282
+ // Element Interactions
283
+ // ============================================================================
284
+
285
+ async click(locatorOrElement, options = {}) {
286
+ await locatorOrElement.click(options);
287
+ }
288
+
289
+ async type(locatorOrElement, text) {
290
+ await locatorOrElement.type(text);
291
+ }
292
+
293
+ async fill(locatorOrElement, text) {
294
+ await locatorOrElement.fill(text);
295
+ }
296
+
297
+ async focus(locatorOrElement) {
298
+ await locatorOrElement.focus();
299
+ }
300
+
301
+ // ============================================================================
302
+ // Page-level Operations
303
+ // ============================================================================
304
+
305
+ async evaluateOnPage(fn, args = []) {
306
+ // Playwright only accepts a single argument (can be array/object)
307
+ if (args.length === 0) {
308
+ return await this.page.evaluate(fn);
309
+ } else if (args.length === 1) {
310
+ return await this.page.evaluate(fn, args[0]);
311
+ } else {
312
+ // Multiple args - pass as array
313
+ return await this.page.evaluate(fn, args);
314
+ }
315
+ }
316
+
317
+ getMainFrame() {
318
+ return this.page.mainFrame();
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Puppeteer adapter implementation
324
+ */
325
+ export class PuppeteerAdapter extends EngineAdapter {
326
+ getEngineName() {
327
+ return 'puppeteer';
328
+ }
329
+
330
+ // ============================================================================
331
+ // Element Selection and Locators
332
+ // ============================================================================
333
+
334
+ createLocator(selector) {
335
+ // Puppeteer doesn't have locators - just returns selector
336
+ // The actual element will be queried when needed
337
+ return selector;
338
+ }
339
+
340
+ async querySelector(selector) {
341
+ return await this.page.$(selector);
342
+ }
343
+
344
+ async querySelectorAll(selector) {
345
+ return await this.page.$$(selector);
346
+ }
347
+
348
+ async waitForSelector(selector, options = {}) {
349
+ const { visible = true, timeout = 5000 } = options;
350
+ await this.page.waitForSelector(selector, { visible, timeout });
351
+ }
352
+
353
+ async waitForVisible(locatorOrElement, timeout = TIMING.DEFAULT_TIMEOUT) {
354
+ // For Puppeteer, locatorOrElement is already an ElementHandle
355
+ // We can't wait on it directly, so we just return it
356
+ // The caller should have already used waitForSelector
357
+ return locatorOrElement;
358
+ }
359
+
360
+ async count(selector) {
361
+ const elements = await this.page.$$(selector);
362
+ return elements.length;
363
+ }
364
+
365
+ // ============================================================================
366
+ // Element Evaluation and Properties
367
+ // ============================================================================
368
+
369
+ async evaluateOnElement(locatorOrElement, fn, args) {
370
+ // Puppeteer accepts the element as first arg, then spread args
371
+ if (args === undefined) {
372
+ return await this.page.evaluate(fn, locatorOrElement);
373
+ }
374
+ return await this.page.evaluate(fn, locatorOrElement, args);
375
+ }
376
+
377
+ async getTextContent(locatorOrElement) {
378
+ return await this.page.evaluate((el) => el.textContent, locatorOrElement);
379
+ }
380
+
381
+ async getInputValue(locatorOrElement) {
382
+ return await this.page.evaluate((el) => el.value, locatorOrElement);
383
+ }
384
+
385
+ async getAttribute(locatorOrElement, attribute) {
386
+ return await this.page.evaluate(
387
+ (el, attr) => el.getAttribute(attr),
388
+ locatorOrElement,
389
+ attribute
390
+ );
391
+ }
392
+
393
+ // ============================================================================
394
+ // Element Interactions
395
+ // ============================================================================
396
+
397
+ async click(locatorOrElement, options = {}) {
398
+ await locatorOrElement.click(options);
399
+ }
400
+
401
+ async type(locatorOrElement, text) {
402
+ // Puppeteer requires focus before typing
403
+ await locatorOrElement.focus();
404
+ await this.page.keyboard.type(text);
405
+ }
406
+
407
+ async fill(locatorOrElement, text) {
408
+ // Puppeteer doesn't have fill() - use evaluate to set value
409
+ await this.page.evaluate(
410
+ (el, value) => {
411
+ el.value = value;
412
+ el.dispatchEvent(new Event('input', { bubbles: true }));
413
+ el.dispatchEvent(new Event('change', { bubbles: true }));
414
+ },
415
+ locatorOrElement,
416
+ text
417
+ );
418
+ }
419
+
420
+ async focus(locatorOrElement) {
421
+ await locatorOrElement.focus();
422
+ }
423
+
424
+ // ============================================================================
425
+ // Page-level Operations
426
+ // ============================================================================
427
+
428
+ async evaluateOnPage(fn, args = []) {
429
+ // Puppeteer accepts spread arguments
430
+ return await this.page.evaluate(fn, ...args);
431
+ }
432
+
433
+ getMainFrame() {
434
+ return this.page.mainFrame();
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Factory function to create appropriate adapter
440
+ * @param {Object} page - Playwright or Puppeteer page object
441
+ * @param {string} engine - Engine type ('playwright' or 'puppeteer')
442
+ * @returns {EngineAdapter} - Appropriate adapter instance
443
+ */
444
+ export function createEngineAdapter(page, engine) {
445
+ if (!page) {
446
+ const errorDetails = {
447
+ page,
448
+ pageType: typeof page,
449
+ engine,
450
+ stackTrace: new Error().stack,
451
+ };
452
+ throw new Error(
453
+ `page is required in createEngineAdapter. Received: page=${page} (type: ${typeof page}), engine=${engine}. This may indicate that the page object was not properly passed through the function call chain. Stack trace: ${errorDetails.stackTrace}`
454
+ );
455
+ }
456
+
457
+ if (engine === 'playwright') {
458
+ return new PlaywrightAdapter(page);
459
+ } else if (engine === 'puppeteer') {
460
+ return new PuppeteerAdapter(page);
461
+ } else {
462
+ throw new Error(
463
+ `Unsupported engine: ${engine}. Expected 'playwright' or 'puppeteer'`
464
+ );
465
+ }
466
+ }
@@ -0,0 +1,49 @@
1
+ import { isVerboseEnabled } from './logger.js';
2
+
3
+ /**
4
+ * Detect which browser automation engine is being used
5
+ * @param {Object} pageOrContext - Page or context object from Playwright or Puppeteer
6
+ * @returns {string} - 'playwright' or 'puppeteer'
7
+ */
8
+ export function detectEngine(pageOrContext) {
9
+ const hasEval = !!pageOrContext.$eval;
10
+ const hasEvalAll = !!pageOrContext.$$eval;
11
+ const locatorType = typeof pageOrContext.locator;
12
+ const contextType = typeof pageOrContext.context;
13
+ const hasContext = contextType === 'function' || contextType === 'object';
14
+
15
+ // Debug logging
16
+ if (isVerboseEnabled()) {
17
+ console.log('🔍 [ENGINE DETECTION]', {
18
+ hasEval,
19
+ hasEvalAll,
20
+ locatorType,
21
+ contextType,
22
+ hasContext,
23
+ });
24
+ }
25
+
26
+ // Check for Playwright-specific methods first
27
+ // Playwright has locator as a function and context() method
28
+ // Both engines have $eval and $$eval, so we check for unique Playwright features first
29
+ if (locatorType === 'function' && hasContext) {
30
+ if (isVerboseEnabled()) {
31
+ console.log('🔍 [ENGINE DETECTION] Detected: playwright');
32
+ }
33
+ return 'playwright';
34
+ }
35
+ // Check for Puppeteer-specific methods
36
+ // Puppeteer has $eval, $$eval but no context() method
37
+ if (hasEval && hasEvalAll && !hasContext) {
38
+ if (isVerboseEnabled()) {
39
+ console.log('🔍 [ENGINE DETECTION] Detected: puppeteer');
40
+ }
41
+ return 'puppeteer';
42
+ }
43
+ if (isVerboseEnabled()) {
44
+ console.log('🔍 [ENGINE DETECTION] Could not detect engine!');
45
+ }
46
+ throw new Error(
47
+ 'Unknown browser automation engine. Expected Playwright or Puppeteer page object.'
48
+ );
49
+ }
@@ -0,0 +1,21 @@
1
+ import makeLog from 'log-lazy';
2
+
3
+ /**
4
+ * Check if verbose logging is enabled via environment or CLI args
5
+ * @returns {boolean} - True if verbose mode is enabled
6
+ */
7
+ export function isVerboseEnabled() {
8
+ return !!(process.env.VERBOSE || process.argv.includes('--verbose'));
9
+ }
10
+
11
+ /**
12
+ * Create a logger instance with verbose level control
13
+ * @param {Object} options - Configuration options
14
+ * @param {boolean} options.verbose - Enable verbose logging
15
+ * @returns {Object} - Logger instance
16
+ */
17
+ export function createLogger(options = {}) {
18
+ const { verbose = false } = options;
19
+ const log = makeLog({ level: verbose ? 'debug' : 'error' });
20
+ return log;
21
+ }