cdp-skill 1.0.2 → 1.0.4
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 +3 -0
- package/SKILL.md +34 -5
- package/package.json +2 -1
- package/src/capture/console-capture.js +241 -0
- package/src/capture/debug-capture.js +144 -0
- package/src/capture/error-aggregator.js +151 -0
- package/src/capture/eval-serializer.js +320 -0
- package/src/capture/index.js +40 -0
- package/src/capture/network-capture.js +211 -0
- package/src/capture/pdf-capture.js +256 -0
- package/src/capture/screenshot-capture.js +325 -0
- package/src/cdp/browser.js +569 -0
- package/src/cdp/connection.js +369 -0
- package/src/cdp/discovery.js +138 -0
- package/src/cdp/index.js +29 -0
- package/src/cdp/target-and-session.js +439 -0
- package/src/cdp-skill.js +25 -11
- package/src/constants.js +79 -0
- package/src/dom/actionability.js +638 -0
- package/src/dom/click-executor.js +923 -0
- package/src/dom/element-handle.js +496 -0
- package/src/dom/element-locator.js +475 -0
- package/src/dom/element-validator.js +120 -0
- package/src/dom/fill-executor.js +489 -0
- package/src/dom/index.js +248 -0
- package/src/dom/input-emulator.js +406 -0
- package/src/dom/keyboard-executor.js +202 -0
- package/src/dom/quad-helpers.js +89 -0
- package/src/dom/react-filler.js +94 -0
- package/src/dom/wait-executor.js +423 -0
- package/src/index.js +6 -6
- package/src/page/cookie-manager.js +202 -0
- package/src/page/dom-stability.js +181 -0
- package/src/page/index.js +36 -0
- package/src/{page.js → page/page-controller.js} +109 -839
- package/src/page/wait-utilities.js +302 -0
- package/src/page/web-storage-manager.js +108 -0
- package/src/runner/context-helpers.js +224 -0
- package/src/runner/execute-browser.js +518 -0
- package/src/runner/execute-form.js +315 -0
- package/src/runner/execute-input.js +308 -0
- package/src/runner/execute-interaction.js +672 -0
- package/src/runner/execute-navigation.js +180 -0
- package/src/runner/execute-query.js +771 -0
- package/src/runner/index.js +51 -0
- package/src/runner/step-executors.js +421 -0
- package/src/runner/step-validator.js +641 -0
- package/src/tests/Actionability.test.js +613 -0
- package/src/tests/BrowserClient.test.js +1 -1
- package/src/tests/ChromeDiscovery.test.js +1 -1
- package/src/tests/ClickExecutor.test.js +554 -0
- package/src/tests/ConsoleCapture.test.js +1 -1
- package/src/tests/ContextHelpers.test.js +453 -0
- package/src/tests/CookieManager.test.js +450 -0
- package/src/tests/DebugCapture.test.js +307 -0
- package/src/tests/ElementHandle.test.js +1 -1
- package/src/tests/ElementLocator.test.js +1 -1
- package/src/tests/ErrorAggregator.test.js +1 -1
- package/src/tests/EvalSerializer.test.js +391 -0
- package/src/tests/FillExecutor.test.js +611 -0
- package/src/tests/InputEmulator.test.js +1 -1
- package/src/tests/KeyboardExecutor.test.js +430 -0
- package/src/tests/NetworkErrorCapture.test.js +1 -1
- package/src/tests/PageController.test.js +1 -1
- package/src/tests/PdfCapture.test.js +333 -0
- package/src/tests/ScreenshotCapture.test.js +1 -1
- package/src/tests/SessionRegistry.test.js +1 -1
- package/src/tests/StepValidator.test.js +527 -0
- package/src/tests/TargetManager.test.js +1 -1
- package/src/tests/TestRunner.test.js +1 -1
- package/src/tests/WaitStrategy.test.js +1 -1
- package/src/tests/WaitUtilities.test.js +508 -0
- package/src/tests/WebStorageManager.test.js +333 -0
- package/src/types.js +309 -0
- package/src/capture.js +0 -1400
- package/src/cdp.js +0 -1286
- package/src/dom.js +0 -4379
- package/src/runner.js +0 -3676
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait Executor
|
|
3
|
+
* Wait operations for selectors, text, visibility, and timing
|
|
4
|
+
*
|
|
5
|
+
* EXPORTS:
|
|
6
|
+
* - createWaitExecutor(session, elementLocator) → WaitExecutor
|
|
7
|
+
* Methods: execute, waitForSelector, waitForHidden, waitForCount,
|
|
8
|
+
* waitForText, waitForTextRegex, waitForUrlContains, waitForTime
|
|
9
|
+
*
|
|
10
|
+
* DEPENDENCIES:
|
|
11
|
+
* - ../constants.js: TIMEOUTS, POLL_INTERVALS
|
|
12
|
+
* - ../utils.js: sleep, timeoutError
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { TIMEOUTS, POLL_INTERVALS } from '../constants.js';
|
|
16
|
+
import { sleep, timeoutError } from '../utils.js';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_TIMEOUT = TIMEOUTS.DEFAULT;
|
|
19
|
+
const MAX_TIMEOUT = TIMEOUTS.MAX;
|
|
20
|
+
const POLL_INTERVAL = POLL_INTERVALS.DEFAULT;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a wait executor for handling wait operations
|
|
24
|
+
* @param {Object} session - CDP session
|
|
25
|
+
* @param {Object} elementLocator - Element locator instance
|
|
26
|
+
* @returns {Object} Wait executor interface
|
|
27
|
+
*/
|
|
28
|
+
export function createWaitExecutor(session, elementLocator) {
|
|
29
|
+
if (!session) throw new Error('CDP session is required');
|
|
30
|
+
if (!elementLocator) throw new Error('Element locator is required');
|
|
31
|
+
|
|
32
|
+
function validateTimeout(timeout) {
|
|
33
|
+
if (typeof timeout !== 'number' || !Number.isFinite(timeout)) {
|
|
34
|
+
return DEFAULT_TIMEOUT;
|
|
35
|
+
}
|
|
36
|
+
if (timeout < 0) return 0;
|
|
37
|
+
if (timeout > MAX_TIMEOUT) return MAX_TIMEOUT;
|
|
38
|
+
return timeout;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Wait for selector using browser-side MutationObserver
|
|
43
|
+
* Much faster than Node.js polling as it avoids network round-trips
|
|
44
|
+
*/
|
|
45
|
+
async function waitForSelector(selector, timeout = DEFAULT_TIMEOUT) {
|
|
46
|
+
const validatedTimeout = validateTimeout(timeout);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Use browser-side polling with MutationObserver for better performance
|
|
50
|
+
const result = await session.send('Runtime.evaluate', {
|
|
51
|
+
expression: `
|
|
52
|
+
new Promise((resolve, reject) => {
|
|
53
|
+
const selector = ${JSON.stringify(selector)};
|
|
54
|
+
const timeout = ${validatedTimeout};
|
|
55
|
+
|
|
56
|
+
// Check if element already exists
|
|
57
|
+
const existing = document.querySelector(selector);
|
|
58
|
+
if (existing) {
|
|
59
|
+
resolve({ found: true, immediate: true });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let resolved = false;
|
|
64
|
+
const timeoutId = setTimeout(() => {
|
|
65
|
+
if (!resolved) {
|
|
66
|
+
resolved = true;
|
|
67
|
+
observer.disconnect();
|
|
68
|
+
reject(new Error('Timeout waiting for selector: ' + selector));
|
|
69
|
+
}
|
|
70
|
+
}, timeout);
|
|
71
|
+
|
|
72
|
+
const observer = new MutationObserver((mutations, obs) => {
|
|
73
|
+
const el = document.querySelector(selector);
|
|
74
|
+
if (el && !resolved) {
|
|
75
|
+
resolved = true;
|
|
76
|
+
obs.disconnect();
|
|
77
|
+
clearTimeout(timeoutId);
|
|
78
|
+
resolve({ found: true, mutations: mutations.length });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
observer.observe(document.documentElement || document.body, {
|
|
83
|
+
childList: true,
|
|
84
|
+
subtree: true,
|
|
85
|
+
attributes: true,
|
|
86
|
+
attributeFilter: ['class', 'id', 'style', 'hidden']
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Also check with RAF as a fallback
|
|
90
|
+
const checkWithRAF = () => {
|
|
91
|
+
if (resolved) return;
|
|
92
|
+
const el = document.querySelector(selector);
|
|
93
|
+
if (el) {
|
|
94
|
+
resolved = true;
|
|
95
|
+
observer.disconnect();
|
|
96
|
+
clearTimeout(timeoutId);
|
|
97
|
+
resolve({ found: true, raf: true });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
requestAnimationFrame(checkWithRAF);
|
|
101
|
+
};
|
|
102
|
+
requestAnimationFrame(checkWithRAF);
|
|
103
|
+
})
|
|
104
|
+
`,
|
|
105
|
+
awaitPromise: true,
|
|
106
|
+
returnByValue: true
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (result.exceptionDetails) {
|
|
110
|
+
throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result.result.value;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// Fall back to original Node.js polling if browser-side fails
|
|
116
|
+
const element = await elementLocator.waitForSelector(selector, {
|
|
117
|
+
timeout: validatedTimeout
|
|
118
|
+
});
|
|
119
|
+
if (element) await element.dispose();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function checkElementHidden(selector) {
|
|
124
|
+
try {
|
|
125
|
+
const result = await session.send('Runtime.evaluate', {
|
|
126
|
+
expression: `
|
|
127
|
+
(function() {
|
|
128
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
129
|
+
if (!el) return true;
|
|
130
|
+
const style = window.getComputedStyle(el);
|
|
131
|
+
if (style.display === 'none') return true;
|
|
132
|
+
if (style.visibility === 'hidden') return true;
|
|
133
|
+
if (style.opacity === '0') return true;
|
|
134
|
+
const rect = el.getBoundingClientRect();
|
|
135
|
+
if (rect.width === 0 && rect.height === 0) return true;
|
|
136
|
+
return false;
|
|
137
|
+
})()
|
|
138
|
+
`,
|
|
139
|
+
returnByValue: true
|
|
140
|
+
});
|
|
141
|
+
return result.result.value === true;
|
|
142
|
+
} catch {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function waitForHidden(selector, timeout = DEFAULT_TIMEOUT) {
|
|
148
|
+
const validatedTimeout = validateTimeout(timeout);
|
|
149
|
+
const startTime = Date.now();
|
|
150
|
+
|
|
151
|
+
while (Date.now() - startTime < validatedTimeout) {
|
|
152
|
+
const isHidden = await checkElementHidden(selector);
|
|
153
|
+
if (isHidden) return;
|
|
154
|
+
await sleep(POLL_INTERVAL);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw timeoutError(
|
|
158
|
+
`Timeout (${validatedTimeout}ms) waiting for element to disappear: "${selector}"`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function getElementCount(selector) {
|
|
163
|
+
try {
|
|
164
|
+
const result = await session.send('Runtime.evaluate', {
|
|
165
|
+
expression: `document.querySelectorAll(${JSON.stringify(selector)}).length`,
|
|
166
|
+
returnByValue: true
|
|
167
|
+
});
|
|
168
|
+
return result.result.value || 0;
|
|
169
|
+
} catch {
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function waitForCount(selector, minCount, timeout = DEFAULT_TIMEOUT) {
|
|
175
|
+
if (typeof minCount !== 'number' || minCount < 0) {
|
|
176
|
+
throw new Error('minCount must be a non-negative number');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const validatedTimeout = validateTimeout(timeout);
|
|
180
|
+
const startTime = Date.now();
|
|
181
|
+
|
|
182
|
+
while (Date.now() - startTime < validatedTimeout) {
|
|
183
|
+
const count = await getElementCount(selector);
|
|
184
|
+
if (count >= minCount) return;
|
|
185
|
+
await sleep(POLL_INTERVAL);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const finalCount = await getElementCount(selector);
|
|
189
|
+
throw timeoutError(
|
|
190
|
+
`Timeout (${validatedTimeout}ms) waiting for ${minCount} elements matching "${selector}" (found ${finalCount})`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Wait for text using browser-side MutationObserver
|
|
196
|
+
*/
|
|
197
|
+
async function waitForText(text, opts = {}) {
|
|
198
|
+
const { timeout = DEFAULT_TIMEOUT, caseSensitive = false } = opts;
|
|
199
|
+
const validatedTimeout = validateTimeout(timeout);
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// Use browser-side polling with MutationObserver
|
|
203
|
+
const result = await session.send('Runtime.evaluate', {
|
|
204
|
+
expression: `
|
|
205
|
+
new Promise((resolve, reject) => {
|
|
206
|
+
const searchText = ${JSON.stringify(text)};
|
|
207
|
+
const caseSensitive = ${caseSensitive};
|
|
208
|
+
const timeout = ${validatedTimeout};
|
|
209
|
+
|
|
210
|
+
const checkText = () => {
|
|
211
|
+
const bodyText = document.body ? document.body.innerText : '';
|
|
212
|
+
if (caseSensitive) {
|
|
213
|
+
return bodyText.includes(searchText);
|
|
214
|
+
}
|
|
215
|
+
return bodyText.toLowerCase().includes(searchText.toLowerCase());
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Check if text already exists
|
|
219
|
+
if (checkText()) {
|
|
220
|
+
resolve({ found: true, immediate: true });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let resolved = false;
|
|
225
|
+
const timeoutId = setTimeout(() => {
|
|
226
|
+
if (!resolved) {
|
|
227
|
+
resolved = true;
|
|
228
|
+
observer.disconnect();
|
|
229
|
+
reject(new Error('Timeout waiting for text: ' + searchText));
|
|
230
|
+
}
|
|
231
|
+
}, timeout);
|
|
232
|
+
|
|
233
|
+
const observer = new MutationObserver((mutations, obs) => {
|
|
234
|
+
if (!resolved && checkText()) {
|
|
235
|
+
resolved = true;
|
|
236
|
+
obs.disconnect();
|
|
237
|
+
clearTimeout(timeoutId);
|
|
238
|
+
resolve({ found: true, mutations: mutations.length });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
observer.observe(document.documentElement || document.body, {
|
|
243
|
+
childList: true,
|
|
244
|
+
subtree: true,
|
|
245
|
+
characterData: true
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Also check with RAF as a fallback
|
|
249
|
+
const checkWithRAF = () => {
|
|
250
|
+
if (resolved) return;
|
|
251
|
+
if (checkText()) {
|
|
252
|
+
resolved = true;
|
|
253
|
+
observer.disconnect();
|
|
254
|
+
clearTimeout(timeoutId);
|
|
255
|
+
resolve({ found: true, raf: true });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
requestAnimationFrame(checkWithRAF);
|
|
259
|
+
};
|
|
260
|
+
requestAnimationFrame(checkWithRAF);
|
|
261
|
+
})
|
|
262
|
+
`,
|
|
263
|
+
awaitPromise: true,
|
|
264
|
+
returnByValue: true
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (result.exceptionDetails) {
|
|
268
|
+
throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return result.result.value;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// Fall back to original Node.js polling
|
|
274
|
+
const startTime = Date.now();
|
|
275
|
+
const checkExpr = caseSensitive
|
|
276
|
+
? `document.body.innerText.includes(${JSON.stringify(text)})`
|
|
277
|
+
: `document.body.innerText.toLowerCase().includes(${JSON.stringify(text.toLowerCase())})`;
|
|
278
|
+
|
|
279
|
+
while (Date.now() - startTime < validatedTimeout) {
|
|
280
|
+
try {
|
|
281
|
+
const result = await session.send('Runtime.evaluate', {
|
|
282
|
+
expression: checkExpr,
|
|
283
|
+
returnByValue: true
|
|
284
|
+
});
|
|
285
|
+
if (result.result.value === true) return;
|
|
286
|
+
} catch {
|
|
287
|
+
// Continue polling
|
|
288
|
+
}
|
|
289
|
+
await sleep(POLL_INTERVAL);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw timeoutError(
|
|
293
|
+
`Timeout (${validatedTimeout}ms) waiting for text: "${text}"${caseSensitive ? ' (case-sensitive)' : ''}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function waitForTextRegex(pattern, timeout = DEFAULT_TIMEOUT) {
|
|
299
|
+
const validatedTimeout = validateTimeout(timeout);
|
|
300
|
+
const startTime = Date.now();
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
new RegExp(pattern);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
throw new Error(`Invalid regex pattern: ${pattern} - ${e.message}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
while (Date.now() - startTime < validatedTimeout) {
|
|
309
|
+
try {
|
|
310
|
+
const result = await session.send('Runtime.evaluate', {
|
|
311
|
+
expression: `
|
|
312
|
+
(function() {
|
|
313
|
+
try {
|
|
314
|
+
const regex = new RegExp(${JSON.stringify(pattern)});
|
|
315
|
+
return regex.test(document.body.innerText);
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
})()
|
|
320
|
+
`,
|
|
321
|
+
returnByValue: true
|
|
322
|
+
});
|
|
323
|
+
if (result.result.value === true) return;
|
|
324
|
+
} catch {
|
|
325
|
+
// Continue polling
|
|
326
|
+
}
|
|
327
|
+
await sleep(POLL_INTERVAL);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
throw timeoutError(
|
|
331
|
+
`Timeout (${validatedTimeout}ms) waiting for text matching pattern: /${pattern}/`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function waitForUrlContains(substring, timeout = DEFAULT_TIMEOUT) {
|
|
336
|
+
const validatedTimeout = validateTimeout(timeout);
|
|
337
|
+
const startTime = Date.now();
|
|
338
|
+
|
|
339
|
+
while (Date.now() - startTime < validatedTimeout) {
|
|
340
|
+
try {
|
|
341
|
+
const result = await session.send('Runtime.evaluate', {
|
|
342
|
+
expression: 'window.location.href',
|
|
343
|
+
returnByValue: true
|
|
344
|
+
});
|
|
345
|
+
const currentUrl = result.result.value;
|
|
346
|
+
if (currentUrl && currentUrl.includes(substring)) return;
|
|
347
|
+
} catch {
|
|
348
|
+
// Continue polling
|
|
349
|
+
}
|
|
350
|
+
await sleep(POLL_INTERVAL);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let finalUrl = 'unknown';
|
|
354
|
+
try {
|
|
355
|
+
const result = await session.send('Runtime.evaluate', {
|
|
356
|
+
expression: 'window.location.href',
|
|
357
|
+
returnByValue: true
|
|
358
|
+
});
|
|
359
|
+
finalUrl = result.result.value || 'unknown';
|
|
360
|
+
} catch {
|
|
361
|
+
// Ignore
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
throw timeoutError(
|
|
365
|
+
`Timeout (${validatedTimeout}ms) waiting for URL to contain "${substring}" (current: ${finalUrl})`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function waitForTime(ms) {
|
|
370
|
+
if (typeof ms !== 'number' || ms < 0) {
|
|
371
|
+
throw new Error('wait time must be a non-negative number');
|
|
372
|
+
}
|
|
373
|
+
await sleep(ms);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function execute(params) {
|
|
377
|
+
if (typeof params === 'string') {
|
|
378
|
+
return waitForSelector(params);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (params.time !== undefined) {
|
|
382
|
+
return waitForTime(params.time);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (params.selector !== undefined) {
|
|
386
|
+
if (params.hidden === true) {
|
|
387
|
+
return waitForHidden(params.selector, params.timeout);
|
|
388
|
+
}
|
|
389
|
+
if (params.minCount !== undefined) {
|
|
390
|
+
return waitForCount(params.selector, params.minCount, params.timeout);
|
|
391
|
+
}
|
|
392
|
+
return waitForSelector(params.selector, params.timeout);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (params.text !== undefined) {
|
|
396
|
+
return waitForText(params.text, {
|
|
397
|
+
timeout: params.timeout,
|
|
398
|
+
caseSensitive: params.caseSensitive
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (params.textRegex !== undefined) {
|
|
403
|
+
return waitForTextRegex(params.textRegex, params.timeout);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (params.urlContains !== undefined) {
|
|
407
|
+
return waitForUrlContains(params.urlContains, params.timeout);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
throw new Error(`Invalid wait params: ${JSON.stringify(params)}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
execute,
|
|
415
|
+
waitForSelector,
|
|
416
|
+
waitForHidden,
|
|
417
|
+
waitForCount,
|
|
418
|
+
waitForText,
|
|
419
|
+
waitForTextRegex,
|
|
420
|
+
waitForUrlContains,
|
|
421
|
+
waitForTime
|
|
422
|
+
};
|
|
423
|
+
}
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,7 @@ export {
|
|
|
21
21
|
getChromeStatus,
|
|
22
22
|
isChromeProcessRunning,
|
|
23
23
|
createNewTab
|
|
24
|
-
} from './cdp.js';
|
|
24
|
+
} from './cdp/index.js';
|
|
25
25
|
|
|
26
26
|
// ============================================================================
|
|
27
27
|
// Page Operations and Waiting
|
|
@@ -42,7 +42,7 @@ export {
|
|
|
42
42
|
lcsSimilarity,
|
|
43
43
|
getDOMSignature,
|
|
44
44
|
waitForDOMStability
|
|
45
|
-
} from './page.js';
|
|
45
|
+
} from './page/index.js';
|
|
46
46
|
|
|
47
47
|
// ============================================================================
|
|
48
48
|
// DOM Operations and Input
|
|
@@ -70,7 +70,7 @@ export {
|
|
|
70
70
|
createActionabilityChecker,
|
|
71
71
|
createElementValidator,
|
|
72
72
|
createReactInputFiller
|
|
73
|
-
} from './dom.js';
|
|
73
|
+
} from './dom/index.js';
|
|
74
74
|
|
|
75
75
|
// ============================================================================
|
|
76
76
|
// ARIA and Role-based Queries
|
|
@@ -97,7 +97,7 @@ export {
|
|
|
97
97
|
createPdfCapture,
|
|
98
98
|
createDebugCapture,
|
|
99
99
|
createEvalSerializer
|
|
100
|
-
} from './capture.js';
|
|
100
|
+
} from './capture/index.js';
|
|
101
101
|
|
|
102
102
|
// ============================================================================
|
|
103
103
|
// Test Execution
|
|
@@ -107,7 +107,7 @@ export {
|
|
|
107
107
|
executeStep,
|
|
108
108
|
runSteps,
|
|
109
109
|
createTestRunner
|
|
110
|
-
} from './runner.js';
|
|
110
|
+
} from './runner/index.js';
|
|
111
111
|
|
|
112
112
|
// ============================================================================
|
|
113
113
|
// Utilities and Errors
|
|
@@ -156,5 +156,5 @@ export {
|
|
|
156
156
|
// ============================================================================
|
|
157
157
|
// Default Export - High-level browser factory
|
|
158
158
|
// ============================================================================
|
|
159
|
-
import { createBrowser } from './cdp.js';
|
|
159
|
+
import { createBrowser } from './cdp/index.js';
|
|
160
160
|
export default createBrowser;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie Manager Module
|
|
3
|
+
* CDP-based cookie management for getting, setting, and clearing cookies
|
|
4
|
+
*
|
|
5
|
+
* PUBLIC EXPORTS:
|
|
6
|
+
* - createCookieManager(session) - Factory for cookie manager
|
|
7
|
+
*
|
|
8
|
+
* @module cdp-skill/page/cookie-manager
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a cookie manager for getting, setting, and clearing cookies
|
|
13
|
+
* @param {import('../types.js').CDPSession} session - CDP session
|
|
14
|
+
* @returns {Object} Cookie manager interface
|
|
15
|
+
*/
|
|
16
|
+
export function createCookieManager(session) {
|
|
17
|
+
/**
|
|
18
|
+
* Get all cookies, optionally filtered by URLs
|
|
19
|
+
* @param {string[]} [urls=[]] - Optional URLs to filter cookies
|
|
20
|
+
* @returns {Promise<import('../types.js').CookieObject[]>} Array of cookie objects
|
|
21
|
+
*/
|
|
22
|
+
async function getCookies(urls = []) {
|
|
23
|
+
const result = await session.send('Storage.getCookies', {});
|
|
24
|
+
let cookies = result.cookies || [];
|
|
25
|
+
|
|
26
|
+
// Filter by URLs if provided
|
|
27
|
+
if (urls.length > 0) {
|
|
28
|
+
cookies = cookies.filter(cookie => {
|
|
29
|
+
return urls.some(url => {
|
|
30
|
+
try {
|
|
31
|
+
const parsed = new URL(url);
|
|
32
|
+
// Domain matching
|
|
33
|
+
const domainMatch = cookie.domain.startsWith('.')
|
|
34
|
+
? parsed.hostname.endsWith(cookie.domain.slice(1))
|
|
35
|
+
: parsed.hostname === cookie.domain;
|
|
36
|
+
// Path matching
|
|
37
|
+
const pathMatch = parsed.pathname.startsWith(cookie.path);
|
|
38
|
+
// Secure matching
|
|
39
|
+
const secureMatch = !cookie.secure || parsed.protocol === 'https:';
|
|
40
|
+
return domainMatch && pathMatch && secureMatch;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return cookies.map(cookie => ({
|
|
49
|
+
name: cookie.name,
|
|
50
|
+
value: cookie.value,
|
|
51
|
+
domain: cookie.domain,
|
|
52
|
+
path: cookie.path,
|
|
53
|
+
expires: cookie.expires,
|
|
54
|
+
httpOnly: cookie.httpOnly,
|
|
55
|
+
secure: cookie.secure,
|
|
56
|
+
sameSite: cookie.sameSite || 'Lax'
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Set one or more cookies
|
|
62
|
+
* @param {import('../types.js').CookieObject[]} cookies - Array of cookie objects to set
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
async function setCookies(cookies) {
|
|
66
|
+
const processedCookies = cookies.map(cookie => {
|
|
67
|
+
const processed = {
|
|
68
|
+
name: cookie.name,
|
|
69
|
+
value: cookie.value
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// If URL provided, derive domain/path/secure from it
|
|
73
|
+
if (cookie.url) {
|
|
74
|
+
try {
|
|
75
|
+
const parsed = new URL(cookie.url);
|
|
76
|
+
processed.domain = cookie.domain || parsed.hostname;
|
|
77
|
+
processed.path = cookie.path || '/';
|
|
78
|
+
processed.secure = cookie.secure !== undefined ? cookie.secure : parsed.protocol === 'https:';
|
|
79
|
+
} catch {
|
|
80
|
+
throw new Error(`Invalid cookie URL: ${cookie.url}`);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// Require domain and path if no URL
|
|
84
|
+
if (!cookie.domain) {
|
|
85
|
+
throw new Error('Cookie requires either url or domain');
|
|
86
|
+
}
|
|
87
|
+
processed.domain = cookie.domain;
|
|
88
|
+
processed.path = cookie.path || '/';
|
|
89
|
+
processed.secure = cookie.secure || false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Optional properties
|
|
93
|
+
if (cookie.expires !== undefined) {
|
|
94
|
+
processed.expires = cookie.expires;
|
|
95
|
+
}
|
|
96
|
+
if (cookie.httpOnly !== undefined) {
|
|
97
|
+
processed.httpOnly = cookie.httpOnly;
|
|
98
|
+
}
|
|
99
|
+
if (cookie.sameSite) {
|
|
100
|
+
processed.sameSite = cookie.sameSite;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return processed;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await session.send('Storage.setCookies', { cookies: processedCookies });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear all cookies or cookies matching specific domains
|
|
111
|
+
* @param {string[]} [urls=[]] - Optional URLs to filter cookies by domain
|
|
112
|
+
* @param {Object} [options] - Optional filters
|
|
113
|
+
* @param {string} [options.domain] - Clear cookies for specific domain
|
|
114
|
+
* @returns {Promise<{count: number}>} Number of cookies deleted
|
|
115
|
+
*/
|
|
116
|
+
async function clearCookies(urls = [], options = {}) {
|
|
117
|
+
const { domain } = options;
|
|
118
|
+
|
|
119
|
+
if (urls.length === 0 && !domain) {
|
|
120
|
+
// Clear all cookies
|
|
121
|
+
const allCookies = await getCookies();
|
|
122
|
+
const count = allCookies.length;
|
|
123
|
+
await session.send('Storage.clearCookies', {});
|
|
124
|
+
return { count };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Get cookies to filter
|
|
128
|
+
let cookiesToDelete;
|
|
129
|
+
if (domain) {
|
|
130
|
+
// Filter by domain - get all cookies and match domain
|
|
131
|
+
const allCookies = await getCookies();
|
|
132
|
+
cookiesToDelete = allCookies.filter(cookie =>
|
|
133
|
+
cookie.domain === domain ||
|
|
134
|
+
cookie.domain === `.${domain}` ||
|
|
135
|
+
cookie.domain.endsWith(`.${domain}`)
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
// Filter by URLs (original behavior)
|
|
139
|
+
cookiesToDelete = await getCookies(urls);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let deletedCount = 0;
|
|
143
|
+
|
|
144
|
+
for (const cookie of cookiesToDelete) {
|
|
145
|
+
try {
|
|
146
|
+
await session.send('Network.deleteCookies', {
|
|
147
|
+
name: cookie.name,
|
|
148
|
+
domain: cookie.domain,
|
|
149
|
+
path: cookie.path
|
|
150
|
+
});
|
|
151
|
+
deletedCount++;
|
|
152
|
+
} catch {
|
|
153
|
+
// Ignore individual deletion failures
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { count: deletedCount };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Delete specific cookies by name
|
|
162
|
+
* @param {string|string[]} names - Cookie name(s) to delete
|
|
163
|
+
* @param {Object} [options] - Optional filters
|
|
164
|
+
* @param {string} [options.domain] - Limit deletion to specific domain
|
|
165
|
+
* @param {string} [options.path] - Limit deletion to specific path
|
|
166
|
+
* @returns {Promise<{count: number}>} Number of cookies deleted
|
|
167
|
+
*/
|
|
168
|
+
async function deleteCookies(names, options = {}) {
|
|
169
|
+
const nameList = Array.isArray(names) ? names : [names];
|
|
170
|
+
const { domain, path } = options;
|
|
171
|
+
let deletedCount = 0;
|
|
172
|
+
|
|
173
|
+
// Get all cookies to find matching ones
|
|
174
|
+
const allCookies = await getCookies();
|
|
175
|
+
|
|
176
|
+
for (const cookie of allCookies) {
|
|
177
|
+
if (!nameList.includes(cookie.name)) continue;
|
|
178
|
+
if (domain && cookie.domain !== domain && !cookie.domain.endsWith(`.${domain}`)) continue;
|
|
179
|
+
if (path && cookie.path !== path) continue;
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await session.send('Network.deleteCookies', {
|
|
183
|
+
name: cookie.name,
|
|
184
|
+
domain: cookie.domain,
|
|
185
|
+
path: cookie.path
|
|
186
|
+
});
|
|
187
|
+
deletedCount++;
|
|
188
|
+
} catch {
|
|
189
|
+
// Ignore individual deletion failures
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { count: deletedCount };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
getCookies,
|
|
198
|
+
setCookies,
|
|
199
|
+
clearCookies,
|
|
200
|
+
deleteCookies
|
|
201
|
+
};
|
|
202
|
+
}
|