cdp-skill 1.0.8 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -35
- package/SKILL.md +151 -239
- 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 +237 -43
- package/src/cdp/browser.js +22 -4
- package/src/cdp-skill.js +245 -69
- package/src/dom/click-executor.js +240 -76
- package/src/dom/element-locator.js +34 -25
- package/src/dom/fill-executor.js +55 -27
- package/src/page/dialog-handler.js +119 -0
- package/src/page/page-controller.js +190 -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 +99 -120
- package/src/runner/execute-navigation.js +11 -26
- package/src/runner/execute-query.js +8 -5
- 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/ContextHelpers.test.js +39 -28
- 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 +2 -2
- package/src/tests/StepValidator.test.js +222 -76
- package/src/tests/TestRunner.test.js +36 -25
- 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,540 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
executeFill,
|
|
6
|
+
executeFillActive,
|
|
7
|
+
executeSelectOption
|
|
8
|
+
} from '../runner/execute-input.js';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Helpers
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
function createMockElementLocator(opts = {}) {
|
|
15
|
+
const mockHandle = {
|
|
16
|
+
objectId: opts.objectId || 'obj-123',
|
|
17
|
+
scrollIntoView: mock.fn(() => Promise.resolve()),
|
|
18
|
+
waitForStability: mock.fn(() => {
|
|
19
|
+
if (opts.unstable) throw new Error('Element not stable');
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}),
|
|
22
|
+
isActionable: mock.fn(() => Promise.resolve({
|
|
23
|
+
actionable: opts.actionable !== false,
|
|
24
|
+
reason: opts.actionable === false ? opts.reason || 'Element not visible' : null
|
|
25
|
+
})),
|
|
26
|
+
getBoundingBox: mock.fn(() => Promise.resolve({
|
|
27
|
+
x: 100,
|
|
28
|
+
y: 100,
|
|
29
|
+
width: 200,
|
|
30
|
+
height: 40
|
|
31
|
+
})),
|
|
32
|
+
focus: mock.fn(() => Promise.resolve()),
|
|
33
|
+
dispose: mock.fn(() => Promise.resolve())
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const mockElement = {
|
|
37
|
+
_handle: mockHandle,
|
|
38
|
+
objectId: mockHandle.objectId
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
session: {
|
|
43
|
+
send: mock.fn((method) => {
|
|
44
|
+
if (method === 'Runtime.callFunctionOn') {
|
|
45
|
+
if (opts.notEditable) {
|
|
46
|
+
return Promise.resolve({
|
|
47
|
+
result: {
|
|
48
|
+
value: {
|
|
49
|
+
editable: false,
|
|
50
|
+
reason: 'Element is disabled'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (opts.notSelect) {
|
|
56
|
+
return Promise.resolve({
|
|
57
|
+
result: {
|
|
58
|
+
value: {
|
|
59
|
+
error: 'Element is not a <select> element'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (opts.noMatch) {
|
|
65
|
+
return Promise.resolve({
|
|
66
|
+
result: {
|
|
67
|
+
value: {
|
|
68
|
+
error: 'No option matched',
|
|
69
|
+
matchBy: 'value',
|
|
70
|
+
matchValue: 'unknown',
|
|
71
|
+
availableOptions: [{ value: 'opt1', label: 'Option 1' }]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return Promise.resolve({
|
|
77
|
+
result: {
|
|
78
|
+
value: {
|
|
79
|
+
editable: true,
|
|
80
|
+
success: true,
|
|
81
|
+
selected: ['opt1'],
|
|
82
|
+
multiple: false
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return Promise.resolve({});
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
findElement: mock.fn(() => {
|
|
91
|
+
if (opts.notFound) return Promise.resolve(null);
|
|
92
|
+
return Promise.resolve(mockElement);
|
|
93
|
+
})
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createMockInputEmulator() {
|
|
98
|
+
return {
|
|
99
|
+
click: mock.fn(() => Promise.resolve()),
|
|
100
|
+
type: mock.fn(() => Promise.resolve()),
|
|
101
|
+
selectAll: mock.fn(() => Promise.resolve())
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createMockPageController(opts = {}) {
|
|
106
|
+
return {
|
|
107
|
+
session: {
|
|
108
|
+
send: mock.fn((method, params) => {
|
|
109
|
+
if (method === 'Runtime.evaluate') {
|
|
110
|
+
if (opts.noFocus) {
|
|
111
|
+
return Promise.resolve({
|
|
112
|
+
result: {
|
|
113
|
+
value: {
|
|
114
|
+
error: 'No element is focused'
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (opts.notEditable) {
|
|
120
|
+
return Promise.resolve({
|
|
121
|
+
result: {
|
|
122
|
+
value: {
|
|
123
|
+
error: 'Focused element is not editable',
|
|
124
|
+
tag: 'DIV'
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (opts.disabled) {
|
|
130
|
+
return Promise.resolve({
|
|
131
|
+
result: {
|
|
132
|
+
value: {
|
|
133
|
+
error: 'Focused element is disabled',
|
|
134
|
+
tag: 'INPUT'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (opts.readonly) {
|
|
140
|
+
return Promise.resolve({
|
|
141
|
+
result: {
|
|
142
|
+
value: {
|
|
143
|
+
error: 'Focused element is readonly',
|
|
144
|
+
tag: 'INPUT'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (opts.exception) {
|
|
150
|
+
return Promise.resolve({
|
|
151
|
+
result: { value: undefined },
|
|
152
|
+
exceptionDetails: {
|
|
153
|
+
text: opts.exception
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return Promise.resolve({
|
|
158
|
+
result: {
|
|
159
|
+
value: {
|
|
160
|
+
editable: true,
|
|
161
|
+
tag: 'INPUT',
|
|
162
|
+
type: 'text',
|
|
163
|
+
selector: '#username',
|
|
164
|
+
valueBefore: ''
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return Promise.resolve({});
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Tests: executeFill
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
describe('executeFill', () => {
|
|
180
|
+
afterEach(() => { mock.reset(); });
|
|
181
|
+
|
|
182
|
+
it('should throw if selector is missing', async () => {
|
|
183
|
+
const locator = createMockElementLocator();
|
|
184
|
+
const emulator = createMockInputEmulator();
|
|
185
|
+
|
|
186
|
+
await assert.rejects(
|
|
187
|
+
executeFill(locator, emulator, { value: 'test' }),
|
|
188
|
+
{ message: 'Fill requires selector and value' }
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should throw if value is missing', async () => {
|
|
193
|
+
const locator = createMockElementLocator();
|
|
194
|
+
const emulator = createMockInputEmulator();
|
|
195
|
+
|
|
196
|
+
await assert.rejects(
|
|
197
|
+
executeFill(locator, emulator, { selector: '#input' }),
|
|
198
|
+
{ message: 'Fill requires selector and value' }
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should throw if element not found', async () => {
|
|
203
|
+
const locator = createMockElementLocator({ notFound: true });
|
|
204
|
+
const emulator = createMockInputEmulator();
|
|
205
|
+
|
|
206
|
+
await assert.rejects(
|
|
207
|
+
executeFill(locator, emulator, { selector: '#missing', value: 'test' }),
|
|
208
|
+
{ message: /element not found/i }
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should throw if element is not editable', async () => {
|
|
213
|
+
const locator = createMockElementLocator({ notEditable: true });
|
|
214
|
+
const emulator = createMockInputEmulator();
|
|
215
|
+
|
|
216
|
+
await assert.rejects(
|
|
217
|
+
executeFill(locator, emulator, { selector: '#disabled', value: 'test' }),
|
|
218
|
+
{ message: /not editable/i }
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should throw if element is not actionable after all scroll strategies', async () => {
|
|
223
|
+
const locator = createMockElementLocator({ actionable: false, reason: 'Element is covered' });
|
|
224
|
+
const emulator = createMockInputEmulator();
|
|
225
|
+
|
|
226
|
+
await assert.rejects(
|
|
227
|
+
executeFill(locator, emulator, { selector: '#input', value: 'test' }),
|
|
228
|
+
{ message: /not actionable/i }
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should fill element with standard approach', async () => {
|
|
233
|
+
const locator = createMockElementLocator();
|
|
234
|
+
const emulator = createMockInputEmulator();
|
|
235
|
+
|
|
236
|
+
await executeFill(locator, emulator, { selector: '#input', value: 'hello' });
|
|
237
|
+
|
|
238
|
+
assert.strictEqual(locator.findElement.mock.calls.length, 1);
|
|
239
|
+
assert.strictEqual(emulator.click.mock.calls.length, 1);
|
|
240
|
+
assert.strictEqual(emulator.selectAll.mock.calls.length, 1);
|
|
241
|
+
assert.strictEqual(emulator.type.mock.calls.length, 1);
|
|
242
|
+
assert.strictEqual(emulator.type.mock.calls[0].arguments[0], 'hello');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should skip clear if clear option is false', async () => {
|
|
246
|
+
const locator = createMockElementLocator();
|
|
247
|
+
const emulator = createMockInputEmulator();
|
|
248
|
+
|
|
249
|
+
await executeFill(locator, emulator, { selector: '#input', value: 'hello', clear: false });
|
|
250
|
+
|
|
251
|
+
assert.strictEqual(emulator.selectAll.mock.calls.length, 0);
|
|
252
|
+
assert.strictEqual(emulator.type.mock.calls.length, 1);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should use React filler if react option is true', async () => {
|
|
256
|
+
const locator = createMockElementLocator();
|
|
257
|
+
const emulator = createMockInputEmulator();
|
|
258
|
+
|
|
259
|
+
// Mock createReactInputFiller
|
|
260
|
+
await executeFill(locator, emulator, { selector: '#input', value: 'hello', react: true });
|
|
261
|
+
|
|
262
|
+
// React mode skips click and type
|
|
263
|
+
assert.strictEqual(emulator.click.mock.calls.length, 0);
|
|
264
|
+
assert.strictEqual(emulator.type.mock.calls.length, 0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should dispose element handle on success', async () => {
|
|
268
|
+
const locator = createMockElementLocator();
|
|
269
|
+
const emulator = createMockInputEmulator();
|
|
270
|
+
|
|
271
|
+
await executeFill(locator, emulator, { selector: '#input', value: 'test' });
|
|
272
|
+
|
|
273
|
+
const element = await locator.findElement('#input');
|
|
274
|
+
// Can't directly check if dispose was called, but ensure no errors
|
|
275
|
+
assert.ok(element);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should try multiple scroll strategies if element not immediately actionable', async () => {
|
|
279
|
+
let callCount = 0;
|
|
280
|
+
const locator = createMockElementLocator({
|
|
281
|
+
actionable: true
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Override isActionable to succeed on second call
|
|
285
|
+
const originalHandle = await locator.findElement('#input');
|
|
286
|
+
originalHandle._handle.isActionable = mock.fn(() => {
|
|
287
|
+
callCount++;
|
|
288
|
+
return Promise.resolve({
|
|
289
|
+
actionable: callCount > 1,
|
|
290
|
+
reason: callCount > 1 ? null : 'Not visible'
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
locator.findElement = mock.fn(() => Promise.resolve(originalHandle));
|
|
294
|
+
|
|
295
|
+
const emulator = createMockInputEmulator();
|
|
296
|
+
|
|
297
|
+
await executeFill(locator, emulator, { selector: '#input', value: 'test' });
|
|
298
|
+
|
|
299
|
+
// Should have tried multiple times
|
|
300
|
+
assert.ok(originalHandle._handle.isActionable.mock.calls.length >= 2);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Tests: executeFillActive
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
describe('executeFillActive', () => {
|
|
309
|
+
afterEach(() => { mock.reset(); });
|
|
310
|
+
|
|
311
|
+
it('should throw if no element is focused', async () => {
|
|
312
|
+
const pc = createMockPageController({ noFocus: true });
|
|
313
|
+
const emulator = createMockInputEmulator();
|
|
314
|
+
|
|
315
|
+
await assert.rejects(
|
|
316
|
+
executeFillActive(pc, emulator, 'test'),
|
|
317
|
+
{ message: 'No element is focused' }
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should throw if focused element is not editable', async () => {
|
|
322
|
+
const pc = createMockPageController({ notEditable: true });
|
|
323
|
+
const emulator = createMockInputEmulator();
|
|
324
|
+
|
|
325
|
+
await assert.rejects(
|
|
326
|
+
executeFillActive(pc, emulator, 'test'),
|
|
327
|
+
{ message: 'Focused element is not editable' }
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should throw if focused element is disabled', async () => {
|
|
332
|
+
const pc = createMockPageController({ disabled: true });
|
|
333
|
+
const emulator = createMockInputEmulator();
|
|
334
|
+
|
|
335
|
+
await assert.rejects(
|
|
336
|
+
executeFillActive(pc, emulator, 'test'),
|
|
337
|
+
{ message: 'Focused element is disabled' }
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should throw if focused element is readonly', async () => {
|
|
342
|
+
const pc = createMockPageController({ readonly: true });
|
|
343
|
+
const emulator = createMockInputEmulator();
|
|
344
|
+
|
|
345
|
+
await assert.rejects(
|
|
346
|
+
executeFillActive(pc, emulator, 'test'),
|
|
347
|
+
{ message: 'Focused element is readonly' }
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should throw on Runtime.evaluate exception', async () => {
|
|
352
|
+
const pc = createMockPageController({ exception: 'eval error' });
|
|
353
|
+
const emulator = createMockInputEmulator();
|
|
354
|
+
|
|
355
|
+
await assert.rejects(
|
|
356
|
+
executeFillActive(pc, emulator, 'test'),
|
|
357
|
+
{ message: /fillActive error/i }
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should fill active element with string param', async () => {
|
|
362
|
+
const pc = createMockPageController();
|
|
363
|
+
const emulator = createMockInputEmulator();
|
|
364
|
+
|
|
365
|
+
const result = await executeFillActive(pc, emulator, 'hello');
|
|
366
|
+
|
|
367
|
+
assert.strictEqual(result.filled, true);
|
|
368
|
+
assert.strictEqual(result.tag, 'INPUT');
|
|
369
|
+
assert.strictEqual(result.type, 'text');
|
|
370
|
+
assert.strictEqual(result.selector, '#username');
|
|
371
|
+
assert.strictEqual(result.valueAfter, 'hello');
|
|
372
|
+
assert.strictEqual(emulator.selectAll.mock.calls.length, 1);
|
|
373
|
+
assert.strictEqual(emulator.type.mock.calls.length, 1);
|
|
374
|
+
assert.strictEqual(emulator.type.mock.calls[0].arguments[0], 'hello');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should fill active element with object param', async () => {
|
|
378
|
+
const pc = createMockPageController();
|
|
379
|
+
const emulator = createMockInputEmulator();
|
|
380
|
+
|
|
381
|
+
const result = await executeFillActive(pc, emulator, { value: 'world' });
|
|
382
|
+
|
|
383
|
+
assert.strictEqual(result.filled, true);
|
|
384
|
+
assert.strictEqual(result.valueAfter, 'world');
|
|
385
|
+
assert.strictEqual(emulator.type.mock.calls[0].arguments[0], 'world');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should skip clear if clear option is false', async () => {
|
|
389
|
+
const pc = createMockPageController();
|
|
390
|
+
const emulator = createMockInputEmulator();
|
|
391
|
+
|
|
392
|
+
await executeFillActive(pc, emulator, { value: 'test', clear: false });
|
|
393
|
+
|
|
394
|
+
assert.strictEqual(emulator.selectAll.mock.calls.length, 0);
|
|
395
|
+
assert.strictEqual(emulator.type.mock.calls.length, 1);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should return valueBefore and valueAfter', async () => {
|
|
399
|
+
const pc = createMockPageController();
|
|
400
|
+
const emulator = createMockInputEmulator();
|
|
401
|
+
|
|
402
|
+
const result = await executeFillActive(pc, emulator, 'new value');
|
|
403
|
+
|
|
404
|
+
assert.strictEqual(result.valueBefore, '');
|
|
405
|
+
assert.strictEqual(result.valueAfter, 'new value');
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// Tests: executeSelectOption
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
describe('executeSelectOption', () => {
|
|
414
|
+
afterEach(() => { mock.reset(); });
|
|
415
|
+
|
|
416
|
+
it('should throw if selector is missing', async () => {
|
|
417
|
+
const locator = createMockElementLocator();
|
|
418
|
+
|
|
419
|
+
await assert.rejects(
|
|
420
|
+
executeSelectOption(locator, { value: 'opt1' }),
|
|
421
|
+
{ message: 'selectOption requires selector' }
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should throw if no value, label, index, or values provided', async () => {
|
|
426
|
+
const locator = createMockElementLocator();
|
|
427
|
+
|
|
428
|
+
await assert.rejects(
|
|
429
|
+
executeSelectOption(locator, { selector: '#dropdown' }),
|
|
430
|
+
{ message: 'selectOption requires value, label, index, or values' }
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should throw if element not found', async () => {
|
|
435
|
+
const locator = createMockElementLocator({ notFound: true });
|
|
436
|
+
|
|
437
|
+
await assert.rejects(
|
|
438
|
+
executeSelectOption(locator, { selector: '#missing', value: 'opt1' }),
|
|
439
|
+
{ message: /element not found/i }
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should throw if element is not a select', async () => {
|
|
444
|
+
const locator = createMockElementLocator({ notSelect: true });
|
|
445
|
+
|
|
446
|
+
await assert.rejects(
|
|
447
|
+
executeSelectOption(locator, { selector: '#notselect', value: 'opt1' }),
|
|
448
|
+
{ message: /not a <select> element/i }
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should throw if no option matched', async () => {
|
|
453
|
+
const locator = createMockElementLocator({ noMatch: true });
|
|
454
|
+
|
|
455
|
+
await assert.rejects(
|
|
456
|
+
executeSelectOption(locator, { selector: '#dropdown', value: 'unknown' }),
|
|
457
|
+
{ message: /No option matched/i }
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should select option by value', async () => {
|
|
462
|
+
const locator = createMockElementLocator();
|
|
463
|
+
|
|
464
|
+
const result = await executeSelectOption(locator, {
|
|
465
|
+
selector: '#dropdown',
|
|
466
|
+
value: 'opt1'
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
assert.strictEqual(result.selected.length, 1);
|
|
470
|
+
assert.strictEqual(result.selected[0], 'opt1');
|
|
471
|
+
assert.strictEqual(result.multiple, false);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should select option by label', async () => {
|
|
475
|
+
const locator = createMockElementLocator();
|
|
476
|
+
|
|
477
|
+
const result = await executeSelectOption(locator, {
|
|
478
|
+
selector: '#dropdown',
|
|
479
|
+
label: 'Option 1'
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
assert.strictEqual(result.selected.length, 1);
|
|
483
|
+
assert.strictEqual(result.multiple, false);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should select option by index', async () => {
|
|
487
|
+
const locator = createMockElementLocator();
|
|
488
|
+
|
|
489
|
+
const result = await executeSelectOption(locator, {
|
|
490
|
+
selector: '#dropdown',
|
|
491
|
+
index: 0
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
assert.strictEqual(result.selected.length, 1);
|
|
495
|
+
assert.strictEqual(result.multiple, false);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should select multiple options with values array', async () => {
|
|
499
|
+
const locator = createMockElementLocator();
|
|
500
|
+
|
|
501
|
+
await executeSelectOption(locator, {
|
|
502
|
+
selector: '#dropdown',
|
|
503
|
+
values: ['opt1', 'opt2']
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Mock returns single value, but in real impl would return multiple
|
|
507
|
+
assert.ok(locator.findElement.mock.calls.length > 0);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should dispose element handle after execution', async () => {
|
|
511
|
+
const locator = createMockElementLocator();
|
|
512
|
+
const element = await locator.findElement('#dropdown');
|
|
513
|
+
|
|
514
|
+
await executeSelectOption(locator, {
|
|
515
|
+
selector: '#dropdown',
|
|
516
|
+
value: 'opt1'
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Dispose is called in finally block
|
|
520
|
+
assert.strictEqual(element._handle.dispose.mock.calls.length, 1);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('should pass correct arguments to Runtime.callFunctionOn', async () => {
|
|
524
|
+
const locator = createMockElementLocator();
|
|
525
|
+
|
|
526
|
+
await executeSelectOption(locator, {
|
|
527
|
+
selector: '#dropdown',
|
|
528
|
+
value: 'opt1'
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const calls = locator.session.send.mock.calls.filter(
|
|
532
|
+
call => call.arguments[0] === 'Runtime.callFunctionOn'
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
assert.strictEqual(calls.length, 1); // executeSelectOption only calls once (no editable check in that function)
|
|
536
|
+
const selectCall = calls[0];
|
|
537
|
+
assert.ok(selectCall.arguments[1].functionDeclaration);
|
|
538
|
+
assert.ok(selectCall.arguments[1].arguments);
|
|
539
|
+
});
|
|
540
|
+
});
|