ftmocks-utils 1.3.7 → 1.3.9

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