screenci 0.0.4 → 0.0.6

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.
Files changed (94) hide show
  1. package/README.md +227 -0
  2. package/cli.ts +1111 -0
  3. package/dist/cli.d.ts +4 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +896 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/e2e/instrument.e2e.d.ts +2 -0
  8. package/dist/e2e/instrument.e2e.d.ts.map +1 -0
  9. package/dist/e2e/instrument.e2e.js +661 -0
  10. package/dist/e2e/instrument.e2e.js.map +1 -0
  11. package/dist/index.d.ts +18 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +15 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/playwright.config.d.ts +3 -0
  16. package/dist/playwright.config.d.ts.map +1 -0
  17. package/dist/playwright.config.js +21 -0
  18. package/dist/playwright.config.js.map +1 -0
  19. package/dist/reporter.d.ts +9 -0
  20. package/dist/reporter.d.ts.map +1 -0
  21. package/dist/reporter.js +49 -0
  22. package/dist/reporter.js.map +1 -0
  23. package/dist/src/asset.d.ts +90 -0
  24. package/dist/src/asset.d.ts.map +1 -0
  25. package/dist/src/asset.js +74 -0
  26. package/dist/src/asset.js.map +1 -0
  27. package/dist/src/autoZoom.d.ts +40 -0
  28. package/dist/src/autoZoom.d.ts.map +1 -0
  29. package/dist/src/autoZoom.js +88 -0
  30. package/dist/src/autoZoom.js.map +1 -0
  31. package/dist/src/caption.d.ts +152 -0
  32. package/dist/src/caption.d.ts.map +1 -0
  33. package/dist/src/caption.js +240 -0
  34. package/dist/src/caption.js.map +1 -0
  35. package/dist/src/caption.test-d.d.ts +2 -0
  36. package/dist/src/caption.test-d.d.ts.map +1 -0
  37. package/dist/src/caption.test-d.js +50 -0
  38. package/dist/src/caption.test-d.js.map +1 -0
  39. package/dist/src/config.d.ts +42 -0
  40. package/dist/src/config.d.ts.map +1 -0
  41. package/dist/src/config.js +147 -0
  42. package/dist/src/config.js.map +1 -0
  43. package/dist/src/defaults.d.ts +63 -0
  44. package/dist/src/defaults.d.ts.map +1 -0
  45. package/dist/src/defaults.js +66 -0
  46. package/dist/src/defaults.js.map +1 -0
  47. package/dist/src/dimensions.d.ts +29 -0
  48. package/dist/src/dimensions.d.ts.map +1 -0
  49. package/dist/src/dimensions.js +47 -0
  50. package/dist/src/dimensions.js.map +1 -0
  51. package/dist/src/events.d.ts +203 -0
  52. package/dist/src/events.d.ts.map +1 -0
  53. package/dist/src/events.js +227 -0
  54. package/dist/src/events.js.map +1 -0
  55. package/dist/src/hide.d.ts +27 -0
  56. package/dist/src/hide.d.ts.map +1 -0
  57. package/dist/src/hide.js +49 -0
  58. package/dist/src/hide.js.map +1 -0
  59. package/dist/src/instrument.d.ts +15 -0
  60. package/dist/src/instrument.d.ts.map +1 -0
  61. package/dist/src/instrument.js +910 -0
  62. package/dist/src/instrument.js.map +1 -0
  63. package/dist/src/logger.d.ts +7 -0
  64. package/dist/src/logger.d.ts.map +1 -0
  65. package/dist/src/logger.js +13 -0
  66. package/dist/src/logger.js.map +1 -0
  67. package/dist/src/reporter.d.ts +9 -0
  68. package/dist/src/reporter.d.ts.map +1 -0
  69. package/dist/src/reporter.js +50 -0
  70. package/dist/src/reporter.js.map +1 -0
  71. package/dist/src/sanitize.d.ts +5 -0
  72. package/dist/src/sanitize.d.ts.map +1 -0
  73. package/dist/src/sanitize.js +11 -0
  74. package/dist/src/sanitize.js.map +1 -0
  75. package/dist/src/types.d.ts +544 -0
  76. package/dist/src/types.d.ts.map +1 -0
  77. package/dist/src/types.js +2 -0
  78. package/dist/src/types.js.map +1 -0
  79. package/dist/src/video.d.ts +138 -0
  80. package/dist/src/video.d.ts.map +1 -0
  81. package/dist/src/video.js +415 -0
  82. package/dist/src/video.js.map +1 -0
  83. package/dist/src/voices.d.ts +60 -0
  84. package/dist/src/voices.d.ts.map +1 -0
  85. package/dist/src/voices.js +42 -0
  86. package/dist/src/voices.js.map +1 -0
  87. package/dist/src/xvfb.d.ts +22 -0
  88. package/dist/src/xvfb.d.ts.map +1 -0
  89. package/dist/src/xvfb.js +87 -0
  90. package/dist/src/xvfb.js.map +1 -0
  91. package/dist/tsconfig.tsbuildinfo +1 -0
  92. package/package.json +45 -4
  93. package/bin/index.js +0 -3
  94. package/index.js +0 -1
@@ -0,0 +1,661 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { readFileSync } from 'fs';
3
+ import { resolve, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { instrumentPage, setActiveClickRecorder } from '../src/instrument.js';
6
+ import { EventRecorder } from '../src/events.js';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const fixtureHtml = readFileSync(resolve(__dirname, 'fixtures/index.html'), 'utf-8');
9
+ function checkLocator(locator) {
10
+ return locator;
11
+ }
12
+ function uncheckLocator(locator) {
13
+ return locator;
14
+ }
15
+ function tapLocator(locator) {
16
+ return locator;
17
+ }
18
+ function clickableLocator(locator) {
19
+ return locator;
20
+ }
21
+ function fillableLocator(locator) {
22
+ return locator;
23
+ }
24
+ function typeableLocator(locator) {
25
+ return locator;
26
+ }
27
+ function selectableLocator(locator) {
28
+ return locator;
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // Shared setup helpers
32
+ // ---------------------------------------------------------------------------
33
+ let recorder;
34
+ function inputEvents() {
35
+ return recorder.getEvents().filter((e) => e.type === 'input');
36
+ }
37
+ function clickEvents() {
38
+ return recorder
39
+ .getEvents()
40
+ .filter((e) => e.type === 'input' && e.subType === 'click');
41
+ }
42
+ function mouseMoveEvents() {
43
+ return inputEvents()
44
+ .filter((e) => e.subType === 'mouseMove')
45
+ .flatMap((e) => e.events.filter((ie) => ie.type === 'mouseMove'));
46
+ }
47
+ function mouseHideEventsIn(event) {
48
+ return event.events.filter((e) => e.type === 'mouseHide');
49
+ }
50
+ async function scrollY(page) {
51
+ return page.evaluate(() => window.scrollY);
52
+ }
53
+ test.beforeEach(async ({ page }) => {
54
+ await instrumentPage(page);
55
+ await page.setContent(fixtureHtml);
56
+ recorder = new EventRecorder();
57
+ recorder.start();
58
+ setActiveClickRecorder(recorder);
59
+ });
60
+ test.afterEach(() => {
61
+ setActiveClickRecorder(null);
62
+ });
63
+ // ---------------------------------------------------------------------------
64
+ // click
65
+ // ---------------------------------------------------------------------------
66
+ test.describe('click instrumentation', () => {
67
+ test('records a click event', async ({ page }) => {
68
+ await clickableLocator(page.locator('#click-button')).click({
69
+ moveDuration: 50,
70
+ });
71
+ const events = clickEvents();
72
+ expect(events).toHaveLength(1);
73
+ const [event] = events;
74
+ const move = event.events.find((e) => e.type === 'mouseMove');
75
+ const down = event.events.find((e) => e.type === 'mouseDown');
76
+ const up = event.events.find((e) => e.type === 'mouseUp');
77
+ expect(move.x).toBeGreaterThan(0);
78
+ expect(move.y).toBeGreaterThan(0);
79
+ expect(move.endMs).toBeGreaterThanOrEqual(move.startMs);
80
+ expect(up.endMs).toBeGreaterThanOrEqual(down.startMs);
81
+ });
82
+ test('actually clicks the button', async ({ page }) => {
83
+ await clickableLocator(page.locator('#click-button')).click({
84
+ moveDuration: 50,
85
+ });
86
+ await expect(page.locator('#click-status')).not.toHaveText('Not clicked');
87
+ });
88
+ test('records elementRect', async ({ page }) => {
89
+ await clickableLocator(page.locator('#click-button')).click({
90
+ moveDuration: 50,
91
+ });
92
+ const events = clickEvents();
93
+ const [event] = events;
94
+ expect(event.elementRect).toBeDefined();
95
+ expect(event.elementRect.width).toBeGreaterThan(0);
96
+ expect(event.elementRect.height).toBeGreaterThan(0);
97
+ });
98
+ test('scrolls into view before clicking off-screen element', async ({ page, }) => {
99
+ expect(await scrollY(page)).toBe(0);
100
+ await clickableLocator(page.locator('#offscreen-click-button')).click({
101
+ moveDuration: 50,
102
+ });
103
+ expect(await scrollY(page)).toBeGreaterThan(0);
104
+ });
105
+ test('actually clicks an off-screen button after scrolling', async ({ page, }) => {
106
+ await clickableLocator(page.locator('#offscreen-click-button')).click({
107
+ moveDuration: 50,
108
+ });
109
+ await expect(page.locator('#offscreen-click-status')).not.toHaveText('Not clicked');
110
+ });
111
+ });
112
+ // ---------------------------------------------------------------------------
113
+ // fill
114
+ // ---------------------------------------------------------------------------
115
+ test.describe('fill instrumentation', () => {
116
+ test('records an input event with subType pressSequentially', async ({ page, }) => {
117
+ await fillableLocator(page.locator('#text-input')).fill('hi', {
118
+ duration: 100,
119
+ });
120
+ const events = inputEvents();
121
+ expect(events).toHaveLength(1);
122
+ const [event] = events;
123
+ expect(event.subType).toBe('pressSequentially');
124
+ });
125
+ test('actually fills the input', async ({ page }) => {
126
+ await fillableLocator(page.locator('#text-input')).fill('hello', {
127
+ duration: 100,
128
+ });
129
+ await expect(page.locator('#text-input')).toHaveValue('hello');
130
+ });
131
+ test('records elementRect', async ({ page }) => {
132
+ await fillableLocator(page.locator('#text-input')).fill('x', {
133
+ duration: 50,
134
+ });
135
+ const events = inputEvents();
136
+ const [event] = events;
137
+ expect(event.elementRect).toBeDefined();
138
+ expect(event.elementRect.width).toBeGreaterThan(0);
139
+ expect(event.elementRect.height).toBeGreaterThan(0);
140
+ });
141
+ test('scrolls into view before filling off-screen input', async ({ page, }) => {
142
+ expect(await scrollY(page)).toBe(0);
143
+ await fillableLocator(page.locator('#offscreen-text-input')).fill('scroll test', { duration: 100 });
144
+ expect(await scrollY(page)).toBeGreaterThan(0);
145
+ });
146
+ test('actually fills an off-screen input after scrolling', async ({ page, }) => {
147
+ await fillableLocator(page.locator('#offscreen-text-input')).fill('works', {
148
+ duration: 100,
149
+ });
150
+ await expect(page.locator('#offscreen-text-input')).toHaveValue('works');
151
+ });
152
+ test('with click option: records click sub-events', async ({ page }) => {
153
+ await fillableLocator(page.locator('#text-input')).fill('hi', {
154
+ duration: 100,
155
+ click: { moveDuration: 50 },
156
+ });
157
+ const events = inputEvents();
158
+ expect(events).toHaveLength(1);
159
+ const [event] = events;
160
+ expect(event.subType).toBe('pressSequentially');
161
+ const move = event.events.find((e) => e.type === 'mouseMove');
162
+ const down = event.events.find((e) => e.type === 'mouseDown');
163
+ const up = event.events.find((e) => e.type === 'mouseUp');
164
+ expect(move).toBeDefined();
165
+ expect(move.x).toBeGreaterThan(0);
166
+ expect(move.endMs).toBeGreaterThanOrEqual(move.startMs);
167
+ expect(up.endMs).toBeGreaterThanOrEqual(down.startMs);
168
+ });
169
+ test('hideMouse: true hides the cursor during fill', async ({ page }) => {
170
+ await fillableLocator(page.locator('#text-input')).fill('hi', {
171
+ duration: 100,
172
+ hideMouse: true,
173
+ });
174
+ const events = inputEvents();
175
+ const [event] = events;
176
+ expect(mouseHideEventsIn(event)).toHaveLength(1);
177
+ });
178
+ test('hideMouse: false does not hide the cursor during fill', async ({ page, }) => {
179
+ await fillableLocator(page.locator('#text-input')).fill('hi', {
180
+ duration: 100,
181
+ hideMouse: false,
182
+ });
183
+ const events = inputEvents();
184
+ const [event] = events;
185
+ expect(mouseHideEventsIn(event)).toHaveLength(0);
186
+ });
187
+ test('does not hide the cursor by default', async ({ page }) => {
188
+ await fillableLocator(page.locator('#text-input')).fill('hi', {
189
+ duration: 100,
190
+ });
191
+ const events = inputEvents();
192
+ const [event] = events;
193
+ expect(mouseHideEventsIn(event)).toHaveLength(0);
194
+ });
195
+ });
196
+ // ---------------------------------------------------------------------------
197
+ // pressSequentially
198
+ // ---------------------------------------------------------------------------
199
+ test.describe('pressSequentially instrumentation', () => {
200
+ test('records an input event with subType pressSequentially', async ({ page, }) => {
201
+ await page.locator('#text-input').pressSequentially('hi', { delay: 30 });
202
+ const events = inputEvents();
203
+ expect(events).toHaveLength(1);
204
+ const [event] = events;
205
+ expect(event.subType).toBe('pressSequentially');
206
+ });
207
+ test('actually types into the input', async ({ page }) => {
208
+ await page.locator('#text-input').pressSequentially('abc', { delay: 30 });
209
+ await expect(page.locator('#text-input')).toHaveValue('abc');
210
+ });
211
+ test('records elementRect', async ({ page }) => {
212
+ await page.locator('#text-input').pressSequentially('x', { delay: 30 });
213
+ const events = inputEvents();
214
+ const [event] = events;
215
+ expect(event.elementRect).toBeDefined();
216
+ expect(event.elementRect.width).toBeGreaterThan(0);
217
+ });
218
+ test('scrolls into view before typing in off-screen input', async ({ page, }) => {
219
+ expect(await scrollY(page)).toBe(0);
220
+ await page
221
+ .locator('#offscreen-text-input')
222
+ .pressSequentially('scroll', { delay: 30 });
223
+ expect(await scrollY(page)).toBeGreaterThan(0);
224
+ });
225
+ test('actually types into an off-screen input after scrolling', async ({ page, }) => {
226
+ await page
227
+ .locator('#offscreen-text-input')
228
+ .pressSequentially('done', { delay: 30 });
229
+ await expect(page.locator('#offscreen-text-input')).toHaveValue('done');
230
+ });
231
+ test('with click option: records click sub-events', async ({ page }) => {
232
+ await typeableLocator(page.locator('#text-input')).pressSequentially('hi', {
233
+ delay: 30,
234
+ click: { moveDuration: 50 },
235
+ });
236
+ const events = inputEvents();
237
+ expect(events).toHaveLength(1);
238
+ const [event] = events;
239
+ expect(event.subType).toBe('pressSequentially');
240
+ const move = event.events.find((e) => e.type === 'mouseMove');
241
+ expect(move).toBeDefined();
242
+ expect(move.endMs).toBeGreaterThanOrEqual(move.startMs);
243
+ });
244
+ test('hideMouse: true hides the cursor during pressSequentially', async ({ page, }) => {
245
+ await typeableLocator(page.locator('#text-input')).pressSequentially('hi', {
246
+ delay: 30,
247
+ hideMouse: true,
248
+ });
249
+ const events = inputEvents();
250
+ const [event] = events;
251
+ expect(mouseHideEventsIn(event)).toHaveLength(1);
252
+ });
253
+ test('hideMouse: false does not hide the cursor during pressSequentially', async ({ page, }) => {
254
+ await typeableLocator(page.locator('#text-input')).pressSequentially('hi', {
255
+ delay: 30,
256
+ hideMouse: false,
257
+ });
258
+ const events = inputEvents();
259
+ const [event] = events;
260
+ expect(mouseHideEventsIn(event)).toHaveLength(0);
261
+ });
262
+ test('does not hide the cursor by default', async ({ page }) => {
263
+ await page.locator('#text-input').pressSequentially('hi', { delay: 30 });
264
+ const events = inputEvents();
265
+ const [event] = events;
266
+ expect(mouseHideEventsIn(event)).toHaveLength(0);
267
+ });
268
+ });
269
+ // ---------------------------------------------------------------------------
270
+ // check
271
+ // ---------------------------------------------------------------------------
272
+ test.describe('check instrumentation', () => {
273
+ test('records an input event with subType check', async ({ page }) => {
274
+ await checkLocator(page.locator('#checkbox-unchecked')).check();
275
+ const events = inputEvents();
276
+ expect(events).toHaveLength(1);
277
+ const [event] = events;
278
+ expect(event.subType).toBe('check');
279
+ });
280
+ test('actually checks the checkbox', async ({ page }) => {
281
+ await checkLocator(page.locator('#checkbox-unchecked')).check();
282
+ await expect(page.locator('#checkbox-unchecked')).toBeChecked();
283
+ });
284
+ test('records elementRect', async ({ page }) => {
285
+ await checkLocator(page.locator('#checkbox-unchecked')).check();
286
+ const events = inputEvents();
287
+ const [event] = events;
288
+ expect(event.elementRect).toBeDefined();
289
+ expect(event.elementRect.width).toBeGreaterThan(0);
290
+ expect(event.elementRect.height).toBeGreaterThan(0);
291
+ });
292
+ test('scrolls into view before checking off-screen checkbox', async ({ page, }) => {
293
+ expect(await scrollY(page)).toBe(0);
294
+ await checkLocator(page.locator('#offscreen-checkbox-unchecked')).check();
295
+ expect(await scrollY(page)).toBeGreaterThan(0);
296
+ });
297
+ test('actually checks an off-screen checkbox after scrolling', async ({ page, }) => {
298
+ await checkLocator(page.locator('#offscreen-checkbox-unchecked')).check();
299
+ await expect(page.locator('#offscreen-checkbox-unchecked')).toBeChecked();
300
+ });
301
+ test('with click option: animates cursor and records sub-events', async ({ page, }) => {
302
+ await checkLocator(page.locator('#checkbox-unchecked')).check({
303
+ click: { moveDuration: 100 },
304
+ });
305
+ const events = inputEvents();
306
+ expect(events).toHaveLength(1);
307
+ const [event] = events;
308
+ expect(event.subType).toBe('check');
309
+ const move = event.events.find((e) => e.type === 'mouseMove');
310
+ const down = event.events.find((e) => e.type === 'mouseDown');
311
+ const up = event.events.find((e) => e.type === 'mouseUp');
312
+ expect(move).toBeDefined();
313
+ expect(move.x).toBeGreaterThan(0);
314
+ expect(move.y).toBeGreaterThan(0);
315
+ expect(move.endMs).toBeGreaterThanOrEqual(move.startMs);
316
+ expect(up.endMs).toBeGreaterThanOrEqual(down.startMs);
317
+ });
318
+ test('with click option: actually checks the checkbox', async ({ page }) => {
319
+ await checkLocator(page.locator('#checkbox-unchecked')).check({
320
+ click: { moveDuration: 50 },
321
+ });
322
+ await expect(page.locator('#checkbox-unchecked')).toBeChecked();
323
+ });
324
+ });
325
+ // ---------------------------------------------------------------------------
326
+ // uncheck
327
+ // ---------------------------------------------------------------------------
328
+ test.describe('uncheck instrumentation', () => {
329
+ test('records an input event with subType uncheck', async ({ page }) => {
330
+ await uncheckLocator(page.locator('#checkbox-checked')).uncheck();
331
+ const events = inputEvents();
332
+ expect(events).toHaveLength(1);
333
+ const [event] = events;
334
+ expect(event.subType).toBe('uncheck');
335
+ });
336
+ test('actually unchecks the checkbox', async ({ page }) => {
337
+ await uncheckLocator(page.locator('#checkbox-checked')).uncheck();
338
+ await expect(page.locator('#checkbox-checked')).not.toBeChecked();
339
+ });
340
+ test('records elementRect', async ({ page }) => {
341
+ await uncheckLocator(page.locator('#checkbox-checked')).uncheck();
342
+ const events = inputEvents();
343
+ const [event] = events;
344
+ expect(event.elementRect).toBeDefined();
345
+ expect(event.elementRect.width).toBeGreaterThan(0);
346
+ });
347
+ test('scrolls into view before unchecking off-screen checkbox', async ({ page, }) => {
348
+ expect(await scrollY(page)).toBe(0);
349
+ await uncheckLocator(page.locator('#offscreen-checkbox-checked')).uncheck();
350
+ expect(await scrollY(page)).toBeGreaterThan(0);
351
+ });
352
+ test('actually unchecks an off-screen checkbox after scrolling', async ({ page, }) => {
353
+ await uncheckLocator(page.locator('#offscreen-checkbox-checked')).uncheck();
354
+ await expect(page.locator('#offscreen-checkbox-checked')).not.toBeChecked();
355
+ });
356
+ test('with click option: animates cursor and records sub-events', async ({ page, }) => {
357
+ await uncheckLocator(page.locator('#checkbox-checked')).uncheck({
358
+ click: { moveDuration: 100 },
359
+ });
360
+ const events = inputEvents();
361
+ expect(events).toHaveLength(1);
362
+ const [event] = events;
363
+ expect(event.subType).toBe('uncheck');
364
+ const move = event.events.find((e) => e.type === 'mouseMove');
365
+ expect(move).toBeDefined();
366
+ expect(move.endMs).toBeGreaterThanOrEqual(move.startMs);
367
+ });
368
+ test('with click option: actually unchecks the checkbox', async ({ page, }) => {
369
+ await uncheckLocator(page.locator('#checkbox-checked')).uncheck({
370
+ click: { moveDuration: 50 },
371
+ });
372
+ await expect(page.locator('#checkbox-checked')).not.toBeChecked();
373
+ });
374
+ });
375
+ // ---------------------------------------------------------------------------
376
+ // tap
377
+ // ---------------------------------------------------------------------------
378
+ test.describe('tap instrumentation', () => {
379
+ test('records an input event with subType tap', async ({ page }) => {
380
+ await tapLocator(page.locator('#tap-target')).tap();
381
+ const events = inputEvents();
382
+ expect(events).toHaveLength(1);
383
+ const [event] = events;
384
+ expect(event.subType).toBe('tap');
385
+ });
386
+ test('actually triggers the element interaction', async ({ page }) => {
387
+ await tapLocator(page.locator('#tap-target')).tap();
388
+ // The page's pointerup listener updates #tap-status when tapped
389
+ await expect(page.locator('#tap-status')).not.toHaveText('Not yet tapped');
390
+ });
391
+ test('records elementRect', async ({ page }) => {
392
+ await tapLocator(page.locator('#tap-target')).tap();
393
+ const events = inputEvents();
394
+ const [event] = events;
395
+ expect(event.elementRect).toBeDefined();
396
+ expect(event.elementRect.width).toBeGreaterThan(0);
397
+ expect(event.elementRect.height).toBeGreaterThan(0);
398
+ });
399
+ test('scrolls into view before tapping off-screen element', async ({ page, }) => {
400
+ expect(await scrollY(page)).toBe(0);
401
+ await tapLocator(page.locator('#offscreen-tap-target')).tap();
402
+ expect(await scrollY(page)).toBeGreaterThan(0);
403
+ });
404
+ test('actually taps an off-screen element after scrolling', async ({ page, }) => {
405
+ await tapLocator(page.locator('#offscreen-tap-target')).tap();
406
+ await expect(page.locator('#offscreen-tap-status')).not.toHaveText('Not yet tapped');
407
+ });
408
+ test('with click option: animates cursor and records sub-events', async ({ page, }) => {
409
+ await tapLocator(page.locator('#tap-target')).tap({
410
+ click: { moveDuration: 100 },
411
+ });
412
+ const events = inputEvents();
413
+ expect(events).toHaveLength(1);
414
+ const [event] = events;
415
+ expect(event.subType).toBe('tap');
416
+ // tap doesn't fire a DOM click, so click sub-events use fallback bounding box coords
417
+ const move = event.events.find((e) => e.type === 'mouseMove');
418
+ expect(move).toBeDefined();
419
+ expect(move.x).toBeGreaterThan(0);
420
+ expect(move.y).toBeGreaterThan(0);
421
+ expect(event.elementRect.width).toBeGreaterThan(0);
422
+ expect(move.endMs).toBeGreaterThanOrEqual(move.startMs);
423
+ });
424
+ test('with click option: actually triggers the element interaction', async ({ page, }) => {
425
+ await tapLocator(page.locator('#tap-target')).tap({
426
+ click: { moveDuration: 50 },
427
+ });
428
+ await expect(page.locator('#tap-status')).not.toHaveText('Not yet tapped');
429
+ });
430
+ });
431
+ // ---------------------------------------------------------------------------
432
+ // selectOption
433
+ // ---------------------------------------------------------------------------
434
+ test.describe('selectOption instrumentation', () => {
435
+ test('records an input event with subType select', async ({ page }) => {
436
+ await selectableLocator(page.locator('#cars')).selectOption('audi');
437
+ const events = inputEvents();
438
+ expect(events).toHaveLength(1);
439
+ const [event] = events;
440
+ expect(event.subType).toBe('select');
441
+ });
442
+ test('actually selects the option', async ({ page }) => {
443
+ await selectableLocator(page.locator('#cars')).selectOption('audi');
444
+ await expect(page.locator('#cars')).toHaveValue('audi');
445
+ });
446
+ test('records elementRect', async ({ page }) => {
447
+ await selectableLocator(page.locator('#cars')).selectOption('saab');
448
+ const events = inputEvents();
449
+ const [event] = events;
450
+ expect(event.elementRect).toBeDefined();
451
+ expect(event.elementRect.width).toBeGreaterThan(0);
452
+ expect(event.elementRect.height).toBeGreaterThan(0);
453
+ });
454
+ test('scrolls into view before selecting off-screen option', async ({ page, }) => {
455
+ expect(await scrollY(page)).toBe(0);
456
+ await selectableLocator(page.locator('#offscreen-cars')).selectOption('opel');
457
+ expect(await scrollY(page)).toBeGreaterThan(0);
458
+ });
459
+ test('actually selects an off-screen option after scrolling', async ({ page, }) => {
460
+ await selectableLocator(page.locator('#offscreen-cars')).selectOption('volvo');
461
+ await expect(page.locator('#offscreen-cars')).toHaveValue('volvo');
462
+ });
463
+ test('with click option: animates cursor and records sub-events', async ({ page, }) => {
464
+ await selectableLocator(page.locator('#cars')).selectOption('audi', {
465
+ click: { moveDuration: 100 },
466
+ });
467
+ const events = inputEvents();
468
+ expect(events).toHaveLength(1);
469
+ const [event] = events;
470
+ expect(event.subType).toBe('select');
471
+ const move = event.events.find((e) => e.type === 'mouseMove');
472
+ const down = event.events.find((e) => e.type === 'mouseDown');
473
+ const up = event.events.find((e) => e.type === 'mouseUp');
474
+ expect(move).toBeDefined();
475
+ expect(move.x).toBeGreaterThan(0);
476
+ expect(move.y).toBeGreaterThan(0);
477
+ expect(event.elementRect.width).toBeGreaterThan(0);
478
+ expect(move.endMs).toBeGreaterThanOrEqual(move.startMs);
479
+ expect(up.endMs).toBeGreaterThanOrEqual(down.startMs);
480
+ });
481
+ test('with click option: actually selects the option', async ({ page }) => {
482
+ await selectableLocator(page.locator('#cars')).selectOption('saab', {
483
+ click: { moveDuration: 50 },
484
+ });
485
+ await expect(page.locator('#cars')).toHaveValue('saab');
486
+ });
487
+ test('with click option: cursor y is at select center', async ({ page }) => {
488
+ const selectBb = await page.locator('#cars').boundingBox();
489
+ await selectableLocator(page.locator('#cars')).selectOption('audi', {
490
+ click: { moveDuration: 100 },
491
+ });
492
+ const events = inputEvents();
493
+ const [event] = events;
494
+ const move = event.events.find((e) => e.type === 'mouseMove');
495
+ // The click position should be at the select element's center
496
+ expect(move.y).toBeCloseTo(selectBb.y + selectBb.height / 2, 0);
497
+ });
498
+ });
499
+ // ---------------------------------------------------------------------------
500
+ // mouse.move
501
+ // ---------------------------------------------------------------------------
502
+ test.describe('mouse.move instrumentation', () => {
503
+ test('records a mouseMove event with startMs, endMs, x, y', async ({ page, }) => {
504
+ const bb = await page.locator('#click-button').boundingBox();
505
+ const targetX = bb.x + bb.width / 2;
506
+ const targetY = bb.y + bb.height / 2;
507
+ await page.mouse.move(targetX, targetY, {
508
+ duration: 100,
509
+ });
510
+ const events = mouseMoveEvents();
511
+ expect(events).toHaveLength(1);
512
+ const [event] = events;
513
+ expect(event.startMs).toBeGreaterThanOrEqual(0);
514
+ expect(event.endMs).toBeGreaterThanOrEqual(event.startMs);
515
+ expect(event.endMs - event.startMs).toBeGreaterThanOrEqual(100);
516
+ expect(event.x).toBeCloseTo(targetX, 0);
517
+ expect(event.y).toBeCloseTo(targetY, 0);
518
+ });
519
+ test('records easing when duration is provided', async ({ page }) => {
520
+ await page.mouse.move(200, 300, {
521
+ duration: 100,
522
+ easing: 'ease-out',
523
+ });
524
+ const events = mouseMoveEvents();
525
+ expect(events).toHaveLength(1);
526
+ expect(events[0].easing).toBe('ease-out');
527
+ });
528
+ test('records mouseMove without easing for instant move', async ({ page, }) => {
529
+ await page.mouse.move(100, 150);
530
+ const events = mouseMoveEvents();
531
+ expect(events).toHaveLength(1);
532
+ expect(events[0].x).toBe(100);
533
+ expect(events[0].y).toBe(150);
534
+ expect(events[0].easing).toBeUndefined();
535
+ });
536
+ test('cursor ends at target position so subsequent click animates from there', async ({ page, }) => {
537
+ // Move cursor to the button, then click it — the click's moveStartTime
538
+ // should be close to moveEndTime (short travel) since cursor is already there.
539
+ const bb = await page.locator('#click-button').boundingBox();
540
+ const targetX = bb.x + bb.width / 2;
541
+ const targetY = bb.y + bb.height / 2;
542
+ await page.mouse.move(targetX, targetY, {
543
+ duration: 100,
544
+ });
545
+ await clickableLocator(page.locator('#click-button')).click({
546
+ moveDuration: 50,
547
+ });
548
+ const [click] = clickEvents();
549
+ const move = click.events.find((e) => e.type === 'mouseMove');
550
+ // The move should be very short since cursor was already at the target
551
+ expect(move.endMs - move.startMs).toBeLessThan(100);
552
+ });
553
+ });
554
+ // ---------------------------------------------------------------------------
555
+ // hover
556
+ // ---------------------------------------------------------------------------
557
+ function hoverableLocator(locator) {
558
+ return locator;
559
+ }
560
+ test.describe('hover instrumentation', () => {
561
+ test('records a hover event with mouseMove and mouseWait inner events', async ({ page, }) => {
562
+ await hoverableLocator(page.locator('#hover-target')).hover({
563
+ moveDuration: 50,
564
+ hoverDuration: 100,
565
+ });
566
+ const events = inputEvents().filter((e) => e.subType === 'hover');
567
+ expect(events).toHaveLength(1);
568
+ const [event] = events;
569
+ const move = event.events.find((e) => e.type === 'mouseMove');
570
+ const wait = event.events.find((e) => e.type === 'mouseWait');
571
+ expect(move.x).toBeGreaterThan(0);
572
+ expect(move.y).toBeGreaterThan(0);
573
+ expect(move.endMs).toBeGreaterThanOrEqual(move.startMs);
574
+ expect(wait.endMs).toBeGreaterThanOrEqual(wait.startMs);
575
+ });
576
+ test('actually hovers the element', async ({ page }) => {
577
+ await hoverableLocator(page.locator('#hover-target')).hover({
578
+ moveDuration: 50,
579
+ });
580
+ await expect(page.locator('#hover-status')).toHaveText('Hovered!');
581
+ });
582
+ test('records elementRect on hover', async ({ page }) => {
583
+ await hoverableLocator(page.locator('#hover-target')).hover({
584
+ moveDuration: 50,
585
+ });
586
+ const events = inputEvents().filter((e) => e.subType === 'hover');
587
+ const [event] = events;
588
+ expect(event.elementRect).toBeDefined();
589
+ expect(event.elementRect.width).toBeGreaterThan(0);
590
+ expect(event.elementRect.height).toBeGreaterThan(0);
591
+ });
592
+ });
593
+ // ---------------------------------------------------------------------------
594
+ // selectText
595
+ // ---------------------------------------------------------------------------
596
+ function selectTextLocator(locator) {
597
+ return locator;
598
+ }
599
+ test.describe('selectText instrumentation', () => {
600
+ test('records a selectText event with mouseMove and 3 down+up pairs', async ({ page, }) => {
601
+ await selectTextLocator(page.locator('#select-text-input')).selectText({
602
+ moveDuration: 50,
603
+ selectDuration: 60,
604
+ });
605
+ const events = inputEvents().filter((e) => e.subType === 'selectText');
606
+ expect(events).toHaveLength(1);
607
+ const [event] = events;
608
+ expect(event.events.some((e) => e.type === 'mouseMove')).toBe(true);
609
+ expect(event.events.filter((e) => e.type === 'mouseDown')).toHaveLength(3);
610
+ expect(event.events.filter((e) => e.type === 'mouseUp')).toHaveLength(3);
611
+ });
612
+ test('actually selects the text', async ({ page }) => {
613
+ await selectTextLocator(page.locator('#select-text-input')).selectText({
614
+ moveDuration: 50,
615
+ selectDuration: 60,
616
+ });
617
+ await expect(page.locator('#select-text-status')).toHaveText('Selected!');
618
+ });
619
+ test('records elementRect on selectText', async ({ page }) => {
620
+ await selectTextLocator(page.locator('#select-text-input')).selectText({
621
+ moveDuration: 50,
622
+ selectDuration: 60,
623
+ });
624
+ const events = inputEvents().filter((e) => e.subType === 'selectText');
625
+ const [event] = events;
626
+ expect(event.elementRect).toBeDefined();
627
+ expect(event.elementRect.width).toBeGreaterThan(0);
628
+ expect(event.elementRect.height).toBeGreaterThan(0);
629
+ });
630
+ });
631
+ // ---------------------------------------------------------------------------
632
+ // dragTo
633
+ // ---------------------------------------------------------------------------
634
+ function draggableLocator(locator) {
635
+ return locator;
636
+ }
637
+ test.describe('dragTo instrumentation', () => {
638
+ test('records a dragTo event with 2 mouseMoves, mouseDown and mouseUp', async ({ page, }) => {
639
+ await draggableLocator(page.locator('#drag-source')).dragTo(page.locator('#drop-target'), { moveDuration: 50, dragDuration: 50 });
640
+ const events = inputEvents().filter((e) => e.subType === 'dragTo');
641
+ expect(events).toHaveLength(1);
642
+ const [event] = events;
643
+ const moves = event.events.filter((e) => e.type === 'mouseMove');
644
+ expect(moves).toHaveLength(2);
645
+ expect(event.events.some((e) => e.type === 'mouseDown')).toBe(true);
646
+ expect(event.events.some((e) => e.type === 'mouseUp')).toBe(true);
647
+ });
648
+ test('actually drags the element', async ({ page }) => {
649
+ await draggableLocator(page.locator('#drag-source')).dragTo(page.locator('#drop-target'), { moveDuration: 50, dragDuration: 50 });
650
+ await expect(page.locator('#drag-status')).toHaveText('Dropped!');
651
+ });
652
+ test('records elementRect on dragTo', async ({ page }) => {
653
+ await draggableLocator(page.locator('#drag-source')).dragTo(page.locator('#drop-target'), { moveDuration: 50, dragDuration: 50 });
654
+ const events = inputEvents().filter((e) => e.subType === 'dragTo');
655
+ const [event] = events;
656
+ expect(event.elementRect).toBeDefined();
657
+ expect(event.elementRect.width).toBeGreaterThan(0);
658
+ expect(event.elementRect.height).toBeGreaterThan(0);
659
+ });
660
+ });
661
+ //# sourceMappingURL=instrument.e2e.js.map