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.
Files changed (47) hide show
  1. package/README.md +80 -35
  2. package/SKILL.md +198 -1344
  3. package/install.js +1 -0
  4. package/package.json +1 -1
  5. package/src/aria/index.js +8 -0
  6. package/src/aria/output-processor.js +173 -0
  7. package/src/aria/role-query.js +1229 -0
  8. package/src/aria/snapshot.js +459 -0
  9. package/src/aria.js +237 -43
  10. package/src/cdp/browser.js +22 -4
  11. package/src/cdp-skill.js +268 -68
  12. package/src/dom/click-executor.js +240 -76
  13. package/src/dom/element-locator.js +34 -25
  14. package/src/dom/fill-executor.js +55 -27
  15. package/src/page/dialog-handler.js +119 -0
  16. package/src/page/page-controller.js +190 -3
  17. package/src/runner/context-helpers.js +33 -55
  18. package/src/runner/execute-dynamic.js +34 -143
  19. package/src/runner/execute-form.js +11 -11
  20. package/src/runner/execute-input.js +2 -2
  21. package/src/runner/execute-interaction.js +99 -120
  22. package/src/runner/execute-navigation.js +11 -26
  23. package/src/runner/execute-query.js +8 -5
  24. package/src/runner/step-executors.js +256 -95
  25. package/src/runner/step-registry.js +1064 -0
  26. package/src/runner/step-validator.js +16 -740
  27. package/src/tests/Aria.test.js +1025 -0
  28. package/src/tests/ContextHelpers.test.js +39 -28
  29. package/src/tests/ExecuteBrowser.test.js +572 -0
  30. package/src/tests/ExecuteDynamic.test.js +34 -736
  31. package/src/tests/ExecuteForm.test.js +700 -0
  32. package/src/tests/ExecuteInput.test.js +540 -0
  33. package/src/tests/ExecuteInteraction.test.js +319 -0
  34. package/src/tests/ExecuteQuery.test.js +820 -0
  35. package/src/tests/FillExecutor.test.js +2 -2
  36. package/src/tests/StepValidator.test.js +222 -76
  37. package/src/tests/TestRunner.test.js +36 -25
  38. package/src/tests/integration.test.js +2 -1
  39. package/src/types.js +9 -9
  40. package/src/utils/backoff.js +118 -0
  41. package/src/utils/cdp-helpers.js +130 -0
  42. package/src/utils/devices.js +140 -0
  43. package/src/utils/errors.js +242 -0
  44. package/src/utils/index.js +65 -0
  45. package/src/utils/temp.js +75 -0
  46. package/src/utils/validators.js +433 -0
  47. 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';