cdp-skill 1.0.8 → 1.0.15
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 +157 -241
- 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 +251 -50
- package/src/cdp/browser.js +22 -4
- package/src/cdp-skill.js +246 -69
- package/src/dom/LazyResolver.js +634 -0
- package/src/dom/click-executor.js +366 -94
- package/src/dom/element-locator.js +34 -25
- package/src/dom/fill-executor.js +83 -50
- package/src/dom/index.js +3 -0
- package/src/page/dialog-handler.js +119 -0
- package/src/page/page-controller.js +236 -3
- package/src/runner/context-helpers.js +33 -55
- package/src/runner/execute-dynamic.js +8 -7
- package/src/runner/execute-form.js +11 -11
- package/src/runner/execute-input.js +2 -2
- package/src/runner/execute-interaction.js +105 -126
- package/src/runner/execute-navigation.js +14 -29
- package/src/runner/execute-query.js +17 -11
- package/src/runner/step-executors.js +225 -84
- package/src/runner/step-registry.js +1064 -0
- package/src/runner/step-validator.js +16 -754
- package/src/tests/Aria.test.js +1025 -0
- package/src/tests/ClickExecutor.test.js +170 -50
- package/src/tests/ContextHelpers.test.js +41 -30
- package/src/tests/ExecuteBrowser.test.js +572 -0
- package/src/tests/ExecuteDynamic.test.js +2 -457
- 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 +89 -37
- package/src/tests/LazyResolver.test.js +383 -0
- package/src/tests/StepValidator.test.js +224 -78
- package/src/tests/TestRunner.test.js +38 -27
- 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,572 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
executePdf,
|
|
6
|
+
executeEval,
|
|
7
|
+
executeCookies,
|
|
8
|
+
executeListTabs,
|
|
9
|
+
executeCloseTab,
|
|
10
|
+
executeConsole,
|
|
11
|
+
parseExpiration,
|
|
12
|
+
formatStackTrace,
|
|
13
|
+
formatCommandConsole
|
|
14
|
+
} from '../runner/execute-browser.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
function createMockPdfCapture(opts = {}) {
|
|
21
|
+
return {
|
|
22
|
+
saveToFile: mock.fn((path, options) => {
|
|
23
|
+
if (opts.saveError) throw new Error(opts.saveError);
|
|
24
|
+
return Promise.resolve({ path, size: 12345 });
|
|
25
|
+
})
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createMockPageController(opts = {}) {
|
|
30
|
+
return {
|
|
31
|
+
session: {
|
|
32
|
+
send: mock.fn(() => Promise.resolve({}))
|
|
33
|
+
},
|
|
34
|
+
evaluateInFrame: mock.fn((expression, options) => {
|
|
35
|
+
if (opts.exception) {
|
|
36
|
+
return Promise.resolve({
|
|
37
|
+
result: { value: undefined },
|
|
38
|
+
exceptionDetails: {
|
|
39
|
+
exception: { description: opts.exception },
|
|
40
|
+
text: opts.exception
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (opts.syntaxError) {
|
|
45
|
+
return Promise.resolve({
|
|
46
|
+
result: { value: undefined },
|
|
47
|
+
exceptionDetails: {
|
|
48
|
+
text: 'SyntaxError: Unexpected token'
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (opts.timeout) {
|
|
53
|
+
return new Promise(() => {}); // Never resolves
|
|
54
|
+
}
|
|
55
|
+
return Promise.resolve({
|
|
56
|
+
result: {
|
|
57
|
+
value: opts.evalResult || { success: true },
|
|
58
|
+
type: 'object'
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}),
|
|
62
|
+
getFrameContext: mock.fn(() => opts.contextId || null)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createMockCookieManager(opts = {}) {
|
|
67
|
+
return {
|
|
68
|
+
getCookies: mock.fn((urls) => {
|
|
69
|
+
if (opts.getError) throw new Error(opts.getError);
|
|
70
|
+
return Promise.resolve(opts.cookies || [
|
|
71
|
+
{ name: 'session', value: 'abc123', domain: 'example.com' }
|
|
72
|
+
]);
|
|
73
|
+
}),
|
|
74
|
+
setCookies: mock.fn((cookies) => {
|
|
75
|
+
if (opts.setError) throw new Error(opts.setError);
|
|
76
|
+
return Promise.resolve({ count: cookies.length });
|
|
77
|
+
}),
|
|
78
|
+
clearCookies: mock.fn((urls, options) => {
|
|
79
|
+
if (opts.clearError) throw new Error(opts.clearError);
|
|
80
|
+
return Promise.resolve({ count: opts.clearCount || 5 });
|
|
81
|
+
}),
|
|
82
|
+
deleteCookies: mock.fn((names, options) => {
|
|
83
|
+
if (opts.deleteError) throw new Error(opts.deleteError);
|
|
84
|
+
return Promise.resolve({ count: Array.isArray(names) ? names.length : 1 });
|
|
85
|
+
})
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createMockBrowser(opts = {}) {
|
|
90
|
+
return {
|
|
91
|
+
getPages: mock.fn(() => {
|
|
92
|
+
if (opts.getError) throw new Error(opts.getError);
|
|
93
|
+
return Promise.resolve(opts.pages || [
|
|
94
|
+
{ targetId: 't1', url: 'https://example.com', title: 'Example' }
|
|
95
|
+
]);
|
|
96
|
+
}),
|
|
97
|
+
closePage: mock.fn((targetId) => {
|
|
98
|
+
if (opts.closeError) throw new Error(opts.closeError);
|
|
99
|
+
return Promise.resolve();
|
|
100
|
+
})
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function createMockConsoleCapture(opts = {}) {
|
|
105
|
+
return {
|
|
106
|
+
getMessages: mock.fn(() => opts.messages || []),
|
|
107
|
+
getMessagesByLevel: mock.fn((level) => opts.messagesByLevel || []),
|
|
108
|
+
getMessagesByType: mock.fn((type) => opts.messagesByType || []),
|
|
109
|
+
clear: mock.fn(() => {}),
|
|
110
|
+
getMessageCount: mock.fn(() => opts.messageCount || 0)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Tests: executePdf
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
describe('executePdf', () => {
|
|
119
|
+
afterEach(() => { mock.reset(); });
|
|
120
|
+
|
|
121
|
+
it('should throw if pdfCapture not available', async () => {
|
|
122
|
+
await assert.rejects(
|
|
123
|
+
executePdf(null, {}, 'test.pdf'),
|
|
124
|
+
{ message: 'PDF capture not available' }
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should save PDF with string path param', async () => {
|
|
129
|
+
const pdfCapture = createMockPdfCapture();
|
|
130
|
+
const result = await executePdf(pdfCapture, {}, 'report.pdf');
|
|
131
|
+
|
|
132
|
+
assert.strictEqual(pdfCapture.saveToFile.mock.calls.length, 1);
|
|
133
|
+
assert.ok(result.path);
|
|
134
|
+
assert.strictEqual(result.size, 12345);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should save PDF with object params', async () => {
|
|
138
|
+
const pdfCapture = createMockPdfCapture();
|
|
139
|
+
const result = await executePdf(pdfCapture, {}, {
|
|
140
|
+
path: 'report.pdf',
|
|
141
|
+
landscape: true
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
assert.ok(result.path);
|
|
145
|
+
assert.strictEqual(pdfCapture.saveToFile.mock.calls.length, 1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should propagate saveToFile errors', async () => {
|
|
149
|
+
const pdfCapture = createMockPdfCapture({ saveError: 'Write failed' });
|
|
150
|
+
await assert.rejects(
|
|
151
|
+
executePdf(pdfCapture, {}, 'test.pdf'),
|
|
152
|
+
{ message: 'Write failed' }
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Tests: executeEval
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
describe('executeEval', () => {
|
|
162
|
+
afterEach(() => { mock.reset(); });
|
|
163
|
+
|
|
164
|
+
it('should throw if expression is missing', async () => {
|
|
165
|
+
const pc = createMockPageController();
|
|
166
|
+
await assert.rejects(
|
|
167
|
+
executeEval(pc, {}),
|
|
168
|
+
{ message: /requires a non-empty expression/i }
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should execute string expression', async () => {
|
|
173
|
+
const pc = createMockPageController({ evalResult: { value: 42 } });
|
|
174
|
+
const result = await executeEval(pc, 'document.title');
|
|
175
|
+
|
|
176
|
+
assert.strictEqual(pc.evaluateInFrame.mock.calls.length, 1);
|
|
177
|
+
assert.ok(result.value);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should execute object params with expression', async () => {
|
|
181
|
+
const pc = createMockPageController({ evalResult: 123 });
|
|
182
|
+
const result = await executeEval(pc, { expression: '1 + 2' });
|
|
183
|
+
|
|
184
|
+
assert.ok(result);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should support await option', async () => {
|
|
188
|
+
const pc = createMockPageController();
|
|
189
|
+
await executeEval(pc, { expression: 'Promise.resolve(42)', await: true });
|
|
190
|
+
|
|
191
|
+
const call = pc.evaluateInFrame.mock.calls[0];
|
|
192
|
+
assert.strictEqual(call.arguments[1].awaitPromise, true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should detect unbalanced quotes', async () => {
|
|
196
|
+
const pc = createMockPageController();
|
|
197
|
+
await assert.rejects(
|
|
198
|
+
executeEval(pc, 'document.querySelector("test)'),
|
|
199
|
+
{ message: /unbalanced quotes/i }
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should detect unbalanced braces', async () => {
|
|
204
|
+
const pc = createMockPageController();
|
|
205
|
+
await assert.rejects(
|
|
206
|
+
executeEval(pc, '{ foo: "bar" '),
|
|
207
|
+
{ message: /unbalanced braces/i }
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should detect unbalanced parentheses', async () => {
|
|
212
|
+
const pc = createMockPageController();
|
|
213
|
+
await assert.rejects(
|
|
214
|
+
executeEval(pc, 'Math.max(1, 2 '),
|
|
215
|
+
{ message: /unbalanced parentheses/i }
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should handle syntax errors with context', async () => {
|
|
220
|
+
const pc = createMockPageController({ syntaxError: true });
|
|
221
|
+
await assert.rejects(
|
|
222
|
+
executeEval(pc, 'invalid syntax here'),
|
|
223
|
+
{ message: /syntax error/i }
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle evaluation exceptions', async () => {
|
|
228
|
+
const pc = createMockPageController({ exception: 'ReferenceError: x is not defined' });
|
|
229
|
+
await assert.rejects(
|
|
230
|
+
executeEval(pc, 'x'),
|
|
231
|
+
{ message: /Eval error/i }
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should support timeout option', async () => {
|
|
236
|
+
const pc = createMockPageController({ timeout: true });
|
|
237
|
+
await assert.rejects(
|
|
238
|
+
executeEval(pc, { expression: 'new Promise(() => {})', timeout: 100 }),
|
|
239
|
+
{ message: /timed out/i }
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should support serialize option (default true)', async () => {
|
|
244
|
+
const pc = createMockPageController();
|
|
245
|
+
await executeEval(pc, { expression: 'document.title' });
|
|
246
|
+
|
|
247
|
+
// Serialize wraps expression
|
|
248
|
+
const call = pc.evaluateInFrame.mock.calls[0];
|
|
249
|
+
assert.ok(call.arguments[0].includes('document.title'));
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should support serialize: false', async () => {
|
|
253
|
+
const pc = createMockPageController();
|
|
254
|
+
await executeEval(pc, { expression: 'document.title', serialize: false });
|
|
255
|
+
|
|
256
|
+
const call = pc.evaluateInFrame.mock.calls[0];
|
|
257
|
+
assert.strictEqual(call.arguments[0], 'document.title');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Tests: parseExpiration
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
describe('parseExpiration', () => {
|
|
266
|
+
it('should return number as-is', () => {
|
|
267
|
+
assert.strictEqual(parseExpiration(12345), 12345);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should parse minutes', () => {
|
|
271
|
+
const result = parseExpiration('5m');
|
|
272
|
+
assert.ok(result > Date.now() / 1000);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should parse hours', () => {
|
|
276
|
+
const result = parseExpiration('2h');
|
|
277
|
+
assert.ok(result > Date.now() / 1000);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should parse days', () => {
|
|
281
|
+
const result = parseExpiration('7d');
|
|
282
|
+
assert.ok(result > Date.now() / 1000);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should parse weeks', () => {
|
|
286
|
+
const result = parseExpiration('2w');
|
|
287
|
+
assert.ok(result > Date.now() / 1000);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('should parse years', () => {
|
|
291
|
+
const result = parseExpiration('1y');
|
|
292
|
+
assert.ok(result > Date.now() / 1000);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should parse number strings', () => {
|
|
296
|
+
const result = parseExpiration('9999');
|
|
297
|
+
assert.strictEqual(result, 9999);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should return undefined for invalid formats', () => {
|
|
301
|
+
assert.strictEqual(parseExpiration('invalid'), undefined);
|
|
302
|
+
// '5x' parses as number 5 (parseInt extracts leading digits)
|
|
303
|
+
assert.strictEqual(parseExpiration('5x'), 5);
|
|
304
|
+
assert.strictEqual(parseExpiration(true), undefined);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
// Tests: executeCookies
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
describe('executeCookies', () => {
|
|
313
|
+
afterEach(() => { mock.reset(); });
|
|
314
|
+
|
|
315
|
+
it('should throw if cookieManager not available', async () => {
|
|
316
|
+
const pc = createMockPageController();
|
|
317
|
+
await assert.rejects(
|
|
318
|
+
executeCookies(null, pc, { get: true }),
|
|
319
|
+
{ message: 'Cookie manager not available' }
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should get cookies for current page', async () => {
|
|
324
|
+
const manager = createMockCookieManager();
|
|
325
|
+
const pc = createMockPageController();
|
|
326
|
+
|
|
327
|
+
const result = await executeCookies(manager, pc, { get: true });
|
|
328
|
+
|
|
329
|
+
assert.strictEqual(result.action, 'get');
|
|
330
|
+
assert.ok(Array.isArray(result.cookies));
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should get cookies with name filter', async () => {
|
|
334
|
+
const manager = createMockCookieManager({
|
|
335
|
+
cookies: [
|
|
336
|
+
{ name: 'session', value: 'abc' },
|
|
337
|
+
{ name: 'tracking', value: 'xyz' }
|
|
338
|
+
]
|
|
339
|
+
});
|
|
340
|
+
const pc = createMockPageController();
|
|
341
|
+
|
|
342
|
+
const result = await executeCookies(manager, pc, { get: true, name: 'session' });
|
|
343
|
+
|
|
344
|
+
assert.strictEqual(result.cookies.length, 1);
|
|
345
|
+
assert.strictEqual(result.cookies[0].name, 'session');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should set cookies', async () => {
|
|
349
|
+
const manager = createMockCookieManager();
|
|
350
|
+
const pc = createMockPageController();
|
|
351
|
+
|
|
352
|
+
const result = await executeCookies(manager, pc, {
|
|
353
|
+
set: [{ name: 'test', value: '123', domain: 'example.com' }]
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
assert.strictEqual(result.action, 'set');
|
|
357
|
+
assert.strictEqual(result.count, 1);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should parse human-readable expiration in cookies', async () => {
|
|
361
|
+
const manager = createMockCookieManager();
|
|
362
|
+
const pc = createMockPageController();
|
|
363
|
+
|
|
364
|
+
await executeCookies(manager, pc, {
|
|
365
|
+
set: [{ name: 'test', value: '123', expires: '7d' }]
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const call = manager.setCookies.mock.calls[0];
|
|
369
|
+
const cookie = call.arguments[0][0];
|
|
370
|
+
assert.ok(typeof cookie.expires === 'number');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should clear cookies', async () => {
|
|
374
|
+
const manager = createMockCookieManager({ clearCount: 10 });
|
|
375
|
+
const pc = createMockPageController();
|
|
376
|
+
|
|
377
|
+
const result = await executeCookies(manager, pc, { clear: [] });
|
|
378
|
+
|
|
379
|
+
assert.strictEqual(result.action, 'clear');
|
|
380
|
+
assert.strictEqual(result.count, 10);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should delete cookies by name', async () => {
|
|
384
|
+
const manager = createMockCookieManager();
|
|
385
|
+
const pc = createMockPageController();
|
|
386
|
+
|
|
387
|
+
const result = await executeCookies(manager, pc, {
|
|
388
|
+
delete: ['session', 'tracking']
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
assert.strictEqual(result.action, 'delete');
|
|
392
|
+
assert.strictEqual(result.count, 2);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should throw for invalid action', async () => {
|
|
396
|
+
const manager = createMockCookieManager();
|
|
397
|
+
const pc = createMockPageController();
|
|
398
|
+
|
|
399
|
+
await assert.rejects(
|
|
400
|
+
executeCookies(manager, pc, {}),
|
|
401
|
+
{ message: /requires action/i }
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// Tests: executeListTabs
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
describe('executeListTabs', () => {
|
|
411
|
+
afterEach(() => { mock.reset(); });
|
|
412
|
+
|
|
413
|
+
it('should throw if browser not available', async () => {
|
|
414
|
+
// executeListTabs checks browser synchronously
|
|
415
|
+
try {
|
|
416
|
+
await executeListTabs(null);
|
|
417
|
+
assert.fail('Should have thrown');
|
|
418
|
+
} catch (e) {
|
|
419
|
+
assert.ok(e.message.includes('Browser not available'));
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should list all tabs', async () => {
|
|
424
|
+
const browser = createMockBrowser({
|
|
425
|
+
pages: [
|
|
426
|
+
{ targetId: 't1', url: 'https://a.com', title: 'A' },
|
|
427
|
+
{ targetId: 't2', url: 'https://b.com', title: 'B' }
|
|
428
|
+
]
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const result = await executeListTabs(browser);
|
|
432
|
+
|
|
433
|
+
assert.strictEqual(result.count, 2);
|
|
434
|
+
assert.strictEqual(result.tabs.length, 2);
|
|
435
|
+
assert.strictEqual(result.tabs[0].targetId, 't1');
|
|
436
|
+
assert.strictEqual(result.tabs[1].targetId, 't2');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('should return empty tabs list', async () => {
|
|
440
|
+
const browser = createMockBrowser({ pages: [] });
|
|
441
|
+
const result = await executeListTabs(browser);
|
|
442
|
+
|
|
443
|
+
assert.strictEqual(result.count, 0);
|
|
444
|
+
assert.strictEqual(result.tabs.length, 0);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// Tests: executeCloseTab
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
|
|
452
|
+
describe('executeCloseTab', () => {
|
|
453
|
+
afterEach(() => { mock.reset(); });
|
|
454
|
+
|
|
455
|
+
it('should throw if browser not available', async () => {
|
|
456
|
+
// executeCloseTab checks browser synchronously
|
|
457
|
+
try {
|
|
458
|
+
await executeCloseTab(null, 't1');
|
|
459
|
+
assert.fail('Should have thrown');
|
|
460
|
+
} catch (e) {
|
|
461
|
+
assert.ok(e.message.includes('Browser not available'));
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should close tab by targetId', async () => {
|
|
466
|
+
const browser = createMockBrowser();
|
|
467
|
+
const result = await executeCloseTab(browser, 't1');
|
|
468
|
+
|
|
469
|
+
assert.strictEqual(result.closed, 't1');
|
|
470
|
+
assert.strictEqual(browser.closePage.mock.calls.length, 1);
|
|
471
|
+
assert.strictEqual(browser.closePage.mock.calls[0].arguments[0], 't1');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should propagate close errors', async () => {
|
|
475
|
+
const browser = createMockBrowser({ closeError: 'Tab not found' });
|
|
476
|
+
await assert.rejects(
|
|
477
|
+
executeCloseTab(browser, 't999'),
|
|
478
|
+
{ message: 'Tab not found' }
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
// Tests: formatStackTrace
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
describe('formatStackTrace', () => {
|
|
488
|
+
it('should return null for empty stack', () => {
|
|
489
|
+
assert.strictEqual(formatStackTrace(null), null);
|
|
490
|
+
assert.strictEqual(formatStackTrace({}), null);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('should format stack frames', () => {
|
|
494
|
+
const stack = {
|
|
495
|
+
callFrames: [
|
|
496
|
+
{ functionName: 'foo', url: 'https://example.com/app.js', lineNumber: 10, columnNumber: 5 },
|
|
497
|
+
{ functionName: '', url: 'https://example.com/app.js', lineNumber: 20, columnNumber: 10 }
|
|
498
|
+
]
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const result = formatStackTrace(stack);
|
|
502
|
+
|
|
503
|
+
assert.strictEqual(result.length, 2);
|
|
504
|
+
assert.strictEqual(result[0].functionName, 'foo');
|
|
505
|
+
assert.strictEqual(result[0].lineNumber, 10);
|
|
506
|
+
assert.strictEqual(result[1].functionName, '(anonymous)');
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
// Tests: executeConsole
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
describe('executeConsole', () => {
|
|
515
|
+
afterEach(() => { mock.reset(); });
|
|
516
|
+
|
|
517
|
+
it('should return error if consoleCapture not available', async () => {
|
|
518
|
+
const result = await executeConsole(null, {});
|
|
519
|
+
|
|
520
|
+
assert.ok(result.error);
|
|
521
|
+
assert.strictEqual(result.messages.length, 0);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('should get console messages', async () => {
|
|
525
|
+
const capture = createMockConsoleCapture({
|
|
526
|
+
messages: [
|
|
527
|
+
{ level: 'log', text: 'Hello', timestamp: 1000 },
|
|
528
|
+
{ level: 'error', text: 'Error!', timestamp: 2000 }
|
|
529
|
+
]
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const result = await executeConsole(capture, {});
|
|
533
|
+
|
|
534
|
+
assert.strictEqual(capture.getMessages.mock.calls.length, 1);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('should filter by level', async () => {
|
|
538
|
+
const capture = createMockConsoleCapture({
|
|
539
|
+
messagesByLevel: [
|
|
540
|
+
{ level: 'error', text: 'Error 1' }
|
|
541
|
+
]
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
await executeConsole(capture, { level: 'error' });
|
|
545
|
+
|
|
546
|
+
assert.strictEqual(capture.getMessagesByLevel.mock.calls.length, 1);
|
|
547
|
+
assert.strictEqual(capture.getMessagesByLevel.mock.calls[0].arguments[0], 'error');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('should filter by type', async () => {
|
|
551
|
+
const capture = createMockConsoleCapture({
|
|
552
|
+
messagesByType: [
|
|
553
|
+
{ type: 'exception', text: 'Exception!' }
|
|
554
|
+
]
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
await executeConsole(capture, { type: 'exception' });
|
|
558
|
+
|
|
559
|
+
assert.strictEqual(capture.getMessagesByType.mock.calls.length, 1);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should apply limit', async () => {
|
|
563
|
+
const capture = createMockConsoleCapture({
|
|
564
|
+
messages: Array(100).fill().map((_, i) => ({ level: 'log', text: `Msg ${i}` }))
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
await executeConsole(capture, { limit: 10 });
|
|
568
|
+
|
|
569
|
+
// Limit is applied by slicing messages
|
|
570
|
+
assert.ok(capture.getMessages.mock.calls.length > 0);
|
|
571
|
+
});
|
|
572
|
+
});
|