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
|
@@ -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
|
+
}
|