cdp-skill 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +543 -0
- package/install.js +92 -0
- package/package.json +47 -0
- package/src/aria.js +1302 -0
- package/src/capture.js +1359 -0
- package/src/cdp.js +905 -0
- package/src/cli.js +244 -0
- package/src/dom.js +3525 -0
- package/src/index.js +155 -0
- package/src/page.js +1720 -0
- package/src/runner.js +2111 -0
- package/src/tests/BrowserClient.test.js +588 -0
- package/src/tests/CDPConnection.test.js +598 -0
- package/src/tests/ChromeDiscovery.test.js +181 -0
- package/src/tests/ConsoleCapture.test.js +302 -0
- package/src/tests/ElementHandle.test.js +586 -0
- package/src/tests/ElementLocator.test.js +586 -0
- package/src/tests/ErrorAggregator.test.js +327 -0
- package/src/tests/InputEmulator.test.js +641 -0
- package/src/tests/NetworkErrorCapture.test.js +458 -0
- package/src/tests/PageController.test.js +822 -0
- package/src/tests/ScreenshotCapture.test.js +356 -0
- package/src/tests/SessionRegistry.test.js +257 -0
- package/src/tests/TargetManager.test.js +274 -0
- package/src/tests/TestRunner.test.js +1529 -0
- package/src/tests/WaitStrategy.test.js +406 -0
- package/src/tests/integration.test.js +431 -0
- package/src/utils.js +1034 -0
- package/uninstall.js +44 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { createElementLocator } from '../dom.js';
|
|
4
|
+
import { ErrorTypes } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
describe('ElementLocator', () => {
|
|
7
|
+
let mockCdp;
|
|
8
|
+
let locator;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
mockCdp = {
|
|
12
|
+
send: mock.fn(async () => ({}))
|
|
13
|
+
};
|
|
14
|
+
locator = createElementLocator(mockCdp);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('createElementLocator', () => {
|
|
18
|
+
it('should throw if session is not provided', () => {
|
|
19
|
+
assert.throws(() => createElementLocator(null), {
|
|
20
|
+
message: 'CDP session is required'
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should use default timeout of 30000ms', () => {
|
|
25
|
+
assert.strictEqual(locator.getDefaultTimeout(), 30000);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should accept custom timeout option', () => {
|
|
29
|
+
const customLocator = createElementLocator(mockCdp, { timeout: 5000 });
|
|
30
|
+
assert.strictEqual(customLocator.getDefaultTimeout(), 5000);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('querySelector', () => {
|
|
35
|
+
it('should call Runtime.evaluate with correct expression', async () => {
|
|
36
|
+
mockCdp.send = mock.fn(async () => ({
|
|
37
|
+
result: { objectId: 'obj-123' }
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
await locator.querySelector('.my-class');
|
|
41
|
+
|
|
42
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 1);
|
|
43
|
+
const [method, params] = mockCdp.send.mock.calls[0].arguments;
|
|
44
|
+
assert.strictEqual(method, 'Runtime.evaluate');
|
|
45
|
+
assert.strictEqual(params.expression, 'document.querySelector(".my-class")');
|
|
46
|
+
assert.strictEqual(params.returnByValue, false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return ElementHandle when element found', async () => {
|
|
50
|
+
mockCdp.send = mock.fn(async () => ({
|
|
51
|
+
result: { objectId: 'obj-456' }
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const handle = await locator.querySelector('#my-id');
|
|
55
|
+
|
|
56
|
+
assert.ok(handle);
|
|
57
|
+
assert.strictEqual(handle.objectId, 'obj-456');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return null when element not found (subtype null)', async () => {
|
|
61
|
+
mockCdp.send = mock.fn(async () => ({
|
|
62
|
+
result: { subtype: 'null' }
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const handle = await locator.querySelector('.not-found');
|
|
66
|
+
|
|
67
|
+
assert.strictEqual(handle, null);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should return null when result type is undefined', async () => {
|
|
71
|
+
mockCdp.send = mock.fn(async () => ({
|
|
72
|
+
result: { type: 'undefined' }
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const handle = await locator.querySelector('.undefined');
|
|
76
|
+
|
|
77
|
+
assert.strictEqual(handle, null);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should throw on selector error', async () => {
|
|
81
|
+
mockCdp.send = mock.fn(async () => ({
|
|
82
|
+
exceptionDetails: { text: 'Invalid selector' }
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
await assert.rejects(() => locator.querySelector('[invalid'), {
|
|
86
|
+
message: 'Selector error: Invalid selector'
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should escape selector properly', async () => {
|
|
91
|
+
mockCdp.send = mock.fn(async () => ({
|
|
92
|
+
result: { subtype: 'null' }
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
await locator.querySelector('[data-attr="value"]');
|
|
96
|
+
|
|
97
|
+
const [, params] = mockCdp.send.mock.calls[0].arguments;
|
|
98
|
+
assert.strictEqual(params.expression, 'document.querySelector("[data-attr=\\"value\\"]")');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should pass selector to ElementHandle', async () => {
|
|
102
|
+
mockCdp.send = mock.fn(async () => ({
|
|
103
|
+
result: { objectId: 'obj-123' }
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
const handle = await locator.querySelector('#my-button');
|
|
107
|
+
|
|
108
|
+
assert.strictEqual(handle.selector, '#my-button');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('querySelectorAll', () => {
|
|
113
|
+
it('should call Runtime.evaluate with Array.from wrapper', async () => {
|
|
114
|
+
mockCdp.send = mock.fn(async (method) => {
|
|
115
|
+
if (method === 'Runtime.evaluate') {
|
|
116
|
+
return { result: { objectId: 'array-obj' } };
|
|
117
|
+
}
|
|
118
|
+
if (method === 'Runtime.getProperties') {
|
|
119
|
+
return { result: [] };
|
|
120
|
+
}
|
|
121
|
+
return {};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await locator.querySelectorAll('div');
|
|
125
|
+
|
|
126
|
+
const evalCall = mockCdp.send.mock.calls[0];
|
|
127
|
+
assert.strictEqual(evalCall.arguments[0], 'Runtime.evaluate');
|
|
128
|
+
assert.ok(evalCall.arguments[1].expression.includes('Array.from'));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should return empty array when no elements found', async () => {
|
|
132
|
+
mockCdp.send = mock.fn(async () => ({
|
|
133
|
+
result: {} // No objectId means empty result
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
const handles = await locator.querySelectorAll('.not-found');
|
|
137
|
+
|
|
138
|
+
assert.deepStrictEqual(handles, []);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should return ElementHandles for each element', async () => {
|
|
142
|
+
mockCdp.send = mock.fn(async (method) => {
|
|
143
|
+
if (method === 'Runtime.evaluate') {
|
|
144
|
+
return { result: { objectId: 'array-obj' } };
|
|
145
|
+
}
|
|
146
|
+
if (method === 'Runtime.getProperties') {
|
|
147
|
+
return {
|
|
148
|
+
result: [
|
|
149
|
+
{ name: '0', value: { objectId: 'elem-0' } },
|
|
150
|
+
{ name: '1', value: { objectId: 'elem-1' } },
|
|
151
|
+
{ name: '2', value: { objectId: 'elem-2' } },
|
|
152
|
+
{ name: 'length', value: { value: 3 } }
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {};
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const handles = await locator.querySelectorAll('p');
|
|
160
|
+
|
|
161
|
+
assert.strictEqual(handles.length, 3);
|
|
162
|
+
assert.strictEqual(handles[0].objectId, 'elem-0');
|
|
163
|
+
assert.strictEqual(handles[1].objectId, 'elem-1');
|
|
164
|
+
assert.strictEqual(handles[2].objectId, 'elem-2');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should filter out non-numeric properties', async () => {
|
|
168
|
+
mockCdp.send = mock.fn(async (method) => {
|
|
169
|
+
if (method === 'Runtime.evaluate') {
|
|
170
|
+
return { result: { objectId: 'array-obj' } };
|
|
171
|
+
}
|
|
172
|
+
if (method === 'Runtime.getProperties') {
|
|
173
|
+
return {
|
|
174
|
+
result: [
|
|
175
|
+
{ name: '0', value: { objectId: 'elem-0' } },
|
|
176
|
+
{ name: 'length', value: { value: 1 } },
|
|
177
|
+
{ name: '__proto__', value: {} }
|
|
178
|
+
]
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const handles = await locator.querySelectorAll('span');
|
|
185
|
+
|
|
186
|
+
assert.strictEqual(handles.length, 1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should release temporary array object after extraction', async () => {
|
|
190
|
+
let releaseObjectCalled = false;
|
|
191
|
+
let releasedObjectId = null;
|
|
192
|
+
mockCdp.send = mock.fn(async (method, params) => {
|
|
193
|
+
if (method === 'Runtime.evaluate') {
|
|
194
|
+
return { result: { objectId: 'array-obj-123' } };
|
|
195
|
+
}
|
|
196
|
+
if (method === 'Runtime.getProperties') {
|
|
197
|
+
return {
|
|
198
|
+
result: [
|
|
199
|
+
{ name: '0', value: { objectId: 'elem-0' } },
|
|
200
|
+
{ name: 'length', value: { value: 1 } }
|
|
201
|
+
]
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (method === 'Runtime.releaseObject') {
|
|
205
|
+
releaseObjectCalled = true;
|
|
206
|
+
releasedObjectId = params.objectId;
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
return {};
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await locator.querySelectorAll('div');
|
|
213
|
+
|
|
214
|
+
assert.strictEqual(releaseObjectCalled, true, 'Should release array object');
|
|
215
|
+
assert.strictEqual(releasedObjectId, 'array-obj-123', 'Should release the correct object');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should release array object even when getProperties fails', async () => {
|
|
219
|
+
let releaseObjectCalled = false;
|
|
220
|
+
mockCdp.send = mock.fn(async (method) => {
|
|
221
|
+
if (method === 'Runtime.evaluate') {
|
|
222
|
+
return { result: { objectId: 'array-obj-456' } };
|
|
223
|
+
}
|
|
224
|
+
if (method === 'Runtime.getProperties') {
|
|
225
|
+
throw new Error('Connection lost');
|
|
226
|
+
}
|
|
227
|
+
if (method === 'Runtime.releaseObject') {
|
|
228
|
+
releaseObjectCalled = true;
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
return {};
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await assert.rejects(() => locator.querySelectorAll('div'));
|
|
235
|
+
assert.strictEqual(releaseObjectCalled, true, 'Should release array object on error');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should throw on selector error', async () => {
|
|
239
|
+
mockCdp.send = mock.fn(async () => ({
|
|
240
|
+
exceptionDetails: { text: 'Syntax error' }
|
|
241
|
+
}));
|
|
242
|
+
|
|
243
|
+
await assert.rejects(() => locator.querySelectorAll(':::invalid'), {
|
|
244
|
+
message: 'Selector error: Syntax error'
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('waitForSelector', () => {
|
|
250
|
+
it('should return element immediately if found', async () => {
|
|
251
|
+
mockCdp.send = mock.fn(async () => ({
|
|
252
|
+
result: { objectId: 'found-obj' }
|
|
253
|
+
}));
|
|
254
|
+
|
|
255
|
+
const handle = await locator.waitForSelector('#exists');
|
|
256
|
+
|
|
257
|
+
assert.ok(handle);
|
|
258
|
+
assert.strictEqual(handle.objectId, 'found-obj');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should poll until element appears', async () => {
|
|
262
|
+
let callCount = 0;
|
|
263
|
+
mockCdp.send = mock.fn(async () => {
|
|
264
|
+
callCount++;
|
|
265
|
+
if (callCount < 3) {
|
|
266
|
+
return { result: { subtype: 'null' } };
|
|
267
|
+
}
|
|
268
|
+
return { result: { objectId: 'appeared-obj' } };
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const handle = await locator.waitForSelector('#appearing', { timeout: 1000 });
|
|
272
|
+
|
|
273
|
+
assert.ok(handle);
|
|
274
|
+
assert.ok(callCount >= 3);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should throw ElementNotFoundError on timeout', async () => {
|
|
278
|
+
mockCdp.send = mock.fn(async () => ({
|
|
279
|
+
result: { subtype: 'null' }
|
|
280
|
+
}));
|
|
281
|
+
|
|
282
|
+
await assert.rejects(
|
|
283
|
+
() => locator.waitForSelector('#never', { timeout: 200 }),
|
|
284
|
+
(err) => {
|
|
285
|
+
assert.strictEqual(err.name, ErrorTypes.ELEMENT_NOT_FOUND);
|
|
286
|
+
assert.strictEqual(err.selector, '#never');
|
|
287
|
+
assert.strictEqual(err.timeout, 200);
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should wait for visibility when visible option is true', async () => {
|
|
294
|
+
let callCount = 0;
|
|
295
|
+
mockCdp.send = mock.fn(async (method) => {
|
|
296
|
+
if (method === 'Runtime.evaluate') {
|
|
297
|
+
return { result: { objectId: 'obj-' + callCount++ } };
|
|
298
|
+
}
|
|
299
|
+
if (method === 'Runtime.callFunctionOn') {
|
|
300
|
+
// Return not visible for first 2 calls, then visible
|
|
301
|
+
return { result: { value: callCount > 3 } };
|
|
302
|
+
}
|
|
303
|
+
if (method === 'Runtime.releaseObject') {
|
|
304
|
+
return {};
|
|
305
|
+
}
|
|
306
|
+
return {};
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const handle = await locator.waitForSelector('.visible', { visible: true, timeout: 2000 });
|
|
310
|
+
|
|
311
|
+
assert.ok(handle);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should dispose non-visible elements during polling', async () => {
|
|
315
|
+
let releaseCount = 0;
|
|
316
|
+
mockCdp.send = mock.fn(async (method) => {
|
|
317
|
+
if (method === 'Runtime.evaluate') {
|
|
318
|
+
return { result: { objectId: 'obj-123' } };
|
|
319
|
+
}
|
|
320
|
+
if (method === 'Runtime.callFunctionOn') {
|
|
321
|
+
// Always invisible to trigger dispose
|
|
322
|
+
return { result: { value: false } };
|
|
323
|
+
}
|
|
324
|
+
if (method === 'Runtime.releaseObject') {
|
|
325
|
+
releaseCount++;
|
|
326
|
+
return {};
|
|
327
|
+
}
|
|
328
|
+
return {};
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
await assert.rejects(
|
|
332
|
+
() => locator.waitForSelector('.invisible', { visible: true, timeout: 300 })
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
assert.ok(releaseCount > 0, 'Should have released at least one object');
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('waitForText', () => {
|
|
340
|
+
it('should return true when text found', async () => {
|
|
341
|
+
mockCdp.send = mock.fn(async () => ({
|
|
342
|
+
result: { value: true }
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
const result = await locator.waitForText('Hello');
|
|
346
|
+
|
|
347
|
+
assert.strictEqual(result, true);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should use case-insensitive search by default', async () => {
|
|
351
|
+
mockCdp.send = mock.fn(async () => ({
|
|
352
|
+
result: { value: true }
|
|
353
|
+
}));
|
|
354
|
+
|
|
355
|
+
await locator.waitForText('HELLO');
|
|
356
|
+
|
|
357
|
+
const [, params] = mockCdp.send.mock.calls[0].arguments;
|
|
358
|
+
assert.ok(params.expression.includes('toLowerCase()'));
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should use exact match when exact option is true', async () => {
|
|
362
|
+
mockCdp.send = mock.fn(async () => ({
|
|
363
|
+
result: { value: true }
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
await locator.waitForText('Exact Text', { exact: true });
|
|
367
|
+
|
|
368
|
+
const [, params] = mockCdp.send.mock.calls[0].arguments;
|
|
369
|
+
assert.ok(!params.expression.includes('toLowerCase'));
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should throw TimeoutError on timeout', async () => {
|
|
373
|
+
mockCdp.send = mock.fn(async () => ({
|
|
374
|
+
result: { value: false }
|
|
375
|
+
}));
|
|
376
|
+
|
|
377
|
+
await assert.rejects(
|
|
378
|
+
() => locator.waitForText('missing text', { timeout: 200 }),
|
|
379
|
+
(err) => {
|
|
380
|
+
assert.strictEqual(err.name, ErrorTypes.TIMEOUT);
|
|
381
|
+
assert.ok(err.message.includes('missing text'));
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should poll until text appears', async () => {
|
|
388
|
+
let callCount = 0;
|
|
389
|
+
mockCdp.send = mock.fn(async () => {
|
|
390
|
+
callCount++;
|
|
391
|
+
return { result: { value: callCount >= 3 } };
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await locator.waitForText('appearing text', { timeout: 1000 });
|
|
395
|
+
|
|
396
|
+
assert.ok(callCount >= 3);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe('setDefaultTimeout', () => {
|
|
401
|
+
it('should update the default timeout', () => {
|
|
402
|
+
locator.setDefaultTimeout(10000);
|
|
403
|
+
assert.strictEqual(locator.getDefaultTimeout(), 10000);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should clamp very long timeouts to max', () => {
|
|
407
|
+
locator.setDefaultTimeout(999999999);
|
|
408
|
+
assert.strictEqual(locator.getDefaultTimeout(), 300000);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should handle negative timeout', () => {
|
|
412
|
+
locator.setDefaultTimeout(-100);
|
|
413
|
+
assert.strictEqual(locator.getDefaultTimeout(), 0);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('edge cases', () => {
|
|
418
|
+
describe('querySelector edge cases', () => {
|
|
419
|
+
it('should throw on empty selector', async () => {
|
|
420
|
+
await assert.rejects(
|
|
421
|
+
() => locator.querySelector(''),
|
|
422
|
+
{ message: 'Selector must be a non-empty string' }
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should throw on null selector', async () => {
|
|
427
|
+
await assert.rejects(
|
|
428
|
+
() => locator.querySelector(null),
|
|
429
|
+
{ message: 'Selector must be a non-empty string' }
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('should throw error when connection drops', async () => {
|
|
434
|
+
mockCdp.send = mock.fn(async () => {
|
|
435
|
+
throw new Error('WebSocket closed');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
await assert.rejects(
|
|
439
|
+
() => locator.querySelector('.test'),
|
|
440
|
+
(err) => {
|
|
441
|
+
assert.ok(err.message.includes('WebSocket closed'));
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
describe('querySelectorAll edge cases', () => {
|
|
449
|
+
it('should throw on empty selector', async () => {
|
|
450
|
+
await assert.rejects(
|
|
451
|
+
() => locator.querySelectorAll(''),
|
|
452
|
+
{ message: 'Selector must be a non-empty string' }
|
|
453
|
+
);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should throw error when getProperties fails', async () => {
|
|
457
|
+
mockCdp.send = mock.fn(async (method) => {
|
|
458
|
+
if (method === 'Runtime.evaluate') {
|
|
459
|
+
return { result: { objectId: 'array-obj' } };
|
|
460
|
+
}
|
|
461
|
+
if (method === 'Runtime.getProperties') {
|
|
462
|
+
throw new Error('Connection lost');
|
|
463
|
+
}
|
|
464
|
+
return {};
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
await assert.rejects(
|
|
468
|
+
() => locator.querySelectorAll('.test'),
|
|
469
|
+
(err) => {
|
|
470
|
+
assert.ok(err.message.includes('Connection lost'));
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
describe('waitForSelector edge cases', () => {
|
|
478
|
+
it('should throw on empty selector', async () => {
|
|
479
|
+
await assert.rejects(
|
|
480
|
+
() => locator.waitForSelector(''),
|
|
481
|
+
{ message: 'Selector must be a non-empty string' }
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('should handle element removed during visibility check', async () => {
|
|
486
|
+
let callCount = 0;
|
|
487
|
+
mockCdp.send = mock.fn(async (method) => {
|
|
488
|
+
if (method === 'Runtime.evaluate') {
|
|
489
|
+
return { result: { objectId: 'obj-' + callCount++ } };
|
|
490
|
+
}
|
|
491
|
+
if (method === 'Runtime.callFunctionOn') {
|
|
492
|
+
// Simulate element being removed
|
|
493
|
+
throw new Error('Object reference not found');
|
|
494
|
+
}
|
|
495
|
+
if (method === 'Runtime.releaseObject') {
|
|
496
|
+
return {};
|
|
497
|
+
}
|
|
498
|
+
return {};
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
await assert.rejects(
|
|
502
|
+
() => locator.waitForSelector('.removed', { visible: true, timeout: 300 }),
|
|
503
|
+
(err) => err.name === ErrorTypes.ELEMENT_NOT_FOUND
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should handle very long timeout by clamping', async () => {
|
|
508
|
+
mockCdp.send = mock.fn(async () => ({
|
|
509
|
+
result: { objectId: 'found-obj' }
|
|
510
|
+
}));
|
|
511
|
+
|
|
512
|
+
const handle = await locator.waitForSelector('#exists', { timeout: 999999999 });
|
|
513
|
+
assert.ok(handle);
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
describe('waitForText edge cases', () => {
|
|
518
|
+
it('should throw on null text', async () => {
|
|
519
|
+
await assert.rejects(
|
|
520
|
+
() => locator.waitForText(null),
|
|
521
|
+
{ message: 'Text must be provided' }
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should throw on undefined text', async () => {
|
|
526
|
+
await assert.rejects(
|
|
527
|
+
() => locator.waitForText(undefined),
|
|
528
|
+
{ message: 'Text must be provided' }
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('should handle number text by converting to string', async () => {
|
|
533
|
+
mockCdp.send = mock.fn(async () => ({
|
|
534
|
+
result: { value: true }
|
|
535
|
+
}));
|
|
536
|
+
|
|
537
|
+
const result = await locator.waitForText(123);
|
|
538
|
+
assert.strictEqual(result, true);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('should throw error when connection drops', async () => {
|
|
542
|
+
mockCdp.send = mock.fn(async () => {
|
|
543
|
+
throw new Error('Connection reset');
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await assert.rejects(
|
|
547
|
+
() => locator.waitForText('test'),
|
|
548
|
+
(err) => {
|
|
549
|
+
assert.ok(err.message.includes('Connection reset'));
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
describe('getBoundingBox edge cases', () => {
|
|
557
|
+
it('should return null for null nodeId', async () => {
|
|
558
|
+
const result = await locator.getBoundingBox(null);
|
|
559
|
+
assert.strictEqual(result, null);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should return null for undefined nodeId', async () => {
|
|
563
|
+
const result = await locator.getBoundingBox(undefined);
|
|
564
|
+
assert.strictEqual(result, null);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('should return null when element is removed', async () => {
|
|
568
|
+
mockCdp.send = mock.fn(async () => {
|
|
569
|
+
throw new Error('Object not found');
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
const result = await locator.getBoundingBox('removed-obj');
|
|
573
|
+
assert.strictEqual(result, null);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('should return null when result has exception', async () => {
|
|
577
|
+
mockCdp.send = mock.fn(async () => ({
|
|
578
|
+
exceptionDetails: { text: 'Element detached' }
|
|
579
|
+
}));
|
|
580
|
+
|
|
581
|
+
const result = await locator.getBoundingBox('detached-obj');
|
|
582
|
+
assert.strictEqual(result, null);
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
});
|