@sqaitech/recorder 0.30.10

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.
@@ -0,0 +1,442 @@
1
+ (()=>{
2
+ "use strict";
3
+ function isFormElement(node) {
4
+ return node instanceof HTMLElement && ('input' === node.tagName.toLowerCase() || 'textarea' === node.tagName.toLowerCase() || 'select' === node.tagName.toLowerCase() || 'option' === node.tagName.toLowerCase());
5
+ }
6
+ function isButtonElement(node) {
7
+ return node instanceof HTMLElement && 'button' === node.tagName.toLowerCase();
8
+ }
9
+ function isAElement(node) {
10
+ return node instanceof HTMLElement && 'a' === node.tagName.toLowerCase();
11
+ }
12
+ function isSvgElement(node) {
13
+ return node instanceof SVGElement;
14
+ }
15
+ function isImgElement(node) {
16
+ if (!includeBaseElement(node) && node instanceof Element) {
17
+ const computedStyle = window.getComputedStyle(node);
18
+ const backgroundImage = computedStyle.getPropertyValue('background-image');
19
+ if ('none' !== backgroundImage) return true;
20
+ }
21
+ if (isIconfont(node)) return true;
22
+ return node instanceof HTMLElement && 'img' === node.tagName.toLowerCase() || node instanceof SVGElement && 'svg' === node.tagName.toLowerCase();
23
+ }
24
+ function isIconfont(node) {
25
+ if (node instanceof Element) {
26
+ const computedStyle = window.getComputedStyle(node);
27
+ const fontFamilyValue = computedStyle.fontFamily || '';
28
+ return fontFamilyValue.toLowerCase().indexOf('iconfont') >= 0;
29
+ }
30
+ return false;
31
+ }
32
+ function isNotContainerElement(node) {
33
+ return isTextElement(node) || isIconfont(node) || isImgElement(node) || isButtonElement(node) || isAElement(node) || isFormElement(node);
34
+ }
35
+ function isTextElement(node) {
36
+ var _node_nodeName_toLowerCase, _node_nodeName;
37
+ if (node instanceof Element) {
38
+ var _node_childNodes;
39
+ if ((null == node ? void 0 : null == (_node_childNodes = node.childNodes) ? void 0 : _node_childNodes.length) === 1 && (null == node ? void 0 : node.childNodes[0]) instanceof Text) return true;
40
+ }
41
+ return (null == (_node_nodeName = node.nodeName) ? void 0 : null == (_node_nodeName_toLowerCase = _node_nodeName.toLowerCase) ? void 0 : _node_nodeName_toLowerCase.call(_node_nodeName)) === '#text' && !isIconfont(node);
42
+ }
43
+ function includeBaseElement(node) {
44
+ if (!(node instanceof HTMLElement)) return false;
45
+ if (node.innerText) return true;
46
+ const includeList = [
47
+ 'svg',
48
+ 'button',
49
+ 'input',
50
+ 'textarea',
51
+ 'select',
52
+ 'option',
53
+ 'img',
54
+ 'a'
55
+ ];
56
+ for (const tagName of includeList){
57
+ const element = node.querySelectorAll(tagName);
58
+ if (element.length > 0) return true;
59
+ }
60
+ return false;
61
+ }
62
+ const getElementXpathIndex = (element)=>{
63
+ let index = 1;
64
+ let prev = element.previousElementSibling;
65
+ while(prev){
66
+ if (prev.nodeName.toLowerCase() === element.nodeName.toLowerCase()) index++;
67
+ prev = prev.previousElementSibling;
68
+ }
69
+ return index;
70
+ };
71
+ const normalizeXpathText = (text)=>{
72
+ if ('string' != typeof text) return '';
73
+ return text.replace(/\s+/g, ' ').trim();
74
+ };
75
+ const buildCurrentElementXpath = (element, isOrderSensitive, isLeafElement)=>{
76
+ var _element_textContent;
77
+ const parentPath = element.parentNode ? getElementXpath(element.parentNode, isOrderSensitive) : '';
78
+ const prefix = parentPath ? `${parentPath}/` : '/';
79
+ const tagName = element.nodeName.toLowerCase();
80
+ const textContent = null == (_element_textContent = element.textContent) ? void 0 : _element_textContent.trim();
81
+ if (isOrderSensitive) {
82
+ const index = getElementXpathIndex(element);
83
+ return `${prefix}${tagName}[${index}]`;
84
+ }
85
+ if (isLeafElement && textContent) return `${prefix}${tagName}[normalize-space()="${normalizeXpathText(textContent)}"]`;
86
+ const index = getElementXpathIndex(element);
87
+ return `${prefix}${tagName}[${index}]`;
88
+ };
89
+ const getElementXpath = (element, isOrderSensitive = false, isLeafElement = false)=>{
90
+ if (element.nodeType === Node.TEXT_NODE) {
91
+ const parentNode = element.parentNode;
92
+ if (parentNode && parentNode.nodeType === Node.ELEMENT_NODE) {
93
+ var _element_textContent;
94
+ const parentXPath = getElementXpath(parentNode, isOrderSensitive, true);
95
+ const textContent = null == (_element_textContent = element.textContent) ? void 0 : _element_textContent.trim();
96
+ if (textContent) return `${parentXPath}/text()[normalize-space()="${normalizeXpathText(textContent)}"]`;
97
+ return `${parentXPath}/text()`;
98
+ }
99
+ return '';
100
+ }
101
+ if (element.nodeType !== Node.ELEMENT_NODE) return '';
102
+ const el = element;
103
+ if (el === document.documentElement) return '/html';
104
+ if (el === document.body) return '/html/body';
105
+ if (isSvgElement(el)) {
106
+ let parent = el.parentNode;
107
+ while(parent && parent.nodeType === Node.ELEMENT_NODE){
108
+ if (!isSvgElement(parent)) return getElementXpath(parent, isOrderSensitive, isLeafElement);
109
+ parent = parent.parentNode;
110
+ }
111
+ return getElementXpath(el.parentNode, isOrderSensitive, isLeafElement);
112
+ }
113
+ return buildCurrentElementXpath(el, isOrderSensitive, isLeafElement);
114
+ };
115
+ const DEBUG = 'true' === localStorage.getItem('DEBUG');
116
+ function debugLog(...args) {
117
+ if (DEBUG) console.log('[EventRecorder]', ...args);
118
+ }
119
+ function recorder_generateHashId(type, elementRect) {
120
+ const rectStr = elementRect ? `${elementRect.left}_${elementRect.top}_${elementRect.width}_${elementRect.height}${void 0 !== elementRect.x ? `_${elementRect.x}` : ''}${void 0 !== elementRect.y ? `_${elementRect.y}` : ''}` : 'no_rect';
121
+ const combined = `${type}_${rectStr}`;
122
+ let hash = 0;
123
+ for(let i = 0; i < combined.length; i++){
124
+ const char = combined.charCodeAt(i);
125
+ hash = (hash << 5) - hash + char;
126
+ hash &= hash;
127
+ }
128
+ return Math.abs(hash).toString(36);
129
+ }
130
+ const isSameInputTarget = (event1, event2)=>event1.element === event2.element;
131
+ const isSameScrollTarget = (event1, event2)=>event1.element === event2.element;
132
+ const getLastLabelClick = (events)=>{
133
+ for(let i = events.length - 1; i >= 0; i--){
134
+ const event = events[i];
135
+ if ('click' === event.type && event.isLabelClick) return event;
136
+ }
137
+ };
138
+ function getAllScrollableElements() {
139
+ const elements = [];
140
+ const all = document.querySelectorAll('body *');
141
+ all.forEach((el)=>{
142
+ const style = window.getComputedStyle(el);
143
+ const overflowY = style.overflowY;
144
+ const overflowX = style.overflowX;
145
+ const isScrollableY = ('auto' === overflowY || 'scroll' === overflowY) && el.scrollHeight > el.clientHeight;
146
+ const isScrollableX = ('auto' === overflowX || 'scroll' === overflowX) && el.scrollWidth > el.clientWidth;
147
+ if (isScrollableY || isScrollableX) elements.push(el);
148
+ });
149
+ return elements;
150
+ }
151
+ class EventRecorder {
152
+ isRecording = false;
153
+ eventCallback;
154
+ scrollThrottleTimer = null;
155
+ scrollThrottleDelay = 200;
156
+ inputThrottleTimer = null;
157
+ inputThrottleDelay = 300;
158
+ lastViewportScroll = null;
159
+ scrollTargets = [];
160
+ sessionId;
161
+ constructor(eventCallback, sessionId){
162
+ this.eventCallback = eventCallback;
163
+ this.sessionId = sessionId;
164
+ }
165
+ createNavigationEvent(url, title) {
166
+ return {
167
+ type: 'navigation',
168
+ url,
169
+ title,
170
+ pageInfo: {
171
+ width: window.innerWidth,
172
+ height: window.innerHeight
173
+ },
174
+ timestamp: Date.now(),
175
+ hashId: `navigation_${Date.now()}`
176
+ };
177
+ }
178
+ start() {
179
+ if (this.isRecording) return void debugLog('Recording already active, ignoring start request');
180
+ this.isRecording = true;
181
+ debugLog('Starting event recording');
182
+ this.scrollTargets = [];
183
+ if (0 === this.scrollTargets.length) {
184
+ this.scrollTargets = getAllScrollableElements();
185
+ this.scrollTargets.push(document.body);
186
+ }
187
+ debugLog('Added event listeners for', this.scrollTargets.length, 'scroll targets');
188
+ setTimeout(()=>{
189
+ const navigationEvent = this.createNavigationEvent(window.location.href, document.title);
190
+ this.eventCallback(navigationEvent);
191
+ debugLog('Added final navigation event', navigationEvent);
192
+ }, 0);
193
+ document.addEventListener('click', this.handleClick, true);
194
+ document.addEventListener('input', this.handleInput);
195
+ document.addEventListener('scroll', this.handleScroll, {
196
+ passive: true
197
+ });
198
+ this.scrollTargets.forEach((target)=>{
199
+ target.addEventListener('scroll', this.handleScroll, {
200
+ passive: true
201
+ });
202
+ });
203
+ }
204
+ stop() {
205
+ if (!this.isRecording) return void debugLog('Recording not active, ignoring stop request');
206
+ this.isRecording = false;
207
+ debugLog('Stopping event recording');
208
+ if (this.scrollThrottleTimer) {
209
+ clearTimeout(this.scrollThrottleTimer);
210
+ this.scrollThrottleTimer = null;
211
+ }
212
+ if (this.inputThrottleTimer) {
213
+ clearTimeout(this.inputThrottleTimer);
214
+ this.inputThrottleTimer = null;
215
+ }
216
+ document.removeEventListener('click', this.handleClick);
217
+ document.removeEventListener('input', this.handleInput);
218
+ this.scrollTargets.forEach((target)=>{
219
+ target.removeEventListener('scroll', this.handleScroll);
220
+ });
221
+ debugLog('Removed all event listeners');
222
+ }
223
+ handleClick = (event)=>{
224
+ if (!this.isRecording) return;
225
+ const target = event.target;
226
+ const { isLabelClick, labelInfo } = this.checkLabelClick(target);
227
+ const rect = target.getBoundingClientRect();
228
+ const elementRect = {
229
+ x: Number(event.clientX.toFixed(2)),
230
+ y: Number(event.clientY.toFixed(2))
231
+ };
232
+ console.log('isNotContainerElement', isNotContainerElement(target));
233
+ if (isNotContainerElement(target)) {
234
+ elementRect.left = Number(rect.left.toFixed(2));
235
+ elementRect.top = Number(rect.top.toFixed(2));
236
+ elementRect.width = Number(rect.width.toFixed(2));
237
+ elementRect.height = Number(rect.height.toFixed(2));
238
+ }
239
+ const clickEvent = {
240
+ type: 'click',
241
+ elementRect,
242
+ pageInfo: {
243
+ width: window.innerWidth,
244
+ height: window.innerHeight
245
+ },
246
+ value: '',
247
+ timestamp: Date.now(),
248
+ hashId: recorder_generateHashId('click', {
249
+ ...elementRect
250
+ }),
251
+ element: target,
252
+ isLabelClick,
253
+ labelInfo,
254
+ isTrusted: event.isTrusted,
255
+ detail: event.detail
256
+ };
257
+ this.eventCallback(clickEvent);
258
+ };
259
+ handleScroll = (event)=>{
260
+ if (!this.isRecording) return;
261
+ function isDocument(target) {
262
+ return target instanceof Document;
263
+ }
264
+ const target = event.target;
265
+ const scrollXTarget = isDocument(target) ? window.scrollX : target.scrollLeft;
266
+ const scrollYTarget = isDocument(target) ? window.scrollY : target.scrollTop;
267
+ const rect = isDocument(target) ? {
268
+ left: 0,
269
+ top: 0,
270
+ width: window.innerWidth,
271
+ height: window.innerHeight
272
+ } : target.getBoundingClientRect();
273
+ if (this.scrollThrottleTimer) clearTimeout(this.scrollThrottleTimer);
274
+ this.scrollThrottleTimer = window.setTimeout(()=>{
275
+ if (this.isRecording) {
276
+ const elementRect = {
277
+ left: isDocument(target) ? 0 : Number(rect.left.toFixed(2)),
278
+ top: isDocument(target) ? 0 : Number(rect.top.toFixed(2)),
279
+ width: isDocument(target) ? window.innerWidth : Number(rect.width.toFixed(2)),
280
+ height: isDocument(target) ? window.innerHeight : Number(rect.height.toFixed(2))
281
+ };
282
+ const scrollEvent = {
283
+ type: 'scroll',
284
+ elementRect,
285
+ pageInfo: {
286
+ width: window.innerWidth,
287
+ height: window.innerHeight
288
+ },
289
+ value: `${scrollXTarget.toFixed(2)},${scrollYTarget.toFixed(2)}`,
290
+ timestamp: Date.now(),
291
+ hashId: recorder_generateHashId('scroll', {
292
+ ...elementRect
293
+ }),
294
+ element: target
295
+ };
296
+ this.eventCallback(scrollEvent);
297
+ }
298
+ this.scrollThrottleTimer = null;
299
+ }, this.scrollThrottleDelay);
300
+ };
301
+ handleInput = (event)=>{
302
+ if (!this.isRecording) return;
303
+ const target = event.target;
304
+ if ('checkbox' === target.type) return;
305
+ const rect = target.getBoundingClientRect();
306
+ const elementRect = {
307
+ left: Number(rect.left.toFixed(2)),
308
+ top: Number(rect.top.toFixed(2)),
309
+ width: Number(rect.width.toFixed(2)),
310
+ height: Number(rect.height.toFixed(2))
311
+ };
312
+ if (this.inputThrottleTimer) clearTimeout(this.inputThrottleTimer);
313
+ this.inputThrottleTimer = window.setTimeout(()=>{
314
+ if (this.isRecording) {
315
+ const inputEvent = {
316
+ type: 'input',
317
+ value: 'password' !== target.type ? target.value : '*****',
318
+ timestamp: Date.now(),
319
+ hashId: recorder_generateHashId('input', {
320
+ ...elementRect
321
+ }),
322
+ element: target,
323
+ inputType: target.type || 'text',
324
+ elementRect,
325
+ pageInfo: {
326
+ width: window.innerWidth,
327
+ height: window.innerHeight
328
+ }
329
+ };
330
+ debugLog('Throttled input event:', {
331
+ value: inputEvent.value,
332
+ timestamp: inputEvent.timestamp,
333
+ target: target.tagName,
334
+ inputType: target.type
335
+ });
336
+ this.eventCallback(inputEvent);
337
+ }
338
+ this.inputThrottleTimer = null;
339
+ }, this.inputThrottleDelay);
340
+ };
341
+ checkLabelClick(target) {
342
+ let isLabelClick = false;
343
+ let labelInfo;
344
+ if (target) if ('LABEL' === target.tagName) {
345
+ isLabelClick = true;
346
+ labelInfo = {
347
+ htmlFor: target.htmlFor,
348
+ textContent: target.textContent?.trim(),
349
+ xpath: getElementXpath(target)
350
+ };
351
+ } else {
352
+ let parent = target.parentElement;
353
+ while(parent){
354
+ if ('LABEL' === parent.tagName) {
355
+ isLabelClick = true;
356
+ labelInfo = {
357
+ htmlFor: parent.htmlFor,
358
+ textContent: parent.textContent?.trim(),
359
+ xpath: getElementXpath(parent)
360
+ };
361
+ break;
362
+ }
363
+ parent = parent.parentElement;
364
+ }
365
+ }
366
+ return {
367
+ isLabelClick,
368
+ labelInfo
369
+ };
370
+ }
371
+ isActive() {
372
+ return this.isRecording;
373
+ }
374
+ optimizeEvent(event, events) {
375
+ const lastEvent = events[events.length - 1];
376
+ if ('click' === event.type) {
377
+ const lastEvent = getLastLabelClick(events);
378
+ if (event.element) {
379
+ const { isLabelClick, labelInfo } = this.checkLabelClick(event.element);
380
+ if (lastEvent && isLabelClick && 'click' === lastEvent.type && lastEvent.isLabelClick && (lastEvent.labelInfo?.htmlFor && event.element.id && lastEvent.labelInfo?.htmlFor === event.element.id || labelInfo?.xpath && lastEvent.labelInfo?.xpath && lastEvent.labelInfo?.xpath === labelInfo?.xpath)) {
381
+ debugLog('Skip input event triggered by label click:', event.element);
382
+ return events;
383
+ }
384
+ return [
385
+ ...events,
386
+ event
387
+ ];
388
+ }
389
+ }
390
+ if ('input' === event.type) {
391
+ if (lastEvent && 'click' === lastEvent.type && lastEvent.isLabelClick && lastEvent.labelInfo?.htmlFor === event.targetId) {
392
+ debugLog('Skipping input event - triggered by label click:', {
393
+ labelHtmlFor: getLastLabelClick(events)?.labelInfo?.htmlFor,
394
+ inputId: event.targetId,
395
+ element: event.element
396
+ });
397
+ return events;
398
+ }
399
+ if (lastEvent && 'input' === lastEvent.type && isSameInputTarget(lastEvent, event)) {
400
+ const oldInputEvent = events[events.length - 1];
401
+ const newEvents = [
402
+ ...events
403
+ ];
404
+ newEvents[events.length - 1] = {
405
+ value: event.element?.value,
406
+ ...event
407
+ };
408
+ debugLog('Merging input event:', {
409
+ oldValue: oldInputEvent.value,
410
+ newValue: event.value,
411
+ oldTimestamp: oldInputEvent.timestamp,
412
+ newTimestamp: event.timestamp,
413
+ target: event.targetTagName
414
+ });
415
+ return newEvents;
416
+ }
417
+ }
418
+ if ('scroll' === event.type) {
419
+ if (lastEvent && 'scroll' === lastEvent.type && isSameScrollTarget(lastEvent, event)) {
420
+ const oldScrollEvent = events[events.length - 1];
421
+ const newEvents = [
422
+ ...events
423
+ ];
424
+ newEvents[events.length - 1] = event;
425
+ debugLog('Replacing last scroll event with new scroll event:', {
426
+ oldPosition: `${oldScrollEvent.elementRect?.left},${oldScrollEvent.elementRect?.top}`,
427
+ newPosition: `${event.elementRect?.left},${event.elementRect?.top}`,
428
+ oldTimestamp: oldScrollEvent.timestamp,
429
+ newTimestamp: event.timestamp,
430
+ target: event.targetTagName
431
+ });
432
+ return newEvents;
433
+ }
434
+ }
435
+ return [
436
+ ...events,
437
+ event
438
+ ];
439
+ }
440
+ }
441
+ window.EventRecorder = EventRecorder;
442
+ })();