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,641 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { createInputEmulator } from '../dom.js';
|
|
4
|
+
|
|
5
|
+
describe('InputEmulator', () => {
|
|
6
|
+
let mockCdp;
|
|
7
|
+
let input;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
mockCdp = {
|
|
11
|
+
send: mock.fn(async () => ({}))
|
|
12
|
+
};
|
|
13
|
+
input = createInputEmulator(mockCdp);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('constructor', () => {
|
|
17
|
+
it('should throw if cdp is not provided', () => {
|
|
18
|
+
assert.throws(() => createInputEmulator(null), {
|
|
19
|
+
message: 'CDP session is required'
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('click', () => {
|
|
25
|
+
it('should send correct click sequence (mouseMoved, mousePressed, mouseReleased)', async () => {
|
|
26
|
+
await input.click(100, 200);
|
|
27
|
+
|
|
28
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 3);
|
|
29
|
+
|
|
30
|
+
// Verify mouseMoved
|
|
31
|
+
const moveCall = mockCdp.send.mock.calls[0].arguments;
|
|
32
|
+
assert.strictEqual(moveCall[0], 'Input.dispatchMouseEvent');
|
|
33
|
+
assert.strictEqual(moveCall[1].type, 'mouseMoved');
|
|
34
|
+
assert.strictEqual(moveCall[1].x, 100);
|
|
35
|
+
assert.strictEqual(moveCall[1].y, 200);
|
|
36
|
+
|
|
37
|
+
// Verify mousePressed
|
|
38
|
+
const pressCall = mockCdp.send.mock.calls[1].arguments;
|
|
39
|
+
assert.strictEqual(pressCall[0], 'Input.dispatchMouseEvent');
|
|
40
|
+
assert.strictEqual(pressCall[1].type, 'mousePressed');
|
|
41
|
+
assert.strictEqual(pressCall[1].x, 100);
|
|
42
|
+
assert.strictEqual(pressCall[1].y, 200);
|
|
43
|
+
assert.strictEqual(pressCall[1].button, 'left');
|
|
44
|
+
assert.strictEqual(pressCall[1].clickCount, 1);
|
|
45
|
+
assert.strictEqual(pressCall[1].buttons, 1); // Left button mask
|
|
46
|
+
|
|
47
|
+
// Verify mouseReleased
|
|
48
|
+
const releaseCall = mockCdp.send.mock.calls[2].arguments;
|
|
49
|
+
assert.strictEqual(releaseCall[0], 'Input.dispatchMouseEvent');
|
|
50
|
+
assert.strictEqual(releaseCall[1].type, 'mouseReleased');
|
|
51
|
+
assert.strictEqual(releaseCall[1].x, 100);
|
|
52
|
+
assert.strictEqual(releaseCall[1].y, 200);
|
|
53
|
+
assert.strictEqual(releaseCall[1].button, 'left');
|
|
54
|
+
assert.strictEqual(releaseCall[1].buttons, 0); // No buttons on release
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should use right button when specified', async () => {
|
|
58
|
+
await input.click(50, 75, { button: 'right' });
|
|
59
|
+
|
|
60
|
+
const pressCall = mockCdp.send.mock.calls[1].arguments;
|
|
61
|
+
assert.strictEqual(pressCall[1].button, 'right');
|
|
62
|
+
assert.strictEqual(pressCall[1].buttons, 2); // Right button mask
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should use middle button when specified', async () => {
|
|
66
|
+
await input.click(50, 75, { button: 'middle' });
|
|
67
|
+
|
|
68
|
+
const pressCall = mockCdp.send.mock.calls[1].arguments;
|
|
69
|
+
assert.strictEqual(pressCall[1].button, 'middle');
|
|
70
|
+
assert.strictEqual(pressCall[1].buttons, 4); // Middle button mask
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should include modifiers in events', async () => {
|
|
74
|
+
await input.click(10, 20, { modifiers: { ctrl: true, shift: true } });
|
|
75
|
+
|
|
76
|
+
const expectedModifiers = 2 | 8; // ctrl=2, shift=8
|
|
77
|
+
|
|
78
|
+
const moveCall = mockCdp.send.mock.calls[0].arguments;
|
|
79
|
+
assert.strictEqual(moveCall[1].modifiers, expectedModifiers);
|
|
80
|
+
|
|
81
|
+
const pressCall = mockCdp.send.mock.calls[1].arguments;
|
|
82
|
+
assert.strictEqual(pressCall[1].modifiers, expectedModifiers);
|
|
83
|
+
|
|
84
|
+
const releaseCall = mockCdp.send.mock.calls[2].arguments;
|
|
85
|
+
assert.strictEqual(releaseCall[1].modifiers, expectedModifiers);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should support all modifier combinations', async () => {
|
|
89
|
+
await input.click(10, 20, { modifiers: { alt: true, ctrl: true, meta: true, shift: true } });
|
|
90
|
+
|
|
91
|
+
const expectedModifiers = 1 | 2 | 4 | 8; // alt=1, ctrl=2, meta=4, shift=8
|
|
92
|
+
|
|
93
|
+
const pressCall = mockCdp.send.mock.calls[1].arguments;
|
|
94
|
+
assert.strictEqual(pressCall[1].modifiers, expectedModifiers);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should set clickCount for double clicks', async () => {
|
|
98
|
+
await input.click(100, 100, { clickCount: 2 });
|
|
99
|
+
|
|
100
|
+
const pressCall = mockCdp.send.mock.calls[1].arguments;
|
|
101
|
+
assert.strictEqual(pressCall[1].clickCount, 2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should throw on invalid coordinates', async () => {
|
|
105
|
+
await assert.rejects(() => input.click('a', 100), {
|
|
106
|
+
message: 'Coordinates must be finite numbers'
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await assert.rejects(() => input.click(NaN, 100), {
|
|
110
|
+
message: 'Coordinates must be finite numbers'
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await assert.rejects(() => input.click(Infinity, 100), {
|
|
114
|
+
message: 'Coordinates must be finite numbers'
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should throw on negative coordinates', async () => {
|
|
119
|
+
await assert.rejects(() => input.click(-10, 100), {
|
|
120
|
+
message: 'Coordinates must be non-negative'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await assert.rejects(() => input.click(100, -5), {
|
|
124
|
+
message: 'Coordinates must be non-negative'
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should throw on invalid button', async () => {
|
|
129
|
+
await assert.rejects(() => input.click(100, 100, { button: 'invalid' }), {
|
|
130
|
+
message: /Invalid button: invalid/
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should throw on invalid clickCount (zero)', async () => {
|
|
135
|
+
await assert.rejects(() => input.click(100, 100, { clickCount: 0 }), {
|
|
136
|
+
message: 'Click count must be a positive integer'
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should throw on invalid clickCount (negative)', async () => {
|
|
141
|
+
await assert.rejects(() => input.click(100, 100, { clickCount: -1 }), {
|
|
142
|
+
message: 'Click count must be a positive integer'
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should throw on invalid clickCount (non-integer)', async () => {
|
|
147
|
+
await assert.rejects(() => input.click(100, 100, { clickCount: 1.5 }), {
|
|
148
|
+
message: 'Click count must be a positive integer'
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('doubleClick', () => {
|
|
154
|
+
it('should call click with clickCount 2', async () => {
|
|
155
|
+
await input.doubleClick(50, 50);
|
|
156
|
+
|
|
157
|
+
const pressCall = mockCdp.send.mock.calls[1].arguments;
|
|
158
|
+
assert.strictEqual(pressCall[1].clickCount, 2);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('rightClick', () => {
|
|
163
|
+
it('should call click with right button', async () => {
|
|
164
|
+
await input.rightClick(50, 50);
|
|
165
|
+
|
|
166
|
+
const pressCall = mockCdp.send.mock.calls[1].arguments;
|
|
167
|
+
assert.strictEqual(pressCall[1].button, 'right');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('type', () => {
|
|
172
|
+
it('should send char events for each character', async () => {
|
|
173
|
+
await input.type('abc');
|
|
174
|
+
|
|
175
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 3);
|
|
176
|
+
|
|
177
|
+
for (let i = 0; i < 3; i++) {
|
|
178
|
+
const call = mockCdp.send.mock.calls[i].arguments;
|
|
179
|
+
assert.strictEqual(call[0], 'Input.dispatchKeyEvent');
|
|
180
|
+
assert.strictEqual(call[1].type, 'char');
|
|
181
|
+
assert.strictEqual(call[1].text, 'abc'[i]);
|
|
182
|
+
assert.strictEqual(call[1].key, 'abc'[i]);
|
|
183
|
+
assert.strictEqual(call[1].unmodifiedText, 'abc'[i]);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should throw if text is not a string', async () => {
|
|
188
|
+
await assert.rejects(() => input.type(123), {
|
|
189
|
+
message: 'Text must be a string'
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await assert.rejects(() => input.type(null), {
|
|
193
|
+
message: 'Text must be a string'
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should handle empty string', async () => {
|
|
198
|
+
await input.type('');
|
|
199
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should handle special characters', async () => {
|
|
203
|
+
await input.type('@#$');
|
|
204
|
+
|
|
205
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 3);
|
|
206
|
+
assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].text, '@');
|
|
207
|
+
assert.strictEqual(mockCdp.send.mock.calls[1].arguments[1].text, '#');
|
|
208
|
+
assert.strictEqual(mockCdp.send.mock.calls[2].arguments[1].text, '$');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('press', () => {
|
|
213
|
+
it('should send rawKeyDown and keyUp events', async () => {
|
|
214
|
+
await input.press('Enter');
|
|
215
|
+
|
|
216
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 3); // rawKeyDown, char, keyUp
|
|
217
|
+
|
|
218
|
+
// Verify rawKeyDown
|
|
219
|
+
const downCall = mockCdp.send.mock.calls[0].arguments;
|
|
220
|
+
assert.strictEqual(downCall[0], 'Input.dispatchKeyEvent');
|
|
221
|
+
assert.strictEqual(downCall[1].type, 'rawKeyDown');
|
|
222
|
+
assert.strictEqual(downCall[1].key, 'Enter');
|
|
223
|
+
assert.strictEqual(downCall[1].code, 'Enter');
|
|
224
|
+
assert.strictEqual(downCall[1].windowsVirtualKeyCode, 13);
|
|
225
|
+
|
|
226
|
+
// Verify char event (Enter has text)
|
|
227
|
+
const charCall = mockCdp.send.mock.calls[1].arguments;
|
|
228
|
+
assert.strictEqual(charCall[1].type, 'char');
|
|
229
|
+
assert.strictEqual(charCall[1].text, '\r');
|
|
230
|
+
|
|
231
|
+
// Verify keyUp
|
|
232
|
+
const upCall = mockCdp.send.mock.calls[2].arguments;
|
|
233
|
+
assert.strictEqual(upCall[1].type, 'keyUp');
|
|
234
|
+
assert.strictEqual(upCall[1].key, 'Enter');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should not send char event for keys without text', async () => {
|
|
238
|
+
await input.press('Tab');
|
|
239
|
+
|
|
240
|
+
// Tab has no text, so only rawKeyDown and keyUp
|
|
241
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 2);
|
|
242
|
+
assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].type, 'rawKeyDown');
|
|
243
|
+
assert.strictEqual(mockCdp.send.mock.calls[1].arguments[1].type, 'keyUp');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should include modifiers', async () => {
|
|
247
|
+
await input.press('a', { modifiers: { ctrl: true } });
|
|
248
|
+
|
|
249
|
+
const downCall = mockCdp.send.mock.calls[0].arguments;
|
|
250
|
+
assert.strictEqual(downCall[1].modifiers, 2); // ctrl = 2
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should handle lowercase letters', async () => {
|
|
254
|
+
await input.press('a');
|
|
255
|
+
|
|
256
|
+
const downCall = mockCdp.send.mock.calls[0].arguments;
|
|
257
|
+
assert.strictEqual(downCall[1].key, 'a');
|
|
258
|
+
assert.strictEqual(downCall[1].code, 'KeyA');
|
|
259
|
+
assert.strictEqual(downCall[1].windowsVirtualKeyCode, 65);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should handle uppercase letters', async () => {
|
|
263
|
+
await input.press('A');
|
|
264
|
+
|
|
265
|
+
const downCall = mockCdp.send.mock.calls[0].arguments;
|
|
266
|
+
assert.strictEqual(downCall[1].key, 'A');
|
|
267
|
+
assert.strictEqual(downCall[1].code, 'KeyA');
|
|
268
|
+
assert.strictEqual(downCall[1].windowsVirtualKeyCode, 65);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should handle numbers', async () => {
|
|
272
|
+
await input.press('5');
|
|
273
|
+
|
|
274
|
+
const downCall = mockCdp.send.mock.calls[0].arguments;
|
|
275
|
+
assert.strictEqual(downCall[1].key, '5');
|
|
276
|
+
assert.strictEqual(downCall[1].code, 'Digit5');
|
|
277
|
+
assert.strictEqual(downCall[1].windowsVirtualKeyCode, 53);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should handle arrow keys', async () => {
|
|
281
|
+
await input.press('ArrowDown');
|
|
282
|
+
|
|
283
|
+
const downCall = mockCdp.send.mock.calls[0].arguments;
|
|
284
|
+
assert.strictEqual(downCall[1].key, 'ArrowDown');
|
|
285
|
+
assert.strictEqual(downCall[1].code, 'ArrowDown');
|
|
286
|
+
assert.strictEqual(downCall[1].windowsVirtualKeyCode, 40);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should handle Escape key', async () => {
|
|
290
|
+
await input.press('Escape');
|
|
291
|
+
|
|
292
|
+
const downCall = mockCdp.send.mock.calls[0].arguments;
|
|
293
|
+
assert.strictEqual(downCall[1].key, 'Escape');
|
|
294
|
+
assert.strictEqual(downCall[1].windowsVirtualKeyCode, 27);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should handle function keys', async () => {
|
|
298
|
+
await input.press('F5');
|
|
299
|
+
|
|
300
|
+
const downCall = mockCdp.send.mock.calls[0].arguments;
|
|
301
|
+
assert.strictEqual(downCall[1].key, 'F5');
|
|
302
|
+
assert.strictEqual(downCall[1].code, 'F5');
|
|
303
|
+
assert.strictEqual(downCall[1].windowsVirtualKeyCode, 116);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('fill', () => {
|
|
308
|
+
it('should click, select all, then type', async () => {
|
|
309
|
+
await input.fill(100, 200, 'new text');
|
|
310
|
+
|
|
311
|
+
const calls = mockCdp.send.mock.calls.map(c => c.arguments);
|
|
312
|
+
|
|
313
|
+
// First 3 calls: click (mouseMoved, mousePressed, mouseReleased)
|
|
314
|
+
assert.strictEqual(calls[0][1].type, 'mouseMoved');
|
|
315
|
+
assert.strictEqual(calls[1][1].type, 'mousePressed');
|
|
316
|
+
assert.strictEqual(calls[2][1].type, 'mouseReleased');
|
|
317
|
+
|
|
318
|
+
// Next: Select all (rawKeyDown, keyUp for 'a')
|
|
319
|
+
// On macOS uses meta (4), on other platforms uses ctrl (2)
|
|
320
|
+
const selectAllDown = calls[3];
|
|
321
|
+
assert.strictEqual(selectAllDown[1].type, 'rawKeyDown');
|
|
322
|
+
assert.strictEqual(selectAllDown[1].key, 'a');
|
|
323
|
+
const expectedModifier = process.platform === 'darwin' ? 4 : 2;
|
|
324
|
+
assert.strictEqual(selectAllDown[1].modifiers, expectedModifier);
|
|
325
|
+
|
|
326
|
+
// Then type the text
|
|
327
|
+
const typeStart = 5; // After click (3) + select-all down/up (2)
|
|
328
|
+
assert.strictEqual(calls[typeStart][1].type, 'char');
|
|
329
|
+
assert.strictEqual(calls[typeStart][1].text, 'n');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('moveMouse', () => {
|
|
334
|
+
it('should send mouseMoved event', async () => {
|
|
335
|
+
await input.moveMouse(300, 400);
|
|
336
|
+
|
|
337
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 1);
|
|
338
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
339
|
+
assert.strictEqual(call[0], 'Input.dispatchMouseEvent');
|
|
340
|
+
assert.strictEqual(call[1].type, 'mouseMoved');
|
|
341
|
+
assert.strictEqual(call[1].x, 300);
|
|
342
|
+
assert.strictEqual(call[1].y, 400);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should validate coordinates', async () => {
|
|
346
|
+
await assert.rejects(() => input.moveMouse('invalid', 100), {
|
|
347
|
+
message: 'Coordinates must be finite numbers'
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('scroll', () => {
|
|
353
|
+
it('should send mouseWheel event', async () => {
|
|
354
|
+
await input.scroll(0, 100);
|
|
355
|
+
|
|
356
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 1);
|
|
357
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
358
|
+
assert.strictEqual(call[0], 'Input.dispatchMouseEvent');
|
|
359
|
+
assert.strictEqual(call[1].type, 'mouseWheel');
|
|
360
|
+
assert.strictEqual(call[1].deltaX, 0);
|
|
361
|
+
assert.strictEqual(call[1].deltaY, 100);
|
|
362
|
+
assert.strictEqual(call[1].x, 100); // default origin
|
|
363
|
+
assert.strictEqual(call[1].y, 100);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should allow custom origin coordinates', async () => {
|
|
367
|
+
await input.scroll(50, 200, 250, 350);
|
|
368
|
+
|
|
369
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
370
|
+
assert.strictEqual(call[1].x, 250);
|
|
371
|
+
assert.strictEqual(call[1].y, 350);
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
describe('modifier calculations', () => {
|
|
376
|
+
it('should calculate alt modifier correctly', async () => {
|
|
377
|
+
await input.click(0, 0, { modifiers: { alt: true } });
|
|
378
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
379
|
+
assert.strictEqual(call[1].modifiers, 1);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should calculate ctrl modifier correctly', async () => {
|
|
383
|
+
await input.click(0, 0, { modifiers: { ctrl: true } });
|
|
384
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
385
|
+
assert.strictEqual(call[1].modifiers, 2);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should calculate meta modifier correctly', async () => {
|
|
389
|
+
await input.click(0, 0, { modifiers: { meta: true } });
|
|
390
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
391
|
+
assert.strictEqual(call[1].modifiers, 4);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should calculate shift modifier correctly', async () => {
|
|
395
|
+
await input.click(0, 0, { modifiers: { shift: true } });
|
|
396
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
397
|
+
assert.strictEqual(call[1].modifiers, 8);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe('button masks', () => {
|
|
402
|
+
it('should use correct mask for left button', async () => {
|
|
403
|
+
await input.click(0, 0, { button: 'left' });
|
|
404
|
+
const call = mockCdp.send.mock.calls[1].arguments;
|
|
405
|
+
assert.strictEqual(call[1].buttons, 1);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should use correct mask for back button', async () => {
|
|
409
|
+
await input.click(0, 0, { button: 'back' });
|
|
410
|
+
const call = mockCdp.send.mock.calls[1].arguments;
|
|
411
|
+
assert.strictEqual(call[1].buttons, 8);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should use correct mask for forward button', async () => {
|
|
415
|
+
await input.click(0, 0, { button: 'forward' });
|
|
416
|
+
const call = mockCdp.send.mock.calls[1].arguments;
|
|
417
|
+
assert.strictEqual(call[1].buttons, 16);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('edge cases', () => {
|
|
422
|
+
describe('click edge cases', () => {
|
|
423
|
+
it('should handle zero coordinates', async () => {
|
|
424
|
+
await input.click(0, 0);
|
|
425
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
426
|
+
assert.strictEqual(call[1].x, 0);
|
|
427
|
+
assert.strictEqual(call[1].y, 0);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should handle very large coordinates', async () => {
|
|
431
|
+
await input.click(10000, 10000);
|
|
432
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
433
|
+
assert.strictEqual(call[1].x, 10000);
|
|
434
|
+
assert.strictEqual(call[1].y, 10000);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should handle decimal coordinates', async () => {
|
|
438
|
+
await input.click(100.5, 200.7);
|
|
439
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
440
|
+
assert.strictEqual(call[1].x, 100.5);
|
|
441
|
+
assert.strictEqual(call[1].y, 200.7);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should handle empty modifiers object', async () => {
|
|
445
|
+
await input.click(100, 100, { modifiers: {} });
|
|
446
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
447
|
+
assert.strictEqual(call[1].modifiers, 0);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe('type edge cases', () => {
|
|
452
|
+
it('should handle unicode characters', async () => {
|
|
453
|
+
await input.type('日本語');
|
|
454
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 3);
|
|
455
|
+
assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].text, '日');
|
|
456
|
+
assert.strictEqual(mockCdp.send.mock.calls[1].arguments[1].text, '本');
|
|
457
|
+
assert.strictEqual(mockCdp.send.mock.calls[2].arguments[1].text, '語');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should handle emoji characters', async () => {
|
|
461
|
+
await input.type('👍');
|
|
462
|
+
// Simple emoji is 2 UTF-16 code units but JS for...of treats it as 1 char
|
|
463
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 1);
|
|
464
|
+
assert.strictEqual(mockCdp.send.mock.calls[0].arguments[1].text, '👍');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should handle emoji with skin tone modifier', async () => {
|
|
468
|
+
await input.type('👍🏻');
|
|
469
|
+
// Skin tone emoji is 2 graphemes in for...of (base + modifier)
|
|
470
|
+
assert.strictEqual(mockCdp.send.mock.calls.length, 2);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('should handle ZWJ emoji sequences', async () => {
|
|
474
|
+
await input.type('👨👩👧');
|
|
475
|
+
// ZWJ family emoji is multiple code points joined by ZWJ
|
|
476
|
+
// for...of iterates code points, not graphemes
|
|
477
|
+
assert.ok(mockCdp.send.mock.calls.length >= 5); // family emoji has multiple parts
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should handle newline characters', async () => {
|
|
481
|
+
await input.type('line1\nline2');
|
|
482
|
+
const chars = mockCdp.send.mock.calls.map(c => c.arguments[1].text);
|
|
483
|
+
assert.ok(chars.includes('\n'));
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should handle tab characters', async () => {
|
|
487
|
+
await input.type('col1\tcol2');
|
|
488
|
+
const chars = mockCdp.send.mock.calls.map(c => c.arguments[1].text);
|
|
489
|
+
assert.ok(chars.includes('\t'));
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
describe('press edge cases', () => {
|
|
494
|
+
it('should handle unknown key gracefully', async () => {
|
|
495
|
+
await input.press('!');
|
|
496
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
497
|
+
assert.strictEqual(call[1].key, '!');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should handle modifier keys', async () => {
|
|
501
|
+
await input.press('Shift');
|
|
502
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
503
|
+
assert.strictEqual(call[1].key, 'Shift');
|
|
504
|
+
assert.strictEqual(call[1].code, 'ShiftLeft');
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
describe('fill edge cases', () => {
|
|
509
|
+
it('should handle empty string fill', async () => {
|
|
510
|
+
await input.fill(100, 200, '');
|
|
511
|
+
// Should still click and select all, just no chars typed
|
|
512
|
+
assert.ok(mockCdp.send.mock.calls.length > 0);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('should use meta modifier when useMeta option is true', async () => {
|
|
516
|
+
await input.fill(100, 200, 'test', { useMeta: true });
|
|
517
|
+
|
|
518
|
+
const calls = mockCdp.send.mock.calls.map(c => c.arguments);
|
|
519
|
+
// Find the Ctrl/Cmd+A keydown
|
|
520
|
+
const ctrlADown = calls.find(c => c[1].type === 'rawKeyDown' && c[1].key === 'a');
|
|
521
|
+
assert.strictEqual(ctrlADown[1].modifiers, 4); // meta = 4
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('should use ctrl modifier when useMeta option is false', async () => {
|
|
525
|
+
await input.fill(100, 200, 'test', { useMeta: false });
|
|
526
|
+
|
|
527
|
+
const calls = mockCdp.send.mock.calls.map(c => c.arguments);
|
|
528
|
+
// Find the Ctrl/Cmd+A keydown
|
|
529
|
+
const ctrlADown = calls.find(c => c[1].type === 'rawKeyDown' && c[1].key === 'a');
|
|
530
|
+
assert.strictEqual(ctrlADown[1].modifiers, 2); // ctrl = 2
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe('scroll edge cases', () => {
|
|
535
|
+
it('should handle negative scroll values', async () => {
|
|
536
|
+
await input.scroll(-100, -200);
|
|
537
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
538
|
+
assert.strictEqual(call[1].deltaX, -100);
|
|
539
|
+
assert.strictEqual(call[1].deltaY, -200);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should handle zero scroll values', async () => {
|
|
543
|
+
await input.scroll(0, 0);
|
|
544
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
545
|
+
assert.strictEqual(call[1].deltaX, 0);
|
|
546
|
+
assert.strictEqual(call[1].deltaY, 0);
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe('hover', () => {
|
|
552
|
+
it('should dispatch mouseMoved event', async () => {
|
|
553
|
+
await input.hover(100, 200);
|
|
554
|
+
|
|
555
|
+
const call = mockCdp.send.mock.calls[0].arguments;
|
|
556
|
+
assert.strictEqual(call[0], 'Input.dispatchMouseEvent');
|
|
557
|
+
assert.strictEqual(call[1].type, 'mouseMoved');
|
|
558
|
+
assert.strictEqual(call[1].x, 100);
|
|
559
|
+
assert.strictEqual(call[1].y, 200);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should validate coordinates', async () => {
|
|
563
|
+
await assert.rejects(
|
|
564
|
+
() => input.hover(-1, 100),
|
|
565
|
+
/non-negative/
|
|
566
|
+
);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe('pressCombo', () => {
|
|
571
|
+
it('should parse and press Control+a', async () => {
|
|
572
|
+
await input.pressCombo('Control+a');
|
|
573
|
+
|
|
574
|
+
const keyDownCall = mockCdp.send.mock.calls[0].arguments;
|
|
575
|
+
assert.strictEqual(keyDownCall[0], 'Input.dispatchKeyEvent');
|
|
576
|
+
assert.strictEqual(keyDownCall[1].key, 'a');
|
|
577
|
+
assert.strictEqual(keyDownCall[1].modifiers, 2); // ctrl = 2
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should parse and press Meta+c', async () => {
|
|
581
|
+
await input.pressCombo('Meta+c');
|
|
582
|
+
|
|
583
|
+
const keyDownCall = mockCdp.send.mock.calls[0].arguments;
|
|
584
|
+
assert.strictEqual(keyDownCall[1].key, 'c');
|
|
585
|
+
assert.strictEqual(keyDownCall[1].modifiers, 4); // meta = 4
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('should parse complex combos like Control+Shift+Enter', async () => {
|
|
589
|
+
await input.pressCombo('Control+Shift+Enter');
|
|
590
|
+
|
|
591
|
+
const keyDownCall = mockCdp.send.mock.calls[0].arguments;
|
|
592
|
+
assert.strictEqual(keyDownCall[1].key, 'Enter');
|
|
593
|
+
assert.strictEqual(keyDownCall[1].modifiers, 10); // ctrl=2 + shift=8 = 10
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('should handle Alt modifier', async () => {
|
|
597
|
+
await input.pressCombo('Alt+Tab');
|
|
598
|
+
|
|
599
|
+
const keyDownCall = mockCdp.send.mock.calls[0].arguments;
|
|
600
|
+
assert.strictEqual(keyDownCall[1].key, 'Tab');
|
|
601
|
+
assert.strictEqual(keyDownCall[1].modifiers, 1); // alt = 1
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('should throw for combo without main key', async () => {
|
|
605
|
+
await assert.rejects(
|
|
606
|
+
() => input.pressCombo('Control+Shift'),
|
|
607
|
+
/no main key/
|
|
608
|
+
);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
describe('parseKeyCombo', () => {
|
|
613
|
+
it('should parse simple combo', () => {
|
|
614
|
+
const { key, modifiers } = input.parseKeyCombo('Control+a');
|
|
615
|
+
assert.strictEqual(key, 'a');
|
|
616
|
+
assert.strictEqual(modifiers.ctrl, true);
|
|
617
|
+
assert.strictEqual(modifiers.shift, false);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should parse Cmd alias for Meta', () => {
|
|
621
|
+
const { key, modifiers } = input.parseKeyCombo('Cmd+v');
|
|
622
|
+
assert.strictEqual(key, 'v');
|
|
623
|
+
assert.strictEqual(modifiers.meta, true);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('should parse Ctrl alias for Control', () => {
|
|
627
|
+
const { key, modifiers } = input.parseKeyCombo('Ctrl+z');
|
|
628
|
+
assert.strictEqual(key, 'z');
|
|
629
|
+
assert.strictEqual(modifiers.ctrl, true);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('should parse all modifiers', () => {
|
|
633
|
+
const { key, modifiers } = input.parseKeyCombo('Control+Alt+Shift+Meta+x');
|
|
634
|
+
assert.strictEqual(key, 'x');
|
|
635
|
+
assert.strictEqual(modifiers.ctrl, true);
|
|
636
|
+
assert.strictEqual(modifiers.alt, true);
|
|
637
|
+
assert.strictEqual(modifiers.shift, true);
|
|
638
|
+
assert.strictEqual(modifiers.meta, true);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
});
|