screenci 0.0.4 → 0.0.5

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,910 @@
1
+ import { logger } from './logger.js';
2
+ import { isInsideAutoZoom, getZoomDuration, getPostZoomInOutDelay, getLastZoomLocation, setLastZoomLocation, } from './autoZoom.js';
3
+ let activeClickRecorder = null;
4
+ export function setActiveClickRecorder(recorder) {
5
+ activeClickRecorder = recorder;
6
+ }
7
+ /**
8
+ * Evaluate a polynomial easing function at normalized time t ∈ [0, 1].
9
+ * Returns the eased progress value (0–1).
10
+ */
11
+ function evaluateEasingAtT(t, easing) {
12
+ if (t <= 0)
13
+ return 0;
14
+ if (t >= 1)
15
+ return 1;
16
+ switch (easing) {
17
+ case 'linear':
18
+ return t;
19
+ case 'ease-in':
20
+ return t * t * t;
21
+ case 'ease-out':
22
+ return 1 - (1 - t) * (1 - t) * (1 - t);
23
+ case 'ease-in-out':
24
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
25
+ case 'ease-in-strong':
26
+ return t * t * t * t;
27
+ case 'ease-out-strong':
28
+ return 1 - (1 - t) * (1 - t) * (1 - t) * (1 - t);
29
+ case 'ease-in-out-strong':
30
+ return t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
31
+ default: {
32
+ const _ = easing;
33
+ throw new Error(`Unknown easing: ${_}`);
34
+ }
35
+ }
36
+ }
37
+ /** Tracked cursor position per page, updated after each animated move. */
38
+ const mousePositions = new WeakMap();
39
+ /** Tracks cursor visibility per page (true = visible). Defaults to true. */
40
+ const mouseVisibilities = new WeakMap();
41
+ /** Stores the original (un-instrumented) mouse.move per page so internal
42
+ * cursor animations don't emit addInput recorder events. */
43
+ const originalMouseMoves = new WeakMap();
44
+ const instrumented = new WeakSet();
45
+ function sleep(ms) {
46
+ return new Promise((resolve) => setTimeout(resolve, ms));
47
+ }
48
+ const CLICK_DURATION_MS = 200;
49
+ const LOCATOR_RETURN_METHODS = [
50
+ 'locator',
51
+ 'getByAltText',
52
+ 'getByLabel',
53
+ 'getByPlaceholder',
54
+ 'getByRole',
55
+ 'getByTestId',
56
+ 'getByText',
57
+ 'getByTitle',
58
+ ];
59
+ const LOCATOR_ONLY_SYNC_RETURN_METHODS = [
60
+ 'and',
61
+ 'describe',
62
+ 'filter',
63
+ 'first',
64
+ 'last',
65
+ 'nth',
66
+ 'or',
67
+ ];
68
+ const FRAME_LOCATOR_LOCATOR_RETURN_METHODS = [
69
+ 'locator',
70
+ 'getByAltText',
71
+ 'getByLabel',
72
+ 'getByPlaceholder',
73
+ 'getByRole',
74
+ 'getByTestId',
75
+ 'getByText',
76
+ 'getByTitle',
77
+ 'owner',
78
+ ];
79
+ const FRAME_LOCATOR_SELF_RETURN_METHODS = [
80
+ 'frameLocator',
81
+ 'first',
82
+ 'last',
83
+ 'nth',
84
+ ];
85
+ // Per-page storage for the most recently captured DOM click event data.
86
+ // Reset to null before each instrumented click; set by the exposeFunction callback.
87
+ const pendingClickData = new WeakMap();
88
+ export function scrollIntoViewAsync(locator, options = {}) {
89
+ const { behavior = 'smooth', block = 'center', timeout = 5000, postScrollTimeout = 500, } = options;
90
+ return locator.evaluate((element, opts) => new Promise((resolve) => {
91
+ let settled = false;
92
+ const finish = () => {
93
+ if (settled)
94
+ return;
95
+ settled = true;
96
+ clearTimeout(fallback);
97
+ setTimeout(resolve, opts.postScrollTimeout);
98
+ };
99
+ const fallback = setTimeout(finish, opts.timeout);
100
+ // scrollend fires once the smooth-scroll animation completes (Chrome 114+).
101
+ // It captures scroll on any ancestor element via the capture phase.
102
+ window.addEventListener('scrollend', finish, {
103
+ capture: true,
104
+ once: true,
105
+ });
106
+ element.scrollIntoView({ behavior: opts.behavior, block: opts.block });
107
+ // If the element is already in the target position no scroll occurs and
108
+ // scrollend never fires. Detect this: if no scroll event appears within
109
+ // two animation frames the element was already in place.
110
+ let didScroll = false;
111
+ window.addEventListener('scroll', () => {
112
+ didScroll = true;
113
+ }, {
114
+ capture: true,
115
+ passive: true,
116
+ once: true,
117
+ });
118
+ requestAnimationFrame(() => requestAnimationFrame(() => {
119
+ if (!didScroll)
120
+ finish();
121
+ }));
122
+ }), { behavior, block, timeout, postScrollTimeout });
123
+ }
124
+ /**
125
+ * Scrolls the locator into view only if it is at least partially outside the
126
+ * viewport, then returns the (possibly updated) bounding box as an ElementRect.
127
+ * Returns undefined if the bounding box cannot be determined.
128
+ */
129
+ async function scrollIntoViewIfNeeded(locator) {
130
+ const page = locator.page();
131
+ const bb = await locator.boundingBox();
132
+ if (!bb)
133
+ return undefined;
134
+ const viewportSize = page.viewportSize();
135
+ if (viewportSize) {
136
+ const isFullyInViewport = bb.x >= 0 &&
137
+ bb.y >= 0 &&
138
+ bb.x + bb.width <= viewportSize.width &&
139
+ bb.y + bb.height <= viewportSize.height;
140
+ if (!isFullyInViewport) {
141
+ await scrollIntoViewAsync(locator);
142
+ const newBb = await locator.boundingBox();
143
+ if (newBb)
144
+ return {
145
+ x: newBb.x,
146
+ y: newBb.y,
147
+ width: newBb.width,
148
+ height: newBb.height,
149
+ };
150
+ }
151
+ }
152
+ else {
153
+ logger.warn('[screenci] Unable to determine viewport size; skipping auto-scroll check.');
154
+ }
155
+ return { x: bb.x, y: bb.y, width: bb.width, height: bb.height };
156
+ }
157
+ const CURSOR_STEP_MS = 1000 / 60;
158
+ /**
159
+ * Physically moves the mouse from its current tracked position to (targetX,
160
+ * targetY) over `duration` ms using `easing`, then returns a MouseMoveEvent
161
+ * whose startMs is `eventStartMs` (which may predate this call, e.g. when a
162
+ * scroll consumed part of moveDuration). When duration ≤ 0 the cursor is
163
+ * snapped directly to the target with a single move call.
164
+ * Always updates the internal mousePositions tracker.
165
+ */
166
+ async function animateMouseMove(page, mouseMoveInternal, targetX, targetY, duration, easing, eventStartMs, elementRect) {
167
+ if (duration > 0) {
168
+ const startPos = mousePositions.get(page) ?? { x: 0, y: 0 };
169
+ const steps = Math.max(1, Math.floor(duration / CURSOR_STEP_MS));
170
+ const stepMs = duration / steps;
171
+ for (let i = 0; i <= steps; i++) {
172
+ const t = i / steps;
173
+ const easedT = evaluateEasingAtT(t, easing);
174
+ const x = startPos.x + easedT * (targetX - startPos.x);
175
+ const y = startPos.y + easedT * (targetY - startPos.y);
176
+ await mouseMoveInternal(x, y);
177
+ if (i < steps) {
178
+ await new Promise((resolve) => setTimeout(resolve, stepMs));
179
+ }
180
+ }
181
+ }
182
+ else {
183
+ await mouseMoveInternal(targetX, targetY);
184
+ }
185
+ mousePositions.set(page, { x: targetX, y: targetY });
186
+ return {
187
+ type: 'mouseMove',
188
+ startMs: eventStartMs,
189
+ endMs: Date.now(),
190
+ x: targetX,
191
+ y: targetY,
192
+ easing,
193
+ ...(elementRect !== undefined ? { elementRect } : {}),
194
+ };
195
+ }
196
+ /**
197
+ * Performs all click mechanics (scroll-check, zoom handling, cursor animation,
198
+ * click, post-click move) and returns the collected timing/position data.
199
+ * Returns null if coordinates could not be determined (no DOM event and no
200
+ * locator bounding box).
201
+ */
202
+ async function performClickActions(locator, doClick, clickOptions, position, moveDuration = 1000, beforeClickPause = CLICK_DURATION_MS / 2, moveEasing = 'ease-in-out', postClickPause = CLICK_DURATION_MS / 2, postClickMove) {
203
+ const page = locator.page();
204
+ pendingClickData.set(page, null);
205
+ const halfClickDuration = CLICK_DURATION_MS / 2;
206
+ const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
207
+ // Capture before any setLastZoomLocation call changes the state.
208
+ const isFirstAutoZoomEvent = isInsideAutoZoom() && getLastZoomLocation() === null;
209
+ const moveStartTime = Date.now();
210
+ const locatorRect = await scrollIntoViewIfNeeded(locator);
211
+ const scrollElapsedMs = Date.now() - moveStartTime;
212
+ if (!locatorRect) {
213
+ logger.warn('[screenci] Unable to get locator bounding box; skipping auto-scroll check.');
214
+ }
215
+ const innerEvents = [];
216
+ // If inside autoZoom: optionally await zoom duration for camera pan then
217
+ // update the zoom location tracker.
218
+ if (isInsideAutoZoom() && locatorRect) {
219
+ const targetX = position
220
+ ? locatorRect.x + position.x
221
+ : locatorRect.x + locatorRect.width / 2;
222
+ const targetY = position
223
+ ? locatorRect.y + position.y
224
+ : locatorRect.y + locatorRect.height / 2;
225
+ const lastLoc = getLastZoomLocation();
226
+ if (lastLoc !== null) {
227
+ const zoomDur = getZoomDuration() ?? 0;
228
+ if (zoomDur > 0) {
229
+ await sleep(zoomDur);
230
+ }
231
+ }
232
+ setLastZoomLocation({
233
+ x: targetX,
234
+ y: targetY,
235
+ elementRect: locatorRect,
236
+ eventType: 'click',
237
+ });
238
+ }
239
+ const targetPos = position
240
+ ? position
241
+ : locatorRect
242
+ ? {
243
+ x: locatorRect.width / 2,
244
+ y: locatorRect.height / 2,
245
+ }
246
+ : undefined;
247
+ // No physical mouse movement happens during the scroll. moveStartTime is
248
+ // captured before the scroll so the recorded event spans scroll-start →
249
+ // animation-end, making the cursor appear to move during the scroll in the
250
+ // video. After the scroll, the remaining duration drives the physical easing
251
+ // animation via the shared helper.
252
+ if (targetPos && locatorRect) {
253
+ const targetX = locatorRect.x + targetPos.x;
254
+ const targetY = locatorRect.y + targetPos.y;
255
+ const effectiveDuration = Math.max(0, moveDuration - scrollElapsedMs);
256
+ innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, effectiveDuration, moveEasing, moveStartTime, locatorRect));
257
+ }
258
+ else {
259
+ const remainingMs = Math.max(0, moveDuration - scrollElapsedMs);
260
+ if (remainingMs > 0) {
261
+ await new Promise((resolve) => setTimeout(resolve, remainingMs));
262
+ }
263
+ }
264
+ const effectiveBeforeClickPause = isFirstAutoZoomEvent
265
+ ? Math.max(beforeClickPause, getPostZoomInOutDelay() ?? 0)
266
+ : beforeClickPause;
267
+ await new Promise((resolve) => setTimeout(resolve, effectiveBeforeClickPause));
268
+ await new Promise((resolve) => setTimeout(resolve, halfClickDuration));
269
+ // Note click can take some time, but better to show it before than after
270
+ const clickTime = Date.now();
271
+ const mouseDownStart = clickTime - halfClickDuration;
272
+ innerEvents.push({
273
+ type: 'mouseDown',
274
+ startMs: mouseDownStart,
275
+ endMs: clickTime,
276
+ easing: 'ease-in-out',
277
+ });
278
+ await doClick({
279
+ ...clickOptions,
280
+ ...(targetPos ? { position: targetPos } : {}),
281
+ });
282
+ const domClickData = pendingClickData.get(page);
283
+ const mouseUpEnd = Date.now() + halfClickDuration;
284
+ innerEvents.push({
285
+ type: 'mouseUp',
286
+ startMs: clickTime,
287
+ endMs: mouseUpEnd,
288
+ easing: 'ease-in-out',
289
+ });
290
+ await new Promise((resolve) => setTimeout(resolve, halfClickDuration));
291
+ await new Promise((resolve) => setTimeout(resolve, postClickPause));
292
+ // Animate mouse cursor in the specified direction after the click completes,
293
+ // capturing start/end times and final position for the recorded event.
294
+ if (postClickMove !== undefined) {
295
+ const currentPos = mousePositions.get(page) ?? { x: 0, y: 0 };
296
+ let targetX;
297
+ let targetY;
298
+ if ('direction' in postClickMove) {
299
+ if (locatorRect === undefined) {
300
+ logger.warn('[screenci] postClickMove with direction requires a locator rect; skipping mouse move.');
301
+ }
302
+ else {
303
+ const padding = postClickMove.padding ?? 0;
304
+ switch (postClickMove.direction) {
305
+ case 'up':
306
+ targetX = currentPos.x;
307
+ targetY = locatorRect.y - padding;
308
+ break;
309
+ case 'down':
310
+ targetX = currentPos.x;
311
+ targetY = locatorRect.y + locatorRect.height + padding;
312
+ break;
313
+ case 'left':
314
+ targetX = locatorRect.x - padding;
315
+ targetY = currentPos.y;
316
+ break;
317
+ case 'right':
318
+ targetX = locatorRect.x + locatorRect.width + padding;
319
+ targetY = currentPos.y;
320
+ break;
321
+ default: {
322
+ const _ = postClickMove.direction;
323
+ throw new Error(`Unknown postClickMove direction: ${_}`);
324
+ }
325
+ }
326
+ }
327
+ }
328
+ else {
329
+ targetX = currentPos.x + postClickMove.x;
330
+ targetY = currentPos.y + postClickMove.y;
331
+ }
332
+ if (targetX !== undefined && targetY !== undefined) {
333
+ const easing = postClickMove.easing ?? 'ease-in-out';
334
+ const { duration } = postClickMove;
335
+ const steps = Math.max(1, Math.floor(duration / CURSOR_STEP_MS));
336
+ const stepMs = duration / steps;
337
+ const postClickMoveStartMs = Date.now();
338
+ const startPos = { ...currentPos };
339
+ for (let i = 0; i <= steps; i++) {
340
+ const t = i / steps;
341
+ const easedT = evaluateEasingAtT(t, easing);
342
+ const x = startPos.x + easedT * (targetX - startPos.x);
343
+ const y = startPos.y + easedT * (targetY - startPos.y);
344
+ await mouseMoveInternal(x, y);
345
+ if (i < steps) {
346
+ await new Promise((resolve) => setTimeout(resolve, stepMs));
347
+ }
348
+ }
349
+ innerEvents.push({
350
+ type: 'mouseMove',
351
+ startMs: postClickMoveStartMs,
352
+ endMs: Date.now(),
353
+ x: targetX,
354
+ y: targetY,
355
+ easing,
356
+ zoomFollow: false,
357
+ });
358
+ mousePositions.set(page, { x: targetX, y: targetY });
359
+ }
360
+ }
361
+ let elementRect;
362
+ if (locatorRect) {
363
+ elementRect = locatorRect;
364
+ }
365
+ else if (domClickData) {
366
+ console.warn('[screenci] using DOM click data as fallback for elementRect');
367
+ elementRect = domClickData.targetRect;
368
+ }
369
+ if (elementRect) {
370
+ return {
371
+ elementRect,
372
+ innerEvents,
373
+ };
374
+ }
375
+ else {
376
+ logger.warn('[screenci] Failed to capture click coordinates from both DOM event and locator bounding box.');
377
+ return null;
378
+ }
379
+ }
380
+ async function performSimpleAction(locator, doAction, options, subType, clickOpt) {
381
+ let innerEvents = [];
382
+ let elementRect;
383
+ if (clickOpt !== undefined) {
384
+ const { moveDuration, beforeClickPause, moveEasing, postClickPause, postClickMove, position, } = clickOpt;
385
+ const clickActionResult = await performClickActions(locator, doAction, {}, position, moveDuration, beforeClickPause, moveEasing, postClickPause, postClickMove);
386
+ innerEvents = clickActionResult?.innerEvents ?? [];
387
+ elementRect = clickActionResult?.elementRect;
388
+ }
389
+ else {
390
+ const isFirstAutoZoomEvent = isInsideAutoZoom() && getLastZoomLocation() === null;
391
+ const locatorRect = await scrollIntoViewIfNeeded(locator);
392
+ if (isInsideAutoZoom() && locatorRect) {
393
+ const targetX = locatorRect.x + locatorRect.width / 2;
394
+ const targetY = locatorRect.y + locatorRect.height / 2;
395
+ const lastLoc = getLastZoomLocation();
396
+ if (lastLoc !== null) {
397
+ const zoomDur = getZoomDuration() ?? 0;
398
+ if (zoomDur > 0) {
399
+ await sleep(zoomDur);
400
+ }
401
+ }
402
+ setLastZoomLocation({
403
+ x: targetX,
404
+ y: targetY,
405
+ elementRect: locatorRect,
406
+ eventType: 'fill',
407
+ });
408
+ }
409
+ if (isFirstAutoZoomEvent) {
410
+ const postDelay = getPostZoomInOutDelay() ?? 0;
411
+ if (postDelay > 0)
412
+ await sleep(postDelay);
413
+ }
414
+ const targetPosition = locatorRect
415
+ ? {
416
+ x: locatorRect.width / 2,
417
+ y: locatorRect.height / 2,
418
+ }
419
+ : undefined;
420
+ const startTime = Date.now();
421
+ await doAction({
422
+ ...options,
423
+ ...(targetPosition ? { position: targetPosition } : {}),
424
+ });
425
+ const endTime = Date.now();
426
+ const midTime = (startTime + endTime) / 2;
427
+ innerEvents.push({
428
+ type: 'mouseDown',
429
+ startMs: startTime,
430
+ endMs: midTime,
431
+ });
432
+ innerEvents.push({
433
+ type: 'mouseUp',
434
+ startMs: midTime,
435
+ endMs: endTime,
436
+ });
437
+ elementRect = locatorRect;
438
+ }
439
+ if (activeClickRecorder && innerEvents.length > 0) {
440
+ activeClickRecorder.addInput(subType, elementRect, innerEvents);
441
+ }
442
+ }
443
+ async function recordedClick(locator, doClick, clickOptions, position, moveDuration = 1000, beforeClickPause = CLICK_DURATION_MS / 2, moveEasing = 'ease-in-out', postClickPause = CLICK_DURATION_MS / 2, postClickMove) {
444
+ const result = await performClickActions(locator, doClick, clickOptions, position, moveDuration, beforeClickPause, moveEasing, postClickPause, postClickMove);
445
+ if (activeClickRecorder && result) {
446
+ activeClickRecorder.addInput('click', result.elementRect, result.innerEvents);
447
+ }
448
+ }
449
+ function instrumentLocatorMethods(obj) {
450
+ for (const method of LOCATOR_RETURN_METHODS) {
451
+ const original = obj[method].bind(obj);
452
+ obj[method] = (...args) => instrumentLocator(original(...args));
453
+ }
454
+ }
455
+ export function instrumentFrameLocator(frameLocator) {
456
+ if (instrumented.has(frameLocator))
457
+ return frameLocator;
458
+ instrumented.add(frameLocator);
459
+ for (const method of FRAME_LOCATOR_LOCATOR_RETURN_METHODS) {
460
+ const original = frameLocator[method].bind(frameLocator);
461
+ frameLocator[method] = (...args) => instrumentLocator(original(...args));
462
+ }
463
+ for (const method of FRAME_LOCATOR_SELF_RETURN_METHODS) {
464
+ const original = frameLocator[method].bind(frameLocator);
465
+ frameLocator[method] =
466
+ (...args) => instrumentFrameLocator(original(...args));
467
+ }
468
+ return frameLocator;
469
+ }
470
+ export function instrumentLocator(locator) {
471
+ if (instrumented.has(locator))
472
+ return locator;
473
+ instrumented.add(locator);
474
+ const originalClick = locator.click.bind(locator);
475
+ locator.click = async (options) => {
476
+ const { moveDuration, beforeClickPause, moveEasing, postClickPause, postClickMove, position, steps: _steps, ...clickOptions } = options ?? {};
477
+ return recordedClick(locator, (options) => originalClick(options), clickOptions, position, moveDuration, beforeClickPause, moveEasing, postClickPause, postClickMove);
478
+ };
479
+ const originalPressSequentially = locator.pressSequentially.bind(locator);
480
+ locator.pressSequentially = async (text, options) => {
481
+ const innerEvents = [];
482
+ let elementRect;
483
+ if (options?.click !== undefined) {
484
+ // Click before fill: performClickActions handles scrolling and bounding box.
485
+ const clickOpt = options.click;
486
+ const { moveDuration, beforeClickPause, moveEasing, postClickPause, postClickMove, position, ...clickOptions } = clickOpt;
487
+ const clickActionResult = await performClickActions(locator, (options) => originalClick(options), clickOptions, position, moveDuration, beforeClickPause, moveEasing, postClickPause, postClickMove);
488
+ innerEvents.push(...(clickActionResult?.innerEvents ?? []));
489
+ elementRect = clickActionResult?.elementRect;
490
+ }
491
+ else {
492
+ const isFirstAutoZoomEvent = isInsideAutoZoom() && getLastZoomLocation() === null;
493
+ const locatorRect = await scrollIntoViewIfNeeded(locator);
494
+ // If inside autoZoom: await zoom duration for camera pan, then update zoom tracker
495
+ if (isInsideAutoZoom() && locatorRect) {
496
+ const targetX = locatorRect.x + locatorRect.width / 2;
497
+ const targetY = locatorRect.y + locatorRect.height / 2;
498
+ const lastLoc = getLastZoomLocation();
499
+ if (lastLoc !== null) {
500
+ const zoomDur = getZoomDuration() ?? 0;
501
+ if (zoomDur > 0) {
502
+ await sleep(zoomDur);
503
+ }
504
+ }
505
+ setLastZoomLocation({
506
+ x: targetX,
507
+ y: targetY,
508
+ elementRect: locatorRect,
509
+ eventType: 'fill',
510
+ });
511
+ }
512
+ if (isFirstAutoZoomEvent) {
513
+ const postDelay = getPostZoomInOutDelay() ?? 0;
514
+ if (postDelay > 0)
515
+ await sleep(postDelay);
516
+ }
517
+ elementRect = locatorRect;
518
+ }
519
+ // Hide cursor while typing (will be shown again on next mouse move)
520
+ const page = locator.page();
521
+ const shouldHideMouse = options?.hideMouse === true;
522
+ if (shouldHideMouse) {
523
+ const cursorVisible = mouseVisibilities.get(page) ?? true;
524
+ if (cursorVisible) {
525
+ mouseVisibilities.set(page, false);
526
+ const hideMs = Date.now();
527
+ innerEvents.push({
528
+ type: 'mouseHide',
529
+ startMs: hideMs,
530
+ endMs: hideMs,
531
+ });
532
+ }
533
+ }
534
+ // Strip the click and hideMouse options before forwarding to the original Playwright method
535
+ const { click: _click, hideMouse: _hideMouse, ...pressOptions } = options ?? {};
536
+ const typingStart = Date.now();
537
+ await originalPressSequentially(text, pressOptions);
538
+ const typingEnd = Date.now();
539
+ if (activeClickRecorder) {
540
+ // addInput requires at least one inner event; use typing start/end as a
541
+ // fallback span when no other inner events (click, mouseHide) were collected.
542
+ const eventsToRecord = innerEvents.length > 0
543
+ ? innerEvents
544
+ : [
545
+ { type: 'mouseDown', startMs: typingStart, endMs: typingStart },
546
+ { type: 'mouseUp', startMs: typingStart, endMs: typingEnd },
547
+ ];
548
+ activeClickRecorder.addInput('pressSequentially', elementRect, eventsToRecord);
549
+ }
550
+ };
551
+ locator.fill = async (value, options) => {
552
+ const duration = options?.duration ?? 1000;
553
+ const delay = value.length > 0 ? duration / value.length : 0;
554
+ const pressOptions = { delay };
555
+ if (options?.timeout !== undefined)
556
+ pressOptions.timeout = options.timeout;
557
+ if (options?.click !== undefined)
558
+ pressOptions.click = options.click;
559
+ if (options?.hideMouse !== undefined)
560
+ pressOptions.hideMouse = options.hideMouse;
561
+ return locator.pressSequentially(value, pressOptions);
562
+ };
563
+ const originalTap = locator.tap.bind(locator);
564
+ locator.tap = async (options) => {
565
+ const clickOpt = options?.click;
566
+ const { click: _click, ...tapOpts } = options ?? {};
567
+ return performSimpleAction(locator, (options) => originalTap(options), tapOpts, 'tap', clickOpt);
568
+ };
569
+ const originalCheck = locator.check.bind(locator);
570
+ locator.check = async (options) => {
571
+ const clickOpt = options?.click;
572
+ const { click: _click, position, ...checkOpts } = options ?? {};
573
+ const effectiveClickOpt = clickOpt !== undefined && position !== undefined
574
+ ? { ...clickOpt, position }
575
+ : clickOpt;
576
+ return performSimpleAction(locator, (options) => originalCheck(options), checkOpts, 'check', effectiveClickOpt);
577
+ };
578
+ const originalUncheck = locator.uncheck.bind(locator);
579
+ locator.uncheck = async (options) => {
580
+ const clickOpt = options?.click;
581
+ const { click: _click, position, ...uncheckOpts } = options ?? {};
582
+ const effectiveClickOpt = clickOpt !== undefined && position !== undefined
583
+ ? { ...clickOpt, position }
584
+ : clickOpt;
585
+ return performSimpleAction(locator, (options) => originalUncheck(options), uncheckOpts, 'uncheck', effectiveClickOpt);
586
+ };
587
+ locator.setChecked = async (checked, options) => {
588
+ if (checked) {
589
+ return locator.check(options);
590
+ }
591
+ else {
592
+ return locator.uncheck(options);
593
+ }
594
+ };
595
+ const originalSelectOption = locator.selectOption.bind(locator);
596
+ locator.selectOption = async (values, options) => {
597
+ const clickOpt = options?.click;
598
+ const { click: _click, ...selectOpts } = options ?? {};
599
+ let result = [];
600
+ await performSimpleAction(locator, (options) => originalSelectOption(values, options).then((res) => {
601
+ result = res;
602
+ }), selectOpts, 'select', clickOpt);
603
+ return result;
604
+ };
605
+ const originalHover = locator.hover.bind(locator);
606
+ locator.hover = async (options) => {
607
+ const { moveDuration = 1000, easing: moveEasing = 'ease-in-out', hoverDuration = 1000, position, ...hoverOptions } = options ?? {};
608
+ const page = locator.page();
609
+ const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
610
+ const moveStartTime = Date.now();
611
+ const locatorRect = await scrollIntoViewIfNeeded(locator);
612
+ const scrollElapsedMs = Date.now() - moveStartTime;
613
+ const innerEvents = [];
614
+ const targetPos = position ??
615
+ (locatorRect
616
+ ? { x: locatorRect.width / 2, y: locatorRect.height / 2 }
617
+ : undefined);
618
+ if (targetPos && locatorRect) {
619
+ const targetX = locatorRect.x + targetPos.x;
620
+ const targetY = locatorRect.y + targetPos.y;
621
+ const effectiveDuration = Math.max(0, moveDuration - scrollElapsedMs);
622
+ innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, effectiveDuration, moveEasing, moveStartTime, locatorRect));
623
+ }
624
+ const waitStartMs = Date.now();
625
+ await originalHover({
626
+ ...hoverOptions,
627
+ ...(targetPos ? { position: targetPos } : {}),
628
+ });
629
+ if (hoverDuration > 0) {
630
+ await sleep(hoverDuration);
631
+ }
632
+ const waitEndMs = Date.now();
633
+ innerEvents.push({
634
+ type: 'mouseWait',
635
+ startMs: waitStartMs,
636
+ endMs: waitEndMs,
637
+ });
638
+ if (activeClickRecorder && innerEvents.length > 0) {
639
+ activeClickRecorder.addInput('hover', locatorRect, innerEvents);
640
+ }
641
+ };
642
+ const originalSelectText = locator.selectText.bind(locator);
643
+ locator.selectText = async (options) => {
644
+ const { moveDuration = 1000, easing: moveEasing = 'ease-in-out', beforeClickPause = CLICK_DURATION_MS / 2, selectDuration = 600, ...selectOpts } = options ?? {};
645
+ const page = locator.page();
646
+ const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
647
+ const moveStartTime = Date.now();
648
+ const locatorRect = await scrollIntoViewIfNeeded(locator);
649
+ const scrollElapsedMs = Date.now() - moveStartTime;
650
+ const innerEvents = [];
651
+ const targetPos = locatorRect
652
+ ? { x: locatorRect.width / 2, y: locatorRect.height / 2 }
653
+ : undefined;
654
+ if (targetPos && locatorRect) {
655
+ const targetX = locatorRect.x + targetPos.x;
656
+ const targetY = locatorRect.y + targetPos.y;
657
+ const effectiveDuration = Math.max(0, moveDuration - scrollElapsedMs);
658
+ innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, effectiveDuration, moveEasing, moveStartTime, locatorRect));
659
+ }
660
+ await sleep(beforeClickPause);
661
+ await originalSelectText(selectOpts);
662
+ // Backtrack triple-click events from the moment originalSelectText resolves.
663
+ // Clamp start so events don't precede the prior animation (produces a visible
664
+ // pre-click pause in the recording, which is acceptable).
665
+ // All timestamps use a single base + integer * segmentMs to avoid FP drift.
666
+ const selectEndMs = Date.now();
667
+ const lastEventEndMs = innerEvents.at(-1)?.endMs ?? 0;
668
+ const tripleClickStartMs = Math.max(lastEventEndMs, selectEndMs - selectDuration);
669
+ const segmentMs = selectDuration / 6;
670
+ for (let i = 0; i < 3; i++) {
671
+ const seg = i * 2;
672
+ innerEvents.push({
673
+ type: 'mouseDown',
674
+ startMs: tripleClickStartMs + seg * segmentMs,
675
+ endMs: tripleClickStartMs + (seg + 1) * segmentMs,
676
+ easing: 'ease-in-out',
677
+ });
678
+ innerEvents.push({
679
+ type: 'mouseUp',
680
+ startMs: tripleClickStartMs + (seg + 1) * segmentMs,
681
+ endMs: tripleClickStartMs + (seg + 2) * segmentMs,
682
+ easing: 'ease-in-out',
683
+ });
684
+ }
685
+ if (activeClickRecorder && innerEvents.length > 0) {
686
+ activeClickRecorder.addInput('selectText', locatorRect, innerEvents);
687
+ }
688
+ };
689
+ locator.dragTo = async (target, options) => {
690
+ const { moveDuration = 1000, moveEasing = 'ease-in-out', preDragPause = CLICK_DURATION_MS / 2, dragDuration = 1000, dragEasing = 'ease-in-out', sourcePosition, targetPosition, ...dragOpts } = options ?? {};
691
+ const page = locator.page();
692
+ const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
693
+ const moveStartTime = Date.now();
694
+ const sourceRect = await scrollIntoViewIfNeeded(locator);
695
+ const scrollElapsedMs = Date.now() - moveStartTime;
696
+ const targetBb = await target.boundingBox();
697
+ const targetRect = targetBb
698
+ ? {
699
+ x: targetBb.x,
700
+ y: targetBb.y,
701
+ width: targetBb.width,
702
+ height: targetBb.height,
703
+ }
704
+ : undefined;
705
+ const innerEvents = [];
706
+ const sourcePos = sourcePosition ??
707
+ (sourceRect
708
+ ? { x: sourceRect.width / 2, y: sourceRect.height / 2 }
709
+ : undefined);
710
+ const targetPos = targetPosition ??
711
+ (targetRect
712
+ ? { x: targetRect.width / 2, y: targetRect.height / 2 }
713
+ : undefined);
714
+ // 1. Animate cursor to source
715
+ if (sourcePos && sourceRect) {
716
+ const toX = sourceRect.x + sourcePos.x;
717
+ const toY = sourceRect.y + sourcePos.y;
718
+ const effectiveDuration = Math.max(0, moveDuration - scrollElapsedMs);
719
+ innerEvents.push(await animateMouseMove(page, mouseMoveInternal, toX, toY, effectiveDuration, moveEasing, moveStartTime, sourceRect));
720
+ }
721
+ // 2. preDragPause + mouseDown
722
+ await sleep(preDragPause);
723
+ const mouseDownStart = Date.now();
724
+ await page.mouse.down();
725
+ await sleep(CLICK_DURATION_MS / 2);
726
+ innerEvents.push({
727
+ type: 'mouseDown',
728
+ startMs: mouseDownStart,
729
+ endMs: Date.now(),
730
+ easing: 'ease-in-out',
731
+ });
732
+ // 3. Drag: animate cursor from source to target
733
+ const dragStartTime = Date.now();
734
+ if (targetPos && targetRect) {
735
+ const toX = targetRect.x + targetPos.x;
736
+ const toY = targetRect.y + targetPos.y;
737
+ innerEvents.push(await animateMouseMove(page, mouseMoveInternal, toX, toY, dragDuration, dragEasing, dragStartTime, targetRect));
738
+ }
739
+ // 4. mouseUp at target
740
+ const mouseUpStart = Date.now();
741
+ await page.mouse.up();
742
+ await sleep(CLICK_DURATION_MS / 2);
743
+ innerEvents.push({
744
+ type: 'mouseUp',
745
+ startMs: mouseUpStart,
746
+ endMs: Date.now(),
747
+ easing: 'ease-in-out',
748
+ });
749
+ if (activeClickRecorder && innerEvents.length > 0) {
750
+ activeClickRecorder.addInput('dragTo', sourceRect, innerEvents);
751
+ }
752
+ };
753
+ const originalPage = locator.page.bind(locator);
754
+ locator.page = () => originalPage();
755
+ instrumentLocatorMethods(locator);
756
+ for (const method of LOCATOR_ONLY_SYNC_RETURN_METHODS) {
757
+ const original = locator[method].bind(locator);
758
+ locator[method] = (...args) => instrumentLocator(original(...args));
759
+ }
760
+ const originalAll = locator.all.bind(locator);
761
+ locator.all = async () => {
762
+ const locators = await originalAll();
763
+ return locators.map(instrumentLocator);
764
+ };
765
+ const originalContentFrame = locator.contentFrame.bind(locator);
766
+ locator.contentFrame =
767
+ () => instrumentFrameLocator(originalContentFrame());
768
+ const originalLocatorFrameLocator = locator.frameLocator.bind(locator);
769
+ locator.frameLocator = (...args) => instrumentFrameLocator(originalLocatorFrameLocator(...args));
770
+ return locator;
771
+ }
772
+ export async function instrumentPage(page) {
773
+ if (instrumented.has(page))
774
+ return page;
775
+ instrumented.add(page);
776
+ // Expose a Node.js function to the browser that captures DOM click event data.
777
+ // Called synchronously from the click handler before any navigation can occur.
778
+ await page.exposeFunction('__screenciOnClick', (data) => {
779
+ pendingClickData.set(page, data);
780
+ });
781
+ // Inject a capture listener on every page load (including after navigation).
782
+ await page.addInitScript(() => {
783
+ document.addEventListener('click', (e) => {
784
+ const target = e.target;
785
+ const r = target.getBoundingClientRect();
786
+ window.__screenciOnClick({
787
+ x: e.clientX,
788
+ y: e.clientY,
789
+ targetRect: { x: r.x, y: r.y, width: r.width, height: r.height },
790
+ });
791
+ }, { capture: true });
792
+ });
793
+ instrumentLocatorMethods(page);
794
+ const originalPageFrameLocator = page.frameLocator.bind(page);
795
+ page.frameLocator = (...args) => instrumentFrameLocator(originalPageFrameLocator(...args));
796
+ // Delegate page.click to the instrumented locator so all click recording
797
+ // flows through the same path.
798
+ page.click = async (selector, options) => {
799
+ return page.locator(selector).click(options);
800
+ };
801
+ // Instrument page.mouse to record mouse moves and visibility toggles.
802
+ const originalMouse = page.mouse;
803
+ const originalMove = originalMouse.move.bind(originalMouse);
804
+ originalMouseMoves.set(page, originalMove);
805
+ originalMouse.move = async (x, y, options) => {
806
+ const duration = options?.duration ?? 0;
807
+ const easing = options?.easing ?? 'ease-in-out';
808
+ const startMs = Date.now();
809
+ if (duration > 0) {
810
+ const startPos = mousePositions.get(page) ?? { x: 0, y: 0 };
811
+ const steps = Math.max(1, Math.floor(duration / CURSOR_STEP_MS));
812
+ const stepMs = duration / steps;
813
+ for (let i = 0; i <= steps; i++) {
814
+ const t = i / steps;
815
+ const easedT = evaluateEasingAtT(t, easing);
816
+ const cx = startPos.x + easedT * (x - startPos.x);
817
+ const cy = startPos.y + easedT * (y - startPos.y);
818
+ await originalMove(cx, cy);
819
+ if (i < steps) {
820
+ await new Promise((resolve) => setTimeout(resolve, stepMs));
821
+ }
822
+ }
823
+ }
824
+ else {
825
+ await originalMove(x, y);
826
+ }
827
+ mousePositions.set(page, { x, y });
828
+ const endMs = Date.now();
829
+ if (activeClickRecorder) {
830
+ // Auto-show cursor when moving after a typing auto-hide
831
+ if (!(mouseVisibilities.get(page) ?? true)) {
832
+ mouseVisibilities.set(page, true);
833
+ const showMs = startMs;
834
+ const showEvent = {
835
+ type: 'mouseShow',
836
+ startMs: showMs,
837
+ endMs: showMs,
838
+ };
839
+ activeClickRecorder.addInput('mouseShow', undefined, [showEvent]);
840
+ }
841
+ const moveEvent = {
842
+ type: 'mouseMove',
843
+ startMs,
844
+ endMs,
845
+ x,
846
+ y,
847
+ ...(duration > 0 ? { easing } : {}),
848
+ };
849
+ activeClickRecorder.addInput('mouseMove', undefined, [moveEvent]);
850
+ }
851
+ };
852
+ mouseVisibilities.set(page, true);
853
+ originalMouse.show = () => {
854
+ if (!(mouseVisibilities.get(page) ?? true)) {
855
+ mouseVisibilities.set(page, true);
856
+ if (activeClickRecorder) {
857
+ const timeMs = Date.now();
858
+ const showEvent = {
859
+ type: 'mouseShow',
860
+ startMs: timeMs,
861
+ endMs: timeMs,
862
+ };
863
+ activeClickRecorder.addInput('mouseShow', undefined, [showEvent]);
864
+ }
865
+ }
866
+ };
867
+ originalMouse.hide = () => {
868
+ if (mouseVisibilities.get(page) ?? true) {
869
+ mouseVisibilities.set(page, false);
870
+ if (activeClickRecorder) {
871
+ const timeMs = Date.now();
872
+ const hideEvent = {
873
+ type: 'mouseHide',
874
+ startMs: timeMs,
875
+ endMs: timeMs,
876
+ };
877
+ activeClickRecorder.addInput('mouseHide', undefined, [hideEvent]);
878
+ }
879
+ }
880
+ };
881
+ page.on('popup', (popup) => {
882
+ void instrumentPage(popup);
883
+ });
884
+ return page;
885
+ }
886
+ export function instrumentContext(context) {
887
+ if (instrumented.has(context))
888
+ return context;
889
+ instrumented.add(context);
890
+ const originalNewPage = context.newPage.bind(context);
891
+ context.newPage = async (...args) => {
892
+ return instrumentPage(await originalNewPage(...args));
893
+ };
894
+ return context;
895
+ }
896
+ export function instrumentBrowser(browser) {
897
+ if (instrumented.has(browser))
898
+ return browser;
899
+ instrumented.add(browser);
900
+ const originalNewContext = browser.newContext.bind(browser);
901
+ browser.newContext = async (...args) => {
902
+ return instrumentContext(await originalNewContext(...args));
903
+ };
904
+ const originalNewPage = browser.newPage.bind(browser);
905
+ browser.newPage = async (...args) => {
906
+ return instrumentPage(await originalNewPage(...args));
907
+ };
908
+ return browser;
909
+ }
910
+ //# sourceMappingURL=instrument.js.map