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,508 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
waitForCondition,
|
|
5
|
+
waitForFunction,
|
|
6
|
+
waitForNetworkIdle,
|
|
7
|
+
waitForDocumentReady,
|
|
8
|
+
waitForSelector,
|
|
9
|
+
waitForText
|
|
10
|
+
} from '../page/wait-utilities.js';
|
|
11
|
+
|
|
12
|
+
describe('WaitUtilities', () => {
|
|
13
|
+
let mockSession;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockSession = {
|
|
17
|
+
send: mock.fn(async () => ({})),
|
|
18
|
+
on: mock.fn(),
|
|
19
|
+
off: mock.fn()
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
mock.reset();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('waitForCondition', () => {
|
|
28
|
+
it('should resolve immediately when condition is true', async () => {
|
|
29
|
+
const checkFn = mock.fn(async () => true);
|
|
30
|
+
|
|
31
|
+
await waitForCondition(checkFn);
|
|
32
|
+
|
|
33
|
+
assert.strictEqual(checkFn.mock.calls.length, 1);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should poll until condition becomes true', async () => {
|
|
37
|
+
let callCount = 0;
|
|
38
|
+
const checkFn = mock.fn(async () => {
|
|
39
|
+
callCount++;
|
|
40
|
+
return callCount >= 3;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await waitForCondition(checkFn, { timeout: 1000, pollInterval: 10 });
|
|
44
|
+
|
|
45
|
+
assert.ok(checkFn.mock.calls.length >= 3);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should throw timeout error when condition never becomes true', async () => {
|
|
49
|
+
const checkFn = mock.fn(async () => false);
|
|
50
|
+
|
|
51
|
+
await assert.rejects(
|
|
52
|
+
() => waitForCondition(checkFn, { timeout: 100, pollInterval: 10 }),
|
|
53
|
+
(err) => {
|
|
54
|
+
assert.ok(err.message.includes('Condition not met'));
|
|
55
|
+
assert.ok(err.message.includes('100ms'));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should use custom message in timeout error', async () => {
|
|
62
|
+
const checkFn = mock.fn(async () => false);
|
|
63
|
+
|
|
64
|
+
await assert.rejects(
|
|
65
|
+
() => waitForCondition(checkFn, {
|
|
66
|
+
timeout: 100,
|
|
67
|
+
pollInterval: 10,
|
|
68
|
+
message: 'Custom wait failed'
|
|
69
|
+
}),
|
|
70
|
+
(err) => {
|
|
71
|
+
assert.ok(err.message.includes('Custom wait failed'));
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should use default timeout of 30000ms', async () => {
|
|
78
|
+
const startTime = Date.now();
|
|
79
|
+
let checkCalled = false;
|
|
80
|
+
|
|
81
|
+
const checkFn = mock.fn(async () => {
|
|
82
|
+
if (!checkCalled) {
|
|
83
|
+
checkCalled = true;
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
// After 50ms, return true to avoid waiting full 30s
|
|
87
|
+
if (Date.now() - startTime > 50) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await waitForCondition(checkFn, { pollInterval: 10 });
|
|
94
|
+
assert.ok(checkCalled);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('waitForFunction', () => {
|
|
99
|
+
it('should resolve when expression returns truthy value', async () => {
|
|
100
|
+
mockSession.send = mock.fn(async () => ({
|
|
101
|
+
result: { value: 'some truthy value' }
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
const result = await waitForFunction(mockSession, 'document.title');
|
|
105
|
+
|
|
106
|
+
assert.strictEqual(result, 'some truthy value');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should poll until expression returns truthy', async () => {
|
|
110
|
+
let callCount = 0;
|
|
111
|
+
mockSession.send = mock.fn(async () => {
|
|
112
|
+
callCount++;
|
|
113
|
+
return {
|
|
114
|
+
result: { value: callCount >= 3 ? 'found' : null }
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const result = await waitForFunction(mockSession, 'window.myVar', {
|
|
119
|
+
timeout: 1000,
|
|
120
|
+
pollInterval: 10
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
assert.strictEqual(result, 'found');
|
|
124
|
+
assert.ok(callCount >= 3);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should throw timeout error when expression never returns truthy', async () => {
|
|
128
|
+
mockSession.send = mock.fn(async () => ({
|
|
129
|
+
result: { value: null }
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
await assert.rejects(
|
|
133
|
+
() => waitForFunction(mockSession, 'false', { timeout: 100, pollInterval: 10 }),
|
|
134
|
+
(err) => {
|
|
135
|
+
assert.ok(err.message.includes('did not return truthy'));
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should continue polling on expression exception', async () => {
|
|
142
|
+
let callCount = 0;
|
|
143
|
+
mockSession.send = mock.fn(async () => {
|
|
144
|
+
callCount++;
|
|
145
|
+
if (callCount < 3) {
|
|
146
|
+
return { exceptionDetails: { text: 'ReferenceError' } };
|
|
147
|
+
}
|
|
148
|
+
return { result: { value: 'resolved' } };
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const result = await waitForFunction(mockSession, 'window.myVar', {
|
|
152
|
+
timeout: 1000,
|
|
153
|
+
pollInterval: 10
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
assert.strictEqual(result, 'resolved');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should throw context destroyed error on navigation', async () => {
|
|
160
|
+
mockSession.send = mock.fn(async () => {
|
|
161
|
+
throw new Error('Execution context was destroyed');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await assert.rejects(
|
|
165
|
+
() => waitForFunction(mockSession, 'document.title', { timeout: 100 }),
|
|
166
|
+
(err) => {
|
|
167
|
+
assert.ok(err.message.includes('navigated'));
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('waitForNetworkIdle', () => {
|
|
175
|
+
it('should resolve immediately when no pending requests', async () => {
|
|
176
|
+
const eventHandlers = {};
|
|
177
|
+
mockSession.on = mock.fn((event, handler) => {
|
|
178
|
+
eventHandlers[event] = handler;
|
|
179
|
+
});
|
|
180
|
+
mockSession.off = mock.fn();
|
|
181
|
+
|
|
182
|
+
const promise = waitForNetworkIdle(mockSession, { idleTime: 50, timeout: 1000 });
|
|
183
|
+
const result = await promise;
|
|
184
|
+
|
|
185
|
+
assert.strictEqual(result, undefined);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should wait for pending requests to finish', async () => {
|
|
189
|
+
const eventHandlers = {};
|
|
190
|
+
mockSession.on = mock.fn((event, handler) => {
|
|
191
|
+
eventHandlers[event] = handler;
|
|
192
|
+
});
|
|
193
|
+
mockSession.off = mock.fn();
|
|
194
|
+
|
|
195
|
+
const promise = waitForNetworkIdle(mockSession, { idleTime: 50, timeout: 1000 });
|
|
196
|
+
|
|
197
|
+
// Simulate request start
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
eventHandlers['Network.requestWillBeSent']({ requestId: 'req-1' });
|
|
200
|
+
}, 10);
|
|
201
|
+
|
|
202
|
+
// Simulate request finish
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
eventHandlers['Network.loadingFinished']({ requestId: 'req-1' });
|
|
205
|
+
}, 30);
|
|
206
|
+
|
|
207
|
+
await promise;
|
|
208
|
+
|
|
209
|
+
assert.strictEqual(mockSession.on.mock.calls.length, 3);
|
|
210
|
+
assert.strictEqual(mockSession.off.mock.calls.length, 3);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should handle request failures', async () => {
|
|
214
|
+
const eventHandlers = {};
|
|
215
|
+
mockSession.on = mock.fn((event, handler) => {
|
|
216
|
+
eventHandlers[event] = handler;
|
|
217
|
+
});
|
|
218
|
+
mockSession.off = mock.fn();
|
|
219
|
+
|
|
220
|
+
const promise = waitForNetworkIdle(mockSession, { idleTime: 50, timeout: 1000 });
|
|
221
|
+
|
|
222
|
+
// Simulate request start
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
eventHandlers['Network.requestWillBeSent']({ requestId: 'req-1' });
|
|
225
|
+
}, 10);
|
|
226
|
+
|
|
227
|
+
// Simulate request failure
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
eventHandlers['Network.loadingFailed']({ requestId: 'req-1' });
|
|
230
|
+
}, 30);
|
|
231
|
+
|
|
232
|
+
await promise;
|
|
233
|
+
|
|
234
|
+
assert.ok(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should throw timeout error when network stays busy', async () => {
|
|
238
|
+
const eventHandlers = {};
|
|
239
|
+
mockSession.on = mock.fn((event, handler) => {
|
|
240
|
+
eventHandlers[event] = handler;
|
|
241
|
+
});
|
|
242
|
+
mockSession.off = mock.fn();
|
|
243
|
+
|
|
244
|
+
const promise = waitForNetworkIdle(mockSession, { idleTime: 50, timeout: 100 });
|
|
245
|
+
|
|
246
|
+
// Simulate continuous requests
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
eventHandlers['Network.requestWillBeSent']({ requestId: 'req-1' });
|
|
249
|
+
}, 10);
|
|
250
|
+
|
|
251
|
+
await assert.rejects(
|
|
252
|
+
() => promise,
|
|
253
|
+
(err) => {
|
|
254
|
+
assert.ok(err.message.includes('Network did not become idle'));
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should clean up event listeners on completion', async () => {
|
|
261
|
+
mockSession.on = mock.fn();
|
|
262
|
+
mockSession.off = mock.fn();
|
|
263
|
+
|
|
264
|
+
await waitForNetworkIdle(mockSession, { idleTime: 10, timeout: 1000 });
|
|
265
|
+
|
|
266
|
+
assert.strictEqual(mockSession.on.mock.calls.length, 3);
|
|
267
|
+
assert.strictEqual(mockSession.off.mock.calls.length, 3);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('waitForDocumentReady', () => {
|
|
272
|
+
it('should resolve immediately when document is complete', async () => {
|
|
273
|
+
mockSession.send = mock.fn(async () => ({
|
|
274
|
+
result: { value: 'complete' }
|
|
275
|
+
}));
|
|
276
|
+
|
|
277
|
+
const result = await waitForDocumentReady(mockSession);
|
|
278
|
+
|
|
279
|
+
assert.strictEqual(result, 'complete');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should wait for document to reach target state', async () => {
|
|
283
|
+
let callCount = 0;
|
|
284
|
+
mockSession.send = mock.fn(async () => {
|
|
285
|
+
callCount++;
|
|
286
|
+
const states = ['loading', 'loading', 'interactive', 'complete'];
|
|
287
|
+
return { result: { value: states[Math.min(callCount - 1, 3)] } };
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const result = await waitForDocumentReady(mockSession, 'complete', {
|
|
291
|
+
timeout: 1000,
|
|
292
|
+
pollInterval: 10
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
assert.strictEqual(result, 'complete');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should accept interactive as valid target state', async () => {
|
|
299
|
+
let callCount = 0;
|
|
300
|
+
mockSession.send = mock.fn(async () => {
|
|
301
|
+
callCount++;
|
|
302
|
+
const states = ['loading', 'interactive'];
|
|
303
|
+
return { result: { value: states[Math.min(callCount - 1, 1)] } };
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const result = await waitForDocumentReady(mockSession, 'interactive', {
|
|
307
|
+
timeout: 1000,
|
|
308
|
+
pollInterval: 10
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
assert.strictEqual(result, 'interactive');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should throw for invalid target state', async () => {
|
|
315
|
+
await assert.rejects(
|
|
316
|
+
() => waitForDocumentReady(mockSession, 'invalid'),
|
|
317
|
+
{ message: 'Invalid target state: invalid' }
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should throw timeout error when state not reached', async () => {
|
|
322
|
+
mockSession.send = mock.fn(async () => ({
|
|
323
|
+
result: { value: 'loading' }
|
|
324
|
+
}));
|
|
325
|
+
|
|
326
|
+
await assert.rejects(
|
|
327
|
+
() => waitForDocumentReady(mockSession, 'complete', { timeout: 100, pollInterval: 10 }),
|
|
328
|
+
(err) => {
|
|
329
|
+
assert.ok(err.message.includes("did not reach 'complete'"));
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should throw context destroyed error on navigation', async () => {
|
|
336
|
+
mockSession.send = mock.fn(async () => {
|
|
337
|
+
throw new Error('Execution context was destroyed');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await assert.rejects(
|
|
341
|
+
() => waitForDocumentReady(mockSession, 'complete', { timeout: 100 }),
|
|
342
|
+
(err) => {
|
|
343
|
+
assert.ok(err.message.includes('navigated'));
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should resolve early if state exceeds target', async () => {
|
|
350
|
+
mockSession.send = mock.fn(async () => ({
|
|
351
|
+
result: { value: 'complete' }
|
|
352
|
+
}));
|
|
353
|
+
|
|
354
|
+
// Asking for interactive but getting complete should still resolve
|
|
355
|
+
const result = await waitForDocumentReady(mockSession, 'interactive');
|
|
356
|
+
|
|
357
|
+
assert.strictEqual(result, 'complete');
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe('waitForSelector', () => {
|
|
362
|
+
it('should resolve when selector is found', async () => {
|
|
363
|
+
mockSession.send = mock.fn(async () => ({
|
|
364
|
+
result: { value: true }
|
|
365
|
+
}));
|
|
366
|
+
|
|
367
|
+
await waitForSelector(mockSession, '#element');
|
|
368
|
+
|
|
369
|
+
assert.strictEqual(mockSession.send.mock.calls.length, 1);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should poll until selector appears', async () => {
|
|
373
|
+
let callCount = 0;
|
|
374
|
+
mockSession.send = mock.fn(async () => {
|
|
375
|
+
callCount++;
|
|
376
|
+
return { result: { value: callCount >= 3 } };
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await waitForSelector(mockSession, '#delayed', { timeout: 1000, pollInterval: 10 });
|
|
380
|
+
|
|
381
|
+
assert.ok(callCount >= 3);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should escape selector in expression', async () => {
|
|
385
|
+
mockSession.send = mock.fn(async () => ({
|
|
386
|
+
result: { value: true }
|
|
387
|
+
}));
|
|
388
|
+
|
|
389
|
+
await waitForSelector(mockSession, "[data-attr='value']");
|
|
390
|
+
|
|
391
|
+
const call = mockSession.send.mock.calls[0];
|
|
392
|
+
assert.ok(call.arguments[1].expression.includes("\\'value\\'"));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should check visibility when visible option is true', async () => {
|
|
396
|
+
mockSession.send = mock.fn(async () => ({
|
|
397
|
+
result: { value: true }
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
await waitForSelector(mockSession, '#visible', { visible: true });
|
|
401
|
+
|
|
402
|
+
const call = mockSession.send.mock.calls[0];
|
|
403
|
+
assert.ok(call.arguments[1].expression.includes('getComputedStyle'));
|
|
404
|
+
assert.ok(call.arguments[1].expression.includes('display'));
|
|
405
|
+
assert.ok(call.arguments[1].expression.includes('visibility'));
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should throw timeout error when selector not found', async () => {
|
|
409
|
+
mockSession.send = mock.fn(async () => ({
|
|
410
|
+
result: { value: false }
|
|
411
|
+
}));
|
|
412
|
+
|
|
413
|
+
await assert.rejects(
|
|
414
|
+
() => waitForSelector(mockSession, '#missing', { timeout: 100, pollInterval: 10 }),
|
|
415
|
+
(err) => {
|
|
416
|
+
assert.ok(err.message.includes('did not return truthy'));
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe('waitForText', () => {
|
|
424
|
+
it('should resolve when text is found', async () => {
|
|
425
|
+
mockSession.send = mock.fn(async () => ({
|
|
426
|
+
result: { value: true }
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
const result = await waitForText(mockSession, 'Hello World');
|
|
430
|
+
|
|
431
|
+
assert.strictEqual(result, true);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should use case-insensitive search by default', async () => {
|
|
435
|
+
mockSession.send = mock.fn(async () => ({
|
|
436
|
+
result: { value: true }
|
|
437
|
+
}));
|
|
438
|
+
|
|
439
|
+
await waitForText(mockSession, 'HELLO');
|
|
440
|
+
|
|
441
|
+
const call = mockSession.send.mock.calls[0];
|
|
442
|
+
assert.ok(call.arguments[1].expression.includes('toLowerCase'));
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should use exact match when exact option is true', async () => {
|
|
446
|
+
mockSession.send = mock.fn(async () => ({
|
|
447
|
+
result: { value: true }
|
|
448
|
+
}));
|
|
449
|
+
|
|
450
|
+
await waitForText(mockSession, 'Exact Text', { exact: true });
|
|
451
|
+
|
|
452
|
+
const call = mockSession.send.mock.calls[0];
|
|
453
|
+
assert.ok(!call.arguments[1].expression.includes('toLowerCase'));
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('should poll until text appears', async () => {
|
|
457
|
+
let callCount = 0;
|
|
458
|
+
mockSession.send = mock.fn(async () => {
|
|
459
|
+
callCount++;
|
|
460
|
+
return { result: { value: callCount >= 3 } };
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await waitForText(mockSession, 'delayed text', { timeout: 1000, pollInterval: 10 });
|
|
464
|
+
|
|
465
|
+
assert.ok(callCount >= 3);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('should throw timeout error when text not found', async () => {
|
|
469
|
+
mockSession.send = mock.fn(async () => ({
|
|
470
|
+
result: { value: false }
|
|
471
|
+
}));
|
|
472
|
+
|
|
473
|
+
await assert.rejects(
|
|
474
|
+
() => waitForText(mockSession, 'missing text', { timeout: 100, pollInterval: 10 }),
|
|
475
|
+
(err) => {
|
|
476
|
+
assert.ok(err.message.includes('waiting for text'));
|
|
477
|
+
assert.ok(err.message.includes('missing text'));
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should throw context destroyed error on navigation', async () => {
|
|
484
|
+
mockSession.send = mock.fn(async () => {
|
|
485
|
+
throw new Error('Execution context was destroyed');
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
await assert.rejects(
|
|
489
|
+
() => waitForText(mockSession, 'text', { timeout: 100 }),
|
|
490
|
+
(err) => {
|
|
491
|
+
assert.ok(err.message.includes('navigated'));
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('should convert non-string text to string', async () => {
|
|
498
|
+
mockSession.send = mock.fn(async () => ({
|
|
499
|
+
result: { value: true }
|
|
500
|
+
}));
|
|
501
|
+
|
|
502
|
+
await waitForText(mockSession, 12345);
|
|
503
|
+
|
|
504
|
+
const call = mockSession.send.mock.calls[0];
|
|
505
|
+
assert.ok(call.arguments[1].expression.includes('12345'));
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
});
|