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,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navigation-related browser operations
|
|
3
|
+
*
|
|
4
|
+
* This module provides navigation functions that can work with or without
|
|
5
|
+
* the NavigationManager for backwards compatibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { TIMING } from '../core/constants.js';
|
|
9
|
+
import { isNavigationError } from '../core/navigation-safety.js';
|
|
10
|
+
import { isActionStoppedError } from '../core/page-trigger-manager.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default verification function for navigation operations.
|
|
14
|
+
* Verifies that navigation completed by checking:
|
|
15
|
+
* - URL matches expected pattern (if provided)
|
|
16
|
+
* - Page is in a ready state
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} options - Verification options
|
|
19
|
+
* @param {Object} options.page - Browser page object
|
|
20
|
+
* @param {string} options.expectedUrl - Expected URL or URL pattern (optional)
|
|
21
|
+
* @param {string} options.startUrl - URL before navigation
|
|
22
|
+
* @returns {Promise<{verified: boolean, actualUrl: string, reason: string}>}
|
|
23
|
+
*/
|
|
24
|
+
export async function defaultNavigationVerification(options = {}) {
|
|
25
|
+
const { page, expectedUrl, startUrl } = options;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const actualUrl = page.url();
|
|
29
|
+
|
|
30
|
+
// If expected URL is provided, verify it matches
|
|
31
|
+
if (expectedUrl) {
|
|
32
|
+
// Check for exact match or pattern match
|
|
33
|
+
if (actualUrl === expectedUrl) {
|
|
34
|
+
return { verified: true, actualUrl, reason: 'exact URL match' };
|
|
35
|
+
}
|
|
36
|
+
// Check if expected URL is contained in actual URL (for patterns)
|
|
37
|
+
if (
|
|
38
|
+
actualUrl.includes(expectedUrl) ||
|
|
39
|
+
actualUrl.startsWith(expectedUrl)
|
|
40
|
+
) {
|
|
41
|
+
return { verified: true, actualUrl, reason: 'URL pattern match' };
|
|
42
|
+
}
|
|
43
|
+
// Check if it's a regex pattern
|
|
44
|
+
if (expectedUrl instanceof RegExp && expectedUrl.test(actualUrl)) {
|
|
45
|
+
return { verified: true, actualUrl, reason: 'URL regex match' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
verified: false,
|
|
50
|
+
actualUrl,
|
|
51
|
+
reason: `URL mismatch: expected "${expectedUrl}", got "${actualUrl}"`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// No expected URL - just verify URL changed from start
|
|
56
|
+
if (startUrl && actualUrl !== startUrl) {
|
|
57
|
+
return { verified: true, actualUrl, reason: 'URL changed from start' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If no start URL and no expected URL, assume success
|
|
61
|
+
return { verified: true, actualUrl, reason: 'navigation completed' };
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (isNavigationError(error) || isActionStoppedError(error)) {
|
|
64
|
+
return {
|
|
65
|
+
verified: false,
|
|
66
|
+
actualUrl: '',
|
|
67
|
+
reason: 'error during verification',
|
|
68
|
+
navigationError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Verify navigation operation with retry logic
|
|
77
|
+
* @param {Object} options - Verification options
|
|
78
|
+
* @param {Object} options.page - Browser page object
|
|
79
|
+
* @param {string} options.expectedUrl - Expected URL (optional)
|
|
80
|
+
* @param {string} options.startUrl - URL before navigation
|
|
81
|
+
* @param {Function} options.verifyFn - Custom verification function (optional)
|
|
82
|
+
* @param {number} options.timeout - Verification timeout in ms (default: TIMING.VERIFICATION_TIMEOUT)
|
|
83
|
+
* @param {number} options.retryInterval - Interval between retries (default: TIMING.VERIFICATION_RETRY_INTERVAL)
|
|
84
|
+
* @param {Function} options.log - Logger instance
|
|
85
|
+
* @returns {Promise<{verified: boolean, actualUrl: string, reason: string, attempts: number}>}
|
|
86
|
+
*/
|
|
87
|
+
export async function verifyNavigation(options = {}) {
|
|
88
|
+
const {
|
|
89
|
+
page,
|
|
90
|
+
expectedUrl,
|
|
91
|
+
startUrl,
|
|
92
|
+
verifyFn = defaultNavigationVerification,
|
|
93
|
+
timeout = TIMING.VERIFICATION_TIMEOUT,
|
|
94
|
+
retryInterval = TIMING.VERIFICATION_RETRY_INTERVAL,
|
|
95
|
+
log = { debug: () => {} },
|
|
96
|
+
} = options;
|
|
97
|
+
|
|
98
|
+
const startTime = Date.now();
|
|
99
|
+
let attempts = 0;
|
|
100
|
+
let lastResult = { verified: false, actualUrl: '', reason: '' };
|
|
101
|
+
|
|
102
|
+
while (Date.now() - startTime < timeout) {
|
|
103
|
+
attempts++;
|
|
104
|
+
lastResult = await verifyFn({
|
|
105
|
+
page,
|
|
106
|
+
expectedUrl,
|
|
107
|
+
startUrl,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (lastResult.verified) {
|
|
111
|
+
log.debug(
|
|
112
|
+
() =>
|
|
113
|
+
`✅ Navigation verification succeeded after ${attempts} attempt(s): ${lastResult.reason}`
|
|
114
|
+
);
|
|
115
|
+
return { ...lastResult, attempts };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (lastResult.navigationError) {
|
|
119
|
+
log.debug(() => '⚠️ Navigation/stop detected during verification');
|
|
120
|
+
return { ...lastResult, attempts };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Wait before next retry
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, retryInterval));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
log.debug(
|
|
128
|
+
() =>
|
|
129
|
+
`❌ Navigation verification failed after ${attempts} attempts: ${lastResult.reason}`
|
|
130
|
+
);
|
|
131
|
+
return { ...lastResult, attempts };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Wait for URL to stabilize (no redirects happening)
|
|
136
|
+
* This is a legacy polling-based approach for backwards compatibility.
|
|
137
|
+
* When navigationManager is available, use waitForPageReady instead.
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} options - Configuration options
|
|
140
|
+
* @param {Object} options.page - Browser page object
|
|
141
|
+
* @param {Function} options.log - Logger instance
|
|
142
|
+
* @param {Function} options.wait - Wait function
|
|
143
|
+
* @param {Object} options.navigationManager - NavigationManager instance (optional)
|
|
144
|
+
* @param {number} options.stableChecks - Number of consecutive stable checks required (default: 3)
|
|
145
|
+
* @param {number} options.checkInterval - Interval between stability checks in ms (default: 1000)
|
|
146
|
+
* @param {number} options.timeout - Maximum time to wait for stabilization in ms (default: 30000)
|
|
147
|
+
* @param {string} options.reason - Reason for stabilization (for logging)
|
|
148
|
+
* @returns {Promise<boolean>} - True if stabilized, false if timeout
|
|
149
|
+
*/
|
|
150
|
+
export async function waitForUrlStabilization(options = {}) {
|
|
151
|
+
const {
|
|
152
|
+
page,
|
|
153
|
+
log,
|
|
154
|
+
wait,
|
|
155
|
+
navigationManager,
|
|
156
|
+
stableChecks = 3,
|
|
157
|
+
checkInterval = 1000,
|
|
158
|
+
timeout = 30000,
|
|
159
|
+
reason = 'URL stabilization',
|
|
160
|
+
} = options;
|
|
161
|
+
|
|
162
|
+
// If NavigationManager is available, delegate to it
|
|
163
|
+
if (navigationManager) {
|
|
164
|
+
return navigationManager.waitForPageReady({ timeout, reason });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Legacy polling-based approach
|
|
168
|
+
log.debug(() => `⏳ Waiting for URL to stabilize (${reason})...`);
|
|
169
|
+
let stableCount = 0;
|
|
170
|
+
let lastUrl = page.url();
|
|
171
|
+
const startTime = Date.now();
|
|
172
|
+
|
|
173
|
+
while (stableCount < stableChecks) {
|
|
174
|
+
// Check timeout
|
|
175
|
+
if (Date.now() - startTime > timeout) {
|
|
176
|
+
log.debug(
|
|
177
|
+
() => `⚠️ URL stabilization timeout after ${timeout}ms (${reason})`
|
|
178
|
+
);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await wait({ ms: checkInterval, reason: 'checking URL stability' });
|
|
183
|
+
const currentUrl = page.url();
|
|
184
|
+
|
|
185
|
+
if (currentUrl === lastUrl) {
|
|
186
|
+
stableCount++;
|
|
187
|
+
log.debug(
|
|
188
|
+
() =>
|
|
189
|
+
`🔍 [VERBOSE] URL stable for ${stableCount}/${stableChecks} checks: ${currentUrl}`
|
|
190
|
+
);
|
|
191
|
+
} else {
|
|
192
|
+
stableCount = 0;
|
|
193
|
+
lastUrl = currentUrl;
|
|
194
|
+
log.debug(
|
|
195
|
+
() =>
|
|
196
|
+
`🔍 [VERBOSE] URL changed to: ${currentUrl}, resetting stability counter`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
log.debug(() => `✅ URL stabilized (${reason})`);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Navigate to URL with full wait for page ready
|
|
207
|
+
* @param {Object} options - Configuration options
|
|
208
|
+
* @param {Object} options.page - Browser page object
|
|
209
|
+
* @param {Function} options.waitForUrlStabilization - URL stabilization function (legacy)
|
|
210
|
+
* @param {Object} options.navigationManager - NavigationManager instance (preferred)
|
|
211
|
+
* @param {Function} options.log - Logger instance (optional)
|
|
212
|
+
* @param {string} options.url - URL to navigate to
|
|
213
|
+
* @param {string} options.waitUntil - Wait until condition (default: 'domcontentloaded')
|
|
214
|
+
* @param {boolean} options.waitForStableUrlBefore - Wait for URL to stabilize BEFORE navigation (default: true)
|
|
215
|
+
* @param {boolean} options.waitForStableUrlAfter - Wait for URL to stabilize AFTER navigation (default: true)
|
|
216
|
+
* @param {boolean} options.waitForNetworkIdle - Wait for all network requests to complete (default: true)
|
|
217
|
+
* @param {number} options.stableChecks - Number of consecutive stable checks required (default: 3)
|
|
218
|
+
* @param {number} options.checkInterval - Interval between stability checks in ms (default: 1000)
|
|
219
|
+
* @param {number} options.timeout - Navigation timeout in ms (default: 240000)
|
|
220
|
+
* @param {boolean} options.verify - Whether to verify the navigation (default: true)
|
|
221
|
+
* @param {Function} options.verifyFn - Custom verification function (optional)
|
|
222
|
+
* @param {number} options.verificationTimeout - Verification timeout in ms (default: TIMING.VERIFICATION_TIMEOUT)
|
|
223
|
+
* @returns {Promise<{navigated: boolean, verified: boolean, actualUrl?: string, reason?: string}>}
|
|
224
|
+
*/
|
|
225
|
+
export async function goto(options = {}) {
|
|
226
|
+
const {
|
|
227
|
+
page,
|
|
228
|
+
waitForUrlStabilization: stabilizeFn,
|
|
229
|
+
navigationManager,
|
|
230
|
+
log = { debug: () => {} },
|
|
231
|
+
url,
|
|
232
|
+
waitUntil = 'domcontentloaded',
|
|
233
|
+
waitForStableUrlBefore = true,
|
|
234
|
+
waitForStableUrlAfter = true,
|
|
235
|
+
waitForNetworkIdle = true,
|
|
236
|
+
stableChecks = 3,
|
|
237
|
+
checkInterval = 1000,
|
|
238
|
+
timeout = 240000,
|
|
239
|
+
verify = true,
|
|
240
|
+
verifyFn,
|
|
241
|
+
verificationTimeout = TIMING.VERIFICATION_TIMEOUT,
|
|
242
|
+
} = options;
|
|
243
|
+
|
|
244
|
+
if (!url) {
|
|
245
|
+
throw new Error('url is required in options');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const startUrl = page.url();
|
|
249
|
+
|
|
250
|
+
// If NavigationManager is available, use it for full navigation handling
|
|
251
|
+
if (navigationManager) {
|
|
252
|
+
try {
|
|
253
|
+
const navigated = await navigationManager.navigate({
|
|
254
|
+
url,
|
|
255
|
+
waitUntil,
|
|
256
|
+
timeout,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Verify navigation if requested
|
|
260
|
+
if (verify && navigated) {
|
|
261
|
+
const verificationResult = await verifyNavigation({
|
|
262
|
+
page,
|
|
263
|
+
expectedUrl: url,
|
|
264
|
+
startUrl,
|
|
265
|
+
verifyFn,
|
|
266
|
+
timeout: verificationTimeout,
|
|
267
|
+
log,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
navigated: true,
|
|
272
|
+
verified: verificationResult.verified,
|
|
273
|
+
actualUrl: verificationResult.actualUrl,
|
|
274
|
+
reason: verificationResult.reason,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { navigated, verified: navigated, actualUrl: page.url() };
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (isNavigationError(error) || isActionStoppedError(error)) {
|
|
281
|
+
// Navigation was stopped by page trigger or navigation error
|
|
282
|
+
// This is not a failure - it means another action took over
|
|
283
|
+
return {
|
|
284
|
+
navigated: false,
|
|
285
|
+
verified: false,
|
|
286
|
+
reason: 'navigation stopped/interrupted',
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Legacy approach without NavigationManager
|
|
294
|
+
try {
|
|
295
|
+
// Wait for URL to stabilize BEFORE navigation (to avoid interrupting natural redirects)
|
|
296
|
+
if (waitForStableUrlBefore && stabilizeFn) {
|
|
297
|
+
await stabilizeFn({
|
|
298
|
+
stableChecks,
|
|
299
|
+
checkInterval,
|
|
300
|
+
reason: 'before navigation',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Navigate to the URL
|
|
305
|
+
await page.goto(url, { waitUntil, timeout });
|
|
306
|
+
|
|
307
|
+
// Wait for URL to stabilize AFTER navigation (to ensure all redirects are complete)
|
|
308
|
+
if (waitForStableUrlAfter && stabilizeFn) {
|
|
309
|
+
await stabilizeFn({
|
|
310
|
+
stableChecks,
|
|
311
|
+
checkInterval,
|
|
312
|
+
reason: 'after navigation',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Verify navigation if requested
|
|
317
|
+
if (verify) {
|
|
318
|
+
const verificationResult = await verifyNavigation({
|
|
319
|
+
page,
|
|
320
|
+
expectedUrl: url,
|
|
321
|
+
startUrl,
|
|
322
|
+
verifyFn,
|
|
323
|
+
timeout: verificationTimeout,
|
|
324
|
+
log,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
navigated: true,
|
|
329
|
+
verified: verificationResult.verified,
|
|
330
|
+
actualUrl: verificationResult.actualUrl,
|
|
331
|
+
reason: verificationResult.reason,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { navigated: true, verified: true, actualUrl: page.url() };
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (isNavigationError(error) || isActionStoppedError(error)) {
|
|
338
|
+
console.log(
|
|
339
|
+
'⚠️ Navigation was interrupted/stopped, recovering gracefully'
|
|
340
|
+
);
|
|
341
|
+
return {
|
|
342
|
+
navigated: false,
|
|
343
|
+
verified: false,
|
|
344
|
+
reason: 'navigation interrupted/stopped',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Wait for navigation
|
|
353
|
+
* @param {Object} options - Configuration options
|
|
354
|
+
* @param {Object} options.page - Browser page object
|
|
355
|
+
* @param {Object} options.navigationManager - NavigationManager instance (optional)
|
|
356
|
+
* @param {number} options.timeout - Timeout in ms
|
|
357
|
+
* @returns {Promise<boolean>} - True if navigation completed, false on error
|
|
358
|
+
*/
|
|
359
|
+
export async function waitForNavigation(options = {}) {
|
|
360
|
+
const { page, navigationManager, timeout } = options;
|
|
361
|
+
|
|
362
|
+
// If NavigationManager is available, use it
|
|
363
|
+
if (navigationManager) {
|
|
364
|
+
return navigationManager.waitForNavigation({ timeout });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Legacy approach
|
|
368
|
+
try {
|
|
369
|
+
await page.waitForNavigation(timeout ? { timeout } : undefined);
|
|
370
|
+
return true;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (isNavigationError(error)) {
|
|
373
|
+
console.log(
|
|
374
|
+
'⚠️ waitForNavigation was interrupted, continuing gracefully'
|
|
375
|
+
);
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Wait for page to be fully ready (DOM loaded + network idle + no redirects)
|
|
384
|
+
* This is the recommended method for ensuring page is ready for manipulation.
|
|
385
|
+
*
|
|
386
|
+
* @param {Object} options - Configuration options
|
|
387
|
+
* @param {Object} options.page - Browser page object
|
|
388
|
+
* @param {Object} options.navigationManager - NavigationManager instance (required for full functionality)
|
|
389
|
+
* @param {Object} options.networkTracker - NetworkTracker instance (optional)
|
|
390
|
+
* @param {Function} options.log - Logger instance
|
|
391
|
+
* @param {Function} options.wait - Wait function
|
|
392
|
+
* @param {number} options.timeout - Maximum time to wait (default: 30000ms)
|
|
393
|
+
* @param {string} options.reason - Reason for waiting (for logging)
|
|
394
|
+
* @returns {Promise<boolean>} - True if ready, false if timeout
|
|
395
|
+
*/
|
|
396
|
+
export async function waitForPageReady(options = {}) {
|
|
397
|
+
const {
|
|
398
|
+
page,
|
|
399
|
+
navigationManager,
|
|
400
|
+
networkTracker,
|
|
401
|
+
log,
|
|
402
|
+
wait,
|
|
403
|
+
timeout = 30000,
|
|
404
|
+
reason = 'page ready',
|
|
405
|
+
} = options;
|
|
406
|
+
|
|
407
|
+
// If NavigationManager is available, delegate to it
|
|
408
|
+
if (navigationManager) {
|
|
409
|
+
return navigationManager.waitForPageReady({ timeout, reason });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Fallback: use network tracker directly if available
|
|
413
|
+
if (networkTracker) {
|
|
414
|
+
log.debug(() => `⏳ Waiting for page ready (${reason})...`);
|
|
415
|
+
const startTime = Date.now();
|
|
416
|
+
|
|
417
|
+
// Wait for network idle
|
|
418
|
+
const networkIdle = await networkTracker.waitForNetworkIdle({
|
|
419
|
+
timeout,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const elapsed = Date.now() - startTime;
|
|
423
|
+
if (networkIdle) {
|
|
424
|
+
log.debug(() => `✅ Page ready after ${elapsed}ms (${reason})`);
|
|
425
|
+
} else {
|
|
426
|
+
log.debug(() => `⚠️ Page ready timeout after ${elapsed}ms (${reason})`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return networkIdle;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Minimal fallback: just wait a bit for DOM to settle
|
|
433
|
+
log.debug(() => `⏳ Waiting for page ready - minimal mode (${reason})...`);
|
|
434
|
+
await wait({ ms: 1000, reason: 'page settle time' });
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Wait for any ongoing navigation and network requests to complete.
|
|
440
|
+
* Use this after actions that might trigger navigation (like clicks).
|
|
441
|
+
*
|
|
442
|
+
* @param {Object} options - Configuration options
|
|
443
|
+
* @param {Object} options.page - Browser page object
|
|
444
|
+
* @param {Object} options.navigationManager - NavigationManager instance
|
|
445
|
+
* @param {Object} options.networkTracker - NetworkTracker instance
|
|
446
|
+
* @param {Function} options.log - Logger instance
|
|
447
|
+
* @param {Function} options.wait - Wait function
|
|
448
|
+
* @param {number} options.navigationCheckDelay - Time to wait for potential navigation to start (default: 500ms)
|
|
449
|
+
* @param {number} options.timeout - Maximum time to wait (default: 30000ms)
|
|
450
|
+
* @param {string} options.reason - Reason for waiting (for logging)
|
|
451
|
+
* @returns {Promise<{navigated: boolean, ready: boolean}>}
|
|
452
|
+
*/
|
|
453
|
+
export async function waitAfterAction(options = {}) {
|
|
454
|
+
const {
|
|
455
|
+
page,
|
|
456
|
+
navigationManager,
|
|
457
|
+
networkTracker,
|
|
458
|
+
log,
|
|
459
|
+
wait,
|
|
460
|
+
navigationCheckDelay = 500,
|
|
461
|
+
timeout = 30000,
|
|
462
|
+
reason = 'after action',
|
|
463
|
+
} = options;
|
|
464
|
+
|
|
465
|
+
const startUrl = page.url();
|
|
466
|
+
const startTime = Date.now();
|
|
467
|
+
|
|
468
|
+
log.debug(() => `⏳ Waiting after action (${reason})...`);
|
|
469
|
+
|
|
470
|
+
// Wait briefly for potential navigation to start
|
|
471
|
+
await wait({ ms: navigationCheckDelay, reason: 'checking for navigation' });
|
|
472
|
+
|
|
473
|
+
// Check if navigation is in progress or URL changed
|
|
474
|
+
const currentUrl = page.url();
|
|
475
|
+
const urlChanged = currentUrl !== startUrl;
|
|
476
|
+
|
|
477
|
+
if (navigationManager && navigationManager.isNavigating()) {
|
|
478
|
+
log.debug(() => '🔄 Navigation in progress, waiting for completion...');
|
|
479
|
+
await navigationManager.waitForNavigation({
|
|
480
|
+
timeout: timeout - (Date.now() - startTime),
|
|
481
|
+
});
|
|
482
|
+
return { navigated: true, ready: true };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (urlChanged) {
|
|
486
|
+
log.debug(() => `🔄 URL changed: ${startUrl} → ${currentUrl}`);
|
|
487
|
+
|
|
488
|
+
// Wait for page to be fully ready
|
|
489
|
+
await waitForPageReady({
|
|
490
|
+
page,
|
|
491
|
+
navigationManager,
|
|
492
|
+
networkTracker,
|
|
493
|
+
log,
|
|
494
|
+
wait,
|
|
495
|
+
timeout: timeout - (Date.now() - startTime),
|
|
496
|
+
reason: 'after URL change',
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
return { navigated: true, ready: true };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// No navigation detected, just wait for network idle
|
|
503
|
+
// Use shorter idle time since this is just for XHR completion, not full page load
|
|
504
|
+
if (networkTracker) {
|
|
505
|
+
const idle = await networkTracker.waitForNetworkIdle({
|
|
506
|
+
timeout: Math.max(0, timeout - (Date.now() - startTime)),
|
|
507
|
+
idleTime: 2000, // Shorter idle time for non-navigation actions
|
|
508
|
+
});
|
|
509
|
+
return { navigated: false, ready: idle };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return { navigated: false, ready: true };
|
|
513
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common Chrome arguments used across both Playwright and Puppeteer
|
|
3
|
+
*/
|
|
4
|
+
export const CHROME_ARGS = [
|
|
5
|
+
'--disable-session-crashed-bubble',
|
|
6
|
+
'--hide-crash-restore-bubble',
|
|
7
|
+
'--disable-infobars',
|
|
8
|
+
'--no-first-run',
|
|
9
|
+
'--no-default-browser-check',
|
|
10
|
+
'--disable-crash-restore',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Timing constants for browser operations
|
|
15
|
+
*/
|
|
16
|
+
export const TIMING = {
|
|
17
|
+
SCROLL_ANIMATION_WAIT: 300, // Wait time for scroll animations to complete
|
|
18
|
+
DEFAULT_WAIT_AFTER_SCROLL: 1000, // Default wait after scrolling to element
|
|
19
|
+
VISIBILITY_CHECK_TIMEOUT: 100, // Timeout for quick visibility checks
|
|
20
|
+
DEFAULT_TIMEOUT: 5000, // Default timeout for most operations
|
|
21
|
+
NAVIGATION_TIMEOUT: 30000, // Default timeout for navigation operations
|
|
22
|
+
VERIFICATION_TIMEOUT: 3000, // Default timeout for action verification
|
|
23
|
+
VERIFICATION_RETRY_INTERVAL: 100, // Interval between verification retries
|
|
24
|
+
};
|