ftmocks-utils 1.3.7 → 1.3.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftmocks-utils",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "description": "Util functions for FtMocks",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -0,0 +1,809 @@
1
+ const path = require("path");
2
+ const fs = require("fs");
3
+ const { getMockDir, nameToFolder } = require("./common-utils");
4
+
5
+ const injectEventRecordingScript = async (
6
+ page,
7
+ url,
8
+ ftmocksConifg,
9
+ testName
10
+ ) => {
11
+ console.log("calling injectEventRecordingScript");
12
+ try {
13
+ const eventsFile = path.join(
14
+ getMockDir(ftmocksConifg),
15
+ `test_${nameToFolder(testName)}`,
16
+ `_events.json`
17
+ );
18
+ if (!fs.existsSync(eventsFile)) {
19
+ // Ensure the directory exists before writing the eventsFile
20
+ const dir = path.dirname(eventsFile);
21
+ if (!fs.existsSync(dir)) {
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ }
24
+ fs.writeFileSync(eventsFile, JSON.stringify([], null, 2));
25
+ }
26
+
27
+ // Expose a function to receive click info from the browser
28
+ await page.exposeFunction("saveEventForTest", (event) => {
29
+ event.id = crypto.randomUUID();
30
+ if (!fs.existsSync(eventsFile)) {
31
+ // Ensure the directory exists before writing the eventsFile
32
+ const dir = path.dirname(eventsFile);
33
+ if (!fs.existsSync(dir)) {
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ }
36
+ fs.writeFileSync(eventsFile, JSON.stringify([], null, 2));
37
+ }
38
+ const events = JSON.parse(fs.readFileSync(eventsFile, "utf8"));
39
+ if (
40
+ event.type === "input" &&
41
+ events[events.length - 1]?.type === "input"
42
+ ) {
43
+ events[events.length - 1].value = event.value;
44
+ } else {
45
+ events.push(event);
46
+ }
47
+ fs.writeFileSync(eventsFile, JSON.stringify(events, null, 2));
48
+ });
49
+
50
+ fs.writeFileSync(
51
+ eventsFile,
52
+ JSON.stringify(
53
+ [
54
+ {
55
+ id: crypto.randomUUID(),
56
+ type: "url",
57
+ target: url,
58
+ time: new Date().toISOString(),
59
+ value: url,
60
+ },
61
+ ],
62
+ null,
63
+ 2
64
+ )
65
+ );
66
+ await page.addInitScript(() => {
67
+ console.log("calling addInitScript");
68
+ let prevEventSnapshot = null;
69
+ let currentEventSnapshot = null;
70
+
71
+ const getAbsoluteXPath = (element) => {
72
+ if (element === document.body) return "/html/body";
73
+ const svgTagNames = [
74
+ "svg",
75
+ "path",
76
+ "rect",
77
+ "circle",
78
+ "ellipse",
79
+ "line",
80
+ "polygon",
81
+ "polyline",
82
+ "text",
83
+ "tspan",
84
+ ];
85
+
86
+ let xpath = "";
87
+ for (
88
+ ;
89
+ element && element.nodeType === 1;
90
+ element = element.parentNode
91
+ ) {
92
+ let index = 0;
93
+ let sibling = element;
94
+ while ((sibling = sibling.previousSibling)) {
95
+ if (sibling.nodeType === 1 && sibling.nodeName === element.nodeName)
96
+ index++;
97
+ }
98
+ const tagName = element.nodeName.toLowerCase();
99
+ const position = index ? `[${index + 1}]` : "";
100
+ xpath =
101
+ "/" +
102
+ (svgTagNames.includes(tagName)
103
+ ? `*[local-name()='${tagName}']`
104
+ : tagName) +
105
+ position +
106
+ xpath;
107
+ }
108
+
109
+ return xpath;
110
+ };
111
+
112
+ const filterElementsFromHtml = (html = "", selector) => {
113
+ const doc = new DOMParser().parseFromString(html, "text/html");
114
+ const elements = doc.querySelectorAll(selector);
115
+ return elements;
116
+ };
117
+
118
+ const filterXpathElementsFromHtml = (html, xpath) => {
119
+ try {
120
+ const doc = new DOMParser().parseFromString(html, "text/html");
121
+ // The elements variable should be an array, not an XPathResult snapshot. Convert the snapshot to an array of elements.
122
+ const snapshot = doc.evaluate(
123
+ xpath,
124
+ doc,
125
+ null,
126
+ XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
127
+ null
128
+ );
129
+ const elements = [];
130
+ for (let i = 0; i < snapshot.snapshotLength; i++) {
131
+ elements.push(snapshot.snapshotItem(i));
132
+ }
133
+ return elements;
134
+ } catch (error) {
135
+ console.error("Error filtering XPath elements from HTML", {
136
+ error: error.message,
137
+ stack: error.stack,
138
+ });
139
+ return [];
140
+ }
141
+ };
142
+
143
+ const getElementsByRank = (elements, mainElement) => {
144
+ const ranksAndIndexes = [];
145
+
146
+ for (let i = 0; i < elements.length; i++) {
147
+ // Compare element with mainElement based on attributes and textContent
148
+ let rank = 1;
149
+ const e = elements[i];
150
+ if (e && mainElement) {
151
+ if (e.attributes && mainElement.attributes) {
152
+ if (e.attributes.length !== mainElement.attributes.length) {
153
+ rank =
154
+ rank +
155
+ Math.abs(e.attributes.length - mainElement.attributes.length);
156
+ }
157
+ for (let j = 0; j < e.attributes.length; j++) {
158
+ const attrName = e.attributes[j].name;
159
+ if (
160
+ e.getAttribute(attrName) &&
161
+ mainElement.getAttribute(attrName) &&
162
+ e.getAttribute(attrName) !==
163
+ mainElement.getAttribute(attrName)
164
+ ) {
165
+ rank = rank + 1;
166
+ }
167
+ }
168
+ }
169
+
170
+ if (e.textContent === mainElement.textContent) {
171
+ rank = rank + 1;
172
+ }
173
+ // Compare node depth in the DOM tree
174
+ const getDepth = (node) => {
175
+ let depth = 0;
176
+ let current = node;
177
+ while (current && current.parentNode) {
178
+ depth++;
179
+ current = current.parentNode;
180
+ }
181
+ return depth;
182
+ };
183
+
184
+ if (e && mainElement) {
185
+ const eDepth = getDepth(e);
186
+ const mainDepth = getDepth(mainElement);
187
+ rank = rank + Math.abs(eDepth - mainDepth);
188
+ }
189
+ }
190
+ ranksAndIndexes.push({ index: i, rank });
191
+ }
192
+ return ranksAndIndexes.sort((a, b) => a.rank - b.rank);
193
+ };
194
+
195
+ const isUniqueXpath = (xpath) => {
196
+ try {
197
+ const elements = document.evaluate(
198
+ xpath,
199
+ document,
200
+ null,
201
+ XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
202
+ null
203
+ );
204
+ return elements.snapshotLength === 1;
205
+ } catch (error) {
206
+ console.error("Error checking if XPath is unique", {
207
+ error: error.message,
208
+ stack: error.stack,
209
+ });
210
+ return true;
211
+ }
212
+ };
213
+ const getUniqueXpath = (xpath, mainElement) => {
214
+ const prevElements = filterXpathElementsFromHtml(
215
+ prevEventSnapshot,
216
+ xpath
217
+ );
218
+ if (prevElements.snapshotLength > 1 && mainElement) {
219
+ return `(${xpath})[${
220
+ getElementsByRank(prevElements, mainElement)[0].index + 1
221
+ }]`;
222
+ }
223
+ return xpath;
224
+ };
225
+
226
+ const getUniqueElementSelectorNth = (selector, mainElement) => {
227
+ const prevElements = filterElementsFromHtml(
228
+ prevEventSnapshot,
229
+ selector
230
+ );
231
+ if (prevElements.length > 1) {
232
+ return getElementsByRank(prevElements, mainElement)[0].index + 1;
233
+ }
234
+ return 1;
235
+ };
236
+
237
+ const getSelectorsByConfidence = (selectors) => {
238
+ const selectorCounts = selectors.map((selector) => {
239
+ if (selector.value.startsWith("/")) {
240
+ const prevElements = filterXpathElementsFromHtml(
241
+ prevEventSnapshot,
242
+ selector.value
243
+ );
244
+ const nextElements = filterXpathElementsFromHtml(
245
+ currentEventSnapshot,
246
+ selector.value
247
+ );
248
+ return {
249
+ selector: selector.value,
250
+ type: selector.type,
251
+ count: prevElements.length + nextElements.length,
252
+ };
253
+ } else {
254
+ const prevElements = filterElementsFromHtml(
255
+ prevEventSnapshot,
256
+ selector.value
257
+ );
258
+ const nextElements = filterElementsFromHtml(
259
+ currentEventSnapshot,
260
+ selector.value
261
+ );
262
+ return {
263
+ selector: selector.value,
264
+ type: selector.type,
265
+ count: prevElements.length + nextElements.length,
266
+ };
267
+ }
268
+ });
269
+ const zeroCountSelectors = selectorCounts
270
+ .filter((selector) => selector.count === 0)
271
+ .map((selector) => selector.selector);
272
+ const nonZeroCountSelectors = selectorCounts
273
+ .filter((selector) => selector.count > 0)
274
+ .sort((selObj1, selObj2) => selObj1.count - selObj2.count)
275
+ .map((selObj) => selObj.selector);
276
+ return [...nonZeroCountSelectors, ...zeroCountSelectors];
277
+ };
278
+
279
+ const getBestSelectors = (element, event) => {
280
+ const selectors = [];
281
+ const excludeTagNames = ["script", "style", "link", "meta"];
282
+ try {
283
+ const tagName = element.tagName.toLowerCase();
284
+ if (excludeTagNames.includes(tagName)) {
285
+ return selectors;
286
+ }
287
+ if (element.getAttribute("data-testid")) {
288
+ selectors.push({
289
+ type: "locator",
290
+ value: `${tagName}[data-testid='${element.getAttribute(
291
+ "data-testid"
292
+ )}']`,
293
+ nth: getUniqueElementSelectorNth(
294
+ `${tagName}[data-testid='${element.getAttribute(
295
+ "data-testid"
296
+ )}']`,
297
+ element
298
+ ),
299
+ });
300
+ }
301
+ if (element.getAttribute("data-id")) {
302
+ selectors.push({
303
+ type: "locator",
304
+ value: `${tagName}[data-id='${element.getAttribute("data-id")}']`,
305
+ nth: getUniqueElementSelectorNth(
306
+ `${tagName}[data-id='${element.getAttribute("data-id")}']`,
307
+ element
308
+ ),
309
+ });
310
+ }
311
+ if (element.getAttribute("data-action")) {
312
+ selectors.push({
313
+ type: "locator",
314
+ value: `${tagName}[data-action='${element.getAttribute(
315
+ "data-action"
316
+ )}']`,
317
+ nth: getUniqueElementSelectorNth(
318
+ `${tagName}[data-action='${element.getAttribute(
319
+ "data-action"
320
+ )}']`,
321
+ element
322
+ ),
323
+ });
324
+ }
325
+ if (element.getAttribute("data-cy")) {
326
+ selectors.push({
327
+ type: "locator",
328
+ value: `${tagName}[data-cy='${element.getAttribute("data-cy")}']`,
329
+ nth: getUniqueElementSelectorNth(
330
+ `${tagName}[data-cy='${element.getAttribute("data-cy")}']`,
331
+ element
332
+ ),
333
+ });
334
+ }
335
+ if (
336
+ element.name &&
337
+ tagName === "input" &&
338
+ (element.type === "text" || element.type === "password")
339
+ ) {
340
+ selectors.push({
341
+ type: "locator",
342
+ value: `${tagName}[name='${element.name}']`,
343
+ nth: getUniqueElementSelectorNth(
344
+ `${tagName}[name='${element.name}']`,
345
+ element
346
+ ),
347
+ });
348
+ } else if (
349
+ element.name &&
350
+ tagName === "input" &&
351
+ (element.type === "checkbox" || element.type === "radio")
352
+ ) {
353
+ selectors.push({
354
+ type: "locator",
355
+ value: `${tagName}[name='${element.name}'][value='${element.value}']`,
356
+ nth: getUniqueElementSelectorNth(
357
+ `${tagName}[name='${element.name}'][value='${element.value}']`,
358
+ element
359
+ ),
360
+ });
361
+ }
362
+ if (element.ariaLabel) {
363
+ selectors.push({
364
+ type: "locator",
365
+ value: `${tagName}[aria-label='${element.ariaLabel}']`,
366
+ nth: getUniqueElementSelectorNth(
367
+ `${tagName}[aria-label='${element.ariaLabel}']`,
368
+ element
369
+ ),
370
+ });
371
+ }
372
+ if (element.role && element.name) {
373
+ selectors.push({
374
+ type: "locator",
375
+ value: `${tagName}[role='${element.role}'][name='${element.name}']`,
376
+ nth: getUniqueElementSelectorNth(
377
+ `${tagName}[role='${element.role}'][name='${element.name}']`,
378
+ element
379
+ ),
380
+ });
381
+ }
382
+ if (element.getAttribute("src")) {
383
+ selectors.push({
384
+ type: "locator",
385
+ value: `${tagName}[src='${element.getAttribute("src")}']`,
386
+ nth: getUniqueElementSelectorNth(
387
+ `${tagName}[src='${element.getAttribute("src")}']`,
388
+ element
389
+ ),
390
+ });
391
+ }
392
+ if (element.getAttribute("href")) {
393
+ selectors.push({
394
+ type: "locator",
395
+ value: `${tagName}[href='${element.getAttribute("href")}']`,
396
+ nth: getUniqueElementSelectorNth(
397
+ `${tagName}[href='${element.getAttribute("href")}']`,
398
+ element
399
+ ),
400
+ });
401
+ }
402
+ const escapedText = element.textContent.replace(/"/g, '\\"');
403
+ if (element.role && element.textContent) {
404
+ selectors.push({
405
+ type: "locator",
406
+ value: `${tagName}[role='${element.role}'][contains(text(), '${escapedText}')]`,
407
+ nth: getUniqueElementSelectorNth(
408
+ `${tagName}[role='${element.role}'][contains(text(), '${escapedText}')]`,
409
+ element
410
+ ),
411
+ });
412
+ }
413
+ if (event?.target?.textContent?.length > 0) {
414
+ selectors.push({
415
+ type: "locator",
416
+ value: getUniqueXpath(
417
+ `//*[contains(text(), '${event.target.textContent.replace(
418
+ /"/g,
419
+ '\\"'
420
+ )}')]`,
421
+ event.target
422
+ ),
423
+ });
424
+ }
425
+ return selectors;
426
+ } catch (error) {
427
+ console.error("Error getting best selectors", {
428
+ error: error.message,
429
+ stack: error.stack,
430
+ });
431
+ return selectors;
432
+ }
433
+ };
434
+
435
+ const generateXPathWithNearestParentId = (element) => {
436
+ const otherIdAttributes = [
437
+ "data-id",
438
+ "data-action",
439
+ "data-testid",
440
+ "data-cy",
441
+ "data-role",
442
+ "data-name",
443
+ "data-label",
444
+ ];
445
+ try {
446
+ let path = "";
447
+ let nearestParentId = null;
448
+ let nearestParentAttribute = null;
449
+ let nearestParentAttributeValue = null;
450
+
451
+ // Check if the current element's has an ID
452
+ if (element.id) {
453
+ nearestParentId = element.id;
454
+ }
455
+
456
+ while (!nearestParentId && element !== document.body && element) {
457
+ const tagName = element.tagName.toLowerCase();
458
+ let index = 1;
459
+ let sibling = element.previousElementSibling;
460
+
461
+ while (sibling) {
462
+ if (sibling.tagName.toLowerCase() === tagName) {
463
+ index += 1;
464
+ }
465
+ sibling = sibling.previousElementSibling;
466
+ }
467
+
468
+ let nextSibling = element.nextElementSibling;
469
+ let usedNextSibling = false;
470
+ while (nextSibling) {
471
+ if (nextSibling.tagName.toLowerCase() === tagName) {
472
+ usedNextSibling = true;
473
+ break;
474
+ }
475
+ nextSibling = nextSibling.nextElementSibling;
476
+ }
477
+
478
+ const svgTagNames = [
479
+ "svg",
480
+ "path",
481
+ "rect",
482
+ "circle",
483
+ "ellipse",
484
+ "line",
485
+ "polygon",
486
+ "polyline",
487
+ "text",
488
+ "tspan",
489
+ ];
490
+ let tempTagName = tagName;
491
+ if (svgTagNames.includes(tagName)) {
492
+ tempTagName = `*[local-name()='${tagName}']`;
493
+ }
494
+ if (index === 1) {
495
+ if (usedNextSibling) {
496
+ path = `/${tempTagName}[1]${path}`;
497
+ } else {
498
+ path = `/${tempTagName}${path}`;
499
+ }
500
+ } else {
501
+ path = `/${tempTagName}[${index}]${path}`;
502
+ }
503
+
504
+ // Check if the current element's parent has an ID
505
+ if (element.parentElement && element.parentElement.id) {
506
+ nearestParentId = element.parentElement.id;
507
+ break; // Stop searching when we find the nearest parent with an ID
508
+ } else if (element.parentElement) {
509
+ otherIdAttributes.forEach((attribute) => {
510
+ const parentAttributeValue =
511
+ element.parentElement.getAttribute(attribute);
512
+ if (
513
+ parentAttributeValue &&
514
+ isUniqueXpath(`//*[@${attribute}='${parentAttributeValue}']`)
515
+ ) {
516
+ nearestParentAttribute = attribute;
517
+ nearestParentAttributeValue = parentAttributeValue;
518
+ }
519
+ });
520
+ if (nearestParentAttribute && nearestParentAttributeValue) {
521
+ break;
522
+ }
523
+ }
524
+
525
+ element = element.parentElement;
526
+ }
527
+
528
+ if (nearestParentId) {
529
+ path = `//*[@id='${nearestParentId}']${path}`;
530
+ return path;
531
+ } else if (nearestParentAttribute && nearestParentAttributeValue) {
532
+ path = `//*[@${nearestParentAttribute}='${nearestParentAttributeValue}']${path}`;
533
+ return path;
534
+ }
535
+ } catch (error) {
536
+ console.error("Error generating XPath with nearest parent ID", {
537
+ error: error.message,
538
+ stack: error.stack,
539
+ });
540
+ return null;
541
+ }
542
+ };
543
+
544
+ const getParentElementWithEventOrId = (event, eventType) => {
545
+ let target = event.target;
546
+ const clickableTagNames = [
547
+ "button",
548
+ "a",
549
+ "input",
550
+ "option",
551
+ "details",
552
+ "summary",
553
+ "select",
554
+ "li",
555
+ "h1",
556
+ "h2",
557
+ "h3",
558
+ "h4",
559
+ "h5",
560
+ "h6",
561
+ ];
562
+
563
+ while (target && target !== document) {
564
+ // Check if the target is a clickable element
565
+ // Check for test attributes and accessibility attributes
566
+ const selectors = getBestSelectors(target, event);
567
+ if (selectors.length > 0) {
568
+ return target;
569
+ } else if (target.getAttribute("id")) {
570
+ return target;
571
+ } else if (
572
+ target.getAttribute(eventType) ||
573
+ target[eventType] ||
574
+ target.getAttribute(`on${eventType}`) ||
575
+ target.getAttribute(`${eventType}`) ||
576
+ target.getAttribute(
577
+ `${eventType.charAt(0).toUpperCase() + eventType.slice(1)}`
578
+ )
579
+ ) {
580
+ return target;
581
+ } else if (clickableTagNames.includes(target.tagName.toLowerCase())) {
582
+ return target;
583
+ }
584
+ target = target.parentNode;
585
+ }
586
+ return event.target;
587
+ };
588
+
589
+ const getElement = (target) => {
590
+ return {
591
+ tagName: target.tagName,
592
+ textContent:
593
+ target.textContent?.length > 0 && target.textContent?.length < 100
594
+ ? target.textContent
595
+ : null,
596
+ id: target.id,
597
+ role: target.role,
598
+ name: target.name,
599
+ ariaLabel: target.ariaLabel,
600
+ value: target.value,
601
+ type: target.type,
602
+ checked: target.checked,
603
+ selected: target.selected,
604
+ disabled: target.disabled,
605
+ readonly: target.readonly,
606
+ placeholder: target.placeholder,
607
+ title: target.title,
608
+ href: target.getAttribute("href"),
609
+ src: target.getAttribute("src"),
610
+ alt: target.alt,
611
+ };
612
+ };
613
+
614
+ const getXpathsIncluded = (selectors, currentTarget, event) => {
615
+ selectors.push({
616
+ type: "locator",
617
+ value: generateXPathWithNearestParentId(currentTarget),
618
+ });
619
+ selectors.push({
620
+ type: "locator",
621
+ value: getAbsoluteXPath(event.target),
622
+ });
623
+ };
624
+
625
+ document.addEventListener("click", (event) => {
626
+ console.log('calling document.addEventListener("click")');
627
+ currentEventSnapshot = document.documentElement.innerHTML;
628
+ const currentTarget = getParentElementWithEventOrId(event, "onclick");
629
+ const selectors = getBestSelectors(currentTarget, event);
630
+ getXpathsIncluded(selectors, currentTarget, event);
631
+ window.saveEventForTest({
632
+ type: "click",
633
+ target: selectors[0].value,
634
+ time: new Date().toISOString(),
635
+ value: {
636
+ clientX: event.clientX,
637
+ clientY: event.clientY,
638
+ windowWidth: window.innerWidth,
639
+ windowHeight: window.innerHeight,
640
+ },
641
+ selectors,
642
+ element: getElement(currentTarget),
643
+ });
644
+ prevEventSnapshot = currentEventSnapshot;
645
+ });
646
+ document.addEventListener("dblclick", (event) => {
647
+ currentEventSnapshot = document.documentElement.innerHTML;
648
+ const currentTarget = getParentElementWithEventOrId(
649
+ event,
650
+ "ondblclick"
651
+ );
652
+ const selectors = getBestSelectors(currentTarget, event);
653
+ getXpathsIncluded(selectors, currentTarget, event);
654
+ window.saveEventForTest({
655
+ type: "dblclick",
656
+ target: selectors[0].value,
657
+ time: new Date().toISOString(),
658
+ value: {
659
+ clientX: event.clientX,
660
+ clientY: event.clientY,
661
+ windowWidth: window.innerWidth,
662
+ windowHeight: window.innerHeight,
663
+ },
664
+ selectors,
665
+ element: getElement(currentTarget),
666
+ });
667
+ });
668
+ document.addEventListener("contextmenu", (event) => {
669
+ currentEventSnapshot = document.documentElement.innerHTML;
670
+ const currentTarget = getParentElementWithEventOrId(
671
+ event,
672
+ "oncontextmenu"
673
+ );
674
+ const selectors = getBestSelectors(currentTarget, event);
675
+ getXpathsIncluded(selectors, currentTarget, event);
676
+ window.saveEventForTest({
677
+ type: "contextmenu",
678
+ target: selectors[0].value,
679
+ time: new Date().toISOString(),
680
+ value: {
681
+ clientX: event.clientX,
682
+ clientY: event.clientY,
683
+ windowWidth: window.innerWidth,
684
+ windowHeight: window.innerHeight,
685
+ },
686
+ selectors,
687
+ element: getElement(currentTarget),
688
+ });
689
+ });
690
+ document.addEventListener("input", (event) => {
691
+ currentEventSnapshot = document.documentElement.innerHTML;
692
+ const currentTarget = getParentElementWithEventOrId(event, "oninput");
693
+ const selectors = getBestSelectors(currentTarget, event);
694
+ getXpathsIncluded(selectors, currentTarget, event);
695
+ if (event.target && event.target.tagName === "INPUT") {
696
+ window.saveEventForTest({
697
+ type: "input",
698
+ target: selectors[0].value,
699
+ time: new Date().toISOString(),
700
+ value: event.target.value,
701
+ selectors,
702
+ element: getElement(currentTarget),
703
+ });
704
+ }
705
+ });
706
+ document.addEventListener("keypress", (event) => {
707
+ if (
708
+ event.key === "Enter" ||
709
+ event.key === "Tab" ||
710
+ event.key === "Escape" ||
711
+ event.key === "Backspace" ||
712
+ event.key === "ArrowUp" ||
713
+ event.key === "ArrowDown" ||
714
+ event.key === "ArrowLeft" ||
715
+ event.key === "ArrowRight"
716
+ ) {
717
+ currentEventSnapshot = document.documentElement.innerHTML;
718
+ const currentTarget = getParentElementWithEventOrId(event, "oninput");
719
+ const selectors = getBestSelectors(currentTarget, event);
720
+ getXpathsIncluded(selectors, currentTarget, event);
721
+ window.saveEventForTest({
722
+ type: "keypress",
723
+ key: event.key,
724
+ code: event.code,
725
+ target: selectors[0].value,
726
+ time: new Date().toISOString(),
727
+ value: {
728
+ clientX: event.clientX,
729
+ clientY: event.clientY,
730
+ windowWidth: window.innerWidth,
731
+ windowHeight: window.innerHeight,
732
+ },
733
+ selectors,
734
+ element: getElement(currentTarget),
735
+ });
736
+ }
737
+ });
738
+ // document.addEventListener('change', (event) => {
739
+ // const currentTarget = getParentElementWithEventOrId(event, 'onchange');
740
+ // window.saveEventForTest({
741
+ // type: 'change',
742
+ // target: generateXPathWithNearestParentId(currentTarget),
743
+ // time: new Date().toISOString(),
744
+ // value: event.target.value,
745
+ // selectors: getBestSelectors(currentTarget),
746
+ // element: getElement(currentTarget),
747
+ // });
748
+ // });
749
+ // document.addEventListener('submit', (event) => {
750
+ // event.preventDefault();
751
+ // const currentTarget = getParentElementWithEventOrId(event, 'onsubmit');
752
+ // const formData = new FormData(event.target);
753
+ // const entries = {};
754
+ // formData.forEach((value, key) => {
755
+ // entries[key] = value;
756
+ // });
757
+ // window.saveEventForTest({
758
+ // type: 'submit',
759
+ // target: generateXPathWithNearestParentId(currentTarget),
760
+ // time: new Date().toISOString(),
761
+ // value: entries,
762
+ // selectors: getBestSelectors(currentTarget),
763
+ // element: getElement(currentTarget),
764
+ // });
765
+ // });
766
+ window.addEventListener("popstate", () => {
767
+ window.saveEventForTest({
768
+ type: "popstate-url",
769
+ target: window.location.pathname,
770
+ time: new Date().toISOString(),
771
+ value: window.location.href,
772
+ });
773
+ });
774
+
775
+ // Also track URL changes via history API
776
+ const originalPushState = window.history.pushState;
777
+ window.history.pushState = function () {
778
+ originalPushState.apply(this, arguments);
779
+ window.saveEventForTest({
780
+ type: "pushstate-url",
781
+ target: window.location.pathname,
782
+ time: new Date().toISOString(),
783
+ value: window.location.href,
784
+ });
785
+ };
786
+
787
+ const originalReplaceState = window.history.replaceState;
788
+ window.history.replaceState = function () {
789
+ originalReplaceState.apply(this, arguments);
790
+ window.saveEventForTest({
791
+ type: "replacestate-url",
792
+ target: window.location.pathname,
793
+ time: new Date().toISOString(),
794
+ value: window.location.href,
795
+ });
796
+ };
797
+ });
798
+ console.log("injectEventRecordingScript completed");
799
+ } catch (error) {
800
+ console.error("Error injecting event recording script", {
801
+ error: error.message,
802
+ stack: error.stack,
803
+ });
804
+ }
805
+ };
806
+
807
+ module.exports = {
808
+ injectEventRecordingScript,
809
+ };
package/src/index.js CHANGED
@@ -21,6 +21,7 @@ const {
21
21
  recordPlaywrightRoutes,
22
22
  } = require("./playwright-utils");
23
23
  const { saveSnap, deleteAllSnaps } = require("./snap-utils");
24
+ const { injectEventRecordingScript } = require("./event-utils");
24
25
 
25
26
  // Export functions as a module
26
27
  module.exports = {
@@ -42,4 +43,5 @@ module.exports = {
42
43
  initiatePlaywrightRoutes,
43
44
  initiateJestEventSnaps,
44
45
  recordPlaywrightRoutes,
46
+ injectEventRecordingScript,
45
47
  };