screenci 0.0.18 → 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.
@@ -1,6 +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
3
  import { isInsideHide } from './hide.js';
4
+ import { ZoomScrollHandler } from './scroll.js';
4
5
  let activeClickRecorder = null;
5
6
  export function setActiveClickRecorder(recorder) {
6
7
  activeClickRecorder = recorder;
@@ -106,6 +107,18 @@ const FRAME_LOCATOR_SELF_RETURN_METHODS = [
106
107
  'last',
107
108
  'nth',
108
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
+ }
109
122
  // Per-page storage for the most recently captured DOM click event data.
110
123
  // Reset to null before each instrumented click; set by the exposeFunction callback.
111
124
  const pendingClickData = new WeakMap();
@@ -145,43 +158,10 @@ export function scrollIntoViewAsync(locator, options = {}) {
145
158
  }));
146
159
  }), { behavior, block, timeout, postScrollTimeout });
147
160
  }
148
- /**
149
- * Scrolls the locator into view only if it is at least partially outside the
150
- * viewport, then returns the (possibly updated) bounding box as an ElementRect.
151
- * Returns undefined if the bounding box cannot be determined.
152
- */
153
- async function scrollIntoViewIfNeeded(locator, block = 'center') {
154
- const page = locator.page();
155
- const bb = await locator.boundingBox();
156
- if (!bb)
157
- return undefined;
158
- const viewportSize = page.viewportSize();
159
- if (viewportSize) {
160
- const isFullyInViewport = bb.x >= 0 &&
161
- bb.y >= 0 &&
162
- bb.x + bb.width <= viewportSize.width &&
163
- bb.y + bb.height <= viewportSize.height;
164
- if (!isFullyInViewport) {
165
- await scrollIntoViewAsync(locator, { block });
166
- const newBb = await locator.boundingBox();
167
- if (newBb)
168
- return {
169
- x: newBb.x,
170
- y: newBb.y,
171
- width: newBb.width,
172
- height: newBb.height,
173
- };
174
- }
175
- }
176
- else {
177
- logger.warn('[screenci] Unable to determine viewport size; skipping auto-scroll check.');
178
- }
179
- return { x: bb.x, y: bb.y, width: bb.width, height: bb.height };
180
- }
181
161
  const CURSOR_STEP_MS = 1000 / 60;
182
162
  /**
183
163
  * Physically moves the mouse from its current tracked position to (targetX,
184
- * targetY) over `duration` ms using `easing`, then returns a MouseMoveEvent
164
+ * targetY) over `duration` ms using `easing`, then returns a FocusChangeEvent
185
165
  * whose startMs is `eventStartMs` (which may predate this call, e.g. when a
186
166
  * scroll consumed part of moveDuration). When duration ≤ 0 the cursor is
187
167
  * snapped directly to the target with a single move call.
@@ -209,32 +189,40 @@ async function animateMouseMove(page, mouseMoveInternal, targetX, targetY, durat
209
189
  mousePositions.set(page, { x: targetX, y: targetY });
210
190
  const endMs = Date.now();
211
191
  return {
212
- type: 'mouseMove',
192
+ type: 'focusChange',
213
193
  startMs: eventStartMs,
214
194
  endMs,
215
- duration: Math.max(0, endMs - eventStartMs),
216
195
  x: targetX,
217
196
  y: targetY,
218
197
  easing,
219
198
  ...(elementRect !== undefined ? { elementRect } : {}),
220
199
  };
221
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
+ }
222
211
  /**
223
212
  * Performs all click mechanics (scroll-check, zoom handling, cursor animation,
224
213
  * click, post-click move) and returns the collected timing/position data.
225
214
  * Returns null if coordinates could not be determined (no DOM event and no
226
215
  * locator bounding box).
227
216
  */
228
- 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) {
229
218
  const page = locator.page();
230
219
  pendingClickData.set(page, null);
231
220
  const halfClickDuration = CLICK_DURATION_MS / 2;
232
221
  const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
233
- // Capture before any setLastZoomLocation call changes the state.
234
- const isFirstAutoZoomEvent = isInsideAutoZoom() && getLastZoomLocation() === null;
235
222
  const moveStartTime = Date.now();
236
- const locatorRect = await scrollIntoViewIfNeeded(locator, isInsideAutoZoom() && getLastZoomLocation() !== null ? 'nearest' : 'center');
237
- const scrollElapsedMs = Date.now() - moveStartTime;
223
+ const scrollResult = await new ZoomScrollHandler(autoZoomOptions).scroll(locator);
224
+ const { locatorRect, scrollElapsedMs } = scrollResult;
225
+ const isFirstAutoZoomEvent = scrollResult.isFirstAutoZoomInteraction;
238
226
  if (!locatorRect) {
239
227
  logger.warn('[screenci] Unable to get locator bounding box; skipping auto-scroll check.');
240
228
  }
@@ -270,11 +258,6 @@ async function performClickActions(locator, doClick, clickOptions, position, mov
270
258
  y: locatorRect.height / 2,
271
259
  }
272
260
  : undefined;
273
- // No physical mouse movement happens during the scroll. moveStartTime is
274
- // captured before the scroll so the recorded event spans scroll-start →
275
- // animation-end, making the cursor appear to move during the scroll in the
276
- // video. After the scroll, the remaining duration drives the physical easing
277
- // animation via the shared helper.
278
261
  if (targetPos && locatorRect) {
279
262
  const targetX = locatorRect.x + targetPos.x;
280
263
  const targetY = locatorRect.y + targetPos.y;
@@ -284,19 +267,40 @@ async function performClickActions(locator, doClick, clickOptions, position, mov
284
267
  defaultDuration: 1000,
285
268
  context: 'click move',
286
269
  });
287
- const effectiveDuration = Math.max(0, resolvedDuration - scrollElapsedMs);
288
- 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
+ }
289
292
  }
290
293
  else {
291
294
  assertDurationOrSpeed(moveDuration, moveSpeed, 'click move');
292
- const remainingMs = Math.max(0, (moveSpeed === undefined ? (moveDuration ?? 1000) : 0) - scrollElapsedMs);
295
+ const remainingMs = Math.max(0, moveSpeed === undefined ? (moveDuration ?? 1000) : 0);
293
296
  if (remainingMs > 0) {
294
297
  await new Promise((resolve) => setTimeout(resolve, remainingMs));
295
298
  }
296
299
  }
300
+ const zoomDur = isInsideAutoZoom() && locatorRect ? (getZoomDuration() ?? 0) : 0;
297
301
  const effectiveBeforeClickPause = isFirstAutoZoomEvent
298
- ? Math.max(beforeClickPause, getPostZoomInOutDelay() ?? 0)
299
- : beforeClickPause;
302
+ ? Math.max(beforeClickPause, getPostZoomInOutDelay() ?? 0, zoomDur)
303
+ : Math.max(beforeClickPause, zoomDur);
300
304
  await new Promise((resolve) => setTimeout(resolve, effectiveBeforeClickPause));
301
305
  await new Promise((resolve) => setTimeout(resolve, halfClickDuration));
302
306
  // Note click can take some time, but better to show it before than after
@@ -308,11 +312,47 @@ async function performClickActions(locator, doClick, clickOptions, position, mov
308
312
  endMs: clickTime,
309
313
  easing: 'ease-in-out',
310
314
  });
311
- await doClick({
312
- ...clickOptions,
313
- ...(targetPos ? { position: targetPos } : {}),
314
- });
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
+ }
315
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
+ }
316
356
  const mouseUpEnd = Date.now() + halfClickDuration;
317
357
  innerEvents.push({
318
358
  type: 'mouseUp',
@@ -386,26 +426,23 @@ async function performClickActions(locator, doClick, clickOptions, position, mov
386
426
  }
387
427
  const postClickMoveEndMs = Date.now();
388
428
  innerEvents.push({
389
- type: 'mouseMove',
429
+ type: 'focusChange',
390
430
  startMs: postClickMoveStartMs,
391
431
  endMs: postClickMoveEndMs,
392
- duration: Math.max(0, postClickMoveEndMs - postClickMoveStartMs),
393
432
  x: targetX,
394
433
  y: targetY,
395
434
  easing,
396
- zoomFollow: false,
397
435
  });
398
436
  mousePositions.set(page, { x: targetX, y: targetY });
399
437
  }
400
438
  }
401
439
  let elementRect;
402
- if (locatorRect) {
403
- elementRect = locatorRect;
404
- }
405
- else if (domClickData) {
406
- console.warn('[screenci] using DOM click data as fallback for elementRect');
440
+ if (domClickData) {
407
441
  elementRect = domClickData.targetRect;
408
442
  }
443
+ else if (locatorRect) {
444
+ elementRect = locatorRect ?? undefined;
445
+ }
409
446
  if (elementRect) {
410
447
  return {
411
448
  elementRect,
@@ -417,15 +454,17 @@ async function performClickActions(locator, doClick, clickOptions, position, mov
417
454
  return null;
418
455
  }
419
456
  }
420
- async function prepareAutoZoomForLocator(locator, eventType) {
421
- const hadPreviousZoomLocation = getLastZoomLocation() !== null;
422
- const zoomDur = isInsideAutoZoom() ? (getZoomDuration() ?? 0) : 0;
423
- if (isInsideAutoZoom() && hadPreviousZoomLocation && zoomDur > 0) {
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) {
424
463
  await sleep(zoomDur);
425
464
  }
426
- const locatorRect = await scrollIntoViewIfNeeded(locator, hadPreviousZoomLocation ? 'nearest' : 'center');
427
- if (isInsideAutoZoom() && locatorRect) {
428
- if (!hadPreviousZoomLocation && zoomDur > 0) {
465
+ const { locatorRect, isFirstAutoZoomInteraction } = await scrollHandler.scroll(locator);
466
+ if (scrollHandler.isInsideAutoZoom && locatorRect) {
467
+ if (isFirstAutoZoomInteraction && zoomDur > 0) {
429
468
  await sleep(zoomDur);
430
469
  }
431
470
  setLastZoomLocation({
@@ -435,25 +474,36 @@ async function prepareAutoZoomForLocator(locator, eventType) {
435
474
  eventType,
436
475
  });
437
476
  }
438
- return locatorRect;
477
+ return { locatorRect, isFirstAutoZoomInteraction };
439
478
  }
440
- async function performSimpleAction(locator, doAction, options, subType, clickOpt, position, recordMousePress = subType === 'tap') {
479
+ async function performSimpleAction(locator, doAction, options, subType, clickOpt, autoZoomOptions, position, recordMousePress = subType === 'tap') {
441
480
  await sleep(PRE_ACTION_SLEEP);
442
481
  let innerEvents = [];
443
482
  let elementRect;
444
483
  if (clickOpt !== undefined) {
445
484
  const { moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove, } = clickOpt;
446
- 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);
447
486
  innerEvents = clickActionResult?.innerEvents ?? [];
448
487
  elementRect = clickActionResult?.elementRect;
449
488
  }
450
489
  else {
451
- const isFirstAutoZoomEvent = isInsideAutoZoom() && getLastZoomLocation() === null;
452
- const locatorRect = await prepareAutoZoomForLocator(locator, 'fill');
453
- if (isFirstAutoZoomEvent) {
454
- const postDelay = getPostZoomInOutDelay() ?? 0;
455
- if (postDelay > 0)
456
- await sleep(postDelay);
490
+ const locatorRect = await locator.boundingBox();
491
+ if (isInsideAutoZoom() && locatorRect) {
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,
505
+ elementRect: locatorRect,
506
+ });
457
507
  }
458
508
  const targetPosition = locatorRect
459
509
  ? {
@@ -467,7 +517,7 @@ async function performSimpleAction(locator, doAction, options, subType, clickOpt
467
517
  ...(targetPosition ? { position: targetPosition } : {}),
468
518
  });
469
519
  const endTime = Date.now();
470
- elementRect = locatorRect;
520
+ elementRect = locatorRect ?? undefined;
471
521
  if (recordMousePress) {
472
522
  const midTime = (startTime + endTime) / 2;
473
523
  innerEvents.push({
@@ -494,9 +544,9 @@ async function performSimpleAction(locator, doAction, options, subType, clickOpt
494
544
  activeClickRecorder.addInput(subType, elementRect, innerEvents);
495
545
  }
496
546
  }
497
- 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) {
498
548
  await sleep(PRE_ACTION_SLEEP);
499
- 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);
500
550
  const clickWaitStart = Date.now();
501
551
  await sleep(POST_ACTION_SLEEP);
502
552
  const clickWaitEnd = Date.now();
@@ -536,7 +586,7 @@ export function instrumentLocator(locator) {
536
586
  instrumented.add(locator);
537
587
  const originalClick = locator.click.bind(locator);
538
588
  locator.click = async (options) => {
539
- 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 ?? {};
540
590
  if (isInsideHide()) {
541
591
  return originalClick({
542
592
  ...clickOptions,
@@ -544,11 +594,11 @@ export function instrumentLocator(locator) {
544
594
  });
545
595
  }
546
596
  assertDurationOrSpeed(moveDuration, moveSpeed, 'click move');
547
- 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);
548
598
  };
549
599
  const originalPressSequentially = locator.pressSequentially.bind(locator);
550
600
  locator.pressSequentially = async (text, options) => {
551
- const { click: _click, hideMouse: _hideMouse, position: _position, ...pressOptions } = options ?? {};
601
+ const { click: _click, autoZoomOptions, hideMouse: _hideMouse, position: _position, ...pressOptions } = options ?? {};
552
602
  if (isInsideHide()) {
553
603
  return originalPressSequentially(text, pressOptions);
554
604
  }
@@ -560,19 +610,24 @@ export function instrumentLocator(locator) {
560
610
  const clickOpt = options.click;
561
611
  const position = options.position;
562
612
  const { moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove, ...clickOptions } = clickOpt;
563
- 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);
564
614
  innerEvents.push(...(clickActionResult?.innerEvents ?? []));
565
615
  elementRect = clickActionResult?.elementRect;
566
616
  }
567
617
  else {
568
- const isFirstAutoZoomEvent = isInsideAutoZoom() && getLastZoomLocation() === null;
569
- const locatorRect = await prepareAutoZoomForLocator(locator, 'fill');
570
- if (isFirstAutoZoomEvent) {
618
+ const { locatorRect, isFirstAutoZoomInteraction } = await prepareAutoZoomForLocator(locator, 'fill', options?.autoZoomOptions);
619
+ if (isFirstAutoZoomInteraction) {
571
620
  const postDelay = getPostZoomInOutDelay() ?? 0;
572
621
  if (postDelay > 0)
573
622
  await sleep(postDelay);
574
623
  }
575
624
  elementRect = locatorRect;
625
+ if (isInsideAutoZoom() && locatorRect) {
626
+ const zoomDur = getZoomDuration() ?? 0;
627
+ if (zoomDur > 0) {
628
+ await sleep(zoomDur);
629
+ }
630
+ }
576
631
  }
577
632
  // Hide cursor while typing (will be shown again on next mouse move)
578
633
  const page = locator.page();
@@ -605,47 +660,176 @@ export function instrumentLocator(locator) {
605
660
  const originalFill = locator.fill.bind(locator);
606
661
  locator.fill = async (value, options) => {
607
662
  if (isInsideHide()) {
608
- const { duration: _duration, click: _click, position: _position, hideMouse: _hideMouse, ...fillOptions } = options ?? {};
663
+ const { duration: _duration, click: _click, position: _position, hideMouse: _hideMouse, autoZoomOptions: _autoZoomOptions, ...fillOptions } = options ?? {};
609
664
  return originalFill(value, fillOptions);
610
665
  }
611
- const duration = options?.duration ?? 1000;
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;
612
792
  const delay = value.length > 0 ? duration / value.length : 0;
613
- const pressOptions = { delay };
614
- if (options?.timeout !== undefined)
615
- pressOptions.timeout = options.timeout;
616
- if (options?.click !== undefined)
617
- pressOptions.click = options.click;
618
- if (options?.position !== undefined)
619
- pressOptions.position = options.position;
620
- if (options?.hideMouse !== undefined)
621
- pressOptions.hideMouse = options.hideMouse;
622
- 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;
623
807
  };
624
808
  const originalTap = locator.tap.bind(locator);
625
809
  locator.tap = async (options) => {
626
810
  const clickOpt = options?.click;
627
- const { click: _click, position, ...tapOpts } = options ?? {};
628
- 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);
629
813
  };
630
814
  const originalCheck = locator.check.bind(locator);
631
815
  locator.check = async (options) => {
632
816
  const clickOpt = options?.click;
633
817
  const position = options?.position;
634
- const { click: _click, ...checkOpts } = options ?? {};
818
+ const { click: _click, autoZoomOptions, ...checkOpts } = options ?? {};
635
819
  if (isInsideHide()) {
636
820
  return originalCheck(checkOpts);
637
821
  }
638
- return performSimpleAction(locator, (options) => originalCheck(options), checkOpts, 'check', clickOpt, position, false);
822
+ return performSimpleAction(locator, (options) => originalCheck(options), checkOpts, 'check', clickOpt, autoZoomOptions, position, false);
639
823
  };
640
824
  const originalUncheck = locator.uncheck.bind(locator);
641
825
  locator.uncheck = async (options) => {
642
826
  const clickOpt = options?.click;
643
827
  const position = options?.position;
644
- const { click: _click, ...uncheckOpts } = options ?? {};
828
+ const { click: _click, autoZoomOptions, ...uncheckOpts } = options ?? {};
645
829
  if (isInsideHide()) {
646
830
  return originalUncheck(uncheckOpts);
647
831
  }
648
- return performSimpleAction(locator, (options) => originalUncheck(options), uncheckOpts, 'uncheck', clickOpt, position, false);
832
+ return performSimpleAction(locator, (options) => originalUncheck(options), uncheckOpts, 'uncheck', clickOpt, autoZoomOptions, position, false);
649
833
  };
650
834
  locator.setChecked = async (checked, options) => {
651
835
  if (checked) {
@@ -658,14 +842,14 @@ export function instrumentLocator(locator) {
658
842
  const originalSelectOption = locator.selectOption.bind(locator);
659
843
  locator.selectOption = async (values, options) => {
660
844
  const clickOpt = options?.click;
661
- const { click: _click, position, ...selectOpts } = options ?? {};
845
+ const { click: _click, position, autoZoomOptions, ...selectOpts } = options ?? {};
662
846
  if (isInsideHide()) {
663
847
  return originalSelectOption(values, selectOpts);
664
848
  }
665
849
  let result = [];
666
850
  await performSimpleAction(locator, (options) => originalSelectOption(values, options).then((res) => {
667
851
  result = res;
668
- }), selectOpts, 'select', clickOpt, position, false);
852
+ }), selectOpts, 'select', clickOpt, autoZoomOptions, position, false);
669
853
  return result;
670
854
  };
671
855
  const originalHover = locator.hover.bind(locator);
@@ -675,10 +859,8 @@ export function instrumentLocator(locator) {
675
859
  const page = locator.page();
676
860
  const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
677
861
  const moveStartTime = Date.now();
678
- const locatorRect = await scrollIntoViewIfNeeded(locator, isInsideAutoZoom() && getLastZoomLocation() !== null
679
- ? 'nearest'
680
- : 'center');
681
- const scrollElapsedMs = Date.now() - moveStartTime;
862
+ const scrollResult = await new ZoomScrollHandler().scroll(locator);
863
+ const { locatorRect } = scrollResult;
682
864
  const innerEvents = [];
683
865
  const targetPos = position ??
684
866
  (locatorRect
@@ -693,8 +875,7 @@ export function instrumentLocator(locator) {
693
875
  defaultDuration: 1000,
694
876
  context: 'hover move',
695
877
  });
696
- const effectiveDuration = Math.max(0, resolvedDuration - scrollElapsedMs);
697
- 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));
698
879
  }
699
880
  const waitStartMs = Date.now();
700
881
  await originalHover({
@@ -721,10 +902,8 @@ export function instrumentLocator(locator) {
721
902
  const page = locator.page();
722
903
  const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
723
904
  const moveStartTime = Date.now();
724
- const locatorRect = await scrollIntoViewIfNeeded(locator, isInsideAutoZoom() && getLastZoomLocation() !== null
725
- ? 'nearest'
726
- : 'center');
727
- const scrollElapsedMs = Date.now() - moveStartTime;
905
+ const scrollResult = await new ZoomScrollHandler().scroll(locator);
906
+ const { locatorRect } = scrollResult;
728
907
  const innerEvents = [];
729
908
  const targetPos = locatorRect
730
909
  ? { x: locatorRect.width / 2, y: locatorRect.height / 2 }
@@ -738,8 +917,7 @@ export function instrumentLocator(locator) {
738
917
  defaultDuration: 1000,
739
918
  context: 'selectText move',
740
919
  });
741
- const effectiveDuration = Math.max(0, resolvedDuration - scrollElapsedMs);
742
- 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));
743
921
  }
744
922
  await sleep(beforeClickPause);
745
923
  await originalSelectText(selectOpts);
@@ -778,10 +956,8 @@ export function instrumentLocator(locator) {
778
956
  const page = locator.page();
779
957
  const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
780
958
  const moveStartTime = Date.now();
781
- const sourceRect = await scrollIntoViewIfNeeded(locator, isInsideAutoZoom() && getLastZoomLocation() !== null
782
- ? 'nearest'
783
- : 'center');
784
- const scrollElapsedMs = Date.now() - moveStartTime;
959
+ const scrollResult = await new ZoomScrollHandler().scroll(locator);
960
+ const { locatorRect: sourceRect } = scrollResult;
785
961
  const targetBb = await target.boundingBox();
786
962
  const targetRect = targetBb
787
963
  ? {
@@ -810,8 +986,7 @@ export function instrumentLocator(locator) {
810
986
  defaultDuration: 1000,
811
987
  context: 'dragTo move',
812
988
  });
813
- const effectiveDuration = Math.max(0, resolvedDuration - scrollElapsedMs);
814
- 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));
815
990
  }
816
991
  // 2. preDragPause + mouseDown
817
992
  await sleep(preDragPause);
@@ -953,15 +1128,14 @@ export async function instrumentPage(page) {
953
1128
  activeClickRecorder.addInput('mouseShow', undefined, [showEvent]);
954
1129
  }
955
1130
  const moveEvent = {
956
- type: 'mouseMove',
1131
+ type: 'focusChange',
957
1132
  startMs,
958
1133
  endMs,
959
- duration,
960
1134
  x,
961
1135
  y,
962
1136
  ...(duration > 0 ? { easing } : {}),
963
1137
  };
964
- activeClickRecorder.addInput('mouseMove', undefined, [moveEvent]);
1138
+ activeClickRecorder.addInput('focusChange', undefined, [moveEvent]);
965
1139
  }
966
1140
  };
967
1141
  mouseVisibilities.set(page, true);