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,383 @@
1
+ import { TIMING } from '../core/constants.js';
2
+ import { isNavigationError } from '../core/navigation-safety.js';
3
+ import { waitForLocatorOrElement } from '../elements/locators.js';
4
+ import { scrollIntoViewIfNeeded } from './scroll.js';
5
+ import { clickElement } from './click.js';
6
+ import { getInputValue } from '../elements/content.js';
7
+ import { createEngineAdapter } from '../core/engine-adapter.js';
8
+
9
+ /**
10
+ * Default verification function for fill operations.
11
+ * Verifies that the filled text matches expected text.
12
+ * @param {Object} options - Verification options
13
+ * @param {Object} options.page - Browser page object
14
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
15
+ * @param {Object} options.locatorOrElement - Element that was filled
16
+ * @param {string} options.expectedText - Text that should be in the element
17
+ * @returns {Promise<{verified: boolean, actualValue: string}>}
18
+ */
19
+ export async function defaultFillVerification(options = {}) {
20
+ const { page, engine, locatorOrElement, expectedText } = options;
21
+
22
+ try {
23
+ const actualValue = await getInputValue({ page, engine, locatorOrElement });
24
+ // Verify that the value contains the expected text (handles cases where value may have formatting)
25
+ const verified =
26
+ actualValue === expectedText || actualValue.includes(expectedText);
27
+ return { verified, actualValue };
28
+ } catch (error) {
29
+ if (isNavigationError(error)) {
30
+ return { verified: false, actualValue: '', navigationError: true };
31
+ }
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Verify fill operation with retry logic
38
+ * @param {Object} options - Verification options
39
+ * @param {Object} options.page - Browser page object
40
+ * @param {string} options.engine - Engine type
41
+ * @param {Object} options.locatorOrElement - Element to verify
42
+ * @param {string} options.expectedText - Expected text value
43
+ * @param {Function} options.verifyFn - Custom verification function (optional, defaults to defaultFillVerification)
44
+ * @param {number} options.timeout - Verification timeout in ms (default: TIMING.VERIFICATION_TIMEOUT)
45
+ * @param {number} options.retryInterval - Interval between retries (default: TIMING.VERIFICATION_RETRY_INTERVAL)
46
+ * @param {Function} options.log - Logger instance
47
+ * @returns {Promise<{verified: boolean, actualValue: string, attempts: number}>}
48
+ */
49
+ export async function verifyFill(options = {}) {
50
+ const {
51
+ page,
52
+ engine,
53
+ locatorOrElement,
54
+ expectedText,
55
+ verifyFn = defaultFillVerification,
56
+ timeout = TIMING.VERIFICATION_TIMEOUT,
57
+ retryInterval = TIMING.VERIFICATION_RETRY_INTERVAL,
58
+ log = { debug: () => {} },
59
+ } = options;
60
+
61
+ const startTime = Date.now();
62
+ let attempts = 0;
63
+ let lastResult = { verified: false, actualValue: '' };
64
+
65
+ while (Date.now() - startTime < timeout) {
66
+ attempts++;
67
+ lastResult = await verifyFn({
68
+ page,
69
+ engine,
70
+ locatorOrElement,
71
+ expectedText,
72
+ });
73
+
74
+ if (lastResult.verified) {
75
+ log.debug(
76
+ () => `✅ Fill verification succeeded after ${attempts} attempt(s)`
77
+ );
78
+ return { ...lastResult, attempts };
79
+ }
80
+
81
+ if (lastResult.navigationError) {
82
+ log.debug(() => '⚠️ Navigation detected during fill verification');
83
+ return { ...lastResult, attempts };
84
+ }
85
+
86
+ // Wait before next retry
87
+ await new Promise((resolve) => setTimeout(resolve, retryInterval));
88
+ }
89
+
90
+ log.debug(
91
+ () =>
92
+ `❌ Fill verification failed after ${attempts} attempts. Expected: "${expectedText}", Got: "${lastResult.actualValue}"`
93
+ );
94
+ return { ...lastResult, attempts };
95
+ }
96
+
97
+ /**
98
+ * Check if an input element is empty
99
+ * @param {Object} options - Configuration options
100
+ * @param {Object} options.page - Browser page object
101
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
102
+ * @param {Object} options.locatorOrElement - Element or locator to check
103
+ * @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
104
+ * @returns {Promise<boolean>} - True if empty, false if has content (returns true on navigation)
105
+ */
106
+ export async function checkIfElementEmpty(options = {}) {
107
+ const { page, engine, locatorOrElement, adapter: providedAdapter } = options;
108
+
109
+ if (!locatorOrElement) {
110
+ throw new Error('locatorOrElement is required in options');
111
+ }
112
+
113
+ // Add defensive check for page parameter
114
+ if (!page && !providedAdapter) {
115
+ const availableKeys = Object.keys(options).join(', ');
116
+ throw new Error(
117
+ `checkIfElementEmpty: page is required in options when adapter is not provided. Available option keys: [${availableKeys}]. This indicates the 'page' parameter was not passed correctly from the calling function.`
118
+ );
119
+ }
120
+
121
+ try {
122
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
123
+ const currentValue = await adapter.getInputValue(locatorOrElement);
124
+ return !currentValue || currentValue.trim() === '';
125
+ } catch (error) {
126
+ if (isNavigationError(error)) {
127
+ console.log(
128
+ '⚠️ Navigation detected during checkIfElementEmpty, returning true'
129
+ );
130
+ return true;
131
+ }
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Perform fill/type operation on an element (low-level)
138
+ * @param {Object} options - Configuration options
139
+ * @param {Object} options.page - Browser page object
140
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
141
+ * @param {Object} options.locatorOrElement - Element or locator to fill
142
+ * @param {string} options.text - Text to fill
143
+ * @param {boolean} options.simulateTyping - Whether to simulate typing (default: true)
144
+ * @param {boolean} options.verify - Whether to verify the fill operation (default: true)
145
+ * @param {Function} options.verifyFn - Custom verification function (optional)
146
+ * @param {number} options.verificationTimeout - Verification timeout in ms (default: TIMING.VERIFICATION_TIMEOUT)
147
+ * @param {Function} options.log - Logger instance (optional)
148
+ * @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
149
+ * @returns {Promise<{filled: boolean, verified: boolean, actualValue?: string}>}
150
+ */
151
+ export async function performFill(options = {}) {
152
+ const {
153
+ page,
154
+ engine,
155
+ locatorOrElement,
156
+ text,
157
+ simulateTyping = true,
158
+ verify = true,
159
+ verifyFn,
160
+ verificationTimeout = TIMING.VERIFICATION_TIMEOUT,
161
+ log = { debug: () => {} },
162
+ adapter: providedAdapter,
163
+ } = options;
164
+
165
+ if (!text) {
166
+ throw new Error('text is required in options');
167
+ }
168
+
169
+ if (!locatorOrElement) {
170
+ throw new Error('locatorOrElement is required in options');
171
+ }
172
+
173
+ // Add defensive check for page parameter
174
+ if (!page && !providedAdapter) {
175
+ const availableKeys = Object.keys(options).join(', ');
176
+ throw new Error(
177
+ `performFill: page is required in options when adapter is not provided. Available option keys: [${availableKeys}]. This indicates the 'page' parameter was not passed correctly from the calling function.`
178
+ );
179
+ }
180
+
181
+ try {
182
+ const adapter = providedAdapter || createEngineAdapter(page, engine);
183
+ if (simulateTyping) {
184
+ await adapter.type(locatorOrElement, text);
185
+ } else {
186
+ await adapter.fill(locatorOrElement, text);
187
+ }
188
+
189
+ // Verify fill if requested
190
+ if (verify) {
191
+ const verificationResult = await verifyFill({
192
+ page,
193
+ engine,
194
+ locatorOrElement,
195
+ expectedText: text,
196
+ verifyFn,
197
+ timeout: verificationTimeout,
198
+ log,
199
+ });
200
+
201
+ if (!verificationResult.verified) {
202
+ log.debug(
203
+ () =>
204
+ `⚠️ Fill verification failed: expected "${text}", got "${verificationResult.actualValue}"`
205
+ );
206
+ }
207
+
208
+ return {
209
+ filled: true,
210
+ verified: verificationResult.verified,
211
+ actualValue: verificationResult.actualValue,
212
+ };
213
+ }
214
+
215
+ return { filled: true, verified: true };
216
+ } catch (error) {
217
+ if (isNavigationError(error)) {
218
+ console.log(
219
+ '⚠️ Navigation detected during performFill, recovering gracefully'
220
+ );
221
+ return { filled: false, verified: false };
222
+ }
223
+ throw error;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Fill a textarea with text (high-level with checks and scrolling)
229
+ * @param {Object} options - Configuration options
230
+ * @param {Object} options.page - Browser page object
231
+ * @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
232
+ * @param {Function} options.wait - Wait function
233
+ * @param {Function} options.log - Logger instance
234
+ * @param {string|Object} options.selector - CSS selector or Playwright Locator
235
+ * @param {string} options.text - Text to fill
236
+ * @param {boolean} options.checkEmpty - Only fill if empty (default: true)
237
+ * @param {boolean} options.scrollIntoView - Scroll into view (default: true)
238
+ * @param {boolean} options.simulateTyping - Simulate typing vs direct fill (default: true)
239
+ * @param {number} options.timeout - Timeout in ms (default: TIMING.DEFAULT_TIMEOUT)
240
+ * @param {boolean} options.verify - Whether to verify the fill operation (default: true)
241
+ * @param {Function} options.verifyFn - Custom verification function (optional, uses defaultFillVerification if not provided)
242
+ * @param {number} options.verificationTimeout - Verification timeout in ms (default: TIMING.VERIFICATION_TIMEOUT)
243
+ * @returns {Promise<{filled: boolean, verified: boolean, skipped: boolean, actualValue?: string}>}
244
+ * - filled: true if fill operation was attempted
245
+ * - verified: true if fill was verified successfully (only meaningful if filled is true)
246
+ * - skipped: true if element already had content and checkEmpty was true
247
+ * @throws {Error} - If selector or text is missing, or if operation fails (except navigation)
248
+ */
249
+ export async function fillTextArea(options = {}) {
250
+ const {
251
+ page,
252
+ engine,
253
+ wait,
254
+ log,
255
+ selector,
256
+ text,
257
+ checkEmpty = true,
258
+ scrollIntoView: shouldScroll = true,
259
+ simulateTyping = true,
260
+ timeout = TIMING.DEFAULT_TIMEOUT,
261
+ verify = true,
262
+ verifyFn,
263
+ verificationTimeout = TIMING.VERIFICATION_TIMEOUT,
264
+ } = options;
265
+
266
+ // Defensive check: Validate that page parameter is present
267
+ if (!page) {
268
+ const availableKeys = Object.keys(options).join(', ');
269
+ throw new Error(
270
+ `fillTextArea: page is required in options. Available option keys: [${availableKeys}]. This indicates the 'page' parameter was not passed correctly from the calling function (bindings layer).`
271
+ );
272
+ }
273
+
274
+ if (!selector || !text) {
275
+ throw new Error('fillTextArea: selector and text are required in options');
276
+ }
277
+
278
+ try {
279
+ // Get locator/element and wait for it to be visible (unified for both engines)
280
+ const locatorOrElement = await waitForLocatorOrElement({
281
+ page,
282
+ engine,
283
+ selector,
284
+ timeout,
285
+ });
286
+
287
+ // Check if empty (if requested)
288
+ if (checkEmpty) {
289
+ const isEmpty = await checkIfElementEmpty({
290
+ page,
291
+ engine,
292
+ locatorOrElement,
293
+ });
294
+ if (!isEmpty) {
295
+ const currentValue = await getInputValue({
296
+ page,
297
+ engine,
298
+ locatorOrElement,
299
+ });
300
+ log.debug(
301
+ () =>
302
+ `🔍 [VERBOSE] Textarea already has content, skipping: "${currentValue.substring(0, 30)}..."`
303
+ );
304
+ return {
305
+ filled: false,
306
+ verified: false,
307
+ skipped: true,
308
+ actualValue: currentValue,
309
+ };
310
+ }
311
+ }
312
+
313
+ // Scroll into view (if requested and needed)
314
+ if (shouldScroll) {
315
+ await scrollIntoViewIfNeeded({
316
+ page,
317
+ engine,
318
+ wait,
319
+ log,
320
+ locatorOrElement,
321
+ behavior: 'smooth',
322
+ });
323
+ }
324
+
325
+ // Click the element (prevent auto-scroll if scrollIntoView is disabled)
326
+ const clicked = await clickElement({
327
+ page,
328
+ engine,
329
+ log,
330
+ locatorOrElement,
331
+ noAutoScroll: !shouldScroll,
332
+ });
333
+ if (!clicked) {
334
+ return { filled: false, verified: false, skipped: false }; // Navigation occurred
335
+ }
336
+
337
+ // Fill the text with verification
338
+ const fillResult = await performFill({
339
+ page,
340
+ engine,
341
+ locatorOrElement,
342
+ text,
343
+ simulateTyping,
344
+ verify,
345
+ verifyFn,
346
+ verificationTimeout,
347
+ log,
348
+ });
349
+
350
+ if (!fillResult.filled) {
351
+ return { filled: false, verified: false, skipped: false }; // Navigation occurred
352
+ }
353
+
354
+ log.debug(
355
+ () =>
356
+ `🔍 [VERBOSE] Filled textarea with text: "${text.substring(0, 50)}..."`
357
+ );
358
+
359
+ if (fillResult.verified) {
360
+ log.debug(() => `✅ Fill verification passed`);
361
+ } else {
362
+ log.debug(
363
+ () =>
364
+ `⚠️ Fill verification failed: expected "${text}", got "${fillResult.actualValue}"`
365
+ );
366
+ }
367
+
368
+ return {
369
+ filled: true,
370
+ verified: fillResult.verified,
371
+ skipped: false,
372
+ actualValue: fillResult.actualValue,
373
+ };
374
+ } catch (error) {
375
+ if (isNavigationError(error)) {
376
+ console.log(
377
+ '⚠️ Navigation detected during fillTextArea, recovering gracefully'
378
+ );
379
+ return { filled: false, verified: false, skipped: false };
380
+ }
381
+ throw error;
382
+ }
383
+ }