screenci 0.0.21 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +145 -81
- package/dist/cli.js.map +1 -1
- package/dist/e2e/instrument.e2e.js +98 -68
- package/dist/e2e/instrument.e2e.js.map +1 -1
- package/dist/src/autoZoom.d.ts +34 -15
- package/dist/src/autoZoom.d.ts.map +1 -1
- package/dist/src/autoZoom.js +100 -69
- package/dist/src/autoZoom.js.map +1 -1
- package/dist/src/changeFocus.d.ts +129 -0
- package/dist/src/changeFocus.d.ts.map +1 -0
- package/dist/src/changeFocus.js +885 -0
- package/dist/src/changeFocus.js.map +1 -0
- package/dist/src/defaults.d.ts +2 -17
- package/dist/src/defaults.d.ts.map +1 -1
- package/dist/src/defaults.js +8 -16
- package/dist/src/defaults.js.map +1 -1
- package/dist/src/easing.d.ts +6 -0
- package/dist/src/easing.d.ts.map +1 -0
- package/dist/src/easing.js +30 -0
- package/dist/src/easing.js.map +1 -0
- package/dist/src/errors.d.ts +10 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +26 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/events.d.ts +34 -12
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +111 -30
- package/dist/src/events.js.map +1 -1
- package/dist/src/instrument.d.ts +0 -6
- package/dist/src/instrument.d.ts.map +1 -1
- package/dist/src/instrument.js +385 -812
- package/dist/src/instrument.js.map +1 -1
- package/dist/src/mouse.d.ts +128 -0
- package/dist/src/mouse.d.ts.map +1 -0
- package/dist/src/mouse.js +268 -0
- package/dist/src/mouse.js.map +1 -0
- package/dist/src/recordingData.d.ts +29 -4
- package/dist/src/recordingData.d.ts.map +1 -1
- package/dist/src/types.d.ts +4 -7
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/zoom.d.ts +69 -0
- package/dist/src/zoom.d.ts.map +1 -0
- package/dist/src/zoom.js +90 -0
- package/dist/src/zoom.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/dist/src/scroll.d.ts +0 -36
- package/dist/src/scroll.d.ts.map +0 -1
- package/dist/src/scroll.js +0 -274
- package/dist/src/scroll.js.map +0 -1
package/dist/src/instrument.js
CHANGED
|
@@ -1,76 +1,14 @@
|
|
|
1
|
-
import { logger } from './logger.js';
|
|
2
|
-
import { isInsideAutoZoom, getZoomDuration, getZoomEasing, getPostZoomInOutDelay, getLastZoomLocation, setLastZoomLocation, } from './autoZoom.js';
|
|
3
1
|
import { isInsideHide } from './hide.js';
|
|
4
|
-
import {
|
|
2
|
+
import { changeFocus } from './changeFocus.js';
|
|
3
|
+
import { CLICK_DURATION_MS, assertDurationOrSpeed, buildMouseDownEvent, buildMouseUpEvent, getOriginalLocatorCheck, getOriginalLocatorClick, getOriginalLocatorSelect, getOriginalLocatorTap, getOriginalLocatorUncheck, getOriginalMouseDown, getOriginalMouseHide, getMousePosition, getOriginalMouseShow, getOriginalMouseUp, isMouseVisible, performMouseClickAction, performMouseDown, performMouseHide, performMouseMove, performMouseShow, performMouseUp, resolveMouseMoveDuration, setOriginalLocatorCheck, setOriginalLocatorClick, setOriginalLocatorSelect, setOriginalLocatorTap, setOriginalLocatorUncheck, setMouseVisible, setOriginalMouseClick, setOriginalMouseDown, setOriginalMouseHide, setOriginalMouseMove, setOriginalMouseShow, setOriginalMouseUp, } from './mouse.js';
|
|
5
4
|
let activeClickRecorder = null;
|
|
6
5
|
export function setActiveClickRecorder(recorder) {
|
|
7
6
|
activeClickRecorder = recorder;
|
|
8
7
|
}
|
|
9
|
-
/**
|
|
10
|
-
* Evaluate a polynomial easing function at normalized time t ∈ [0, 1].
|
|
11
|
-
* Returns the eased progress value (0–1).
|
|
12
|
-
*/
|
|
13
|
-
function evaluateEasingAtT(t, easing) {
|
|
14
|
-
if (t <= 0)
|
|
15
|
-
return 0;
|
|
16
|
-
if (t >= 1)
|
|
17
|
-
return 1;
|
|
18
|
-
switch (easing) {
|
|
19
|
-
case 'linear':
|
|
20
|
-
return t;
|
|
21
|
-
case 'ease-in':
|
|
22
|
-
return t * t * t;
|
|
23
|
-
case 'ease-out':
|
|
24
|
-
return 1 - (1 - t) * (1 - t) * (1 - t);
|
|
25
|
-
case 'ease-in-out':
|
|
26
|
-
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
27
|
-
case 'ease-in-strong':
|
|
28
|
-
return t * t * t * t;
|
|
29
|
-
case 'ease-out-strong':
|
|
30
|
-
return 1 - (1 - t) * (1 - t) * (1 - t) * (1 - t);
|
|
31
|
-
case 'ease-in-out-strong':
|
|
32
|
-
return t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2;
|
|
33
|
-
default: {
|
|
34
|
-
const _ = easing;
|
|
35
|
-
throw new Error(`Unknown easing: ${_}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/** Tracked cursor position per page, updated after each animated move. */
|
|
40
|
-
const mousePositions = new WeakMap();
|
|
41
|
-
/** Tracks cursor visibility per page (true = visible). Defaults to true. */
|
|
42
|
-
const mouseVisibilities = new WeakMap();
|
|
43
|
-
/** Stores the original (un-instrumented) mouse.move per page so internal
|
|
44
|
-
* cursor animations don't emit addInput recorder events. */
|
|
45
|
-
const originalMouseMoves = new WeakMap();
|
|
46
8
|
const instrumented = new WeakSet();
|
|
47
9
|
function sleep(ms) {
|
|
48
10
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
11
|
}
|
|
50
|
-
const CLICK_DURATION_MS = 200;
|
|
51
|
-
const PRE_ACTION_SLEEP = 50;
|
|
52
|
-
const POST_ACTION_SLEEP = 250;
|
|
53
|
-
function assertDurationOrSpeed(duration, speed, context) {
|
|
54
|
-
if (duration !== undefined && speed !== undefined) {
|
|
55
|
-
throw new Error(`[screenci] ${context} accepts either duration or speed, not both.`);
|
|
56
|
-
}
|
|
57
|
-
if (duration !== undefined && (!Number.isFinite(duration) || duration < 0)) {
|
|
58
|
-
throw new Error(`[screenci] ${context} duration must be a finite number >= 0.`);
|
|
59
|
-
}
|
|
60
|
-
if (speed !== undefined && (!Number.isFinite(speed) || speed <= 0)) {
|
|
61
|
-
throw new Error(`[screenci] ${context} speed must be a finite number > 0.`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function resolveMouseMoveDuration(page, targetX, targetY, options) {
|
|
65
|
-
const { duration, speed, defaultDuration, context } = options;
|
|
66
|
-
assertDurationOrSpeed(duration, speed, context);
|
|
67
|
-
if (speed !== undefined) {
|
|
68
|
-
const startPos = mousePositions.get(page) ?? { x: 0, y: 0 };
|
|
69
|
-
const distancePx = Math.hypot(targetX - startPos.x, targetY - startPos.y);
|
|
70
|
-
return (distancePx / speed) * 1000;
|
|
71
|
-
}
|
|
72
|
-
return duration ?? defaultDuration ?? 0;
|
|
73
|
-
}
|
|
74
12
|
const LOCATOR_RETURN_METHODS = [
|
|
75
13
|
'locator',
|
|
76
14
|
'getByAltText',
|
|
@@ -107,294 +45,129 @@ const FRAME_LOCATOR_SELF_RETURN_METHODS = [
|
|
|
107
45
|
'last',
|
|
108
46
|
'nth',
|
|
109
47
|
];
|
|
110
|
-
function
|
|
111
|
-
if (
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
'modifiers',
|
|
116
|
-
'noWaitAfter',
|
|
117
|
-
'timeout',
|
|
118
|
-
'trial',
|
|
119
|
-
];
|
|
120
|
-
return unsupported.every((key) => options[key] === undefined);
|
|
121
|
-
}
|
|
122
|
-
// Per-page storage for the most recently captured DOM click event data.
|
|
123
|
-
// Reset to null before each instrumented click; set by the exposeFunction callback.
|
|
124
|
-
const pendingClickData = new WeakMap();
|
|
125
|
-
export function scrollIntoViewAsync(locator, options = {}) {
|
|
126
|
-
const { behavior = 'smooth', block = 'center', timeout = 5000, postScrollTimeout = 500, } = options;
|
|
127
|
-
return locator.evaluate((element, opts) => new Promise((resolve) => {
|
|
128
|
-
let settled = false;
|
|
129
|
-
const finish = () => {
|
|
130
|
-
if (settled)
|
|
131
|
-
return;
|
|
132
|
-
settled = true;
|
|
133
|
-
clearTimeout(fallback);
|
|
134
|
-
setTimeout(resolve, opts.postScrollTimeout);
|
|
135
|
-
};
|
|
136
|
-
const fallback = setTimeout(finish, opts.timeout);
|
|
137
|
-
// scrollend fires once the smooth-scroll animation completes (Chrome 114+).
|
|
138
|
-
// It captures scroll on any ancestor element via the capture phase.
|
|
139
|
-
window.addEventListener('scrollend', finish, {
|
|
140
|
-
capture: true,
|
|
141
|
-
once: true,
|
|
142
|
-
});
|
|
143
|
-
element.scrollIntoView({ behavior: opts.behavior, block: opts.block });
|
|
144
|
-
// If the element is already in the target position no scroll occurs and
|
|
145
|
-
// scrollend never fires. Detect this: if no scroll event appears within
|
|
146
|
-
// two animation frames the element was already in place.
|
|
147
|
-
let didScroll = false;
|
|
148
|
-
window.addEventListener('scroll', () => {
|
|
149
|
-
didScroll = true;
|
|
150
|
-
}, {
|
|
151
|
-
capture: true,
|
|
152
|
-
passive: true,
|
|
153
|
-
once: true,
|
|
154
|
-
});
|
|
155
|
-
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
156
|
-
if (!didScroll)
|
|
157
|
-
finish();
|
|
158
|
-
}));
|
|
159
|
-
}), { behavior, block, timeout, postScrollTimeout });
|
|
48
|
+
function getRecordedInnerEventEndMs(event) {
|
|
49
|
+
if (event.type === 'focusChange') {
|
|
50
|
+
return Math.max(...(event.mouse ? [event.mouse.endMs] : []), ...(event.scroll ? [event.scroll.endMs] : []), ...(event.zoom ? [event.zoom.endMs] : []));
|
|
51
|
+
}
|
|
52
|
+
return event.endMs;
|
|
160
53
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
54
|
+
function resolveLocatorMouseAction(locator, interactionType) {
|
|
55
|
+
switch (interactionType) {
|
|
56
|
+
case 'click': {
|
|
57
|
+
const action = getOriginalLocatorClick(locator);
|
|
58
|
+
if (action)
|
|
59
|
+
return { doClick: action, supportsTrial: true };
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case 'tap': {
|
|
63
|
+
const action = getOriginalLocatorTap(locator);
|
|
64
|
+
if (action)
|
|
65
|
+
return { doClick: action, supportsTrial: true };
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case 'check': {
|
|
69
|
+
const action = getOriginalLocatorCheck(locator);
|
|
70
|
+
if (action)
|
|
71
|
+
return { doClick: action, supportsTrial: true };
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case 'uncheck': {
|
|
75
|
+
const action = getOriginalLocatorUncheck(locator);
|
|
76
|
+
if (action)
|
|
77
|
+
return { doClick: action, supportsTrial: true };
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case 'select': {
|
|
81
|
+
const action = getOriginalLocatorSelect(locator);
|
|
82
|
+
if (action) {
|
|
83
|
+
return {
|
|
84
|
+
doClick: (options) => action(null, options).then(() => { }),
|
|
85
|
+
supportsTrial: false,
|
|
86
|
+
};
|
|
183
87
|
}
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
default: {
|
|
91
|
+
const _ = interactionType;
|
|
92
|
+
throw new Error(`Unknown mouse click interaction type: ${_}`);
|
|
184
93
|
}
|
|
185
94
|
}
|
|
186
|
-
|
|
187
|
-
await mouseMoveInternal(targetX, targetY);
|
|
188
|
-
}
|
|
189
|
-
mousePositions.set(page, { x: targetX, y: targetY });
|
|
190
|
-
const endMs = Date.now();
|
|
191
|
-
return {
|
|
192
|
-
type: 'focusChange',
|
|
193
|
-
startMs: eventStartMs,
|
|
194
|
-
endMs,
|
|
195
|
-
x: targetX,
|
|
196
|
-
y: targetY,
|
|
197
|
-
easing,
|
|
198
|
-
...(elementRect !== undefined ? { elementRect } : {}),
|
|
199
|
-
};
|
|
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);
|
|
95
|
+
throw new Error(`[screenci] Missing original locator action for '${interactionType}'.`);
|
|
210
96
|
}
|
|
211
|
-
|
|
212
|
-
* Performs all click mechanics (scroll-check, zoom handling, cursor animation,
|
|
213
|
-
* click, post-click move) and returns the collected timing/position data.
|
|
214
|
-
* Returns null if coordinates could not be determined (no DOM event and no
|
|
215
|
-
* locator bounding box).
|
|
216
|
-
*/
|
|
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) {
|
|
97
|
+
async function performAction(mouseMoveRequest, locator, doClick, supportsTrial, mode, autoZoomOptions, position, beforeClickPause = 0, postClickPause = 0, postClickMove, shouldHideMouse = false) {
|
|
218
98
|
const page = locator.page();
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
const { locatorRect, scrollElapsedMs } = scrollResult;
|
|
225
|
-
const isFirstAutoZoomEvent = scrollResult.isFirstAutoZoomInteraction;
|
|
226
|
-
if (!locatorRect) {
|
|
227
|
-
logger.warn('[screenci] Unable to get locator bounding box; skipping auto-scroll check.');
|
|
228
|
-
}
|
|
229
|
-
const innerEvents = [];
|
|
230
|
-
// If inside autoZoom: optionally await zoom duration for camera pan then
|
|
231
|
-
// update the zoom location tracker.
|
|
232
|
-
if (isInsideAutoZoom() && locatorRect) {
|
|
233
|
-
const targetX = position
|
|
234
|
-
? locatorRect.x + position.x
|
|
235
|
-
: locatorRect.x + locatorRect.width / 2;
|
|
236
|
-
const targetY = position
|
|
237
|
-
? locatorRect.y + position.y
|
|
238
|
-
: locatorRect.y + locatorRect.height / 2;
|
|
239
|
-
const lastLoc = getLastZoomLocation();
|
|
240
|
-
if (lastLoc !== null) {
|
|
241
|
-
const zoomDur = getZoomDuration() ?? 0;
|
|
242
|
-
if (zoomDur > 0) {
|
|
243
|
-
await sleep(zoomDur);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
setLastZoomLocation({
|
|
247
|
-
x: targetX,
|
|
248
|
-
y: targetY,
|
|
249
|
-
elementRect: locatorRect,
|
|
250
|
-
eventType: 'click',
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
const targetPos = position
|
|
254
|
-
? position
|
|
255
|
-
: locatorRect
|
|
99
|
+
const focusChange = await changeFocus(locator, autoZoomOptions, mouseMoveRequest);
|
|
100
|
+
const elementRect = focusChange.elementRect;
|
|
101
|
+
const innerEvents = [focusChange];
|
|
102
|
+
const targetPosition = position ??
|
|
103
|
+
(elementRect
|
|
256
104
|
? {
|
|
257
|
-
x:
|
|
258
|
-
y:
|
|
259
|
-
}
|
|
260
|
-
: undefined;
|
|
261
|
-
if (targetPos && locatorRect) {
|
|
262
|
-
const targetX = locatorRect.x + targetPos.x;
|
|
263
|
-
const targetY = locatorRect.y + targetPos.y;
|
|
264
|
-
const resolvedDuration = resolveMouseMoveDuration(page, targetX, targetY, {
|
|
265
|
-
duration: moveDuration,
|
|
266
|
-
speed: moveSpeed,
|
|
267
|
-
defaultDuration: 1000,
|
|
268
|
-
context: 'click move',
|
|
269
|
-
});
|
|
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));
|
|
105
|
+
x: elementRect.width / 2,
|
|
106
|
+
y: elementRect.height / 2,
|
|
276
107
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
}
|
|
108
|
+
: undefined);
|
|
109
|
+
if (!elementRect || !targetPosition) {
|
|
110
|
+
throw new Error('[screenci] performAction requires an element rect and target position.');
|
|
292
111
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const remainingMs = Math.max(0, moveSpeed === undefined ? (moveDuration ?? 1000) : 0);
|
|
296
|
-
if (remainingMs > 0) {
|
|
297
|
-
await new Promise((resolve) => setTimeout(resolve, remainingMs));
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
const zoomDur = isInsideAutoZoom() && locatorRect ? (getZoomDuration() ?? 0) : 0;
|
|
301
|
-
const effectiveBeforeClickPause = isFirstAutoZoomEvent
|
|
302
|
-
? Math.max(beforeClickPause, getPostZoomInOutDelay() ?? 0, zoomDur)
|
|
303
|
-
: Math.max(beforeClickPause, zoomDur);
|
|
304
|
-
await new Promise((resolve) => setTimeout(resolve, effectiveBeforeClickPause));
|
|
305
|
-
await new Promise((resolve) => setTimeout(resolve, halfClickDuration));
|
|
306
|
-
// Note click can take some time, but better to show it before than after
|
|
307
|
-
const clickTime = Date.now();
|
|
308
|
-
const mouseDownStart = clickTime - halfClickDuration;
|
|
309
|
-
innerEvents.push({
|
|
310
|
-
type: 'mouseDown',
|
|
311
|
-
startMs: mouseDownStart,
|
|
312
|
-
endMs: clickTime,
|
|
313
|
-
easing: 'ease-in-out',
|
|
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 {
|
|
112
|
+
await sleep(beforeClickPause);
|
|
113
|
+
if (!mouseMoveRequest) {
|
|
334
114
|
await doClick({
|
|
335
|
-
...
|
|
336
|
-
...(
|
|
115
|
+
...(supportsTrial ? { trial: true } : {}),
|
|
116
|
+
...(mode === 'singleDuring' ? { position: targetPosition } : {}),
|
|
337
117
|
});
|
|
118
|
+
await sleep(postClickPause);
|
|
119
|
+
return {
|
|
120
|
+
elementRect,
|
|
121
|
+
innerEvents,
|
|
122
|
+
};
|
|
338
123
|
}
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
endMs: mouseUpEnd,
|
|
361
|
-
easing: 'ease-in-out',
|
|
362
|
-
});
|
|
363
|
-
await new Promise((resolve) => setTimeout(resolve, halfClickDuration));
|
|
364
|
-
await new Promise((resolve) => setTimeout(resolve, postClickPause));
|
|
365
|
-
// Animate mouse cursor in the specified direction after the click completes,
|
|
366
|
-
// capturing start/end times and final position for the recorded event.
|
|
124
|
+
const clickActionBase = {
|
|
125
|
+
locator,
|
|
126
|
+
doClick,
|
|
127
|
+
supportsTrial,
|
|
128
|
+
targetX: elementRect.x + targetPosition.x,
|
|
129
|
+
targetY: elementRect.y + targetPosition.y,
|
|
130
|
+
clickOptions: { position: targetPosition },
|
|
131
|
+
};
|
|
132
|
+
const clickActionOptions = mode === 'singleDuring'
|
|
133
|
+
? {
|
|
134
|
+
...clickActionBase,
|
|
135
|
+
mode,
|
|
136
|
+
}
|
|
137
|
+
: {
|
|
138
|
+
...clickActionBase,
|
|
139
|
+
mode,
|
|
140
|
+
shouldHideMouse,
|
|
141
|
+
};
|
|
142
|
+
const { events, elementRect: actionElementRect } = await performMouseClickAction(clickActionOptions);
|
|
143
|
+
innerEvents.push(...events);
|
|
144
|
+
await sleep(postClickPause);
|
|
367
145
|
if (postClickMove !== undefined) {
|
|
368
|
-
const currentPos =
|
|
146
|
+
const currentPos = getMousePosition(page) ?? { x: 0, y: 0 };
|
|
369
147
|
let targetX;
|
|
370
148
|
let targetY;
|
|
371
149
|
if ('direction' in postClickMove) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
break;
|
|
394
|
-
default: {
|
|
395
|
-
const _ = postClickMove.direction;
|
|
396
|
-
throw new Error(`Unknown postClickMove direction: ${_}`);
|
|
397
|
-
}
|
|
150
|
+
const padding = postClickMove.padding ?? 0;
|
|
151
|
+
switch (postClickMove.direction) {
|
|
152
|
+
case 'up':
|
|
153
|
+
targetX = currentPos.x;
|
|
154
|
+
targetY = elementRect.y - padding;
|
|
155
|
+
break;
|
|
156
|
+
case 'down':
|
|
157
|
+
targetX = currentPos.x;
|
|
158
|
+
targetY = elementRect.y + elementRect.height + padding;
|
|
159
|
+
break;
|
|
160
|
+
case 'left':
|
|
161
|
+
targetX = elementRect.x - padding;
|
|
162
|
+
targetY = currentPos.y;
|
|
163
|
+
break;
|
|
164
|
+
case 'right':
|
|
165
|
+
targetX = elementRect.x + elementRect.width + padding;
|
|
166
|
+
targetY = currentPos.y;
|
|
167
|
+
break;
|
|
168
|
+
default: {
|
|
169
|
+
const _ = postClickMove.direction;
|
|
170
|
+
throw new Error(`Unknown postClickMove direction: ${_}`);
|
|
398
171
|
}
|
|
399
172
|
}
|
|
400
173
|
}
|
|
@@ -410,154 +183,33 @@ async function performClickActions(locator, doClick, clickOptions, autoZoomOptio
|
|
|
410
183
|
defaultDuration: undefined,
|
|
411
184
|
context: 'postClickMove',
|
|
412
185
|
});
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const easedT = evaluateEasingAtT(t, easing);
|
|
420
|
-
const x = startPos.x + easedT * (targetX - startPos.x);
|
|
421
|
-
const y = startPos.y + easedT * (targetY - startPos.y);
|
|
422
|
-
await mouseMoveInternal(x, y);
|
|
423
|
-
if (i < steps) {
|
|
424
|
-
await new Promise((resolve) => setTimeout(resolve, stepMs));
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
const postClickMoveEndMs = Date.now();
|
|
428
|
-
innerEvents.push({
|
|
429
|
-
type: 'focusChange',
|
|
430
|
-
startMs: postClickMoveStartMs,
|
|
431
|
-
endMs: postClickMoveEndMs,
|
|
432
|
-
x: targetX,
|
|
433
|
-
y: targetY,
|
|
186
|
+
const startMs = Date.now();
|
|
187
|
+
await performMouseMove({
|
|
188
|
+
page,
|
|
189
|
+
targetX,
|
|
190
|
+
targetY,
|
|
191
|
+
duration,
|
|
434
192
|
easing,
|
|
435
193
|
});
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
let elementRect;
|
|
440
|
-
if (domClickData) {
|
|
441
|
-
elementRect = domClickData.targetRect;
|
|
442
|
-
}
|
|
443
|
-
else if (locatorRect) {
|
|
444
|
-
elementRect = locatorRect ?? undefined;
|
|
445
|
-
}
|
|
446
|
-
if (elementRect) {
|
|
447
|
-
return {
|
|
448
|
-
elementRect,
|
|
449
|
-
innerEvents,
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
logger.warn('[screenci] Failed to capture click coordinates from both DOM event and locator bounding box.');
|
|
454
|
-
return null;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
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') {
|
|
480
|
-
await sleep(PRE_ACTION_SLEEP);
|
|
481
|
-
let innerEvents = [];
|
|
482
|
-
let elementRect;
|
|
483
|
-
if (clickOpt !== undefined) {
|
|
484
|
-
const { moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove, } = clickOpt;
|
|
485
|
-
const clickActionResult = await performClickActions(locator, doAction, {}, autoZoomOptions, position, moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove);
|
|
486
|
-
innerEvents = clickActionResult?.innerEvents ?? [];
|
|
487
|
-
elementRect = clickActionResult?.elementRect;
|
|
488
|
-
}
|
|
489
|
-
else {
|
|
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();
|
|
194
|
+
const endMs = Date.now();
|
|
497
195
|
innerEvents.push({
|
|
498
196
|
type: 'focusChange',
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const targetPosition = locatorRect
|
|
509
|
-
? {
|
|
510
|
-
x: locatorRect.width / 2,
|
|
511
|
-
y: locatorRect.height / 2,
|
|
512
|
-
}
|
|
513
|
-
: undefined;
|
|
514
|
-
const startTime = Date.now();
|
|
515
|
-
await doAction({
|
|
516
|
-
...options,
|
|
517
|
-
...(targetPosition ? { position: targetPosition } : {}),
|
|
518
|
-
});
|
|
519
|
-
const endTime = Date.now();
|
|
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,
|
|
197
|
+
x: targetX,
|
|
198
|
+
y: targetY,
|
|
199
|
+
startMs,
|
|
200
|
+
endMs,
|
|
201
|
+
mouse: {
|
|
202
|
+
startMs,
|
|
203
|
+
endMs,
|
|
204
|
+
...(duration > 0 ? { easing } : {}),
|
|
205
|
+
},
|
|
532
206
|
});
|
|
533
207
|
}
|
|
534
208
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
type: 'mouseWait',
|
|
540
|
-
startMs: simpleWaitStart,
|
|
541
|
-
endMs: simpleWaitEnd,
|
|
542
|
-
});
|
|
543
|
-
if (activeClickRecorder && innerEvents.length > 0) {
|
|
544
|
-
activeClickRecorder.addInput(subType, elementRect, innerEvents);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
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) {
|
|
548
|
-
await sleep(PRE_ACTION_SLEEP);
|
|
549
|
-
const result = await performClickActions(locator, doClick, clickOptions, autoZoomOptions, position, moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove);
|
|
550
|
-
const clickWaitStart = Date.now();
|
|
551
|
-
await sleep(POST_ACTION_SLEEP);
|
|
552
|
-
const clickWaitEnd = Date.now();
|
|
553
|
-
if (activeClickRecorder && result) {
|
|
554
|
-
result.innerEvents.push({
|
|
555
|
-
type: 'mouseWait',
|
|
556
|
-
startMs: clickWaitStart,
|
|
557
|
-
endMs: clickWaitEnd,
|
|
558
|
-
});
|
|
559
|
-
activeClickRecorder.addInput('click', result.elementRect, result.innerEvents);
|
|
560
|
-
}
|
|
209
|
+
return {
|
|
210
|
+
elementRect: actionElementRect ?? elementRect,
|
|
211
|
+
innerEvents,
|
|
212
|
+
};
|
|
561
213
|
}
|
|
562
214
|
function instrumentLocatorMethods(obj) {
|
|
563
215
|
for (const method of LOCATOR_RETURN_METHODS) {
|
|
@@ -585,6 +237,7 @@ export function instrumentLocator(locator) {
|
|
|
585
237
|
return locator;
|
|
586
238
|
instrumented.add(locator);
|
|
587
239
|
const originalClick = locator.click.bind(locator);
|
|
240
|
+
setOriginalLocatorClick(locator, originalClick);
|
|
588
241
|
locator.click = async (options) => {
|
|
589
242
|
const { moveDuration, moveSpeed, beforeClickPause, moveEasing, postClickPause, postClickMove, autoZoomOptions, position, steps: _steps, ...clickOptions } = options ?? {};
|
|
590
243
|
if (isInsideHide()) {
|
|
@@ -594,66 +247,36 @@ export function instrumentLocator(locator) {
|
|
|
594
247
|
});
|
|
595
248
|
}
|
|
596
249
|
assertDurationOrSpeed(moveDuration, moveSpeed, 'click move');
|
|
597
|
-
|
|
250
|
+
const { doClick, supportsTrial } = resolveLocatorMouseAction(locator, 'click');
|
|
251
|
+
const result = await performAction({
|
|
252
|
+
...(position !== undefined ? { targetPosInElement: position } : {}),
|
|
253
|
+
...(moveDuration !== undefined ? { duration: moveDuration } : {}),
|
|
254
|
+
...(moveSpeed !== undefined ? { speed: moveSpeed } : {}),
|
|
255
|
+
easing: moveEasing ?? 'ease-in-out',
|
|
256
|
+
}, locator, doClick, supportsTrial, 'singleDuring', autoZoomOptions, position, beforeClickPause, postClickPause, postClickMove, false);
|
|
257
|
+
if (activeClickRecorder && result) {
|
|
258
|
+
activeClickRecorder.addInput('click', undefined, result.innerEvents);
|
|
259
|
+
}
|
|
598
260
|
};
|
|
599
261
|
const originalPressSequentially = locator.pressSequentially.bind(locator);
|
|
600
262
|
locator.pressSequentially = async (text, options) => {
|
|
263
|
+
const clickOpt = options?.click ?? {};
|
|
601
264
|
const { click: _click, autoZoomOptions, hideMouse: _hideMouse, position: _position, ...pressOptions } = options ?? {};
|
|
602
265
|
if (isInsideHide()) {
|
|
603
266
|
return originalPressSequentially(text, pressOptions);
|
|
604
267
|
}
|
|
605
|
-
await sleep(PRE_ACTION_SLEEP);
|
|
606
268
|
const innerEvents = [];
|
|
607
|
-
let elementRect;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
else {
|
|
618
|
-
const { locatorRect, isFirstAutoZoomInteraction } = await prepareAutoZoomForLocator(locator, 'fill', options?.autoZoomOptions);
|
|
619
|
-
if (isFirstAutoZoomInteraction) {
|
|
620
|
-
const postDelay = getPostZoomInOutDelay() ?? 0;
|
|
621
|
-
if (postDelay > 0)
|
|
622
|
-
await sleep(postDelay);
|
|
623
|
-
}
|
|
624
|
-
elementRect = locatorRect;
|
|
625
|
-
if (isInsideAutoZoom() && locatorRect) {
|
|
626
|
-
const zoomDur = getZoomDuration() ?? 0;
|
|
627
|
-
if (zoomDur > 0) {
|
|
628
|
-
await sleep(zoomDur);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
// Hide cursor while typing (will be shown again on next mouse move)
|
|
633
|
-
const page = locator.page();
|
|
634
|
-
const shouldHideMouse = options?.hideMouse === true;
|
|
635
|
-
if (shouldHideMouse) {
|
|
636
|
-
const cursorVisible = mouseVisibilities.get(page) ?? true;
|
|
637
|
-
if (cursorVisible) {
|
|
638
|
-
mouseVisibilities.set(page, false);
|
|
639
|
-
const hideMs = Date.now();
|
|
640
|
-
innerEvents.push({
|
|
641
|
-
type: 'mouseHide',
|
|
642
|
-
startMs: hideMs,
|
|
643
|
-
endMs: hideMs,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
await originalPressSequentially(text, pressOptions);
|
|
648
|
-
const pressWaitStart = Date.now();
|
|
649
|
-
await sleep(POST_ACTION_SLEEP);
|
|
650
|
-
const pressWaitEnd = Date.now();
|
|
269
|
+
let elementRect = undefined;
|
|
270
|
+
const { moveDuration, moveSpeed, beforeClickPause, moveEasing = 'ease-in-out', postClickPause, postClickMove, } = clickOpt;
|
|
271
|
+
const clickActionResult = await performAction({
|
|
272
|
+
targetPosInElement: options?.position ?? { x: 0, y: 0 },
|
|
273
|
+
...(moveDuration !== undefined ? { duration: moveDuration } : {}),
|
|
274
|
+
...(moveSpeed !== undefined ? { speed: moveSpeed } : {}),
|
|
275
|
+
easing: moveEasing,
|
|
276
|
+
}, locator, async () => originalPressSequentially(text, pressOptions), false, 'singleBefore', autoZoomOptions, options?.position, beforeClickPause ?? CLICK_DURATION_MS / 2, postClickPause ?? CLICK_DURATION_MS / 2, postClickMove, true);
|
|
277
|
+
innerEvents.push(...(clickActionResult?.innerEvents ?? []));
|
|
278
|
+
elementRect = clickActionResult?.elementRect;
|
|
651
279
|
if (activeClickRecorder) {
|
|
652
|
-
innerEvents.push({
|
|
653
|
-
type: 'mouseWait',
|
|
654
|
-
startMs: pressWaitStart,
|
|
655
|
-
endMs: pressWaitEnd,
|
|
656
|
-
});
|
|
657
280
|
activeClickRecorder.addInput('pressSequentially', elementRect, innerEvents);
|
|
658
281
|
}
|
|
659
282
|
};
|
|
@@ -663,112 +286,16 @@ export function instrumentLocator(locator) {
|
|
|
663
286
|
const { duration: _duration, click: _click, position: _position, hideMouse: _hideMouse, autoZoomOptions: _autoZoomOptions, ...fillOptions } = options ?? {};
|
|
664
287
|
return originalFill(value, fillOptions);
|
|
665
288
|
}
|
|
666
|
-
|
|
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();
|
|
289
|
+
const clickOpt = options?.click ?? {};
|
|
720
290
|
const innerEvents = [];
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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 {
|
|
291
|
+
let elementRect = undefined;
|
|
292
|
+
const { moveDuration, moveSpeed, beforeClickPause, moveEasing = 'ease-in-out', postClickPause, postClickMove, } = clickOpt;
|
|
293
|
+
const clickActionResult = await performAction({
|
|
294
|
+
targetPosInElement: options?.position ?? { x: 0, y: 0 },
|
|
295
|
+
...(moveDuration !== undefined ? { duration: moveDuration } : {}),
|
|
296
|
+
...(moveSpeed !== undefined ? { speed: moveSpeed } : {}),
|
|
297
|
+
easing: moveEasing,
|
|
298
|
+
}, locator, async () => {
|
|
772
299
|
await locator.evaluate((element) => {
|
|
773
300
|
if (element instanceof HTMLInputElement ||
|
|
774
301
|
element instanceof HTMLTextAreaElement) {
|
|
@@ -787,31 +314,43 @@ export function instrumentLocator(locator) {
|
|
|
787
314
|
selection.addRange(range);
|
|
788
315
|
}
|
|
789
316
|
});
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
innerEvents.push({
|
|
798
|
-
type: 'mouseWait',
|
|
799
|
-
startMs: fillWaitStart,
|
|
800
|
-
endMs: fillWaitEnd,
|
|
801
|
-
});
|
|
802
|
-
if (activeClickRecorder &&
|
|
803
|
-
(isInsideAutoZoom() || fillOptions.hideMouse === true)) {
|
|
317
|
+
const duration = options?.duration ?? 1000;
|
|
318
|
+
const delay = value.length > 0 ? duration / value.length : 0;
|
|
319
|
+
await locator.page().keyboard.type(value, { delay });
|
|
320
|
+
}, false, 'singleBefore', options?.autoZoomOptions, options?.position, beforeClickPause ?? CLICK_DURATION_MS / 2, postClickPause ?? CLICK_DURATION_MS / 2, postClickMove, true);
|
|
321
|
+
innerEvents.push(...(clickActionResult?.innerEvents ?? []));
|
|
322
|
+
elementRect = clickActionResult?.elementRect;
|
|
323
|
+
if (activeClickRecorder) {
|
|
804
324
|
activeClickRecorder.addInput('pressSequentially', elementRect, innerEvents);
|
|
805
325
|
}
|
|
806
|
-
return;
|
|
807
326
|
};
|
|
808
327
|
const originalTap = locator.tap.bind(locator);
|
|
328
|
+
setOriginalLocatorTap(locator, originalTap);
|
|
809
329
|
locator.tap = async (options) => {
|
|
810
330
|
const clickOpt = options?.click;
|
|
811
331
|
const { click: _click, position, autoZoomOptions, ...tapOpts } = options ?? {};
|
|
812
|
-
|
|
332
|
+
if (isInsideHide()) {
|
|
333
|
+
return originalTap(tapOpts);
|
|
334
|
+
}
|
|
335
|
+
const { doClick, supportsTrial } = resolveLocatorMouseAction(locator, 'tap');
|
|
336
|
+
const result = await performAction(clickOpt
|
|
337
|
+
? {
|
|
338
|
+
targetPosInElement: position ?? { x: 0, y: 0 },
|
|
339
|
+
...(clickOpt.moveDuration !== undefined
|
|
340
|
+
? { duration: clickOpt.moveDuration }
|
|
341
|
+
: {}),
|
|
342
|
+
...(clickOpt.moveSpeed !== undefined
|
|
343
|
+
? { speed: clickOpt.moveSpeed }
|
|
344
|
+
: {}),
|
|
345
|
+
easing: clickOpt.moveEasing ?? 'ease-in-out',
|
|
346
|
+
}
|
|
347
|
+
: undefined, locator, doClick, supportsTrial, 'singleDuring', autoZoomOptions, position, clickOpt?.beforeClickPause, clickOpt?.postClickPause, clickOpt?.postClickMove, false);
|
|
348
|
+
if (activeClickRecorder && result) {
|
|
349
|
+
activeClickRecorder.addInput('tap', result.elementRect, result.innerEvents);
|
|
350
|
+
}
|
|
813
351
|
};
|
|
814
352
|
const originalCheck = locator.check.bind(locator);
|
|
353
|
+
setOriginalLocatorCheck(locator, originalCheck);
|
|
815
354
|
locator.check = async (options) => {
|
|
816
355
|
const clickOpt = options?.click;
|
|
817
356
|
const position = options?.position;
|
|
@@ -819,9 +358,25 @@ export function instrumentLocator(locator) {
|
|
|
819
358
|
if (isInsideHide()) {
|
|
820
359
|
return originalCheck(checkOpts);
|
|
821
360
|
}
|
|
822
|
-
|
|
361
|
+
const { doClick, supportsTrial } = resolveLocatorMouseAction(locator, 'check');
|
|
362
|
+
const result = await performAction(clickOpt
|
|
363
|
+
? {
|
|
364
|
+
targetPosInElement: position ?? { x: 0, y: 0 },
|
|
365
|
+
...(clickOpt.moveDuration !== undefined
|
|
366
|
+
? { duration: clickOpt.moveDuration }
|
|
367
|
+
: {}),
|
|
368
|
+
...(clickOpt.moveSpeed !== undefined
|
|
369
|
+
? { speed: clickOpt.moveSpeed }
|
|
370
|
+
: {}),
|
|
371
|
+
easing: clickOpt.moveEasing ?? 'ease-in-out',
|
|
372
|
+
}
|
|
373
|
+
: undefined, locator, doClick, supportsTrial, 'singleDuring', autoZoomOptions, position, clickOpt?.beforeClickPause, clickOpt?.postClickPause, clickOpt?.postClickMove, false);
|
|
374
|
+
if (activeClickRecorder && result) {
|
|
375
|
+
activeClickRecorder.addInput('check', result.elementRect, result.innerEvents);
|
|
376
|
+
}
|
|
823
377
|
};
|
|
824
378
|
const originalUncheck = locator.uncheck.bind(locator);
|
|
379
|
+
setOriginalLocatorUncheck(locator, originalUncheck);
|
|
825
380
|
locator.uncheck = async (options) => {
|
|
826
381
|
const clickOpt = options?.click;
|
|
827
382
|
const position = options?.position;
|
|
@@ -829,7 +384,22 @@ export function instrumentLocator(locator) {
|
|
|
829
384
|
if (isInsideHide()) {
|
|
830
385
|
return originalUncheck(uncheckOpts);
|
|
831
386
|
}
|
|
832
|
-
|
|
387
|
+
const { doClick, supportsTrial } = resolveLocatorMouseAction(locator, 'uncheck');
|
|
388
|
+
const result = await performAction(clickOpt
|
|
389
|
+
? {
|
|
390
|
+
targetPosInElement: position ?? { x: 0, y: 0 },
|
|
391
|
+
...(clickOpt.moveDuration !== undefined
|
|
392
|
+
? { duration: clickOpt.moveDuration }
|
|
393
|
+
: {}),
|
|
394
|
+
...(clickOpt.moveSpeed !== undefined
|
|
395
|
+
? { speed: clickOpt.moveSpeed }
|
|
396
|
+
: {}),
|
|
397
|
+
easing: clickOpt.moveEasing ?? 'ease-in-out',
|
|
398
|
+
}
|
|
399
|
+
: undefined, locator, doClick, supportsTrial, 'singleDuring', autoZoomOptions, position, clickOpt?.beforeClickPause, clickOpt?.postClickPause, clickOpt?.postClickMove, false);
|
|
400
|
+
if (activeClickRecorder && result) {
|
|
401
|
+
activeClickRecorder.addInput('uncheck', result.elementRect, result.innerEvents);
|
|
402
|
+
}
|
|
833
403
|
};
|
|
834
404
|
locator.setChecked = async (checked, options) => {
|
|
835
405
|
if (checked) {
|
|
@@ -840,47 +410,61 @@ export function instrumentLocator(locator) {
|
|
|
840
410
|
}
|
|
841
411
|
};
|
|
842
412
|
const originalSelectOption = locator.selectOption.bind(locator);
|
|
413
|
+
let currentSelectValues = null;
|
|
414
|
+
let currentSelectOptions;
|
|
415
|
+
let currentSelectResult = [];
|
|
416
|
+
setOriginalLocatorSelect(locator, (_values, actionOptions) => originalSelectOption(currentSelectValues, {
|
|
417
|
+
...currentSelectOptions,
|
|
418
|
+
...actionOptions,
|
|
419
|
+
}).then((res) => {
|
|
420
|
+
currentSelectResult = res;
|
|
421
|
+
return res;
|
|
422
|
+
}));
|
|
843
423
|
locator.selectOption = async (values, options) => {
|
|
844
424
|
const clickOpt = options?.click;
|
|
845
425
|
const { click: _click, position, autoZoomOptions, ...selectOpts } = options ?? {};
|
|
846
426
|
if (isInsideHide()) {
|
|
847
427
|
return originalSelectOption(values, selectOpts);
|
|
848
428
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
429
|
+
currentSelectValues = values;
|
|
430
|
+
currentSelectOptions = selectOpts;
|
|
431
|
+
currentSelectResult = [];
|
|
432
|
+
const { doClick, supportsTrial } = resolveLocatorMouseAction(locator, 'select');
|
|
433
|
+
const actionResult = await performAction(clickOpt
|
|
434
|
+
? {
|
|
435
|
+
targetPosInElement: position ?? { x: 0, y: 0 },
|
|
436
|
+
...(clickOpt.moveDuration !== undefined
|
|
437
|
+
? { duration: clickOpt.moveDuration }
|
|
438
|
+
: {}),
|
|
439
|
+
...(clickOpt.moveSpeed !== undefined
|
|
440
|
+
? { speed: clickOpt.moveSpeed }
|
|
441
|
+
: {}),
|
|
442
|
+
easing: clickOpt.moveEasing ?? 'ease-in-out',
|
|
443
|
+
}
|
|
444
|
+
: undefined, locator, doClick, supportsTrial, 'singleDuring', autoZoomOptions, position, clickOpt?.beforeClickPause, clickOpt?.postClickPause, clickOpt?.postClickMove);
|
|
445
|
+
if (activeClickRecorder && actionResult) {
|
|
446
|
+
activeClickRecorder.addInput('select', actionResult.elementRect, actionResult.innerEvents);
|
|
447
|
+
}
|
|
448
|
+
return currentSelectResult;
|
|
854
449
|
};
|
|
855
450
|
const originalHover = locator.hover.bind(locator);
|
|
856
451
|
locator.hover = async (options) => {
|
|
857
452
|
const { moveDuration, moveSpeed, easing: moveEasing = 'ease-in-out', hoverDuration = 1000, position, ...hoverOptions } = options ?? {};
|
|
858
453
|
assertDurationOrSpeed(moveDuration, moveSpeed, 'hover move');
|
|
859
|
-
const page = locator.page();
|
|
860
|
-
const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
|
|
861
|
-
const moveStartTime = Date.now();
|
|
862
|
-
const scrollResult = await new ZoomScrollHandler().scroll(locator);
|
|
863
|
-
const { locatorRect } = scrollResult;
|
|
864
454
|
const innerEvents = [];
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
speed: moveSpeed,
|
|
875
|
-
defaultDuration: 1000,
|
|
876
|
-
context: 'hover move',
|
|
877
|
-
});
|
|
878
|
-
innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, resolveMoveDuration(scrollResult, resolvedDuration), moveEasing, resolveMoveStartMs(scrollResult, moveStartTime), locatorRect));
|
|
879
|
-
}
|
|
455
|
+
const mouseMovePlan = {
|
|
456
|
+
targetPosInElement: position,
|
|
457
|
+
...(moveDuration !== undefined ? { duration: moveDuration } : {}),
|
|
458
|
+
...(moveSpeed !== undefined ? { speed: moveSpeed } : {}),
|
|
459
|
+
easing: moveEasing,
|
|
460
|
+
};
|
|
461
|
+
const hoverFocusChange = await changeFocus(locator, options?.autoZoomOptions, mouseMovePlan);
|
|
462
|
+
const locatorRect = hoverFocusChange.elementRect;
|
|
463
|
+
innerEvents.push(hoverFocusChange);
|
|
880
464
|
const waitStartMs = Date.now();
|
|
881
465
|
await originalHover({
|
|
882
466
|
...hoverOptions,
|
|
883
|
-
...(
|
|
467
|
+
...(position ? { position } : {}),
|
|
884
468
|
});
|
|
885
469
|
if (hoverDuration > 0) {
|
|
886
470
|
await sleep(hoverDuration);
|
|
@@ -895,55 +479,38 @@ export function instrumentLocator(locator) {
|
|
|
895
479
|
activeClickRecorder.addInput('hover', locatorRect, innerEvents);
|
|
896
480
|
}
|
|
897
481
|
};
|
|
482
|
+
const originalScrollIntoViewIfNeeded = locator.scrollIntoViewIfNeeded.bind(locator);
|
|
483
|
+
locator.scrollIntoViewIfNeeded = async (options) => {
|
|
484
|
+
if (isInsideHide()) {
|
|
485
|
+
return originalScrollIntoViewIfNeeded(options);
|
|
486
|
+
}
|
|
487
|
+
const { easing = 'ease-in-out', duration, amount, centering, } = options ?? {};
|
|
488
|
+
const result = await changeFocus(locator, {
|
|
489
|
+
easing,
|
|
490
|
+
...(duration !== undefined ? { duration } : {}),
|
|
491
|
+
...(amount !== undefined ? { amount } : {}),
|
|
492
|
+
...(centering !== undefined ? { centering } : {}),
|
|
493
|
+
});
|
|
494
|
+
if (activeClickRecorder) {
|
|
495
|
+
activeClickRecorder.addInput('focusChange', result.elementRect, [result]);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
898
498
|
const originalSelectText = locator.selectText.bind(locator);
|
|
899
499
|
locator.selectText = async (options) => {
|
|
900
|
-
const { moveDuration, moveSpeed, easing: moveEasing = 'ease-in-out', beforeClickPause = CLICK_DURATION_MS / 2,
|
|
500
|
+
const { moveDuration, moveSpeed, easing: moveEasing = 'ease-in-out', beforeClickPause = CLICK_DURATION_MS / 2, ...selectOpts } = options ?? {};
|
|
901
501
|
assertDurationOrSpeed(moveDuration, moveSpeed, 'selectText move');
|
|
902
502
|
const page = locator.page();
|
|
903
|
-
const mouseMoveInternal = originalMouseMoves.get(page) ?? page.mouse.move.bind(page.mouse);
|
|
904
|
-
const moveStartTime = Date.now();
|
|
905
|
-
const scrollResult = await new ZoomScrollHandler().scroll(locator);
|
|
906
|
-
const { locatorRect } = scrollResult;
|
|
907
503
|
const innerEvents = [];
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
:
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
context: 'selectText move',
|
|
919
|
-
});
|
|
920
|
-
innerEvents.push(await animateMouseMove(page, mouseMoveInternal, targetX, targetY, resolveMoveDuration(scrollResult, resolvedDuration), moveEasing, resolveMoveStartMs(scrollResult, moveStartTime), locatorRect));
|
|
921
|
-
}
|
|
922
|
-
await sleep(beforeClickPause);
|
|
923
|
-
await originalSelectText(selectOpts);
|
|
924
|
-
// Backtrack triple-click events from the moment originalSelectText resolves.
|
|
925
|
-
// Clamp start so events don't precede the prior animation (produces a visible
|
|
926
|
-
// pre-click pause in the recording, which is acceptable).
|
|
927
|
-
// All timestamps use a single base + integer * segmentMs to avoid FP drift.
|
|
928
|
-
const selectEndMs = Date.now();
|
|
929
|
-
const lastEventEndMs = innerEvents.at(-1)?.endMs ?? 0;
|
|
930
|
-
const tripleClickStartMs = Math.max(lastEventEndMs, selectEndMs - selectDuration);
|
|
931
|
-
const segmentMs = selectDuration / 6;
|
|
932
|
-
for (let i = 0; i < 3; i++) {
|
|
933
|
-
const seg = i * 2;
|
|
934
|
-
innerEvents.push({
|
|
935
|
-
type: 'mouseDown',
|
|
936
|
-
startMs: tripleClickStartMs + seg * segmentMs,
|
|
937
|
-
endMs: tripleClickStartMs + (seg + 1) * segmentMs,
|
|
938
|
-
easing: 'ease-in-out',
|
|
939
|
-
});
|
|
940
|
-
innerEvents.push({
|
|
941
|
-
type: 'mouseUp',
|
|
942
|
-
startMs: tripleClickStartMs + (seg + 1) * segmentMs,
|
|
943
|
-
endMs: tripleClickStartMs + (seg + 2) * segmentMs,
|
|
944
|
-
easing: 'ease-in-out',
|
|
945
|
-
});
|
|
946
|
-
}
|
|
504
|
+
const selectActionResult = await performAction({
|
|
505
|
+
targetPosInElement: { x: 0, y: 0 },
|
|
506
|
+
...(moveDuration !== undefined ? { duration: moveDuration } : {}),
|
|
507
|
+
...(moveSpeed !== undefined ? { speed: moveSpeed } : {}),
|
|
508
|
+
easing: moveEasing,
|
|
509
|
+
}, locator, async () => {
|
|
510
|
+
await originalSelectText(selectOpts);
|
|
511
|
+
}, false, 'tripleBefore', undefined, undefined, beforeClickPause, undefined, undefined, false);
|
|
512
|
+
const locatorRect = selectActionResult?.elementRect;
|
|
513
|
+
innerEvents.push(...(selectActionResult?.innerEvents ?? []));
|
|
947
514
|
if (activeClickRecorder && innerEvents.length > 0) {
|
|
948
515
|
activeClickRecorder.addInput('selectText', locatorRect, innerEvents);
|
|
949
516
|
}
|
|
@@ -952,12 +519,8 @@ export function instrumentLocator(locator) {
|
|
|
952
519
|
const { moveDuration, moveSpeed, moveEasing = 'ease-in-out', preDragPause = CLICK_DURATION_MS / 2, dragDuration, dragSpeed, dragEasing = 'ease-in-out', sourcePosition, targetPosition, } = options ?? {};
|
|
953
520
|
assertDurationOrSpeed(moveDuration, moveSpeed, 'dragTo move');
|
|
954
521
|
assertDurationOrSpeed(dragDuration, dragSpeed, 'dragTo drag');
|
|
955
|
-
await sleep(PRE_ACTION_SLEEP);
|
|
956
522
|
const page = locator.page();
|
|
957
|
-
const
|
|
958
|
-
const moveStartTime = Date.now();
|
|
959
|
-
const scrollResult = await new ZoomScrollHandler().scroll(locator);
|
|
960
|
-
const { locatorRect: sourceRect } = scrollResult;
|
|
523
|
+
const sourceRectPreview = await locator.boundingBox();
|
|
961
524
|
const targetBb = await target.boundingBox();
|
|
962
525
|
const targetRect = targetBb
|
|
963
526
|
? {
|
|
@@ -968,37 +531,31 @@ export function instrumentLocator(locator) {
|
|
|
968
531
|
}
|
|
969
532
|
: undefined;
|
|
970
533
|
const innerEvents = [];
|
|
971
|
-
const sourcePos = sourcePosition ??
|
|
972
|
-
(sourceRect
|
|
973
|
-
? { x: sourceRect.width / 2, y: sourceRect.height / 2 }
|
|
974
|
-
: undefined);
|
|
975
534
|
const targetPos = targetPosition ??
|
|
976
535
|
(targetRect
|
|
977
536
|
? { x: targetRect.width / 2, y: targetRect.height / 2 }
|
|
978
537
|
: undefined);
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
context: 'dragTo move',
|
|
988
|
-
});
|
|
989
|
-
innerEvents.push(await animateMouseMove(page, mouseMoveInternal, toX, toY, resolveMoveDuration(scrollResult, resolvedDuration), moveEasing, resolveMoveStartMs(scrollResult, moveStartTime), sourceRect));
|
|
538
|
+
const sourceFocusChange = await changeFocus(locator, undefined, {
|
|
539
|
+
targetPosInElement: sourcePosition,
|
|
540
|
+
...(moveDuration !== undefined ? { duration: moveDuration } : {}),
|
|
541
|
+
...(moveSpeed !== undefined ? { speed: moveSpeed } : {}),
|
|
542
|
+
easing: moveEasing,
|
|
543
|
+
});
|
|
544
|
+
if (sourceFocusChange.elementRect) {
|
|
545
|
+
innerEvents.push(sourceFocusChange);
|
|
990
546
|
}
|
|
991
547
|
// 2. preDragPause + mouseDown
|
|
992
548
|
await sleep(preDragPause);
|
|
993
549
|
const mouseDownStart = Date.now();
|
|
994
|
-
await
|
|
550
|
+
await performMouseDown({
|
|
551
|
+
mouseDownInternal: getOriginalMouseDown(page, page.mouse.down.bind(page.mouse)),
|
|
552
|
+
});
|
|
995
553
|
await sleep(CLICK_DURATION_MS / 2);
|
|
996
|
-
innerEvents.push({
|
|
997
|
-
type: 'mouseDown',
|
|
554
|
+
innerEvents.push(buildMouseDownEvent({
|
|
998
555
|
startMs: mouseDownStart,
|
|
999
556
|
endMs: Date.now(),
|
|
1000
557
|
easing: 'ease-in-out',
|
|
1001
|
-
});
|
|
558
|
+
}));
|
|
1002
559
|
// 3. Drag: animate cursor from source to target
|
|
1003
560
|
const dragStartTime = Date.now();
|
|
1004
561
|
if (targetPos && targetRect) {
|
|
@@ -1010,28 +567,36 @@ export function instrumentLocator(locator) {
|
|
|
1010
567
|
defaultDuration: 1000,
|
|
1011
568
|
context: 'dragTo drag',
|
|
1012
569
|
});
|
|
1013
|
-
|
|
570
|
+
await performMouseMove({
|
|
571
|
+
page,
|
|
572
|
+
targetX: toX,
|
|
573
|
+
targetY: toY,
|
|
574
|
+
duration: resolvedDuration,
|
|
575
|
+
easing: dragEasing,
|
|
576
|
+
});
|
|
577
|
+
innerEvents.push({
|
|
578
|
+
type: 'mouseMove',
|
|
579
|
+
startMs: dragStartTime,
|
|
580
|
+
endMs: Date.now(),
|
|
581
|
+
x: toX,
|
|
582
|
+
y: toY,
|
|
583
|
+
...(resolvedDuration > 0 ? { easing: dragEasing } : {}),
|
|
584
|
+
elementRect: targetRect,
|
|
585
|
+
});
|
|
1014
586
|
}
|
|
1015
587
|
// 4. mouseUp at target
|
|
1016
588
|
const mouseUpStart = Date.now();
|
|
1017
|
-
await
|
|
589
|
+
await performMouseUp({
|
|
590
|
+
mouseUpInternal: getOriginalMouseUp(page, page.mouse.up.bind(page.mouse)),
|
|
591
|
+
});
|
|
1018
592
|
await sleep(CLICK_DURATION_MS / 2);
|
|
1019
|
-
innerEvents.push({
|
|
1020
|
-
type: 'mouseUp',
|
|
593
|
+
innerEvents.push(buildMouseUpEvent({
|
|
1021
594
|
startMs: mouseUpStart,
|
|
1022
595
|
endMs: Date.now(),
|
|
1023
596
|
easing: 'ease-in-out',
|
|
1024
|
-
});
|
|
1025
|
-
const dragWaitStart = Date.now();
|
|
1026
|
-
await sleep(POST_ACTION_SLEEP);
|
|
1027
|
-
const dragWaitEnd = Date.now();
|
|
1028
|
-
innerEvents.push({
|
|
1029
|
-
type: 'mouseWait',
|
|
1030
|
-
startMs: dragWaitStart,
|
|
1031
|
-
endMs: dragWaitEnd,
|
|
1032
|
-
});
|
|
597
|
+
}));
|
|
1033
598
|
if (activeClickRecorder && innerEvents.length > 0) {
|
|
1034
|
-
activeClickRecorder.addInput('dragTo',
|
|
599
|
+
activeClickRecorder.addInput('dragTo', sourceFocusChange.elementRect, innerEvents);
|
|
1035
600
|
}
|
|
1036
601
|
};
|
|
1037
602
|
const originalPage = locator.page.bind(locator);
|
|
@@ -1057,23 +622,6 @@ export async function instrumentPage(page) {
|
|
|
1057
622
|
if (instrumented.has(page))
|
|
1058
623
|
return page;
|
|
1059
624
|
instrumented.add(page);
|
|
1060
|
-
// Expose a Node.js function to the browser that captures DOM click event data.
|
|
1061
|
-
// Called synchronously from the click handler before any navigation can occur.
|
|
1062
|
-
await page.exposeFunction('__screenciOnClick', (data) => {
|
|
1063
|
-
pendingClickData.set(page, data);
|
|
1064
|
-
});
|
|
1065
|
-
// Inject a capture listener on every page load (including after navigation).
|
|
1066
|
-
await page.addInitScript(() => {
|
|
1067
|
-
document.addEventListener('click', (e) => {
|
|
1068
|
-
const target = e.target;
|
|
1069
|
-
const r = target.getBoundingClientRect();
|
|
1070
|
-
window.__screenciOnClick({
|
|
1071
|
-
x: e.clientX,
|
|
1072
|
-
y: e.clientY,
|
|
1073
|
-
targetRect: { x: r.x, y: r.y, width: r.width, height: r.height },
|
|
1074
|
-
});
|
|
1075
|
-
}, { capture: true });
|
|
1076
|
-
});
|
|
1077
625
|
instrumentLocatorMethods(page);
|
|
1078
626
|
const originalPageFrameLocator = page.frameLocator.bind(page);
|
|
1079
627
|
page.frameLocator = (...args) => instrumentFrameLocator(originalPageFrameLocator(...args));
|
|
@@ -1085,7 +633,35 @@ export async function instrumentPage(page) {
|
|
|
1085
633
|
// Instrument page.mouse to record mouse moves and visibility toggles.
|
|
1086
634
|
const originalMouse = page.mouse;
|
|
1087
635
|
const originalMove = originalMouse.move.bind(originalMouse);
|
|
1088
|
-
|
|
636
|
+
const originalDown = originalMouse.down.bind(originalMouse);
|
|
637
|
+
const originalUp = originalMouse.up.bind(originalMouse);
|
|
638
|
+
const originalClickMethod = originalMouse.click;
|
|
639
|
+
const originalClick = typeof originalClickMethod === 'function'
|
|
640
|
+
? originalClickMethod.bind(originalMouse)
|
|
641
|
+
: async (x, y, options) => {
|
|
642
|
+
await originalMove(x, y);
|
|
643
|
+
await originalDown(options);
|
|
644
|
+
if (options?.delay) {
|
|
645
|
+
await sleep(options.delay);
|
|
646
|
+
}
|
|
647
|
+
await originalUp(options);
|
|
648
|
+
};
|
|
649
|
+
const originalShowMethod = originalMouse
|
|
650
|
+
.show;
|
|
651
|
+
const originalHideMethod = originalMouse
|
|
652
|
+
.hide;
|
|
653
|
+
const originalShow = typeof originalShowMethod === 'function'
|
|
654
|
+
? originalShowMethod.bind(originalMouse)
|
|
655
|
+
: () => { };
|
|
656
|
+
const originalHide = typeof originalHideMethod === 'function'
|
|
657
|
+
? originalHideMethod.bind(originalMouse)
|
|
658
|
+
: () => { };
|
|
659
|
+
setOriginalMouseMove(page, originalMove);
|
|
660
|
+
setOriginalMouseClick(page, originalClick);
|
|
661
|
+
setOriginalMouseDown(page, originalDown);
|
|
662
|
+
setOriginalMouseUp(page, originalUp);
|
|
663
|
+
setOriginalMouseShow(page, originalShow);
|
|
664
|
+
setOriginalMouseHide(page, originalHide);
|
|
1089
665
|
originalMouse.move = async (x, y, options) => {
|
|
1090
666
|
const duration = resolveMouseMoveDuration(page, x, y, {
|
|
1091
667
|
duration: options?.duration,
|
|
@@ -1095,30 +671,29 @@ export async function instrumentPage(page) {
|
|
|
1095
671
|
});
|
|
1096
672
|
const easing = options?.easing ?? 'ease-in-out';
|
|
1097
673
|
const startMs = Date.now();
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
const endMs = Date.now();
|
|
674
|
+
await performMouseMove({
|
|
675
|
+
page,
|
|
676
|
+
targetX: x,
|
|
677
|
+
targetY: y,
|
|
678
|
+
duration,
|
|
679
|
+
easing,
|
|
680
|
+
});
|
|
681
|
+
const moveEvent = {
|
|
682
|
+
type: 'focusChange',
|
|
683
|
+
startMs,
|
|
684
|
+
endMs: Date.now(),
|
|
685
|
+
x,
|
|
686
|
+
y,
|
|
687
|
+
mouse: {
|
|
688
|
+
startMs,
|
|
689
|
+
endMs: Date.now(),
|
|
690
|
+
...(duration > 0 ? { easing } : {}),
|
|
691
|
+
},
|
|
692
|
+
};
|
|
1118
693
|
if (activeClickRecorder) {
|
|
1119
694
|
// Auto-show cursor when moving after a typing auto-hide
|
|
1120
|
-
if (!(
|
|
1121
|
-
|
|
695
|
+
if (!isMouseVisible(page)) {
|
|
696
|
+
setMouseVisible(page, true);
|
|
1122
697
|
const showMs = startMs;
|
|
1123
698
|
const showEvent = {
|
|
1124
699
|
type: 'mouseShow',
|
|
@@ -1127,21 +702,16 @@ export async function instrumentPage(page) {
|
|
|
1127
702
|
};
|
|
1128
703
|
activeClickRecorder.addInput('mouseShow', undefined, [showEvent]);
|
|
1129
704
|
}
|
|
1130
|
-
const moveEvent = {
|
|
1131
|
-
type: 'focusChange',
|
|
1132
|
-
startMs,
|
|
1133
|
-
endMs,
|
|
1134
|
-
x,
|
|
1135
|
-
y,
|
|
1136
|
-
...(duration > 0 ? { easing } : {}),
|
|
1137
|
-
};
|
|
1138
705
|
activeClickRecorder.addInput('focusChange', undefined, [moveEvent]);
|
|
1139
706
|
}
|
|
1140
707
|
};
|
|
1141
|
-
|
|
708
|
+
setMouseVisible(page, true);
|
|
1142
709
|
originalMouse.show = () => {
|
|
1143
|
-
if (!(
|
|
1144
|
-
|
|
710
|
+
if (!isMouseVisible(page)) {
|
|
711
|
+
performMouseShow({
|
|
712
|
+
mouseShowInternal: getOriginalMouseShow(page, originalShow),
|
|
713
|
+
page,
|
|
714
|
+
});
|
|
1145
715
|
if (activeClickRecorder) {
|
|
1146
716
|
const timeMs = Date.now();
|
|
1147
717
|
const showEvent = {
|
|
@@ -1154,8 +724,11 @@ export async function instrumentPage(page) {
|
|
|
1154
724
|
}
|
|
1155
725
|
};
|
|
1156
726
|
originalMouse.hide = () => {
|
|
1157
|
-
if (
|
|
1158
|
-
|
|
727
|
+
if (isMouseVisible(page)) {
|
|
728
|
+
performMouseHide({
|
|
729
|
+
mouseHideInternal: getOriginalMouseHide(page, originalHide),
|
|
730
|
+
page,
|
|
731
|
+
});
|
|
1159
732
|
if (activeClickRecorder) {
|
|
1160
733
|
const timeMs = Date.now();
|
|
1161
734
|
const hideEvent = {
|