browser-commander 0.26.3 → 0.28.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/.jscpd-baseline.json +24 -23
- package/CHANGELOG.md +22 -0
- package/README.md +6 -0
- package/eslint.config.js +11 -0
- package/package.json +9 -4
- package/src/bindings.js +19 -0
- package/src/browser/attach/snapshot-launch.js +8 -6
- package/src/browser/attach/snapshot.js +39 -2
- package/src/browser/browser-cookie-database.js +71 -2
- package/src/browser/browser-cookie-session.js +183 -0
- package/src/browser/browser-cookies.js +29 -0
- package/src/browser/connector.js +117 -13
- package/src/browser/debugging-port.js +4 -2
- package/src/browser/default-browser.js +33 -9
- package/src/browser/launch-diagnostics.js +102 -0
- package/src/browser/launch-options.js +2 -1
- package/src/browser/launch-restrictions.json +33 -2
- package/src/browser/launcher.js +54 -9
- package/src/browser/migration/sqlite-snapshot.js +5 -27
- package/src/browser/navigation.js +173 -98
- package/src/browser/persistent-session.js +360 -0
- package/src/browser/profile-directory.js +20 -0
- package/src/browser/real-browser.js +142 -70
- package/src/browser/restrictions.js +13 -4
- package/src/browser/session-cookies.js +78 -0
- package/src/browser/session-persistence.js +72 -0
- package/src/browser/session-watchdog.js +71 -0
- package/src/browser/session-worker.js +70 -0
- package/src/browser/storage-state.js +32 -6
- package/src/browser/webdriver.js +4 -0
- package/src/capture/encoding.js +327 -0
- package/src/capture/errors.js +12 -0
- package/src/capture/files.js +9 -0
- package/src/capture/index.js +5 -0
- package/src/capture/recording.js +136 -0
- package/src/capture/screenshot.js +222 -0
- package/src/capture/video.js +186 -0
- package/src/capture/viewport.js +68 -0
- package/src/capture/worker.js +22 -0
- package/src/cli/args.js +66 -1
- package/src/cli/commands.js +113 -2
- package/src/cli/high-level-methods.js +40 -3
- package/src/cli/sessions.js +11 -3
- package/src/core/commander-features.js +105 -0
- package/src/core/constants.js +1 -0
- package/src/core/dialog-manager.js +7 -0
- package/src/core/managed-navigation.js +101 -0
- package/src/core/navigation-manager.js +66 -80
- package/src/core/navigation-operation.js +25 -0
- package/src/core/navigation-readiness.js +68 -16
- package/src/core/navigation-safety.js +51 -0
- package/src/core/network-tracker.js +39 -92
- package/src/core/page-session.js +3 -2
- package/src/core/page-trigger-manager.js +130 -42
- package/src/core/readiness.js +76 -14
- package/src/core/subscriptions.js +20 -0
- package/src/core/webdriver-page.js +38 -0
- package/src/elements/locators.js +61 -37
- package/src/elements/reusable.js +92 -0
- package/src/elements/selectors.js +37 -81
- package/src/elements/visibility.js +38 -21
- package/src/exports.js +14 -0
- package/src/factory.js +17 -16
- package/src/high-level/universal-logic.js +57 -21
- package/src/index.js +2 -0
- package/src/interactions/click.js +47 -0
- package/src/interactions/overlays.js +28 -0
- package/src/interactions/scroll.js +8 -2
- package/src/traces/bundle.js +14 -5
- package/src/traces/checkpoints.js +99 -0
- package/src/traces/links.js +117 -1
- package/src/traces/mutation-stream.js +1 -0
- package/src/traces/network.js +257 -0
- package/src/traces/page-capture.js +143 -52
- package/src/traces/reader.js +8 -10
- package/src/traces/recorder.js +221 -245
- package/src/traces/render.js +168 -0
- package/src/traces/rolling.js +180 -0
- package/src/traces/start-cleanup.js +15 -0
- package/src/traces/stop.js +159 -0
- package/src/traces/storage.js +43 -0
- package/src/utilities/subprocess.js +47 -4
- package/tests/e2e/cli.e2e.test.js +20 -1
- package/tests/e2e/click-readiness.e2e.test.js +1 -1
- package/tests/e2e/navigation-budget.e2e.test.js +132 -0
- package/tests/e2e/reusable-session.e2e.test.js +231 -0
- package/tests/unit/browser/attach/snapshot-launch.test.js +2 -2
- package/tests/unit/browser/attach/snapshot.test.js +33 -0
- package/tests/unit/browser/default-browser.test.js +18 -0
- package/tests/unit/browser/launch-building-blocks.test.js +5 -1
- package/tests/unit/browser/launch-diagnostics.test.js +52 -0
- package/tests/unit/browser/launcher.test.js +5 -1
- package/tests/unit/browser/persistent-session.test.js +59 -0
- package/tests/unit/browser/quiet-ui.test.js +16 -0
- package/tests/unit/browser/real-browser.test.js +1 -1
- package/tests/unit/browser/session-workflows.test.js +206 -0
- package/tests/unit/browser/storage-state.test.js +7 -2
- package/tests/unit/browser/tab-selection.test.js +38 -0
- package/tests/unit/capture/capture.test.js +52 -0
- package/tests/unit/capture/cleanup.test.js +24 -0
- package/tests/unit/capture/viewport.test.js +168 -0
- package/tests/unit/cli/dispatcher.test.js +25 -0
- package/tests/unit/core/navigation-budget.test.js +120 -0
- package/tests/unit/core/navigation-manager.test.js +23 -1
- package/tests/unit/core/readiness.test.js +21 -0
- package/tests/unit/core/trigger-readiness.test.js +39 -0
- package/tests/unit/core/webdriver-page.test.js +33 -0
- package/tests/unit/elements/reusable-helpers.test.js +83 -0
- package/tests/unit/elements/visible-count.test.js +24 -0
- package/tests/unit/elements/wait-navigation.test.js +26 -0
- package/tests/unit/interactions/navigation-timeout.test.js +39 -0
- package/tests/unit/scripts/ci-timeout-budgets.test.js +1 -0
- package/tests/unit/traces/bundle.test.js +16 -0
- package/tests/unit/traces/links.test.js +2 -2
- package/tests/unit/traces/network.test.js +97 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { count, isVisible } from './visibility.js';
|
|
2
|
+
import { getLocatorOrElement } from './locators.js';
|
|
3
|
+
import { clickButton } from '../interactions/click.js';
|
|
4
|
+
import { evaluate } from '../utilities/wait.js';
|
|
5
|
+
|
|
6
|
+
/** Return the first selector in caller order with a match (optionally visible). */
|
|
7
|
+
export async function findFirst({
|
|
8
|
+
selectors,
|
|
9
|
+
visible = false,
|
|
10
|
+
...options
|
|
11
|
+
} = {}) {
|
|
12
|
+
if (!Array.isArray(selectors)) {
|
|
13
|
+
throw new TypeError('selectors must be an array');
|
|
14
|
+
}
|
|
15
|
+
for (const selector of selectors) {
|
|
16
|
+
if (
|
|
17
|
+
(await count({ ...options, selector })) > 0 &&
|
|
18
|
+
(!visible || (await isVisible({ ...options, selector })))
|
|
19
|
+
) {
|
|
20
|
+
return selector;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Match any phrase in body text; Unicode whitespace includes nonbreaking spaces. */
|
|
27
|
+
export function hasText({
|
|
28
|
+
texts,
|
|
29
|
+
normalizeWhitespace = false,
|
|
30
|
+
...options
|
|
31
|
+
} = {}) {
|
|
32
|
+
if (!Array.isArray(texts) || texts.some((text) => typeof text !== 'string')) {
|
|
33
|
+
throw new TypeError('texts must be strings');
|
|
34
|
+
}
|
|
35
|
+
return evaluate({
|
|
36
|
+
...options,
|
|
37
|
+
fn: (phrases, normalize) => {
|
|
38
|
+
const clean = (text) =>
|
|
39
|
+
normalize ? text.replace(/\s+/gu, ' ').trim() : text;
|
|
40
|
+
const body = clean(document.body?.textContent ?? '');
|
|
41
|
+
return phrases.some((text) => body.includes(clean(text)));
|
|
42
|
+
},
|
|
43
|
+
args: [texts, normalizeWhitespace],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Observe checked state of the selected checkbox or radio. */
|
|
48
|
+
export async function isChecked(options = {}) {
|
|
49
|
+
const element = await getLocatorOrElement(options);
|
|
50
|
+
if (!element) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const probe = (el) => Boolean(el.checked);
|
|
54
|
+
return options.engine === 'playwright'
|
|
55
|
+
? element.evaluate(probe)
|
|
56
|
+
: options.page.evaluate(probe, element);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Idempotently set a checkbox or radio and verify the resulting state. */
|
|
60
|
+
export async function check({ checked = true, ...options } = {}) {
|
|
61
|
+
const element = await getLocatorOrElement(options);
|
|
62
|
+
if (!element) {
|
|
63
|
+
return { checked: false, changed: false, verified: false };
|
|
64
|
+
}
|
|
65
|
+
const type =
|
|
66
|
+
options.engine === 'playwright'
|
|
67
|
+
? await element.evaluate((el) => el.type)
|
|
68
|
+
: await options.page.evaluate((el) => el.type, element);
|
|
69
|
+
if (!['checkbox', 'radio'].includes(type)) {
|
|
70
|
+
throw new TypeError('check requires a checkbox or radio');
|
|
71
|
+
}
|
|
72
|
+
if (type === 'radio' && !checked) {
|
|
73
|
+
throw new TypeError('A radio cannot be unchecked by clicking');
|
|
74
|
+
}
|
|
75
|
+
const before = await isChecked({ ...options, selector: element });
|
|
76
|
+
if (before === checked) {
|
|
77
|
+
return { checked: before, changed: false, verified: true };
|
|
78
|
+
}
|
|
79
|
+
await clickButton({
|
|
80
|
+
...options,
|
|
81
|
+
selector: element,
|
|
82
|
+
verify: false,
|
|
83
|
+
waitAfterClick: 0,
|
|
84
|
+
waitForNavigation: false,
|
|
85
|
+
});
|
|
86
|
+
const after = await isChecked({ ...options, selector: element });
|
|
87
|
+
return {
|
|
88
|
+
checked: after,
|
|
89
|
+
changed: before !== after,
|
|
90
|
+
verified: after === checked,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
isNavigationError,
|
|
3
|
+
waitNavigationState,
|
|
4
|
+
recoverWaitError,
|
|
5
|
+
} from '../core/navigation-safety.js';
|
|
6
|
+
import { createPlaywrightLocator, requireSelector } from './locators.js';
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Query single element
|
|
@@ -9,67 +13,38 @@ import { createPlaywrightLocator } from './locators.js';
|
|
|
9
13
|
* @param {string} options.selector - CSS selector
|
|
10
14
|
* @returns {Promise<Object|null>} - Element handle or null
|
|
11
15
|
*/
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (!selector) {
|
|
16
|
-
throw new Error('selector is required in options');
|
|
17
|
-
}
|
|
16
|
+
export function querySelector(options = {}) {
|
|
17
|
+
return queryElements(options, true);
|
|
18
|
+
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const count = await locator.count();
|
|
23
|
-
return count > 0 ? locator : null;
|
|
24
|
-
} else {
|
|
25
|
-
return await page.$(selector);
|
|
26
|
-
}
|
|
27
|
-
} catch (error) {
|
|
28
|
-
if (isNavigationError(error)) {
|
|
29
|
-
console.log(
|
|
30
|
-
'⚠️ Navigation detected during querySelector, returning null'
|
|
31
|
-
);
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
throw error;
|
|
35
|
-
}
|
|
20
|
+
/** Return every matching element with the same navigation recovery as querySelector. */
|
|
21
|
+
export function querySelectorAll(options = {}) {
|
|
22
|
+
return queryElements(options, false);
|
|
36
23
|
}
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* @param {Object} options - Configuration options
|
|
41
|
-
* @param {Object} options.page - Browser page object
|
|
42
|
-
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
43
|
-
* @param {string} options.selector - CSS selector
|
|
44
|
-
* @returns {Promise<Array>} - Array of element handles
|
|
45
|
-
*/
|
|
46
|
-
export async function querySelectorAll(options = {}) {
|
|
25
|
+
async function queryElements(options, single) {
|
|
26
|
+
requireSelector(options.selector);
|
|
47
27
|
const { page, engine, selector } = options;
|
|
48
|
-
|
|
49
|
-
if (!selector) {
|
|
50
|
-
throw new Error('selector is required in options');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
28
|
try {
|
|
54
|
-
if (engine
|
|
55
|
-
|
|
56
|
-
const count = await locator.count();
|
|
57
|
-
const elements = [];
|
|
58
|
-
for (let i = 0; i < count; i++) {
|
|
59
|
-
elements.push(locator.nth(i));
|
|
60
|
-
}
|
|
61
|
-
return elements;
|
|
62
|
-
} else {
|
|
63
|
-
return await page.$$(selector);
|
|
29
|
+
if (engine !== 'playwright') {
|
|
30
|
+
return single ? await page.$(selector) : await page.$$(selector);
|
|
64
31
|
}
|
|
32
|
+
const matching = createPlaywrightLocator({ page, selector });
|
|
33
|
+
const locator = single ? matching.first() : matching;
|
|
34
|
+
const total = await locator.count();
|
|
35
|
+
return single
|
|
36
|
+
? total > 0
|
|
37
|
+
? locator
|
|
38
|
+
: null
|
|
39
|
+
: Array.from({ length: total }, (_, index) => locator.nth(index));
|
|
65
40
|
} catch (error) {
|
|
66
|
-
if (isNavigationError(error)) {
|
|
67
|
-
|
|
68
|
-
'⚠️ Navigation detected during querySelectorAll, returning empty array'
|
|
69
|
-
);
|
|
70
|
-
return [];
|
|
41
|
+
if (!isNavigationError(error)) {
|
|
42
|
+
throw error;
|
|
71
43
|
}
|
|
72
|
-
|
|
44
|
+
console.log(
|
|
45
|
+
`⚠️ Navigation detected during ${single ? 'querySelector' : 'querySelectorAll'}, recovering gracefully`
|
|
46
|
+
);
|
|
47
|
+
return single ? null : [];
|
|
73
48
|
}
|
|
74
49
|
}
|
|
75
50
|
|
|
@@ -175,9 +150,7 @@ function parsePlaywrightTextSelector(selector) {
|
|
|
175
150
|
export async function normalizeSelector(options = {}) {
|
|
176
151
|
const { page, engine, selector } = options;
|
|
177
152
|
|
|
178
|
-
|
|
179
|
-
throw new Error('selector is required in options');
|
|
180
|
-
}
|
|
153
|
+
requireSelector(selector);
|
|
181
154
|
|
|
182
155
|
// DEFENSIVE: Validate selector type - must be string or Puppeteer text selector object
|
|
183
156
|
// Arrays and other invalid types should be rejected to prevent downstream querySelectorAll errors
|
|
@@ -360,19 +333,11 @@ export function withTextSelectorSupport(fn, engine, page) {
|
|
|
360
333
|
* @returns {Promise<boolean>} - True if selector found, false on navigation
|
|
361
334
|
*/
|
|
362
335
|
export async function waitForSelector(options = {}) {
|
|
363
|
-
const {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
selector,
|
|
367
|
-
visible = true,
|
|
368
|
-
timeout = 5000,
|
|
369
|
-
throwOnNavigation = true,
|
|
370
|
-
} = options;
|
|
371
|
-
|
|
372
|
-
if (!selector) {
|
|
373
|
-
throw new Error('selector is required in options');
|
|
374
|
-
}
|
|
336
|
+
const { page, engine, selector, visible = true, timeout = 5000 } = options;
|
|
337
|
+
|
|
338
|
+
requireSelector(selector);
|
|
375
339
|
|
|
340
|
+
const initial = waitNavigationState(page, options.navigationManager);
|
|
376
341
|
try {
|
|
377
342
|
if (engine === 'playwright') {
|
|
378
343
|
const locator = createPlaywrightLocator({ page, selector });
|
|
@@ -384,16 +349,7 @@ export async function waitForSelector(options = {}) {
|
|
|
384
349
|
await page.waitForSelector(selector, { visible, timeout });
|
|
385
350
|
}
|
|
386
351
|
return true;
|
|
387
|
-
} catch (
|
|
388
|
-
|
|
389
|
-
console.log(
|
|
390
|
-
'⚠️ Navigation detected during waitForSelector, recovering gracefully'
|
|
391
|
-
);
|
|
392
|
-
if (throwOnNavigation) {
|
|
393
|
-
throw error;
|
|
394
|
-
}
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
throw error;
|
|
352
|
+
} catch (caught) {
|
|
353
|
+
return recoverWaitError(caught, options, initial, 'waitForSelector', false);
|
|
398
354
|
}
|
|
399
355
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TIMING } from '../core/constants.js';
|
|
2
2
|
import { isNavigationError } from '../core/navigation-safety.js';
|
|
3
|
-
import { getLocatorOrElement } from './locators.js';
|
|
3
|
+
import { getLocatorOrElement, requireSelector } from './locators.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Check if element is visible
|
|
@@ -13,13 +13,11 @@ import { getLocatorOrElement } from './locators.js';
|
|
|
13
13
|
export async function isVisible(options = {}) {
|
|
14
14
|
const { page, engine, selector } = options;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
throw new Error('selector is required in options');
|
|
18
|
-
}
|
|
16
|
+
requireSelector(selector);
|
|
19
17
|
|
|
20
18
|
try {
|
|
21
19
|
if (engine === 'playwright') {
|
|
22
|
-
const locator = await getLocatorOrElement(
|
|
20
|
+
const locator = await getLocatorOrElement(options);
|
|
23
21
|
try {
|
|
24
22
|
await locator.waitFor({
|
|
25
23
|
state: 'visible',
|
|
@@ -30,7 +28,7 @@ export async function isVisible(options = {}) {
|
|
|
30
28
|
return false;
|
|
31
29
|
}
|
|
32
30
|
} else {
|
|
33
|
-
const element = await getLocatorOrElement(
|
|
31
|
+
const element = await getLocatorOrElement(options);
|
|
34
32
|
if (!element) {
|
|
35
33
|
return false;
|
|
36
34
|
}
|
|
@@ -56,27 +54,20 @@ export async function isVisible(options = {}) {
|
|
|
56
54
|
* @param {Object} options.page - Browser page object
|
|
57
55
|
* @param {string} options.engine - Engine type ('playwright' or 'puppeteer')
|
|
58
56
|
* @param {string|Object} options.selector - CSS selector or locator
|
|
59
|
-
* @param {Array<string>} options.disabledClasses - Additional CSS classes that indicate disabled state (default: ['
|
|
57
|
+
* @param {Array<string>} options.disabledClasses - Additional CSS classes that indicate disabled state (default: ['disabled'])
|
|
60
58
|
* @returns {Promise<boolean>} - True if enabled
|
|
61
59
|
*/
|
|
62
60
|
export async function isEnabled(options = {}) {
|
|
63
|
-
const {
|
|
64
|
-
page,
|
|
65
|
-
engine,
|
|
66
|
-
selector,
|
|
67
|
-
disabledClasses = ['magritte-button_loading'],
|
|
68
|
-
} = options;
|
|
61
|
+
const { page, engine, selector, disabledClasses = ['disabled'] } = options;
|
|
69
62
|
|
|
70
|
-
|
|
71
|
-
throw new Error('selector is required in options');
|
|
72
|
-
}
|
|
63
|
+
requireSelector(selector);
|
|
73
64
|
|
|
74
65
|
try {
|
|
75
66
|
if (engine === 'playwright') {
|
|
76
67
|
// For Playwright, use locator API
|
|
77
68
|
const locator =
|
|
78
69
|
typeof selector === 'string'
|
|
79
|
-
?
|
|
70
|
+
? (await getLocatorOrElement(options)).first()
|
|
80
71
|
: selector;
|
|
81
72
|
return await locator.evaluate((el, classes) => {
|
|
82
73
|
const isDisabled =
|
|
@@ -87,7 +78,7 @@ export async function isEnabled(options = {}) {
|
|
|
87
78
|
}, disabledClasses);
|
|
88
79
|
} else {
|
|
89
80
|
// For Puppeteer (selector should already be normalized by withTextSelectorSupport wrapper)
|
|
90
|
-
const element = await getLocatorOrElement(
|
|
81
|
+
const element = await getLocatorOrElement(options);
|
|
91
82
|
if (!element) {
|
|
92
83
|
return false;
|
|
93
84
|
}
|
|
@@ -124,9 +115,7 @@ export async function isEnabled(options = {}) {
|
|
|
124
115
|
export async function count(options = {}) {
|
|
125
116
|
const { page, engine, selector } = options;
|
|
126
117
|
|
|
127
|
-
|
|
128
|
-
throw new Error('selector is required in options');
|
|
129
|
-
}
|
|
118
|
+
requireSelector(selector);
|
|
130
119
|
|
|
131
120
|
try {
|
|
132
121
|
// Handle Puppeteer text selectors
|
|
@@ -155,9 +144,37 @@ export async function count(options = {}) {
|
|
|
155
144
|
}
|
|
156
145
|
|
|
157
146
|
if (engine === 'playwright') {
|
|
147
|
+
if (options.visible) {
|
|
148
|
+
return await page
|
|
149
|
+
.locator(selector)
|
|
150
|
+
.evaluateAll(
|
|
151
|
+
(elements) =>
|
|
152
|
+
elements.filter(
|
|
153
|
+
(element) =>
|
|
154
|
+
element.getClientRects().length > 0 &&
|
|
155
|
+
element.checkVisibility({ visibilityProperty: true })
|
|
156
|
+
).length
|
|
157
|
+
);
|
|
158
|
+
}
|
|
158
159
|
return await page.locator(selector).count();
|
|
159
160
|
} else {
|
|
160
161
|
const elements = await page.$$(selector);
|
|
162
|
+
if (options.visible) {
|
|
163
|
+
let visible = 0;
|
|
164
|
+
for (const element of elements) {
|
|
165
|
+
if (
|
|
166
|
+
await page.evaluate(
|
|
167
|
+
(element) =>
|
|
168
|
+
element.getClientRects().length > 0 &&
|
|
169
|
+
element.checkVisibility({ visibilityProperty: true }),
|
|
170
|
+
element
|
|
171
|
+
)
|
|
172
|
+
) {
|
|
173
|
+
visible += 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return visible;
|
|
177
|
+
}
|
|
161
178
|
return elements.length;
|
|
162
179
|
}
|
|
163
180
|
} catch (error) {
|
package/src/exports.js
CHANGED
|
@@ -328,3 +328,17 @@ export {
|
|
|
328
328
|
SafariUnsupportedError,
|
|
329
329
|
openSafariSettings,
|
|
330
330
|
} from './browser/safari-support.js';
|
|
331
|
+
|
|
332
|
+
export { BrowserLaunchError } from './browser/launch-diagnostics.js';
|
|
333
|
+
|
|
334
|
+
export { findFirst, hasText, isChecked, check } from './elements/reusable.js';
|
|
335
|
+
export { setCookies, clearCookies } from './browser/session-cookies.js';
|
|
336
|
+
export {
|
|
337
|
+
readFlag,
|
|
338
|
+
uninstallClickListener,
|
|
339
|
+
} from './high-level/universal-logic.js';
|
|
340
|
+
|
|
341
|
+
export {
|
|
342
|
+
findSiteSessions,
|
|
343
|
+
readBrowserCookieSession,
|
|
344
|
+
} from './browser/browser-cookie-session.js';
|
package/src/factory.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from './core/page-trigger-manager.js';
|
|
23
23
|
import { createBoundFunctions } from './bindings.js';
|
|
24
24
|
import { attachDownloads } from './downloads/attach.js';
|
|
25
|
-
import {
|
|
25
|
+
import { attachCommanderFeatures } from './core/commander-features.js';
|
|
26
26
|
import { createCdpSession } from './browser/cdp-session.js';
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -66,7 +66,7 @@ export function makeBrowserCommander(options = {}) {
|
|
|
66
66
|
page,
|
|
67
67
|
engine,
|
|
68
68
|
log,
|
|
69
|
-
idleTimeout: 30000, // Wait 30 seconds without requests before considering network idle
|
|
69
|
+
idleTimeout: options.networkIdleForMs ?? 30000, // Wait 30 seconds without requests before considering network idle
|
|
70
70
|
});
|
|
71
71
|
networkTracker.startTracking();
|
|
72
72
|
}
|
|
@@ -92,6 +92,7 @@ export function makeBrowserCommander(options = {}) {
|
|
|
92
92
|
log,
|
|
93
93
|
networkTracker,
|
|
94
94
|
});
|
|
95
|
+
navigationManager.configure(navigationOptions(options));
|
|
95
96
|
navigationManager.startListening();
|
|
96
97
|
|
|
97
98
|
// Create PageSession factory
|
|
@@ -159,7 +160,6 @@ export function makeBrowserCommander(options = {}) {
|
|
|
159
160
|
|
|
160
161
|
// All bound functions
|
|
161
162
|
...boundFunctions,
|
|
162
|
-
|
|
163
163
|
// Managed downloads (issue #88). Present once a manager is attached,
|
|
164
164
|
// either by passing the one launchBrowser() returned or by calling
|
|
165
165
|
// configureDownloads() on an existing commander.
|
|
@@ -192,15 +192,6 @@ export function makeBrowserCommander(options = {}) {
|
|
|
192
192
|
*/
|
|
193
193
|
createCdpSession: () => createCdpSession(page, { engine }),
|
|
194
194
|
|
|
195
|
-
/**
|
|
196
|
-
* Start recording a privacy-aware trace of this session (issue #87).
|
|
197
|
-
*
|
|
198
|
-
* @param {Object} [traceOptions] - {output, mode, screenshots, dom, events, privacy, limits}
|
|
199
|
-
* @returns {Promise<Object>} The running trace: {checkpoint, event, stop}
|
|
200
|
-
*/
|
|
201
|
-
startTrace: (traceOptions = {}) =>
|
|
202
|
-
startTrace({ commander, page, log, ...traceOptions }),
|
|
203
|
-
|
|
204
195
|
// Lifecycle
|
|
205
196
|
destroy,
|
|
206
197
|
|
|
@@ -215,16 +206,16 @@ export function makeBrowserCommander(options = {}) {
|
|
|
215
206
|
// Subscribe to navigation events (legacy API)
|
|
216
207
|
onNavigationStart: navigationManager
|
|
217
208
|
? (fn) => navigationManager.on('onNavigationStart', fn)
|
|
218
|
-
: () => {},
|
|
209
|
+
: () => () => {},
|
|
219
210
|
onNavigationComplete: navigationManager
|
|
220
211
|
? (fn) => navigationManager.on('onNavigationComplete', fn)
|
|
221
|
-
: () => {},
|
|
212
|
+
: () => () => {},
|
|
222
213
|
onUrlChange: navigationManager
|
|
223
214
|
? (fn) => navigationManager.on('onUrlChange', fn)
|
|
224
|
-
: () => {},
|
|
215
|
+
: () => () => {},
|
|
225
216
|
onPageReady: navigationManager
|
|
226
217
|
? (fn) => navigationManager.on('onPageReady', fn)
|
|
227
|
-
: () => {},
|
|
218
|
+
: () => () => {},
|
|
228
219
|
|
|
229
220
|
// Abort handling - check these to stop operations when navigation occurs
|
|
230
221
|
shouldAbort: navigationManager
|
|
@@ -295,5 +286,15 @@ export function makeBrowserCommander(options = {}) {
|
|
|
295
286
|
pageTriggerManager.initialize(commander);
|
|
296
287
|
}
|
|
297
288
|
|
|
289
|
+
attachCommanderFeatures(commander, options, makeBrowserCommander);
|
|
298
290
|
return commander;
|
|
299
291
|
}
|
|
292
|
+
|
|
293
|
+
function navigationOptions(options) {
|
|
294
|
+
return {
|
|
295
|
+
...(options.navigationManager ?? {}),
|
|
296
|
+
...(options.networkIdleTimeout === undefined
|
|
297
|
+
? {}
|
|
298
|
+
: { networkIdleTimeout: options.networkIdleTimeout }),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -101,26 +101,29 @@ export async function installClickListener(options = {}) {
|
|
|
101
101
|
|
|
102
102
|
const result = await safeEvaluate({
|
|
103
103
|
fn: (text, key) => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
104
|
+
window.__browserCommanderClickListeners ??= new Map();
|
|
105
|
+
const previous = window.__browserCommanderClickListeners.get(key);
|
|
106
|
+
if (previous) {
|
|
107
|
+
document.removeEventListener('click', previous, true);
|
|
108
|
+
}
|
|
109
|
+
const listener = (event) => {
|
|
110
|
+
let element = event.target;
|
|
111
|
+
while (element && element !== document.body) {
|
|
112
|
+
const elementText = element.textContent?.trim() || '';
|
|
113
|
+
if (
|
|
114
|
+
elementText === text ||
|
|
115
|
+
((element.tagName === 'A' || element.tagName === 'BUTTON') &&
|
|
116
|
+
elementText.includes(text))
|
|
117
|
+
) {
|
|
118
|
+
console.log(`[Click Listener] Detected click on ${text} button!`);
|
|
119
|
+
window.sessionStorage.setItem(key, 'true');
|
|
120
|
+
break;
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
element = element.parentElement;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
window.__browserCommanderClickListeners.set(key, listener);
|
|
126
|
+
document.addEventListener('click', listener, true);
|
|
124
127
|
},
|
|
125
128
|
args: [buttonText, storageKey],
|
|
126
129
|
});
|
|
@@ -176,6 +179,7 @@ export async function findToggleButton(options = {}) {
|
|
|
176
179
|
findByText,
|
|
177
180
|
dataQaSelectors = [],
|
|
178
181
|
textToFind,
|
|
182
|
+
texts = textToFind ? [textToFind] : [],
|
|
179
183
|
elementTypes = ['button', 'a', 'span'],
|
|
180
184
|
} = options;
|
|
181
185
|
|
|
@@ -188,10 +192,10 @@ export async function findToggleButton(options = {}) {
|
|
|
188
192
|
}
|
|
189
193
|
|
|
190
194
|
// Fallback to text search
|
|
191
|
-
|
|
195
|
+
for (const text of texts) {
|
|
192
196
|
for (const elementType of elementTypes) {
|
|
193
197
|
const selector = await findByText({
|
|
194
|
-
text
|
|
198
|
+
text,
|
|
195
199
|
selector: elementType,
|
|
196
200
|
});
|
|
197
201
|
const elemCount = await count({ selector });
|
|
@@ -203,3 +207,35 @@ export async function findToggleButton(options = {}) {
|
|
|
203
207
|
|
|
204
208
|
return null;
|
|
205
209
|
}
|
|
210
|
+
|
|
211
|
+
/** Read without clearing; interruption is distinct from an unset flag. */
|
|
212
|
+
export async function readFlag({ evaluate, storageKey } = {}) {
|
|
213
|
+
try {
|
|
214
|
+
const set = await evaluate({
|
|
215
|
+
fn: (key) => window.sessionStorage.getItem(key) === 'true',
|
|
216
|
+
args: [storageKey],
|
|
217
|
+
});
|
|
218
|
+
return { status: 'observed', set };
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (isNavigationError(error)) {
|
|
221
|
+
return { status: 'interrupted', set: null };
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Uninstall only the click listener installed for this storage key. */
|
|
228
|
+
export function uninstallClickListener({ evaluate, storageKey } = {}) {
|
|
229
|
+
return evaluate({
|
|
230
|
+
fn: (key) => {
|
|
231
|
+
const listener = window.__browserCommanderClickListeners?.get(key);
|
|
232
|
+
if (!listener) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
document.removeEventListener('click', listener, true);
|
|
236
|
+
window.__browserCommanderClickListeners.delete(key);
|
|
237
|
+
return true;
|
|
238
|
+
},
|
|
239
|
+
args: [storageKey],
|
|
240
|
+
});
|
|
241
|
+
}
|
package/src/index.js
CHANGED
|
@@ -31,6 +31,25 @@ function isInterrupted(error) {
|
|
|
31
31
|
return isNavigationError(error) || isActionStoppedError(error);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function waitInterruptedByNavigation({
|
|
35
|
+
error,
|
|
36
|
+
page,
|
|
37
|
+
navigationManager,
|
|
38
|
+
startUrl,
|
|
39
|
+
startSessionId,
|
|
40
|
+
preparationFinished,
|
|
41
|
+
}) {
|
|
42
|
+
const timedOut =
|
|
43
|
+
error.name === 'TimeoutError' ||
|
|
44
|
+
error.name === 'NavigationInterruptedError' ||
|
|
45
|
+
/timeout|timed out/i.test(error.message);
|
|
46
|
+
const navigationChanged =
|
|
47
|
+
page.url() !== startUrl ||
|
|
48
|
+
(startSessionId !== undefined &&
|
|
49
|
+
navigationManager?.getSessionId?.() !== startSessionId);
|
|
50
|
+
return timedOut && navigationChanged && !preparationFinished;
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
/**
|
|
35
54
|
* Read the target element's observable state under the click's own budget.
|
|
36
55
|
*
|
|
@@ -709,6 +728,7 @@ async function prepareElement(options = {}) {
|
|
|
709
728
|
log,
|
|
710
729
|
verbose = false,
|
|
711
730
|
selector,
|
|
731
|
+
index,
|
|
712
732
|
scrollIntoView: shouldScroll = true,
|
|
713
733
|
waitAfterScroll,
|
|
714
734
|
smoothScroll = true,
|
|
@@ -719,6 +739,7 @@ async function prepareElement(options = {}) {
|
|
|
719
739
|
page,
|
|
720
740
|
engine,
|
|
721
741
|
selector,
|
|
742
|
+
index,
|
|
722
743
|
timeout,
|
|
723
744
|
});
|
|
724
745
|
|
|
@@ -973,6 +994,7 @@ export async function clickButton(options = {}) {
|
|
|
973
994
|
|
|
974
995
|
const startUrl = page.url();
|
|
975
996
|
const startSessionId = navigationManager?.getSessionId?.();
|
|
997
|
+
let preparationFinished = false;
|
|
976
998
|
|
|
977
999
|
try {
|
|
978
1000
|
const prepareResult = await prepareElement({
|
|
@@ -982,6 +1004,7 @@ export async function clickButton(options = {}) {
|
|
|
982
1004
|
log,
|
|
983
1005
|
verbose,
|
|
984
1006
|
selector,
|
|
1007
|
+
index: options.index,
|
|
985
1008
|
scrollIntoView: activationOptions.scroll === CLICK_SCROLL.AUTO,
|
|
986
1009
|
waitAfterScroll,
|
|
987
1010
|
smoothScroll,
|
|
@@ -1009,6 +1032,7 @@ export async function clickButton(options = {}) {
|
|
|
1009
1032
|
});
|
|
1010
1033
|
}
|
|
1011
1034
|
|
|
1035
|
+
preparationFinished = true;
|
|
1012
1036
|
const clickResult = await clickElement({
|
|
1013
1037
|
page,
|
|
1014
1038
|
engine,
|
|
@@ -1052,6 +1076,29 @@ export async function clickButton(options = {}) {
|
|
|
1052
1076
|
actionId,
|
|
1053
1077
|
});
|
|
1054
1078
|
} catch (error) {
|
|
1079
|
+
if (
|
|
1080
|
+
waitInterruptedByNavigation({
|
|
1081
|
+
error,
|
|
1082
|
+
page,
|
|
1083
|
+
navigationManager,
|
|
1084
|
+
startUrl,
|
|
1085
|
+
startSessionId,
|
|
1086
|
+
preparationFinished,
|
|
1087
|
+
})
|
|
1088
|
+
) {
|
|
1089
|
+
return makeClickResult({
|
|
1090
|
+
status: CLICK_STATUS.INTERRUPTED,
|
|
1091
|
+
dispatched: false,
|
|
1092
|
+
effect: CLICK_EFFECT.NOT_OBSERVED,
|
|
1093
|
+
navigated: true,
|
|
1094
|
+
reason: 'navigation interrupted the element wait before the click',
|
|
1095
|
+
evidence: [
|
|
1096
|
+
evidence('interrupted', { message: error.message, phase: 'prepare' }),
|
|
1097
|
+
],
|
|
1098
|
+
elapsedMs: elapsed(),
|
|
1099
|
+
actionId,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1055
1102
|
if (isInterrupted(error)) {
|
|
1056
1103
|
log.debug(
|
|
1057
1104
|
() =>
|