screenci 0.0.17 → 0.0.19

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 (50) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +38 -31
  4. package/dist/cli.js.map +1 -1
  5. package/dist/e2e/instrument.e2e.js +21 -2
  6. package/dist/e2e/instrument.e2e.js.map +1 -1
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +2 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/src/autoZoom.d.ts +5 -1
  12. package/dist/src/autoZoom.d.ts.map +1 -1
  13. package/dist/src/autoZoom.js +30 -1
  14. package/dist/src/autoZoom.js.map +1 -1
  15. package/dist/src/chromiumProfile.d.ts +6 -0
  16. package/dist/src/chromiumProfile.d.ts.map +1 -0
  17. package/dist/src/chromiumProfile.js +20 -0
  18. package/dist/src/chromiumProfile.js.map +1 -0
  19. package/dist/src/cue.d.ts +0 -63
  20. package/dist/src/cue.d.ts.map +1 -1
  21. package/dist/src/cue.js +0 -84
  22. package/dist/src/cue.js.map +1 -1
  23. package/dist/src/dimensions.d.ts +7 -0
  24. package/dist/src/dimensions.d.ts.map +1 -1
  25. package/dist/src/dimensions.js +6 -0
  26. package/dist/src/dimensions.js.map +1 -1
  27. package/dist/src/events.d.ts +19 -8
  28. package/dist/src/events.d.ts.map +1 -1
  29. package/dist/src/events.js +20 -13
  30. package/dist/src/events.js.map +1 -1
  31. package/dist/src/instrument.d.ts.map +1 -1
  32. package/dist/src/instrument.js +354 -172
  33. package/dist/src/instrument.js.map +1 -1
  34. package/dist/src/recordingData.d.ts +14 -7
  35. package/dist/src/recordingData.d.ts.map +1 -1
  36. package/dist/src/scroll.d.ts +47 -0
  37. package/dist/src/scroll.d.ts.map +1 -0
  38. package/dist/src/scroll.js +347 -0
  39. package/dist/src/scroll.js.map +1 -0
  40. package/dist/src/types.d.ts +21 -22
  41. package/dist/src/types.d.ts.map +1 -1
  42. package/dist/src/types.js +2 -2
  43. package/dist/src/types.js.map +1 -1
  44. package/dist/src/video.d.ts.map +1 -1
  45. package/dist/src/video.js +30 -3
  46. package/dist/src/video.js.map +1 -1
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +1 -1
  49. package/skills/screenci/SKILL.md +20 -7
  50. package/skills/screenci/references/record.md +7 -5
@@ -1,5 +1,7 @@
1
1
  import { logger } from './logger.js';
2
- import { isInsideAutoZoom, getZoomDuration, getPostZoomInOutDelay, getLastZoomLocation, setLastZoomLocation, } from './autoZoom.js';
2
+ import { isInsideAutoZoom, getZoomDuration, getZoomEasing, getPostZoomInOutDelay, getLastZoomLocation, setLastZoomLocation, } from './autoZoom.js';
3
+ import { isInsideHide } from './hide.js';
4
+ import { ZoomScrollHandler } from './scroll.js';
3
5
  let activeClickRecorder = null;
4
6
  export function setActiveClickRecorder(recorder) {
5
7
  activeClickRecorder = recorder;
@@ -105,6 +107,18 @@ const FRAME_LOCATOR_SELF_RETURN_METHODS = [
105
107
  'last',
106
108
  'nth',
107
109
  ];
110
+ function canUseDirectMouseClickAfterScroll(options) {
111
+ if (!options)
112
+ return true;
113
+ const unsupported = [
114
+ 'force',
115
+ 'modifiers',
116
+ 'noWaitAfter',
117
+ 'timeout',
118
+ 'trial',
119
+ ];
120
+ return unsupported.every((key) => options[key] === undefined);
121
+ }
108
122
  // Per-page storage for the most recently captured DOM click event data.
109
123
  // Reset to null before each instrumented click; set by the exposeFunction callback.
110
124
  const pendingClickData = new WeakMap();
@@ -144,43 +158,10 @@ export function scrollIntoViewAsync(locator, options = {}) {
144
158
  }));
145
159
  }), { behavior, block, timeout, postScrollTimeout });
146
160
  }
147
- /**
148
- * Scrolls the locator into view only if it is at least partially outside the
149
- * viewport, then returns the (possibly updated) bounding box as an ElementRect.
150
- * Returns undefined if the bounding box cannot be determined.
151
- */
152
- async function scrollIntoViewIfNeeded(locator) {
153
- const page = locator.page();
154
- const bb = await locator.boundingBox();
155
- if (!bb)
156
- return undefined;
157
- const viewportSize = page.viewportSize();
158
- if (viewportSize) {
159
- const isFullyInViewport = bb.x >= 0 &&
160
- bb.y >= 0 &&
161
- bb.x + bb.width <= viewportSize.width &&
162
- bb.y + bb.height <= viewportSize.height;
163
- if (!isFullyInViewport) {
164
- await scrollIntoViewAsync(locator);
165
- const newBb = await locator.boundingBox();
166
- if (newBb)
167
- return {
168
- x: newBb.x,
169
- y: newBb.y,
170
- width: newBb.width,
171
- height: newBb.height,
172
- };
173
- }
174
- }
175
- else {
176
- logger.warn('[screenci] Unable to determine viewport size; skipping auto-scroll check.');
177
- }
178
- return { x: bb.x, y: bb.y, width: bb.width, height: bb.height };
179
- }
180
161
  const CURSOR_STEP_MS = 1000 / 60;
181
162
  /**
182
163
  * Physically moves the mouse from its current tracked position to (targetX,
183
- * targetY) over `duration` ms using `easing`, then returns a MouseMoveEvent
164
+ * targetY) over `duration` ms using `easing`, then returns a FocusChangeEvent
184
165
  * whose startMs is `eventStartMs` (which may predate this call, e.g. when a
185
166
  * scroll consumed part of moveDuration). When duration ≤ 0 the cursor is
186
167
  * snapped directly to the target with a single move call.
@@ -208,32 +189,40 @@ async function animateMouseMove(page, mouseMoveInternal, targetX, targetY, durat
208
189
  mousePositions.set(page, { x: targetX, y: targetY });
209
190
  const endMs = Date.now();
210
191
  return {
211
- type: 'mouseMove',
192
+ type: 'focusChange',
212
193
  startMs: eventStartMs,
213
194
  endMs,
214
- duration: Math.max(0, endMs - eventStartMs),
215
195
  x: targetX,
216
196
  y: targetY,
217
197
  easing,
218
198
  ...(elementRect !== undefined ? { elementRect } : {}),
219
199
  };
220
200
  }
201
+ function resolveMoveStartMs(scrollResult, fallbackStartMs) {
202
+ return scrollResult.shouldScrollBeforeMouseMove
203
+ ? scrollResult.scrollEndMs
204
+ : fallbackStartMs;
205
+ }
206
+ function resolveMoveDuration(scrollResult, resolvedDuration) {
207
+ return scrollResult.shouldScrollBeforeMouseMove
208
+ ? resolvedDuration
209
+ : Math.max(0, resolvedDuration - scrollResult.scrollElapsedMs);
210
+ }
221
211
  /**
222
212
  * Performs all click mechanics (scroll-check, zoom handling, cursor animation,
223
213
  * click, post-click move) and returns the collected timing/position data.
224
214
  * Returns null if coordinates could not be determined (no DOM event and no
225
215
  * locator bounding box).
226
216
  */
227
- async function performClickActions(locator, doClick, clickOptions, position, moveDuration, moveSpeed, beforeClickPause = CLICK_DURATION_MS / 2, moveEasing = 'ease-in-out', postClickPause = CLICK_DURATION_MS / 2, postClickMove) {
217
+ async function performClickActions(locator, doClick, clickOptions, autoZoomOptions, position, moveDuration, moveSpeed, beforeClickPause = CLICK_DURATION_MS / 2, moveEasing = 'ease-in-out', postClickPause = CLICK_DURATION_MS / 2, postClickMove) {
228
218
  const page = locator.page();
229
219
  pendingClickData.set(page, null);
230
220
  const halfClickDuration = CLICK_DURATION_MS / 2;
231
221
  const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
232
- // Capture before any setLastZoomLocation call changes the state.
233
- const isFirstAutoZoomEvent = isInsideAutoZoom() && getLastZoomLocation() === null;
234
222
  const moveStartTime = Date.now();
235
- const locatorRect = await scrollIntoViewIfNeeded(locator);
236
- const scrollElapsedMs = Date.now() - moveStartTime;
223
+ const scrollResult = await new ZoomScrollHandler(autoZoomOptions).scroll(locator);
224
+ const { locatorRect, scrollElapsedMs } = scrollResult;
225
+ const isFirstAutoZoomEvent = scrollResult.isFirstAutoZoomInteraction;
237
226
  if (!locatorRect) {
238
227
  logger.warn('[screenci] Unable to get locator bounding box; skipping auto-scroll check.');
239
228
  }
@@ -269,11 +258,6 @@ async function performClickActions(locator, doClick, clickOptions, position, mov
269
258
  y: locatorRect.height / 2,
270
259
  }
271
260
  : undefined;
272
- // No physical mouse movement happens during the scroll. moveStartTime is
273
- // captured before the scroll so the recorded event spans scroll-start →
274
- // animation-end, making the cursor appear to move during the scroll in the
275
- // video. After the scroll, the remaining duration drives the physical easing
276
- // animation via the shared helper.
277
261
  if (targetPos && locatorRect) {
278
262
  const targetX = locatorRect.x + targetPos.x;
279
263
  const targetY = locatorRect.y + targetPos.y;
@@ -283,19 +267,40 @@ async function performClickActions(locator, doClick, clickOptions, position, mov
283
267
  defaultDuration: 1000,
284
268
  context: 'click move',
285
269
  });
286
- const effectiveDuration = Math.max(0, resolvedDuration - scrollElapsedMs);
287
- innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, effectiveDuration, moveEasing, moveStartTime, locatorRect));
270
+ if (scrollElapsedMs > 0 && !scrollResult.shouldScrollBeforeMouseMove) {
271
+ await mouseMoveInternal(targetX, targetY);
272
+ mousePositions.set(page, { x: targetX, y: targetY });
273
+ const remainingDuration = Math.max(0, resolvedDuration - scrollElapsedMs);
274
+ if (remainingDuration > 0) {
275
+ await new Promise((resolve) => setTimeout(resolve, remainingDuration));
276
+ }
277
+ const moveEndTime = Date.now();
278
+ innerEvents.push({
279
+ type: 'focusChange',
280
+ startMs: moveStartTime,
281
+ endMs: moveEndTime,
282
+ x: targetX,
283
+ y: targetY,
284
+ easing: moveEasing,
285
+ focusOnly: false,
286
+ elementRect: locatorRect,
287
+ });
288
+ }
289
+ else {
290
+ innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, resolveMoveDuration(scrollResult, resolvedDuration), moveEasing, resolveMoveStartMs(scrollResult, moveStartTime), locatorRect));
291
+ }
288
292
  }
289
293
  else {
290
294
  assertDurationOrSpeed(moveDuration, moveSpeed, 'click move');
291
- const remainingMs = Math.max(0, (moveSpeed === undefined ? (moveDuration ?? 1000) : 0) - scrollElapsedMs);
295
+ const remainingMs = Math.max(0, moveSpeed === undefined ? (moveDuration ?? 1000) : 0);
292
296
  if (remainingMs > 0) {
293
297
  await new Promise((resolve) => setTimeout(resolve, remainingMs));
294
298
  }
295
299
  }
300
+ const zoomDur = isInsideAutoZoom() && locatorRect ? (getZoomDuration() ?? 0) : 0;
296
301
  const effectiveBeforeClickPause = isFirstAutoZoomEvent
297
- ? Math.max(beforeClickPause, getPostZoomInOutDelay() ?? 0)
298
- : beforeClickPause;
302
+ ? Math.max(beforeClickPause, getPostZoomInOutDelay() ?? 0, zoomDur)
303
+ : Math.max(beforeClickPause, zoomDur);
299
304
  await new Promise((resolve) => setTimeout(resolve, effectiveBeforeClickPause));
300
305
  await new Promise((resolve) => setTimeout(resolve, halfClickDuration));
301
306
  // Note click can take some time, but better to show it before than after
@@ -307,11 +312,47 @@ async function performClickActions(locator, doClick, clickOptions, position, mov
307
312
  endMs: clickTime,
308
313
  easing: 'ease-in-out',
309
314
  });
310
- await doClick({
311
- ...clickOptions,
312
- ...(targetPos ? { position: targetPos } : {}),
313
- });
315
+ if (scrollElapsedMs > 0 &&
316
+ targetPos &&
317
+ locatorRect &&
318
+ canUseDirectMouseClickAfterScroll(clickOptions)) {
319
+ const mouseClickOptions = {
320
+ ...(clickOptions?.button !== undefined
321
+ ? { button: clickOptions.button }
322
+ : {}),
323
+ ...(clickOptions?.clickCount !== undefined
324
+ ? { clickCount: clickOptions.clickCount }
325
+ : {}),
326
+ };
327
+ await page.mouse.down(mouseClickOptions);
328
+ if (clickOptions?.delay) {
329
+ await new Promise((resolve) => setTimeout(resolve, clickOptions.delay));
330
+ }
331
+ await page.mouse.up(mouseClickOptions);
332
+ }
333
+ else {
334
+ await doClick({
335
+ ...clickOptions,
336
+ ...(targetPos ? { position: targetPos } : {}),
337
+ });
338
+ }
314
339
  const domClickData = pendingClickData.get(page);
340
+ if (domClickData) {
341
+ const lastMouseMoveIndex = innerEvents.findIndex((e) => e.type === 'focusChange' || e.type === 'mouseMove');
342
+ if (lastMouseMoveIndex !== -1) {
343
+ const existingMove = innerEvents[lastMouseMoveIndex];
344
+ if (existingMove?.type === 'focusChange' ||
345
+ existingMove?.type === 'mouseMove') {
346
+ innerEvents[lastMouseMoveIndex] = {
347
+ ...existingMove,
348
+ x: domClickData.x,
349
+ y: domClickData.y,
350
+ elementRect: domClickData.targetRect,
351
+ };
352
+ }
353
+ }
354
+ mousePositions.set(page, { x: domClickData.x, y: domClickData.y });
355
+ }
315
356
  const mouseUpEnd = Date.now() + halfClickDuration;
316
357
  innerEvents.push({
317
358
  type: 'mouseUp',
@@ -385,26 +426,23 @@ async function performClickActions(locator, doClick, clickOptions, position, mov
385
426
  }
386
427
  const postClickMoveEndMs = Date.now();
387
428
  innerEvents.push({
388
- type: 'mouseMove',
429
+ type: 'focusChange',
389
430
  startMs: postClickMoveStartMs,
390
431
  endMs: postClickMoveEndMs,
391
- duration: Math.max(0, postClickMoveEndMs - postClickMoveStartMs),
392
432
  x: targetX,
393
433
  y: targetY,
394
434
  easing,
395
- zoomFollow: false,
396
435
  });
397
436
  mousePositions.set(page, { x: targetX, y: targetY });
398
437
  }
399
438
  }
400
439
  let elementRect;
401
- if (locatorRect) {
402
- elementRect = locatorRect;
403
- }
404
- else if (domClickData) {
405
- console.warn('[screenci] using DOM click data as fallback for elementRect');
440
+ if (domClickData) {
406
441
  elementRect = domClickData.targetRect;
407
442
  }
443
+ else if (locatorRect) {
444
+ elementRect = locatorRect ?? undefined;
445
+ }
408
446
  if (elementRect) {
409
447
  return {
410
448
  elementRect,
@@ -416,41 +454,57 @@ async function performClickActions(locator, doClick, clickOptions, position, mov
416
454
  return null;
417
455
  }
418
456
  }
419
- async function performSimpleAction(locator, doAction, options, subType, clickOpt, position) {
457
+ async function prepareAutoZoomForLocator(locator, eventType, autoZoomOptions) {
458
+ const scrollHandler = new ZoomScrollHandler(autoZoomOptions);
459
+ const zoomDur = scrollHandler.isInsideAutoZoom ? (getZoomDuration() ?? 0) : 0;
460
+ if (scrollHandler.isInsideAutoZoom &&
461
+ scrollHandler.hadPreviousZoomLocation &&
462
+ zoomDur > 0) {
463
+ await sleep(zoomDur);
464
+ }
465
+ const { locatorRect, isFirstAutoZoomInteraction } = await scrollHandler.scroll(locator);
466
+ if (scrollHandler.isInsideAutoZoom && locatorRect) {
467
+ if (isFirstAutoZoomInteraction && zoomDur > 0) {
468
+ await sleep(zoomDur);
469
+ }
470
+ setLastZoomLocation({
471
+ x: locatorRect.x + locatorRect.width / 2,
472
+ y: locatorRect.y + locatorRect.height / 2,
473
+ elementRect: locatorRect,
474
+ eventType,
475
+ });
476
+ }
477
+ return { locatorRect, isFirstAutoZoomInteraction };
478
+ }
479
+ async function performSimpleAction(locator, doAction, options, subType, clickOpt, autoZoomOptions, position, recordMousePress = subType === 'tap') {
420
480
  await sleep(PRE_ACTION_SLEEP);
421
481
  let innerEvents = [];
422
482
  let elementRect;
423
483
  if (clickOpt !== undefined) {
424
484
  const { moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove, } = clickOpt;
425
- const clickActionResult = await performClickActions(locator, doAction, {}, position, moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove);
485
+ const clickActionResult = await performClickActions(locator, doAction, {}, autoZoomOptions, position, moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove);
426
486
  innerEvents = clickActionResult?.innerEvents ?? [];
427
487
  elementRect = clickActionResult?.elementRect;
428
488
  }
429
489
  else {
430
- const isFirstAutoZoomEvent = isInsideAutoZoom() && getLastZoomLocation() === null;
431
- const locatorRect = await scrollIntoViewIfNeeded(locator);
490
+ const locatorRect = await locator.boundingBox();
432
491
  if (isInsideAutoZoom() && locatorRect) {
433
- const targetX = locatorRect.x + locatorRect.width / 2;
434
- const targetY = locatorRect.y + locatorRect.height / 2;
435
- const lastLoc = getLastZoomLocation();
436
- if (lastLoc !== null) {
437
- const zoomDur = getZoomDuration() ?? 0;
438
- if (zoomDur > 0) {
439
- await sleep(zoomDur);
440
- }
441
- }
442
- setLastZoomLocation({
443
- x: targetX,
444
- y: targetY,
492
+ const focusStart = Date.now();
493
+ const zoomDur = getZoomDuration() ?? 0;
494
+ if (zoomDur > 0)
495
+ await sleep(zoomDur);
496
+ const focusEnd = Date.now();
497
+ innerEvents.push({
498
+ type: 'focusChange',
499
+ startMs: focusStart,
500
+ endMs: focusEnd,
501
+ x: locatorRect.x + locatorRect.width / 2,
502
+ y: locatorRect.y + locatorRect.height / 2,
503
+ easing: getZoomEasing() ?? 'ease-in-out',
504
+ focusOnly: true,
445
505
  elementRect: locatorRect,
446
- eventType: 'fill',
447
506
  });
448
507
  }
449
- if (isFirstAutoZoomEvent) {
450
- const postDelay = getPostZoomInOutDelay() ?? 0;
451
- if (postDelay > 0)
452
- await sleep(postDelay);
453
- }
454
508
  const targetPosition = locatorRect
455
509
  ? {
456
510
  x: locatorRect.width / 2,
@@ -463,18 +517,20 @@ async function performSimpleAction(locator, doAction, options, subType, clickOpt
463
517
  ...(targetPosition ? { position: targetPosition } : {}),
464
518
  });
465
519
  const endTime = Date.now();
466
- const midTime = (startTime + endTime) / 2;
467
- innerEvents.push({
468
- type: 'mouseDown',
469
- startMs: startTime,
470
- endMs: midTime,
471
- });
472
- innerEvents.push({
473
- type: 'mouseUp',
474
- startMs: midTime,
475
- endMs: endTime,
476
- });
477
- elementRect = locatorRect;
520
+ elementRect = locatorRect ?? undefined;
521
+ if (recordMousePress) {
522
+ const midTime = (startTime + endTime) / 2;
523
+ innerEvents.push({
524
+ type: 'mouseDown',
525
+ startMs: startTime,
526
+ endMs: midTime,
527
+ });
528
+ innerEvents.push({
529
+ type: 'mouseUp',
530
+ startMs: midTime,
531
+ endMs: endTime,
532
+ });
533
+ }
478
534
  }
479
535
  const simpleWaitStart = Date.now();
480
536
  await sleep(POST_ACTION_SLEEP);
@@ -488,9 +544,9 @@ async function performSimpleAction(locator, doAction, options, subType, clickOpt
488
544
  activeClickRecorder.addInput(subType, elementRect, innerEvents);
489
545
  }
490
546
  }
491
- async function recordedClick(locator, doClick, clickOptions, position, moveDuration, moveSpeed, beforeClickPause = CLICK_DURATION_MS / 2, moveEasing = 'ease-in-out', postClickPause = CLICK_DURATION_MS / 2, postClickMove) {
547
+ async function recordedClick(locator, doClick, clickOptions, autoZoomOptions, position, moveDuration, moveSpeed, beforeClickPause = CLICK_DURATION_MS / 2, moveEasing = 'ease-in-out', postClickPause = CLICK_DURATION_MS / 2, postClickMove) {
492
548
  await sleep(PRE_ACTION_SLEEP);
493
- const result = await performClickActions(locator, doClick, clickOptions, position, moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove);
549
+ const result = await performClickActions(locator, doClick, clickOptions, autoZoomOptions, position, moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove);
494
550
  const clickWaitStart = Date.now();
495
551
  await sleep(POST_ACTION_SLEEP);
496
552
  const clickWaitEnd = Date.now();
@@ -530,12 +586,22 @@ export function instrumentLocator(locator) {
530
586
  instrumented.add(locator);
531
587
  const originalClick = locator.click.bind(locator);
532
588
  locator.click = async (options) => {
533
- const { moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove, position, steps: _steps, ...clickOptions } = options ?? {};
589
+ const { moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove, autoZoomOptions, position, steps: _steps, ...clickOptions } = options ?? {};
590
+ if (isInsideHide()) {
591
+ return originalClick({
592
+ ...clickOptions,
593
+ ...(position !== undefined && { position }),
594
+ });
595
+ }
534
596
  assertDurationOrSpeed(moveDuration, moveSpeed, 'click move');
535
- return recordedClick(locator, (options) => originalClick(options), clickOptions, position, moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove);
597
+ return recordedClick(locator, (options) => originalClick(options), clickOptions, autoZoomOptions, position, moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove);
536
598
  };
537
599
  const originalPressSequentially = locator.pressSequentially.bind(locator);
538
600
  locator.pressSequentially = async (text, options) => {
601
+ const { click: _click, autoZoomOptions, hideMouse: _hideMouse, position: _position, ...pressOptions } = options ?? {};
602
+ if (isInsideHide()) {
603
+ return originalPressSequentially(text, pressOptions);
604
+ }
539
605
  await sleep(PRE_ACTION_SLEEP);
540
606
  const innerEvents = [];
541
607
  let elementRect;
@@ -544,37 +610,24 @@ export function instrumentLocator(locator) {
544
610
  const clickOpt = options.click;
545
611
  const position = options.position;
546
612
  const { moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove, ...clickOptions } = clickOpt;
547
- const clickActionResult = await performClickActions(locator, (options) => originalClick(options), clickOptions, position, moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove);
613
+ const clickActionResult = await performClickActions(locator, (options) => originalClick(options), clickOptions, options.autoZoomOptions, position, moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove);
548
614
  innerEvents.push(...(clickActionResult?.innerEvents ?? []));
549
615
  elementRect = clickActionResult?.elementRect;
550
616
  }
551
617
  else {
552
- const isFirstAutoZoomEvent = isInsideAutoZoom() && getLastZoomLocation() === null;
553
- const locatorRect = await scrollIntoViewIfNeeded(locator);
554
- // If inside autoZoom: await zoom duration for camera pan, then update zoom tracker
555
- if (isInsideAutoZoom() && locatorRect) {
556
- const targetX = locatorRect.x + locatorRect.width / 2;
557
- const targetY = locatorRect.y + locatorRect.height / 2;
558
- const lastLoc = getLastZoomLocation();
559
- if (lastLoc !== null) {
560
- const zoomDur = getZoomDuration() ?? 0;
561
- if (zoomDur > 0) {
562
- await sleep(zoomDur);
563
- }
564
- }
565
- setLastZoomLocation({
566
- x: targetX,
567
- y: targetY,
568
- elementRect: locatorRect,
569
- eventType: 'fill',
570
- });
571
- }
572
- if (isFirstAutoZoomEvent) {
618
+ const { locatorRect, isFirstAutoZoomInteraction } = await prepareAutoZoomForLocator(locator, 'fill', options?.autoZoomOptions);
619
+ if (isFirstAutoZoomInteraction) {
573
620
  const postDelay = getPostZoomInOutDelay() ?? 0;
574
621
  if (postDelay > 0)
575
622
  await sleep(postDelay);
576
623
  }
577
624
  elementRect = locatorRect;
625
+ if (isInsideAutoZoom() && locatorRect) {
626
+ const zoomDur = getZoomDuration() ?? 0;
627
+ if (zoomDur > 0) {
628
+ await sleep(zoomDur);
629
+ }
630
+ }
578
631
  }
579
632
  // Hide cursor while typing (will be shown again on next mouse move)
580
633
  const page = locator.page();
@@ -591,62 +644,192 @@ export function instrumentLocator(locator) {
591
644
  });
592
645
  }
593
646
  }
594
- // Strip the click and hideMouse options before forwarding to the original Playwright method
595
- const { click: _click, hideMouse: _hideMouse, ...pressOptions } = options ?? {};
596
- const typingStart = Date.now();
597
647
  await originalPressSequentially(text, pressOptions);
598
- const typingEnd = Date.now();
599
648
  const pressWaitStart = Date.now();
600
649
  await sleep(POST_ACTION_SLEEP);
601
650
  const pressWaitEnd = Date.now();
602
651
  if (activeClickRecorder) {
603
- // addInput requires at least one inner event; use typing start/end as a
604
- // fallback span when no other inner events (click, mouseHide) were collected.
605
- const eventsToRecord = innerEvents.length > 0
606
- ? innerEvents
607
- : [
608
- { type: 'mouseDown', startMs: typingStart, endMs: typingStart },
609
- { type: 'mouseUp', startMs: typingStart, endMs: typingEnd },
610
- ];
611
- eventsToRecord.push({
652
+ innerEvents.push({
612
653
  type: 'mouseWait',
613
654
  startMs: pressWaitStart,
614
655
  endMs: pressWaitEnd,
615
656
  });
616
- activeClickRecorder.addInput('pressSequentially', elementRect, eventsToRecord);
657
+ activeClickRecorder.addInput('pressSequentially', elementRect, innerEvents);
617
658
  }
618
659
  };
660
+ const originalFill = locator.fill.bind(locator);
619
661
  locator.fill = async (value, options) => {
620
- const duration = options?.duration ?? 1000;
662
+ if (isInsideHide()) {
663
+ const { duration: _duration, click: _click, position: _position, hideMouse: _hideMouse, autoZoomOptions: _autoZoomOptions, ...fillOptions } = options ?? {};
664
+ return originalFill(value, fillOptions);
665
+ }
666
+ if (options?.click !== undefined) {
667
+ await sleep(PRE_ACTION_SLEEP);
668
+ const clickActionResult = await performClickActions(locator, (clickOptions) => originalClick(clickOptions), {}, options.autoZoomOptions, options.position, options.click.moveDuration, options.click.moveSpeed, options.click.beforeClickPause, options.click.moveEasing, options.click.postClickPause, options.click.postClickMove);
669
+ const innerEvents = [...(clickActionResult?.innerEvents ?? [])];
670
+ const elementRect = clickActionResult?.elementRect;
671
+ const page = locator.page();
672
+ if (options.hideMouse === true) {
673
+ const cursorVisible = mouseVisibilities.get(page) ?? true;
674
+ if (cursorVisible) {
675
+ mouseVisibilities.set(page, false);
676
+ const hideMs = Date.now();
677
+ innerEvents.push({
678
+ type: 'mouseHide',
679
+ startMs: hideMs,
680
+ endMs: hideMs,
681
+ });
682
+ }
683
+ }
684
+ await locator.evaluate((element) => {
685
+ if (element instanceof HTMLInputElement ||
686
+ element instanceof HTMLTextAreaElement) {
687
+ element.focus();
688
+ element.select();
689
+ return;
690
+ }
691
+ if (element instanceof HTMLElement && element.isContentEditable) {
692
+ element.focus();
693
+ const selection = element.ownerDocument.getSelection();
694
+ if (!selection)
695
+ return;
696
+ const range = element.ownerDocument.createRange();
697
+ range.selectNodeContents(element);
698
+ selection.removeAllRanges();
699
+ selection.addRange(range);
700
+ }
701
+ });
702
+ const duration = options.duration ?? 1000;
703
+ const delay = value.length > 0 ? duration / value.length : 0;
704
+ await page.keyboard.type(value, { delay });
705
+ const fillWaitStart = Date.now();
706
+ await sleep(POST_ACTION_SLEEP);
707
+ const fillWaitEnd = Date.now();
708
+ innerEvents.push({
709
+ type: 'mouseWait',
710
+ startMs: fillWaitStart,
711
+ endMs: fillWaitEnd,
712
+ });
713
+ if (activeClickRecorder) {
714
+ activeClickRecorder.addInput('pressSequentially', elementRect, innerEvents);
715
+ }
716
+ return;
717
+ }
718
+ await sleep(PRE_ACTION_SLEEP);
719
+ const page = locator.page();
720
+ const innerEvents = [];
721
+ const fillOptions = options ?? {};
722
+ const locatorRect = await locator.boundingBox();
723
+ let elementRect = locatorRect ?? undefined;
724
+ if (fillOptions.hideMouse === true) {
725
+ const cursorVisible = mouseVisibilities.get(page) ?? true;
726
+ if (cursorVisible) {
727
+ mouseVisibilities.set(page, false);
728
+ const hideMs = Date.now();
729
+ innerEvents.push({
730
+ type: 'mouseHide',
731
+ startMs: hideMs,
732
+ endMs: hideMs,
733
+ });
734
+ }
735
+ }
736
+ if (isInsideAutoZoom() && locatorRect) {
737
+ const correctedScrollResult = await new ZoomScrollHandler(options?.autoZoomOptions).scroll(locator);
738
+ const correctedRect = correctedScrollResult.locatorRect ?? locatorRect;
739
+ const focusStart = Date.now();
740
+ await locator.evaluate((element) => {
741
+ if (element instanceof HTMLInputElement ||
742
+ element instanceof HTMLTextAreaElement) {
743
+ element.focus();
744
+ element.select();
745
+ return;
746
+ }
747
+ if (element instanceof HTMLElement && element.isContentEditable) {
748
+ element.focus();
749
+ const selection = element.ownerDocument.getSelection();
750
+ if (!selection)
751
+ return;
752
+ const range = element.ownerDocument.createRange();
753
+ range.selectNodeContents(element);
754
+ selection.removeAllRanges();
755
+ selection.addRange(range);
756
+ }
757
+ });
758
+ const focusEnd = Date.now();
759
+ innerEvents.push({
760
+ type: 'focusChange',
761
+ startMs: focusStart,
762
+ endMs: focusEnd,
763
+ x: correctedRect.x + correctedRect.width / 2,
764
+ y: correctedRect.y + correctedRect.height / 2,
765
+ easing: getZoomEasing() ?? 'ease-in-out',
766
+ focusOnly: true,
767
+ elementRect: correctedRect,
768
+ });
769
+ elementRect = correctedRect;
770
+ }
771
+ else {
772
+ await locator.evaluate((element) => {
773
+ if (element instanceof HTMLInputElement ||
774
+ element instanceof HTMLTextAreaElement) {
775
+ element.focus();
776
+ element.select();
777
+ return;
778
+ }
779
+ if (element instanceof HTMLElement && element.isContentEditable) {
780
+ element.focus();
781
+ const selection = element.ownerDocument.getSelection();
782
+ if (!selection)
783
+ return;
784
+ const range = element.ownerDocument.createRange();
785
+ range.selectNodeContents(element);
786
+ selection.removeAllRanges();
787
+ selection.addRange(range);
788
+ }
789
+ });
790
+ }
791
+ const duration = fillOptions.duration ?? 1000;
621
792
  const delay = value.length > 0 ? duration / value.length : 0;
622
- const pressOptions = { delay };
623
- if (options?.timeout !== undefined)
624
- pressOptions.timeout = options.timeout;
625
- if (options?.click !== undefined)
626
- pressOptions.click = options.click;
627
- if (options?.position !== undefined)
628
- pressOptions.position = options.position;
629
- if (options?.hideMouse !== undefined)
630
- pressOptions.hideMouse = options.hideMouse;
631
- return locator.pressSequentially(value, pressOptions);
793
+ await page.keyboard.type(value, { delay });
794
+ const fillWaitStart = Date.now();
795
+ await sleep(POST_ACTION_SLEEP);
796
+ const fillWaitEnd = Date.now();
797
+ innerEvents.push({
798
+ type: 'mouseWait',
799
+ startMs: fillWaitStart,
800
+ endMs: fillWaitEnd,
801
+ });
802
+ if (activeClickRecorder &&
803
+ (isInsideAutoZoom() || fillOptions.hideMouse === true)) {
804
+ activeClickRecorder.addInput('pressSequentially', elementRect, innerEvents);
805
+ }
806
+ return;
632
807
  };
633
808
  const originalTap = locator.tap.bind(locator);
634
809
  locator.tap = async (options) => {
635
810
  const clickOpt = options?.click;
636
- const { click: _click, position, ...tapOpts } = options ?? {};
637
- return performSimpleAction(locator, (options) => originalTap(options), tapOpts, 'tap', clickOpt, position);
811
+ const { click: _click, position, autoZoomOptions, ...tapOpts } = options ?? {};
812
+ return performSimpleAction(locator, (options) => originalTap(options), tapOpts, 'tap', clickOpt, autoZoomOptions, position);
638
813
  };
639
814
  const originalCheck = locator.check.bind(locator);
640
815
  locator.check = async (options) => {
641
816
  const clickOpt = options?.click;
642
- const { click: _click, position, ...checkOpts } = options ?? {};
643
- return performSimpleAction(locator, (options) => originalCheck(options), checkOpts, 'check', clickOpt, position);
817
+ const position = options?.position;
818
+ const { click: _click, autoZoomOptions, ...checkOpts } = options ?? {};
819
+ if (isInsideHide()) {
820
+ return originalCheck(checkOpts);
821
+ }
822
+ return performSimpleAction(locator, (options) => originalCheck(options), checkOpts, 'check', clickOpt, autoZoomOptions, position, false);
644
823
  };
645
824
  const originalUncheck = locator.uncheck.bind(locator);
646
825
  locator.uncheck = async (options) => {
647
826
  const clickOpt = options?.click;
648
- const { click: _click, position, ...uncheckOpts } = options ?? {};
649
- return performSimpleAction(locator, (options) => originalUncheck(options), uncheckOpts, 'uncheck', clickOpt, position);
827
+ const position = options?.position;
828
+ const { click: _click, autoZoomOptions, ...uncheckOpts } = options ?? {};
829
+ if (isInsideHide()) {
830
+ return originalUncheck(uncheckOpts);
831
+ }
832
+ return performSimpleAction(locator, (options) => originalUncheck(options), uncheckOpts, 'uncheck', clickOpt, autoZoomOptions, position, false);
650
833
  };
651
834
  locator.setChecked = async (checked, options) => {
652
835
  if (checked) {
@@ -659,11 +842,14 @@ export function instrumentLocator(locator) {
659
842
  const originalSelectOption = locator.selectOption.bind(locator);
660
843
  locator.selectOption = async (values, options) => {
661
844
  const clickOpt = options?.click;
662
- const { click: _click, position, ...selectOpts } = options ?? {};
845
+ const { click: _click, position, autoZoomOptions, ...selectOpts } = options ?? {};
846
+ if (isInsideHide()) {
847
+ return originalSelectOption(values, selectOpts);
848
+ }
663
849
  let result = [];
664
850
  await performSimpleAction(locator, (options) => originalSelectOption(values, options).then((res) => {
665
851
  result = res;
666
- }), selectOpts, 'select', clickOpt, position);
852
+ }), selectOpts, 'select', clickOpt, autoZoomOptions, position, false);
667
853
  return result;
668
854
  };
669
855
  const originalHover = locator.hover.bind(locator);
@@ -673,8 +859,8 @@ export function instrumentLocator(locator) {
673
859
  const page = locator.page();
674
860
  const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
675
861
  const moveStartTime = Date.now();
676
- const locatorRect = await scrollIntoViewIfNeeded(locator);
677
- const scrollElapsedMs = Date.now() - moveStartTime;
862
+ const scrollResult = await new ZoomScrollHandler().scroll(locator);
863
+ const { locatorRect } = scrollResult;
678
864
  const innerEvents = [];
679
865
  const targetPos = position ??
680
866
  (locatorRect
@@ -689,8 +875,7 @@ export function instrumentLocator(locator) {
689
875
  defaultDuration: 1000,
690
876
  context: 'hover move',
691
877
  });
692
- const effectiveDuration = Math.max(0, resolvedDuration - scrollElapsedMs);
693
- innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, effectiveDuration, moveEasing, moveStartTime, locatorRect));
878
+ innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, resolveMoveDuration(scrollResult, resolvedDuration), moveEasing, resolveMoveStartMs(scrollResult, moveStartTime), locatorRect));
694
879
  }
695
880
  const waitStartMs = Date.now();
696
881
  await originalHover({
@@ -717,8 +902,8 @@ export function instrumentLocator(locator) {
717
902
  const page = locator.page();
718
903
  const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
719
904
  const moveStartTime = Date.now();
720
- const locatorRect = await scrollIntoViewIfNeeded(locator);
721
- const scrollElapsedMs = Date.now() - moveStartTime;
905
+ const scrollResult = await new ZoomScrollHandler().scroll(locator);
906
+ const { locatorRect } = scrollResult;
722
907
  const innerEvents = [];
723
908
  const targetPos = locatorRect
724
909
  ? { x: locatorRect.width / 2, y: locatorRect.height / 2 }
@@ -732,8 +917,7 @@ export function instrumentLocator(locator) {
732
917
  defaultDuration: 1000,
733
918
  context: 'selectText move',
734
919
  });
735
- const effectiveDuration = Math.max(0, resolvedDuration - scrollElapsedMs);
736
- innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, effectiveDuration, moveEasing, moveStartTime, locatorRect));
920
+ innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, resolveMoveDuration(scrollResult, resolvedDuration), moveEasing, resolveMoveStartMs(scrollResult, moveStartTime), locatorRect));
737
921
  }
738
922
  await sleep(beforeClickPause);
739
923
  await originalSelectText(selectOpts);
@@ -772,8 +956,8 @@ export function instrumentLocator(locator) {
772
956
  const page = locator.page();
773
957
  const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
774
958
  const moveStartTime = Date.now();
775
- const sourceRect = await scrollIntoViewIfNeeded(locator);
776
- const scrollElapsedMs = Date.now() - moveStartTime;
959
+ const scrollResult = await new ZoomScrollHandler().scroll(locator);
960
+ const { locatorRect: sourceRect } = scrollResult;
777
961
  const targetBb = await target.boundingBox();
778
962
  const targetRect = targetBb
779
963
  ? {
@@ -802,8 +986,7 @@ export function instrumentLocator(locator) {
802
986
  defaultDuration: 1000,
803
987
  context: 'dragTo move',
804
988
  });
805
- const effectiveDuration = Math.max(0, resolvedDuration - scrollElapsedMs);
806
- innerEvents.push(await animateMouseMove(page, mouseMoveInternal, toX, toY, effectiveDuration, moveEasing, moveStartTime, sourceRect));
989
+ innerEvents.push(await animateMouseMove(page, mouseMoveInternal, toX, toY, resolveMoveDuration(scrollResult, resolvedDuration), moveEasing, resolveMoveStartMs(scrollResult, moveStartTime), sourceRect));
807
990
  }
808
991
  // 2. preDragPause + mouseDown
809
992
  await sleep(preDragPause);
@@ -945,15 +1128,14 @@ export async function instrumentPage(page) {
945
1128
  activeClickRecorder.addInput('mouseShow', undefined, [showEvent]);
946
1129
  }
947
1130
  const moveEvent = {
948
- type: 'mouseMove',
1131
+ type: 'focusChange',
949
1132
  startMs,
950
1133
  endMs,
951
- duration,
952
1134
  x,
953
1135
  y,
954
1136
  ...(duration > 0 ? { easing } : {}),
955
1137
  };
956
- activeClickRecorder.addInput('mouseMove', undefined, [moveEvent]);
1138
+ activeClickRecorder.addInput('focusChange', undefined, [moveEvent]);
957
1139
  }
958
1140
  };
959
1141
  mouseVisibilities.set(page, true);