cdp-skill 1.0.7 → 1.0.14
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/README.md +80 -35
- package/SKILL.md +198 -1344
- package/install.js +1 -0
- package/package.json +1 -1
- package/src/aria/index.js +8 -0
- package/src/aria/output-processor.js +173 -0
- package/src/aria/role-query.js +1229 -0
- package/src/aria/snapshot.js +459 -0
- package/src/aria.js +237 -43
- package/src/cdp/browser.js +22 -4
- package/src/cdp-skill.js +268 -68
- package/src/dom/click-executor.js +240 -76
- package/src/dom/element-locator.js +34 -25
- package/src/dom/fill-executor.js +55 -27
- package/src/page/dialog-handler.js +119 -0
- package/src/page/page-controller.js +190 -3
- package/src/runner/context-helpers.js +33 -55
- package/src/runner/execute-dynamic.js +34 -143
- package/src/runner/execute-form.js +11 -11
- package/src/runner/execute-input.js +2 -2
- package/src/runner/execute-interaction.js +99 -120
- package/src/runner/execute-navigation.js +11 -26
- package/src/runner/execute-query.js +8 -5
- package/src/runner/step-executors.js +256 -95
- package/src/runner/step-registry.js +1064 -0
- package/src/runner/step-validator.js +16 -740
- package/src/tests/Aria.test.js +1025 -0
- package/src/tests/ContextHelpers.test.js +39 -28
- package/src/tests/ExecuteBrowser.test.js +572 -0
- package/src/tests/ExecuteDynamic.test.js +34 -736
- package/src/tests/ExecuteForm.test.js +700 -0
- package/src/tests/ExecuteInput.test.js +540 -0
- package/src/tests/ExecuteInteraction.test.js +319 -0
- package/src/tests/ExecuteQuery.test.js +820 -0
- package/src/tests/FillExecutor.test.js +2 -2
- package/src/tests/StepValidator.test.js +222 -76
- package/src/tests/TestRunner.test.js +36 -25
- package/src/tests/integration.test.js +2 -1
- package/src/types.js +9 -9
- package/src/utils/backoff.js +118 -0
- package/src/utils/cdp-helpers.js +130 -0
- package/src/utils/devices.js +140 -0
- package/src/utils/errors.js +242 -0
- package/src/utils/index.js +65 -0
- package/src/utils/temp.js +75 -0
- package/src/utils/validators.js +433 -0
- package/src/utils.js +14 -1142
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backoff and Sleep Utilities
|
|
3
|
+
* Exponential backoff with jitter for retry operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Promise-based delay
|
|
8
|
+
* @param {number} ms - Milliseconds to wait
|
|
9
|
+
* @returns {Promise<void>}
|
|
10
|
+
*/
|
|
11
|
+
export function sleep(ms) {
|
|
12
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a backoff sleeper with exponential delay and jitter
|
|
17
|
+
* Prevents thundering herd by randomizing retry delays
|
|
18
|
+
* @param {Object} [options] - Configuration options
|
|
19
|
+
* @param {number} [options.initialDelay=100] - Initial delay in ms
|
|
20
|
+
* @param {number} [options.maxDelay=5000] - Maximum delay in ms
|
|
21
|
+
* @param {number} [options.multiplier=2] - Base multiplier for exponential growth
|
|
22
|
+
* @param {number} [options.jitterMin=0.9] - Minimum jitter factor (e.g., 0.9 = 90%)
|
|
23
|
+
* @param {number} [options.jitterMax=1.1] - Maximum jitter factor (e.g., 1.1 = 110%)
|
|
24
|
+
* @returns {Object} Backoff sleeper interface
|
|
25
|
+
*/
|
|
26
|
+
export function createBackoffSleeper(options = {}) {
|
|
27
|
+
const {
|
|
28
|
+
initialDelay = 100,
|
|
29
|
+
maxDelay = 5000,
|
|
30
|
+
multiplier = 2,
|
|
31
|
+
jitterMin = 0.9,
|
|
32
|
+
jitterMax = 1.1
|
|
33
|
+
} = options;
|
|
34
|
+
|
|
35
|
+
let attempt = 0;
|
|
36
|
+
let currentDelay = initialDelay;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Apply jitter to a delay value
|
|
40
|
+
* @param {number} delay - Base delay
|
|
41
|
+
* @returns {number} Delay with jitter applied
|
|
42
|
+
*/
|
|
43
|
+
function applyJitter(delay) {
|
|
44
|
+
const jitterRange = jitterMax - jitterMin;
|
|
45
|
+
const jitterFactor = jitterMin + Math.random() * jitterRange;
|
|
46
|
+
return Math.floor(delay * jitterFactor);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sleep with exponential backoff and jitter
|
|
51
|
+
* Each call increases the delay exponentially (with random factor)
|
|
52
|
+
* @returns {Promise<number>} The delay that was used
|
|
53
|
+
*/
|
|
54
|
+
async function sleepFn() {
|
|
55
|
+
const delayWithJitter = applyJitter(currentDelay);
|
|
56
|
+
await new Promise(resolve => setTimeout(resolve, delayWithJitter));
|
|
57
|
+
|
|
58
|
+
// Increase delay for next attempt (with random multiplier 1.9-2.1)
|
|
59
|
+
const randomMultiplier = multiplier * (0.95 + Math.random() * 0.1);
|
|
60
|
+
currentDelay = Math.min(currentDelay * randomMultiplier, maxDelay);
|
|
61
|
+
attempt++;
|
|
62
|
+
|
|
63
|
+
return delayWithJitter;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reset the backoff state
|
|
68
|
+
*/
|
|
69
|
+
function reset() {
|
|
70
|
+
attempt = 0;
|
|
71
|
+
currentDelay = initialDelay;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get current attempt count
|
|
76
|
+
* @returns {number}
|
|
77
|
+
*/
|
|
78
|
+
function getAttempt() {
|
|
79
|
+
return attempt;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get next delay without sleeping (preview)
|
|
84
|
+
* @returns {number}
|
|
85
|
+
*/
|
|
86
|
+
function peekDelay() {
|
|
87
|
+
return applyJitter(currentDelay);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
sleep: sleepFn,
|
|
92
|
+
reset,
|
|
93
|
+
getAttempt,
|
|
94
|
+
peekDelay
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Sleep with backoff - simple one-shot function
|
|
100
|
+
* @param {number} attempt - Current attempt number (0-based)
|
|
101
|
+
* @param {Object} [options] - Options
|
|
102
|
+
* @param {number} [options.initialDelay=100] - Initial delay
|
|
103
|
+
* @param {number} [options.maxDelay=5000] - Max delay
|
|
104
|
+
* @returns {Promise<number>} The delay used
|
|
105
|
+
*/
|
|
106
|
+
export async function sleepWithBackoff(attempt, options = {}) {
|
|
107
|
+
const { initialDelay = 100, maxDelay = 5000 } = options;
|
|
108
|
+
|
|
109
|
+
// Calculate delay: initialDelay * 2^attempt with jitter
|
|
110
|
+
const baseDelay = Math.min(initialDelay * Math.pow(2, attempt), maxDelay);
|
|
111
|
+
|
|
112
|
+
// Apply jitter (0.9-1.1x)
|
|
113
|
+
const jitter = 0.9 + Math.random() * 0.2;
|
|
114
|
+
const delay = Math.floor(baseDelay * jitter);
|
|
115
|
+
|
|
116
|
+
await sleep(delay);
|
|
117
|
+
return delay;
|
|
118
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP Helper Utilities
|
|
3
|
+
* Common CDP operations and constants
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sleep } from './backoff.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Release a CDP object reference to prevent memory leaks
|
|
10
|
+
* @param {Object} session - CDP session
|
|
11
|
+
* @param {string} objectId - Object ID to release
|
|
12
|
+
*/
|
|
13
|
+
export async function releaseObject(session, objectId) {
|
|
14
|
+
if (!objectId) return;
|
|
15
|
+
try {
|
|
16
|
+
await session.send('Runtime.releaseObject', { objectId });
|
|
17
|
+
} catch {
|
|
18
|
+
// Ignore errors during cleanup - object may already be released
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reset input state to ensure no pending mouse/keyboard events
|
|
24
|
+
* Helps prevent timeouts on subsequent operations after failures
|
|
25
|
+
* @param {Object} session - CDP session
|
|
26
|
+
*/
|
|
27
|
+
export async function resetInputState(session) {
|
|
28
|
+
try {
|
|
29
|
+
await session.send('Input.dispatchMouseEvent', {
|
|
30
|
+
type: 'mouseReleased',
|
|
31
|
+
x: 0,
|
|
32
|
+
y: 0,
|
|
33
|
+
button: 'left',
|
|
34
|
+
buttons: 0
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
// Ignore errors during cleanup
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Scroll alignment strategies for bringing elements into view
|
|
43
|
+
*/
|
|
44
|
+
export const SCROLL_STRATEGIES = ['center', 'end', 'start', 'nearest'];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Action types for actionability checking
|
|
48
|
+
*/
|
|
49
|
+
export const ActionTypes = {
|
|
50
|
+
CLICK: 'click',
|
|
51
|
+
HOVER: 'hover',
|
|
52
|
+
FILL: 'fill',
|
|
53
|
+
TYPE: 'type',
|
|
54
|
+
SELECT: 'select'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get current page URL
|
|
59
|
+
* @param {Object} session - CDP session
|
|
60
|
+
* @returns {Promise<string|null>}
|
|
61
|
+
*/
|
|
62
|
+
export async function getCurrentUrl(session) {
|
|
63
|
+
try {
|
|
64
|
+
const result = await session.send('Runtime.evaluate', {
|
|
65
|
+
expression: 'window.location.href',
|
|
66
|
+
returnByValue: true
|
|
67
|
+
});
|
|
68
|
+
return result.result.value;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get element info at a specific point (for debug mode)
|
|
76
|
+
* @param {Object} session - CDP session
|
|
77
|
+
* @param {number} x - X coordinate
|
|
78
|
+
* @param {number} y - Y coordinate
|
|
79
|
+
* @returns {Promise<Object|null>}
|
|
80
|
+
*/
|
|
81
|
+
export async function getElementAtPoint(session, x, y) {
|
|
82
|
+
// Validate coordinates are numbers to prevent injection
|
|
83
|
+
const safeX = Number(x);
|
|
84
|
+
const safeY = Number(y);
|
|
85
|
+
if (!Number.isFinite(safeX) || !Number.isFinite(safeY)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const result = await session.send('Runtime.evaluate', {
|
|
90
|
+
expression: `
|
|
91
|
+
(function() {
|
|
92
|
+
const el = document.elementFromPoint(${safeX}, ${safeY});
|
|
93
|
+
if (!el) return null;
|
|
94
|
+
return {
|
|
95
|
+
tagName: el.tagName.toLowerCase(),
|
|
96
|
+
id: el.id || null,
|
|
97
|
+
className: el.className || null,
|
|
98
|
+
textContent: el.textContent ? el.textContent.trim().substring(0, 50) : null
|
|
99
|
+
};
|
|
100
|
+
})()
|
|
101
|
+
`,
|
|
102
|
+
returnByValue: true
|
|
103
|
+
});
|
|
104
|
+
return result.result.value;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect navigation by comparing URLs before and after an action
|
|
112
|
+
* @param {Object} session - CDP session
|
|
113
|
+
* @param {string} urlBeforeAction - URL before the action
|
|
114
|
+
* @param {number} timeout - Timeout to wait for navigation
|
|
115
|
+
* @returns {Promise<{navigated: boolean, newUrl?: string}>}
|
|
116
|
+
*/
|
|
117
|
+
export async function detectNavigation(session, urlBeforeAction, timeout = 100) {
|
|
118
|
+
await sleep(timeout);
|
|
119
|
+
try {
|
|
120
|
+
const urlAfterAction = await getCurrentUrl(session);
|
|
121
|
+
const navigated = urlAfterAction !== urlBeforeAction;
|
|
122
|
+
return {
|
|
123
|
+
navigated,
|
|
124
|
+
newUrl: navigated ? urlAfterAction : undefined
|
|
125
|
+
};
|
|
126
|
+
} catch {
|
|
127
|
+
// If we can't get URL, page likely navigated
|
|
128
|
+
return { navigated: true };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Presets
|
|
3
|
+
* Viewport configurations for device emulation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Device preset configurations for viewport emulation
|
|
8
|
+
*/
|
|
9
|
+
export const DEVICE_PRESETS = new Map([
|
|
10
|
+
// iPhones
|
|
11
|
+
['iphone-se', { width: 375, height: 667, deviceScaleFactor: 2, mobile: true, hasTouch: true }],
|
|
12
|
+
['iphone-12', { width: 390, height: 844, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
13
|
+
['iphone-12-mini', { width: 360, height: 780, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
14
|
+
['iphone-12-pro', { width: 390, height: 844, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
15
|
+
['iphone-12-pro-max', { width: 428, height: 926, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
16
|
+
['iphone-13', { width: 390, height: 844, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
17
|
+
['iphone-13-mini', { width: 375, height: 812, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
18
|
+
['iphone-13-pro', { width: 390, height: 844, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
19
|
+
['iphone-13-pro-max', { width: 428, height: 926, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
20
|
+
['iphone-14', { width: 390, height: 844, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
21
|
+
['iphone-14-plus', { width: 428, height: 926, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
22
|
+
['iphone-14-pro', { width: 393, height: 852, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
23
|
+
['iphone-14-pro-max', { width: 430, height: 932, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
24
|
+
['iphone-15', { width: 393, height: 852, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
25
|
+
['iphone-15-plus', { width: 430, height: 932, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
26
|
+
['iphone-15-pro', { width: 393, height: 852, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
27
|
+
['iphone-15-pro-max', { width: 430, height: 932, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
28
|
+
|
|
29
|
+
// iPads
|
|
30
|
+
['ipad', { width: 768, height: 1024, deviceScaleFactor: 2, mobile: true, hasTouch: true }],
|
|
31
|
+
['ipad-mini', { width: 768, height: 1024, deviceScaleFactor: 2, mobile: true, hasTouch: true }],
|
|
32
|
+
['ipad-air', { width: 820, height: 1180, deviceScaleFactor: 2, mobile: true, hasTouch: true }],
|
|
33
|
+
['ipad-pro-11', { width: 834, height: 1194, deviceScaleFactor: 2, mobile: true, hasTouch: true }],
|
|
34
|
+
['ipad-pro-12.9', { width: 1024, height: 1366, deviceScaleFactor: 2, mobile: true, hasTouch: true }],
|
|
35
|
+
|
|
36
|
+
// Android phones
|
|
37
|
+
['pixel-5', { width: 393, height: 851, deviceScaleFactor: 2.75, mobile: true, hasTouch: true }],
|
|
38
|
+
['pixel-6', { width: 412, height: 915, deviceScaleFactor: 2.625, mobile: true, hasTouch: true }],
|
|
39
|
+
['pixel-7', { width: 412, height: 915, deviceScaleFactor: 2.625, mobile: true, hasTouch: true }],
|
|
40
|
+
['pixel-7-pro', { width: 412, height: 892, deviceScaleFactor: 3.5, mobile: true, hasTouch: true }],
|
|
41
|
+
['samsung-galaxy-s21', { width: 360, height: 800, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
42
|
+
['samsung-galaxy-s22', { width: 360, height: 780, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
43
|
+
['samsung-galaxy-s23', { width: 360, height: 780, deviceScaleFactor: 3, mobile: true, hasTouch: true }],
|
|
44
|
+
|
|
45
|
+
// Android tablets
|
|
46
|
+
['galaxy-tab-s7', { width: 800, height: 1280, deviceScaleFactor: 2, mobile: true, hasTouch: true }],
|
|
47
|
+
|
|
48
|
+
// Desktop presets
|
|
49
|
+
['desktop', { width: 1920, height: 1080, deviceScaleFactor: 1, mobile: false, hasTouch: false }],
|
|
50
|
+
['desktop-hd', { width: 1920, height: 1080, deviceScaleFactor: 1, mobile: false, hasTouch: false }],
|
|
51
|
+
['desktop-4k', { width: 3840, height: 2160, deviceScaleFactor: 1, mobile: false, hasTouch: false }],
|
|
52
|
+
['laptop', { width: 1366, height: 768, deviceScaleFactor: 1, mobile: false, hasTouch: false }],
|
|
53
|
+
['laptop-hd', { width: 1440, height: 900, deviceScaleFactor: 1, mobile: false, hasTouch: false }],
|
|
54
|
+
['macbook-air', { width: 1440, height: 900, deviceScaleFactor: 2, mobile: false, hasTouch: false }],
|
|
55
|
+
['macbook-pro-13', { width: 1440, height: 900, deviceScaleFactor: 2, mobile: false, hasTouch: false }],
|
|
56
|
+
['macbook-pro-14', { width: 1512, height: 982, deviceScaleFactor: 2, mobile: false, hasTouch: false }],
|
|
57
|
+
['macbook-pro-16', { width: 1728, height: 1117, deviceScaleFactor: 2, mobile: false, hasTouch: false }],
|
|
58
|
+
|
|
59
|
+
// Landscape variants (appended with -landscape)
|
|
60
|
+
['iphone-14-landscape', { width: 844, height: 390, deviceScaleFactor: 3, mobile: true, hasTouch: true, isLandscape: true }],
|
|
61
|
+
['iphone-14-pro-landscape', { width: 852, height: 393, deviceScaleFactor: 3, mobile: true, hasTouch: true, isLandscape: true }],
|
|
62
|
+
['ipad-landscape', { width: 1024, height: 768, deviceScaleFactor: 2, mobile: true, hasTouch: true, isLandscape: true }],
|
|
63
|
+
['ipad-pro-11-landscape', { width: 1194, height: 834, deviceScaleFactor: 2, mobile: true, hasTouch: true, isLandscape: true }],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get a device preset by name
|
|
68
|
+
* @param {string} name - Device preset name (case-insensitive)
|
|
69
|
+
* @returns {Object|null} Device configuration or null if not found
|
|
70
|
+
*/
|
|
71
|
+
export function getDevicePreset(name) {
|
|
72
|
+
const normalizedName = name.toLowerCase().replace(/_/g, '-');
|
|
73
|
+
return DEVICE_PRESETS.get(normalizedName) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if a preset exists
|
|
78
|
+
* @param {string} name - Device preset name
|
|
79
|
+
* @returns {boolean}
|
|
80
|
+
*/
|
|
81
|
+
export function hasDevicePreset(name) {
|
|
82
|
+
const normalizedName = name.toLowerCase().replace(/_/g, '-');
|
|
83
|
+
return DEVICE_PRESETS.has(normalizedName);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get all available preset names
|
|
88
|
+
* @returns {string[]}
|
|
89
|
+
*/
|
|
90
|
+
export function listDevicePresets() {
|
|
91
|
+
return Array.from(DEVICE_PRESETS.keys());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get presets by category
|
|
96
|
+
* @param {string} category - 'iphone', 'ipad', 'android', 'desktop', 'landscape'
|
|
97
|
+
* @returns {string[]}
|
|
98
|
+
*/
|
|
99
|
+
export function listDevicePresetsByCategory(category) {
|
|
100
|
+
const categoryLower = category.toLowerCase();
|
|
101
|
+
return listDevicePresets().filter(name => {
|
|
102
|
+
if (categoryLower === 'iphone') return name.startsWith('iphone');
|
|
103
|
+
if (categoryLower === 'ipad') return name.startsWith('ipad');
|
|
104
|
+
if (categoryLower === 'android') return name.startsWith('pixel') || name.startsWith('samsung') || name.startsWith('galaxy');
|
|
105
|
+
if (categoryLower === 'desktop') return name.startsWith('desktop') || name.startsWith('laptop') || name.startsWith('macbook');
|
|
106
|
+
if (categoryLower === 'landscape') return name.endsWith('-landscape');
|
|
107
|
+
return false;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolve viewport options - handles both preset strings and explicit configs
|
|
113
|
+
* @param {string|Object} viewport - Either a preset name string or viewport config object
|
|
114
|
+
* @returns {Object} Resolved viewport configuration
|
|
115
|
+
* @throws {Error} If preset not found
|
|
116
|
+
*/
|
|
117
|
+
export function resolveViewport(viewport) {
|
|
118
|
+
if (typeof viewport === 'string') {
|
|
119
|
+
const preset = getDevicePreset(viewport);
|
|
120
|
+
if (!preset) {
|
|
121
|
+
const available = listDevicePresets().slice(0, 10).join(', ');
|
|
122
|
+
throw new Error(`Unknown device preset "${viewport}". Available presets include: ${available}...`);
|
|
123
|
+
}
|
|
124
|
+
return { ...preset };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// It's an object - validate required fields
|
|
128
|
+
if (!viewport.width || !viewport.height) {
|
|
129
|
+
throw new Error('Viewport requires width and height');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
width: viewport.width,
|
|
134
|
+
height: viewport.height,
|
|
135
|
+
deviceScaleFactor: viewport.deviceScaleFactor || 1,
|
|
136
|
+
mobile: viewport.mobile || false,
|
|
137
|
+
hasTouch: viewport.hasTouch || false,
|
|
138
|
+
isLandscape: viewport.isLandscape || false
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Types and Factory Functions
|
|
3
|
+
* Typed errors for CDP browser driver operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error types for CDP browser driver operations
|
|
8
|
+
*/
|
|
9
|
+
export const ErrorTypes = Object.freeze({
|
|
10
|
+
CONNECTION: 'CDPConnectionError',
|
|
11
|
+
NAVIGATION: 'NavigationError',
|
|
12
|
+
NAVIGATION_ABORTED: 'NavigationAbortedError',
|
|
13
|
+
TIMEOUT: 'TimeoutError',
|
|
14
|
+
ELEMENT_NOT_FOUND: 'ElementNotFoundError',
|
|
15
|
+
ELEMENT_NOT_EDITABLE: 'ElementNotEditableError',
|
|
16
|
+
STALE_ELEMENT: 'StaleElementError',
|
|
17
|
+
PAGE_CRASHED: 'PageCrashedError',
|
|
18
|
+
CONTEXT_DESTROYED: 'ContextDestroyedError',
|
|
19
|
+
STEP_VALIDATION: 'StepValidationError'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a typed error with standard properties
|
|
24
|
+
* @param {string} type - Error type from ErrorTypes
|
|
25
|
+
* @param {string} message - Error message
|
|
26
|
+
* @param {Object} props - Additional properties to attach
|
|
27
|
+
* @returns {Error}
|
|
28
|
+
*/
|
|
29
|
+
export function createError(type, message, props = {}) {
|
|
30
|
+
const error = new Error(message);
|
|
31
|
+
error.name = type;
|
|
32
|
+
Object.assign(error, props);
|
|
33
|
+
if (Error.captureStackTrace) {
|
|
34
|
+
Error.captureStackTrace(error, createError);
|
|
35
|
+
}
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a CDPConnectionError
|
|
41
|
+
* @param {string} message - Error message
|
|
42
|
+
* @param {string} operation - The CDP operation that failed
|
|
43
|
+
* @returns {Error}
|
|
44
|
+
*/
|
|
45
|
+
export function connectionError(message, operation) {
|
|
46
|
+
return createError(
|
|
47
|
+
ErrorTypes.CONNECTION,
|
|
48
|
+
`CDP connection error during ${operation}: ${message}`,
|
|
49
|
+
{ operation, originalMessage: message }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a NavigationError
|
|
55
|
+
* @param {string} message - Error message
|
|
56
|
+
* @param {string} url - URL that failed to load
|
|
57
|
+
* @returns {Error}
|
|
58
|
+
*/
|
|
59
|
+
export function navigationError(message, url) {
|
|
60
|
+
return createError(
|
|
61
|
+
ErrorTypes.NAVIGATION,
|
|
62
|
+
`Navigation to ${url} failed: ${message}`,
|
|
63
|
+
{ url, originalMessage: message }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a NavigationAbortedError
|
|
69
|
+
* @param {string} message - Abort reason
|
|
70
|
+
* @param {string} url - URL being navigated to
|
|
71
|
+
* @returns {Error}
|
|
72
|
+
*/
|
|
73
|
+
export function navigationAbortedError(message, url) {
|
|
74
|
+
return createError(
|
|
75
|
+
ErrorTypes.NAVIGATION_ABORTED,
|
|
76
|
+
`Navigation to ${url} aborted: ${message}`,
|
|
77
|
+
{ url, originalMessage: message }
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a TimeoutError
|
|
83
|
+
* @param {string} message - Description of what timed out
|
|
84
|
+
* @param {number} [timeout] - Timeout duration in ms
|
|
85
|
+
* @returns {Error}
|
|
86
|
+
*/
|
|
87
|
+
export function timeoutError(message, timeout) {
|
|
88
|
+
return createError(
|
|
89
|
+
ErrorTypes.TIMEOUT,
|
|
90
|
+
message,
|
|
91
|
+
timeout !== undefined ? { timeout } : {}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create an ElementNotFoundError
|
|
97
|
+
* @param {string} selector - The selector that wasn't found
|
|
98
|
+
* @param {number} timeout - Timeout duration in ms
|
|
99
|
+
* @returns {Error}
|
|
100
|
+
*/
|
|
101
|
+
export function elementNotFoundError(selector, timeout) {
|
|
102
|
+
return createError(
|
|
103
|
+
ErrorTypes.ELEMENT_NOT_FOUND,
|
|
104
|
+
`Element not found: "${selector}" (timeout: ${timeout}ms)`,
|
|
105
|
+
{ selector, timeout }
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create an ElementNotEditableError
|
|
111
|
+
* @param {string} selector - The selector of the non-editable element
|
|
112
|
+
* @param {string} reason - Reason why element is not editable
|
|
113
|
+
* @returns {Error}
|
|
114
|
+
*/
|
|
115
|
+
export function elementNotEditableError(selector, reason) {
|
|
116
|
+
return createError(
|
|
117
|
+
ErrorTypes.ELEMENT_NOT_EDITABLE,
|
|
118
|
+
`Element "${selector}" is not editable: ${reason}`,
|
|
119
|
+
{ selector, reason }
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a StaleElementError
|
|
125
|
+
* @param {string} objectId - CDP object ID of the stale element
|
|
126
|
+
* @param {Object} [options] - Additional options
|
|
127
|
+
* @param {string} [options.operation] - The operation that was attempted
|
|
128
|
+
* @param {string} [options.selector] - Original selector used
|
|
129
|
+
* @param {Error} [options.cause] - Underlying CDP error
|
|
130
|
+
* @returns {Error}
|
|
131
|
+
*/
|
|
132
|
+
export function staleElementError(objectId, options = {}) {
|
|
133
|
+
if (typeof options === 'string') {
|
|
134
|
+
options = { operation: options };
|
|
135
|
+
}
|
|
136
|
+
const { operation = null, selector = null, cause = null } = options;
|
|
137
|
+
|
|
138
|
+
let message = 'Element is no longer attached to the DOM';
|
|
139
|
+
const details = [];
|
|
140
|
+
if (selector) details.push(`selector: "${selector}"`);
|
|
141
|
+
if (objectId) details.push(`objectId: ${objectId}`);
|
|
142
|
+
if (operation) details.push(`operation: ${operation}`);
|
|
143
|
+
if (details.length > 0) message += ` (${details.join(', ')})`;
|
|
144
|
+
|
|
145
|
+
const error = createError(ErrorTypes.STALE_ELEMENT, message, {
|
|
146
|
+
objectId,
|
|
147
|
+
operation,
|
|
148
|
+
selector
|
|
149
|
+
});
|
|
150
|
+
if (cause) error.cause = cause;
|
|
151
|
+
return error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create a PageCrashedError
|
|
156
|
+
* @param {string} [message] - Optional message
|
|
157
|
+
* @returns {Error}
|
|
158
|
+
*/
|
|
159
|
+
export function pageCrashedError(message = 'Page crashed') {
|
|
160
|
+
return createError(ErrorTypes.PAGE_CRASHED, message);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Create a ContextDestroyedError
|
|
165
|
+
* @param {string} [message] - Optional message
|
|
166
|
+
* @returns {Error}
|
|
167
|
+
*/
|
|
168
|
+
export function contextDestroyedError(message = 'Execution context was destroyed') {
|
|
169
|
+
return createError(ErrorTypes.CONTEXT_DESTROYED, message);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a StepValidationError
|
|
174
|
+
* @param {Array<{index: number, step: Object, errors: string[]}>} invalidSteps
|
|
175
|
+
* @returns {Error}
|
|
176
|
+
*/
|
|
177
|
+
export function stepValidationError(invalidSteps) {
|
|
178
|
+
const messages = invalidSteps.map(({ index, errors }) =>
|
|
179
|
+
`Step ${index + 1}: ${errors.join(', ')}`
|
|
180
|
+
);
|
|
181
|
+
return createError(
|
|
182
|
+
ErrorTypes.STEP_VALIDATION,
|
|
183
|
+
`Invalid step definitions:\n${messages.join('\n')}`,
|
|
184
|
+
{ invalidSteps }
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if an error is of a specific type
|
|
190
|
+
* @param {Error} error - The error to check
|
|
191
|
+
* @param {string} type - Error type from ErrorTypes
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
export function isErrorType(error, type) {
|
|
195
|
+
return error && error.name === type;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Error message patterns for context destruction detection
|
|
199
|
+
const CONTEXT_DESTROYED_PATTERNS = [
|
|
200
|
+
'Cannot find context with specified id',
|
|
201
|
+
'Execution context was destroyed',
|
|
202
|
+
'Inspected target navigated or closed',
|
|
203
|
+
'Context was destroyed'
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if an error indicates context destruction
|
|
208
|
+
* @param {Object} [exceptionDetails] - CDP exception details
|
|
209
|
+
* @param {Error} [error] - Error thrown
|
|
210
|
+
* @returns {boolean}
|
|
211
|
+
*/
|
|
212
|
+
export function isContextDestroyed(exceptionDetails, error) {
|
|
213
|
+
const message = exceptionDetails?.exception?.description ||
|
|
214
|
+
exceptionDetails?.text ||
|
|
215
|
+
error?.message ||
|
|
216
|
+
'';
|
|
217
|
+
return CONTEXT_DESTROYED_PATTERNS.some(pattern => message.includes(pattern));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Stale element error indicators
|
|
221
|
+
const STALE_ELEMENT_PATTERNS = [
|
|
222
|
+
'Could not find object with given id',
|
|
223
|
+
'Object reference not found',
|
|
224
|
+
'Cannot find context with specified id',
|
|
225
|
+
'Node with given id does not belong to the document',
|
|
226
|
+
'No node with given id found',
|
|
227
|
+
'Object is not available',
|
|
228
|
+
'No object with given id',
|
|
229
|
+
'Object with given id not found'
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if an error indicates a stale element
|
|
234
|
+
* @param {Error} error - The error to check
|
|
235
|
+
* @returns {boolean}
|
|
236
|
+
*/
|
|
237
|
+
export function isStaleElementError(error) {
|
|
238
|
+
if (!error || !error.message) return false;
|
|
239
|
+
return STALE_ELEMENT_PATTERNS.some(indicator =>
|
|
240
|
+
error.message.toLowerCase().includes(indicator.toLowerCase())
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utils Module
|
|
3
|
+
* Re-exports all utility functions for backward compatibility
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Temp directory utilities
|
|
7
|
+
export {
|
|
8
|
+
getTempDir,
|
|
9
|
+
getTempDirSync,
|
|
10
|
+
resolveTempPath,
|
|
11
|
+
generateTempPath
|
|
12
|
+
} from './temp.js';
|
|
13
|
+
|
|
14
|
+
// Backoff and sleep utilities
|
|
15
|
+
export {
|
|
16
|
+
sleep,
|
|
17
|
+
createBackoffSleeper,
|
|
18
|
+
sleepWithBackoff
|
|
19
|
+
} from './backoff.js';
|
|
20
|
+
|
|
21
|
+
// CDP helper utilities
|
|
22
|
+
export {
|
|
23
|
+
releaseObject,
|
|
24
|
+
resetInputState,
|
|
25
|
+
SCROLL_STRATEGIES,
|
|
26
|
+
ActionTypes,
|
|
27
|
+
getCurrentUrl,
|
|
28
|
+
getElementAtPoint,
|
|
29
|
+
detectNavigation
|
|
30
|
+
} from './cdp-helpers.js';
|
|
31
|
+
|
|
32
|
+
// Error types and factories
|
|
33
|
+
export {
|
|
34
|
+
ErrorTypes,
|
|
35
|
+
createError,
|
|
36
|
+
connectionError,
|
|
37
|
+
navigationError,
|
|
38
|
+
navigationAbortedError,
|
|
39
|
+
timeoutError,
|
|
40
|
+
elementNotFoundError,
|
|
41
|
+
elementNotEditableError,
|
|
42
|
+
staleElementError,
|
|
43
|
+
pageCrashedError,
|
|
44
|
+
contextDestroyedError,
|
|
45
|
+
stepValidationError,
|
|
46
|
+
isErrorType,
|
|
47
|
+
isContextDestroyed,
|
|
48
|
+
isStaleElementError
|
|
49
|
+
} from './errors.js';
|
|
50
|
+
|
|
51
|
+
// Validators
|
|
52
|
+
export {
|
|
53
|
+
createKeyValidator,
|
|
54
|
+
createFormValidator
|
|
55
|
+
} from './validators.js';
|
|
56
|
+
|
|
57
|
+
// Device presets
|
|
58
|
+
export {
|
|
59
|
+
DEVICE_PRESETS,
|
|
60
|
+
getDevicePreset,
|
|
61
|
+
hasDevicePreset,
|
|
62
|
+
listDevicePresets,
|
|
63
|
+
listDevicePresetsByCategory,
|
|
64
|
+
resolveViewport
|
|
65
|
+
} from './devices.js';
|