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,341 @@
|
|
|
1
|
+
import { TIMING } from '../core/constants.js';
|
|
2
|
+
import { isNavigationError } from '../core/navigation-safety.js';
|
|
3
|
+
import { isActionStoppedError } from '../core/page-trigger-manager.js';
|
|
4
|
+
|
|
5
|
+
// Shared evaluation function for checking if scrolling is needed
|
|
6
|
+
const needsScrollingFn = (el, thresholdPercent) => {
|
|
7
|
+
const rect = el.getBoundingClientRect();
|
|
8
|
+
const viewportHeight = window.innerHeight;
|
|
9
|
+
const elementCenter = rect.top + rect.height / 2;
|
|
10
|
+
const viewportCenter = viewportHeight / 2;
|
|
11
|
+
const distanceFromCenter = Math.abs(elementCenter - viewportCenter);
|
|
12
|
+
const thresholdPixels = (viewportHeight * thresholdPercent) / 100;
|
|
13
|
+
|
|
14
|
+
// Check if element is visible and within threshold
|
|
15
|
+
const isVisible = rect.top >= 0 && rect.bottom <= viewportHeight;
|
|
16
|
+
const isWithinThreshold = distanceFromCenter <= thresholdPixels;
|
|
17
|
+
|
|
18
|
+
return !isVisible || !isWithinThreshold;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Shared evaluation function for verifying element is in viewport
|
|
22
|
+
const isElementInViewportFn = (el, margin = 50) => {
|
|
23
|
+
const rect = el.getBoundingClientRect();
|
|
24
|
+
const viewportHeight = window.innerHeight;
|
|
25
|
+
const viewportWidth = window.innerWidth;
|
|
26
|
+
|
|
27
|
+
// Check if element is at least partially visible with some margin
|
|
28
|
+
const isInVerticalView =
|
|
29
|
+
rect.top < viewportHeight - margin && rect.bottom > margin;
|
|
30
|
+
const isInHorizontalView =
|
|
31
|
+
rect.left < viewportWidth - margin && rect.right > margin;
|
|
32
|
+
|
|
33
|
+
return isInVerticalView && isInHorizontalView;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default verification function for scroll operations.
|
|
38
|
+
* Verifies that the element is now visible in the viewport.
|
|
39
|
+
* @param {Object} options - Verification options
|
|
40
|
+
* @param {Object} options.page - Browser page object
|
|
41
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
42
|
+
* @param {Object} options.locatorOrElement - Element that was scrolled to
|
|
43
|
+
* @param {number} options.margin - Margin in pixels to consider element visible (default: 50)
|
|
44
|
+
* @returns {Promise<{verified: boolean, inViewport: boolean}>}
|
|
45
|
+
*/
|
|
46
|
+
export async function defaultScrollVerification(options = {}) {
|
|
47
|
+
const { page, engine, locatorOrElement, margin = 50 } = options;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
let inViewport;
|
|
51
|
+
if (engine === 'playwright') {
|
|
52
|
+
inViewport = await locatorOrElement.evaluate(
|
|
53
|
+
isElementInViewportFn,
|
|
54
|
+
margin
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
inViewport = await page.evaluate(
|
|
58
|
+
isElementInViewportFn,
|
|
59
|
+
locatorOrElement,
|
|
60
|
+
margin
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return { verified: inViewport, inViewport };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (isNavigationError(error) || isActionStoppedError(error)) {
|
|
66
|
+
return { verified: false, inViewport: false, navigationError: true };
|
|
67
|
+
}
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Verify scroll operation with retry logic
|
|
74
|
+
* @param {Object} options - Verification options
|
|
75
|
+
* @param {Object} options.page - Browser page object
|
|
76
|
+
* @param {string} options.engine - Engine type
|
|
77
|
+
* @param {Object} options.locatorOrElement - Element to verify
|
|
78
|
+
* @param {Function} options.verifyFn - Custom verification function (optional, defaults to defaultScrollVerification)
|
|
79
|
+
* @param {number} options.timeout - Verification timeout in ms (default: TIMING.VERIFICATION_TIMEOUT)
|
|
80
|
+
* @param {number} options.retryInterval - Interval between retries (default: TIMING.VERIFICATION_RETRY_INTERVAL)
|
|
81
|
+
* @param {Function} options.log - Logger instance
|
|
82
|
+
* @returns {Promise<{verified: boolean, inViewport: boolean, attempts: number}>}
|
|
83
|
+
*/
|
|
84
|
+
export async function verifyScroll(options = {}) {
|
|
85
|
+
const {
|
|
86
|
+
page,
|
|
87
|
+
engine,
|
|
88
|
+
locatorOrElement,
|
|
89
|
+
verifyFn = defaultScrollVerification,
|
|
90
|
+
timeout = TIMING.VERIFICATION_TIMEOUT,
|
|
91
|
+
retryInterval = TIMING.VERIFICATION_RETRY_INTERVAL,
|
|
92
|
+
log = { debug: () => {} },
|
|
93
|
+
} = options;
|
|
94
|
+
|
|
95
|
+
const startTime = Date.now();
|
|
96
|
+
let attempts = 0;
|
|
97
|
+
let lastResult = { verified: false, inViewport: false };
|
|
98
|
+
|
|
99
|
+
while (Date.now() - startTime < timeout) {
|
|
100
|
+
attempts++;
|
|
101
|
+
lastResult = await verifyFn({
|
|
102
|
+
page,
|
|
103
|
+
engine,
|
|
104
|
+
locatorOrElement,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (lastResult.verified) {
|
|
108
|
+
log.debug(
|
|
109
|
+
() => `✅ Scroll verification succeeded after ${attempts} attempt(s)`
|
|
110
|
+
);
|
|
111
|
+
return { ...lastResult, attempts };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (lastResult.navigationError) {
|
|
115
|
+
log.debug(
|
|
116
|
+
() => '⚠️ Navigation/stop detected during scroll verification'
|
|
117
|
+
);
|
|
118
|
+
return { ...lastResult, attempts };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Wait before next retry
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, retryInterval));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
log.debug(
|
|
126
|
+
() =>
|
|
127
|
+
`❌ Scroll verification failed after ${attempts} attempts - element not in viewport`
|
|
128
|
+
);
|
|
129
|
+
return { ...lastResult, attempts };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scroll element into view (low-level, does not check if scroll is needed)
|
|
134
|
+
* @param {Object} options - Configuration options
|
|
135
|
+
* @param {Object} options.page - Browser page object
|
|
136
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
137
|
+
* @param {Object} options.locatorOrElement - Playwright locator or Puppeteer element
|
|
138
|
+
* @param {string} options.behavior - 'smooth' or 'instant' (default: 'smooth')
|
|
139
|
+
* @param {boolean} options.verify - Whether to verify the scroll operation (default: true)
|
|
140
|
+
* @param {Function} options.verifyFn - Custom verification function (optional)
|
|
141
|
+
* @param {number} options.verificationTimeout - Verification timeout in ms (default: TIMING.VERIFICATION_TIMEOUT)
|
|
142
|
+
* @param {Function} options.log - Logger instance (optional)
|
|
143
|
+
* @returns {Promise<{scrolled: boolean, verified: boolean}>}
|
|
144
|
+
*/
|
|
145
|
+
export async function scrollIntoView(options = {}) {
|
|
146
|
+
const {
|
|
147
|
+
page,
|
|
148
|
+
engine,
|
|
149
|
+
locatorOrElement,
|
|
150
|
+
behavior = 'smooth',
|
|
151
|
+
verify = true,
|
|
152
|
+
verifyFn,
|
|
153
|
+
verificationTimeout = TIMING.VERIFICATION_TIMEOUT,
|
|
154
|
+
log = { debug: () => {} },
|
|
155
|
+
} = options;
|
|
156
|
+
|
|
157
|
+
if (!locatorOrElement) {
|
|
158
|
+
throw new Error('locatorOrElement is required in options');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
if (engine === 'playwright') {
|
|
163
|
+
await locatorOrElement.evaluate((el, scrollBehavior) => {
|
|
164
|
+
el.scrollIntoView({
|
|
165
|
+
behavior: scrollBehavior,
|
|
166
|
+
block: 'center',
|
|
167
|
+
inline: 'center',
|
|
168
|
+
});
|
|
169
|
+
}, behavior);
|
|
170
|
+
} else {
|
|
171
|
+
await page.evaluate(
|
|
172
|
+
(el, scrollBehavior) => {
|
|
173
|
+
el.scrollIntoView({
|
|
174
|
+
behavior: scrollBehavior,
|
|
175
|
+
block: 'center',
|
|
176
|
+
inline: 'center',
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
locatorOrElement,
|
|
180
|
+
behavior
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Verify scroll if requested
|
|
185
|
+
if (verify) {
|
|
186
|
+
const verificationResult = await verifyScroll({
|
|
187
|
+
page,
|
|
188
|
+
engine,
|
|
189
|
+
locatorOrElement,
|
|
190
|
+
verifyFn,
|
|
191
|
+
timeout: verificationTimeout,
|
|
192
|
+
log,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
scrolled: true,
|
|
197
|
+
verified: verificationResult.verified,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { scrolled: true, verified: true };
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (isNavigationError(error) || isActionStoppedError(error)) {
|
|
204
|
+
console.log(
|
|
205
|
+
'⚠️ Navigation/stop detected during scrollIntoView, skipping'
|
|
206
|
+
);
|
|
207
|
+
return { scrolled: false, verified: false };
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check if element needs scrolling (is it more than threshold% away from viewport center)
|
|
215
|
+
* @param {Object} options - Configuration options
|
|
216
|
+
* @param {Object} options.page - Browser page object
|
|
217
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
218
|
+
* @param {Object} options.locatorOrElement - Playwright locator or Puppeteer element
|
|
219
|
+
* @param {number} options.threshold - Percentage of viewport height to consider "significant" (default: 10)
|
|
220
|
+
* @returns {Promise<boolean>} - True if scroll is needed, false on navigation/stop
|
|
221
|
+
*/
|
|
222
|
+
export async function needsScrolling(options = {}) {
|
|
223
|
+
const { page, engine, locatorOrElement, threshold = 10 } = options;
|
|
224
|
+
|
|
225
|
+
if (!locatorOrElement) {
|
|
226
|
+
throw new Error('locatorOrElement is required in options');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
if (engine === 'playwright') {
|
|
231
|
+
return await locatorOrElement.evaluate(needsScrollingFn, threshold);
|
|
232
|
+
} else {
|
|
233
|
+
return await page.evaluate(needsScrollingFn, locatorOrElement, threshold);
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (isNavigationError(error) || isActionStoppedError(error)) {
|
|
237
|
+
console.log(
|
|
238
|
+
'⚠️ Navigation/stop detected during needsScrolling, returning false'
|
|
239
|
+
);
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Scroll element into view only if needed (>threshold% from center)
|
|
248
|
+
* Automatically waits for scroll animation if scroll was performed
|
|
249
|
+
* @param {Object} options - Configuration options
|
|
250
|
+
* @param {Object} options.page - Browser page object
|
|
251
|
+
* @param {string} options.engine - Engine type
|
|
252
|
+
* @param {Function} options.wait - Wait function
|
|
253
|
+
* @param {Function} options.log - Logger instance
|
|
254
|
+
* @param {Object} options.locatorOrElement - Playwright locator or Puppeteer element
|
|
255
|
+
* @param {string} options.behavior - 'smooth' or 'instant' (default: 'smooth')
|
|
256
|
+
* @param {number} options.threshold - Percentage of viewport height to consider "significant" (default: 10)
|
|
257
|
+
* @param {number} options.waitAfterScroll - Wait time after scroll in ms (default: TIMING.SCROLL_ANIMATION_WAIT for smooth, 0 for instant)
|
|
258
|
+
* @param {boolean} options.verify - Whether to verify the scroll operation (default: true)
|
|
259
|
+
* @param {Function} options.verifyFn - Custom verification function (optional)
|
|
260
|
+
* @param {number} options.verificationTimeout - Verification timeout in ms (default: TIMING.VERIFICATION_TIMEOUT)
|
|
261
|
+
* @returns {Promise<{scrolled: boolean, verified: boolean, skipped: boolean}>}
|
|
262
|
+
* - scrolled: true if scroll was performed
|
|
263
|
+
* - verified: true if element is confirmed in viewport (only meaningful if scrolled is true)
|
|
264
|
+
* - skipped: true if element was already in view
|
|
265
|
+
*/
|
|
266
|
+
export async function scrollIntoViewIfNeeded(options = {}) {
|
|
267
|
+
const {
|
|
268
|
+
page,
|
|
269
|
+
engine,
|
|
270
|
+
wait,
|
|
271
|
+
log,
|
|
272
|
+
locatorOrElement,
|
|
273
|
+
behavior = 'smooth',
|
|
274
|
+
threshold = 10,
|
|
275
|
+
waitAfterScroll = behavior === 'smooth' ? TIMING.SCROLL_ANIMATION_WAIT : 0,
|
|
276
|
+
verify = true,
|
|
277
|
+
verifyFn,
|
|
278
|
+
verificationTimeout = TIMING.VERIFICATION_TIMEOUT,
|
|
279
|
+
} = options;
|
|
280
|
+
|
|
281
|
+
if (!locatorOrElement) {
|
|
282
|
+
throw new Error('locatorOrElement is required in options');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check if scrolling is needed
|
|
286
|
+
const needsScroll = await needsScrolling({
|
|
287
|
+
page,
|
|
288
|
+
engine,
|
|
289
|
+
locatorOrElement,
|
|
290
|
+
threshold,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (!needsScroll) {
|
|
294
|
+
log.debug(
|
|
295
|
+
() =>
|
|
296
|
+
`🔍 [VERBOSE] Element already in view (within ${threshold}% threshold), skipping scroll`
|
|
297
|
+
);
|
|
298
|
+
return { scrolled: false, verified: true, skipped: true };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Perform scroll with verification
|
|
302
|
+
log.debug(() => `🔍 [VERBOSE] Scrolling with behavior: ${behavior}`);
|
|
303
|
+
const scrollResult = await scrollIntoView({
|
|
304
|
+
page,
|
|
305
|
+
engine,
|
|
306
|
+
locatorOrElement,
|
|
307
|
+
behavior,
|
|
308
|
+
verify,
|
|
309
|
+
verifyFn,
|
|
310
|
+
verificationTimeout,
|
|
311
|
+
log,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
if (!scrollResult.scrolled) {
|
|
315
|
+
// Navigation/stop occurred during scroll
|
|
316
|
+
return { scrolled: false, verified: false, skipped: false };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Wait for scroll animation if specified
|
|
320
|
+
if (waitAfterScroll > 0) {
|
|
321
|
+
await wait({
|
|
322
|
+
ms: waitAfterScroll,
|
|
323
|
+
reason: `${behavior} scroll animation to complete`,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (scrollResult.verified) {
|
|
328
|
+
log.debug(() => '✅ Scroll verification passed - element is in viewport');
|
|
329
|
+
} else {
|
|
330
|
+
log.debug(
|
|
331
|
+
() =>
|
|
332
|
+
'⚠️ Scroll verification failed - element may not be fully in viewport'
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
scrolled: true,
|
|
338
|
+
verified: scrollResult.verified,
|
|
339
|
+
skipped: false,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get current URL
|
|
3
|
+
* @param {Object} options - Configuration options
|
|
4
|
+
* @param {Object} options.page - Browser page object
|
|
5
|
+
* @returns {string} - Current URL
|
|
6
|
+
*/
|
|
7
|
+
export function getUrl(options = {}) {
|
|
8
|
+
const { page } = options;
|
|
9
|
+
return page.url();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Unfocus address bar to prevent it from being selected
|
|
14
|
+
* Fixes the annoying issue where address bar is focused after browser launch/navigation
|
|
15
|
+
* Uses page.bringToFront() as recommended by Puppeteer/Playwright communities
|
|
16
|
+
* @param {Object} options - Configuration options
|
|
17
|
+
* @param {Object} options.page - Browser page object
|
|
18
|
+
* @returns {Promise<void>}
|
|
19
|
+
*/
|
|
20
|
+
export async function unfocusAddressBar(options = {}) {
|
|
21
|
+
const { page } = options;
|
|
22
|
+
|
|
23
|
+
if (!page) {
|
|
24
|
+
throw new Error('page is required in options');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// Bring page to front - this removes focus from address bar
|
|
29
|
+
await page.bringToFront();
|
|
30
|
+
} catch {
|
|
31
|
+
// Ignore errors - this is just a UX improvement
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { isNavigationError } from '../core/navigation-safety.js';
|
|
2
|
+
import { createEngineAdapter } from '../core/engine-adapter.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wait/sleep for a specified time with optional verbose logging
|
|
6
|
+
* Now supports abort signals to interrupt the wait when navigation occurs
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} options - Configuration options
|
|
9
|
+
* @param {Function} options.log - Logger instance
|
|
10
|
+
* @param {number} options.ms - Milliseconds to wait
|
|
11
|
+
* @param {string} options.reason - Reason for waiting (for verbose logging)
|
|
12
|
+
* @param {AbortSignal} options.abortSignal - Optional abort signal to interrupt wait
|
|
13
|
+
* @returns {Promise<{completed: boolean, aborted: boolean}>}
|
|
14
|
+
*/
|
|
15
|
+
export async function wait(options = {}) {
|
|
16
|
+
const { log, ms, reason, abortSignal } = options;
|
|
17
|
+
|
|
18
|
+
if (!ms) {
|
|
19
|
+
throw new Error('ms is required in options');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (reason) {
|
|
23
|
+
log.debug(() => `🔍 [VERBOSE] Waiting ${ms}ms: ${reason}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If abort signal provided, use abortable wait
|
|
27
|
+
if (abortSignal) {
|
|
28
|
+
// Check if already aborted
|
|
29
|
+
if (abortSignal.aborted) {
|
|
30
|
+
log.debug(
|
|
31
|
+
() => `🛑 Wait skipped (already aborted): ${reason || 'no reason'}`
|
|
32
|
+
);
|
|
33
|
+
return { completed: false, aborted: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
let timeoutId = null;
|
|
38
|
+
let abortHandler = null;
|
|
39
|
+
|
|
40
|
+
const cleanup = () => {
|
|
41
|
+
if (timeoutId) {
|
|
42
|
+
clearTimeout(timeoutId);
|
|
43
|
+
}
|
|
44
|
+
if (abortHandler) {
|
|
45
|
+
abortSignal.removeEventListener('abort', abortHandler);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
abortHandler = () => {
|
|
50
|
+
cleanup();
|
|
51
|
+
log.debug(() => `🛑 Wait aborted: ${reason || 'no reason'}`);
|
|
52
|
+
resolve({ completed: false, aborted: true });
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
abortSignal.addEventListener('abort', abortHandler);
|
|
56
|
+
|
|
57
|
+
timeoutId = setTimeout(() => {
|
|
58
|
+
cleanup();
|
|
59
|
+
if (reason) {
|
|
60
|
+
log.debug(() => `🔍 [VERBOSE] Wait complete (${ms}ms)`);
|
|
61
|
+
}
|
|
62
|
+
resolve({ completed: true, aborted: false });
|
|
63
|
+
}, ms);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Standard non-abortable wait (backwards compatible)
|
|
68
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
69
|
+
|
|
70
|
+
if (reason) {
|
|
71
|
+
log.debug(() => `🔍 [VERBOSE] Wait complete (${ms}ms)`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { completed: true, aborted: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Evaluate JavaScript in page context
|
|
79
|
+
* @param {Object} options - Configuration options
|
|
80
|
+
* @param {Object} options.page - Browser page object
|
|
81
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
82
|
+
* @param {Function} options.fn - Function to evaluate
|
|
83
|
+
* @param {Array} options.args - Arguments to pass to function (default: [])
|
|
84
|
+
* @param {Object} options.adapter - Engine adapter (optional, will be created if not provided)
|
|
85
|
+
* @returns {Promise<any>} - Result of evaluation
|
|
86
|
+
*/
|
|
87
|
+
export async function evaluate(options = {}) {
|
|
88
|
+
const { page, engine, fn, args = [], adapter: providedAdapter } = options;
|
|
89
|
+
|
|
90
|
+
if (!fn) {
|
|
91
|
+
throw new Error('fn is required in options');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const adapter = providedAdapter || createEngineAdapter(page, engine);
|
|
95
|
+
return await adapter.evaluateOnPage(fn, args);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Safe evaluate that catches navigation errors and returns default value
|
|
100
|
+
* @param {Object} options - Configuration options
|
|
101
|
+
* @param {Object} options.page - Browser page object
|
|
102
|
+
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
103
|
+
* @param {Function} options.fn - Function to evaluate
|
|
104
|
+
* @param {Array} options.args - Arguments to pass to function (default: [])
|
|
105
|
+
* @param {any} options.defaultValue - Value to return on navigation error (default: null)
|
|
106
|
+
* @param {string} options.operationName - Name for logging (default: 'evaluate')
|
|
107
|
+
* @param {boolean} options.silent - Don't log warnings (default: false)
|
|
108
|
+
* @returns {Promise<{success: boolean, value: any, navigationError: boolean}>}
|
|
109
|
+
*/
|
|
110
|
+
export async function safeEvaluate(options = {}) {
|
|
111
|
+
const {
|
|
112
|
+
page,
|
|
113
|
+
engine,
|
|
114
|
+
fn,
|
|
115
|
+
args = [],
|
|
116
|
+
defaultValue = null,
|
|
117
|
+
operationName = 'evaluate',
|
|
118
|
+
silent = false,
|
|
119
|
+
} = options;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const value = await evaluate({ page, engine, fn, args });
|
|
123
|
+
return { success: true, value, navigationError: false };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (isNavigationError(error)) {
|
|
126
|
+
if (!silent) {
|
|
127
|
+
console.log(
|
|
128
|
+
`⚠️ Navigation detected during ${operationName}, recovering gracefully`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return { success: false, value: defaultValue, navigationError: true };
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|