ftmocks-utils 1.3.6 → 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.6",
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/file-utils.js CHANGED
@@ -2,8 +2,13 @@ const path = require("path");
2
2
  const fs = require("fs");
3
3
  const { getMockDir, nameToFolder } = require("./common-utils");
4
4
 
5
- const saveIfItIsFile = async (route, testName, ftmocksConifg) => {
6
- const urlObj = new URL(route.request().url());
5
+ const saveIfItIsFile = async (
6
+ currentRequest,
7
+ response,
8
+ testName,
9
+ ftmocksConifg
10
+ ) => {
11
+ const urlObj = new URL(currentRequest.url());
7
12
 
8
13
  // Check if URL contains file extension like .js, .png, .css etc
9
14
  const fileExtMatch = urlObj.pathname.match(/\.[a-zA-Z0-9]+$/);
@@ -11,7 +16,6 @@ const saveIfItIsFile = async (route, testName, ftmocksConifg) => {
11
16
  let fileExt = null;
12
17
  if (!fileExtMatch) {
13
18
  // Try to get extension from content-type header
14
- const response = await route.fetch();
15
19
  const contentType = response.headers()["content-type"];
16
20
  if (contentType) {
17
21
  // Map common mime types to extensions
@@ -59,7 +63,6 @@ const saveIfItIsFile = async (route, testName, ftmocksConifg) => {
59
63
  const fileName = `${id}${fileExt}`;
60
64
  const filePath = path.join(dirPath, fileName);
61
65
 
62
- const response = await route.fetch();
63
66
  const buffer = await response.body();
64
67
  fs.writeFileSync(filePath, buffer);
65
68
 
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
  };
@@ -234,8 +234,10 @@ async function recordPlaywrightRoutes(
234
234
  }
235
235
  ) {
236
236
  await page.route(config.mockPath, async (route) => {
237
+ const currentRequest = route.request();
238
+ let response = null;
237
239
  try {
238
- const urlObj = new URL(route.request().url());
240
+ const urlObj = new URL(currentRequest.url());
239
241
  if (config.pattern && config.pattern.length > 0) {
240
242
  const patternRegex = new RegExp(config.pattern);
241
243
  if (!patternRegex.test(urlObj.pathname)) {
@@ -249,8 +251,11 @@ async function recordPlaywrightRoutes(
249
251
  await createTest(ftmocksConifg, config.testName);
250
252
  }
251
253
 
254
+ response = await route.fetch();
255
+
252
256
  const fileName = await saveIfItIsFile(
253
- route,
257
+ currentRequest,
258
+ response,
254
259
  config.testName,
255
260
  ftmocksConifg
256
261
  );
@@ -258,10 +263,10 @@ async function recordPlaywrightRoutes(
258
263
  const mockData = {
259
264
  url: urlObj.pathname + urlObj.search,
260
265
  time: new Date().toString(),
261
- method: route.request().method(),
266
+ method: currentRequest.method(),
262
267
  request: {
263
268
  headers: excludeHeaders(
264
- await route.request().headers(),
269
+ await currentRequest.headers(),
265
270
  ftmocksConifg
266
271
  ),
267
272
  queryString: Array.from(urlObj.searchParams.entries()).map(
@@ -270,18 +275,18 @@ async function recordPlaywrightRoutes(
270
275
  value,
271
276
  })
272
277
  ),
273
- postData: route.request().postData()
278
+ postData: currentRequest.postData()
274
279
  ? {
275
280
  mimeType: "application/json",
276
- text: route.request().postData(),
281
+ text: currentRequest.postData(),
277
282
  }
278
283
  : null,
279
284
  },
280
285
  response: {
281
286
  file: fileName,
282
- status: (await route.fetch()).status(),
283
- headers: (await route.fetch()).headers(),
284
- content: fileName ? null : await (await route.fetch()).text(),
287
+ status: response.status(),
288
+ headers: response.headers(),
289
+ content: fileName ? null : await response.text(),
285
290
  },
286
291
  id: crypto.randomUUID(),
287
292
  served: false,
@@ -350,10 +355,22 @@ async function recordPlaywrightRoutes(
350
355
  `mock_${mockData.id}.json`
351
356
  );
352
357
  fs.writeFileSync(mocDataPath, JSON.stringify(mockData, null, 2));
353
- await route.continue();
358
+ await route.fulfill({
359
+ status: response.status(),
360
+ headers: response.headers(),
361
+ body: await response.body(),
362
+ });
354
363
  } catch (error) {
355
364
  console.error(error);
356
- await route.continue();
365
+ if (!response) {
366
+ await route.continue();
367
+ } else {
368
+ await route.fulfill({
369
+ status: response.status(),
370
+ headers: response.headers(),
371
+ body: await response.body(),
372
+ });
373
+ }
357
374
  }
358
375
  });
359
376
  }