ftmocks-utils 1.4.3 → 1.4.5

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.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "Util functions for FtMocks",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -4,68 +4,112 @@ const { getMockDir, nameToFolder } = require("./common-utils");
4
4
 
5
5
  const getLocator = async (page, event) => {
6
6
  // Check if the event.target exists on the page before returning it.
7
+ console.log("➡ Getting locator for event", event);
7
8
  if (event && event.target && typeof page !== "undefined" && page.locator) {
8
- const count = await page.locator(event.target).count();
9
- if (count === 1) {
10
- return event.target;
11
- } else {
12
- await page.waitForTimeout(1000);
13
- for (let i = 0; i < event.selectors.length; i++) {
14
- const count = await page.locator(event.selectors[i].value).count();
9
+ let locator = null;
10
+ while (!locator) {
11
+ const selector = event.target.startsWith("/")
12
+ ? `xpath=${event.target}`
13
+ : event.target;
14
+ try {
15
+ const count = await page.locator(selector).count();
15
16
  if (count === 1) {
16
- return event.selectors[i].value;
17
+ locator = selector;
18
+ } else {
19
+ for (let i = 0; i < event.selectors.length; i++) {
20
+ const selector = event.selectors[i].value.startsWith("/")
21
+ ? `xpath=${event.selectors[i].value}`
22
+ : event.selectors[i].value;
23
+ const count = await page.locator(selector).count();
24
+ if (count === 1) {
25
+ locator = selector;
26
+ }
27
+ }
17
28
  }
29
+ } catch (error) {
30
+ console.error("Error getting locator", error, selector);
18
31
  }
32
+ console.log("➡ Waiting for locator", event);
33
+ await page.waitForTimeout(500);
19
34
  }
35
+ return locator;
20
36
  }
21
37
  return event.target;
22
38
  };
23
39
 
24
- const runEvent = async (page, event, delay = 0) => {
40
+ const getSelectorPosition = async (page, selector) => {
41
+ const element = await page.locator(selector).elementHandle();
42
+ const position = await element.boundingBox();
43
+ console.log("position", position);
44
+ return position;
45
+ };
46
+
47
+ const runEvent = async ({
48
+ page,
49
+ event,
50
+ delay = 0,
51
+ screenshots = false,
52
+ screenshotsDir = null,
53
+ }) => {
25
54
  try {
26
55
  console.log("➡ Running event", event);
56
+ const beforeEvent = async () => {
57
+ await page.waitForTimeout(delay);
58
+ if (screenshots) {
59
+ const locator = await getLocator(page, event);
60
+ const position = await getSelectorPosition(page, locator);
61
+ event.screenshotInfo = {
62
+ name: `${event.id}.png`,
63
+ position,
64
+ };
65
+ await page.screenshot({
66
+ path: path.join(screenshotsDir, `${event.id}.png`),
67
+ fullPage: false,
68
+ });
69
+ }
70
+ };
27
71
  switch (event.type) {
28
72
  case "url":
29
73
  await page.goto(event.value);
30
74
  break;
31
75
  case "click":
32
- await page.waitForTimeout(delay);
76
+ await beforeEvent();
33
77
  await page.click(await getLocator(page, event));
34
78
  break;
35
79
  case "input":
36
- await page.waitForTimeout(delay);
80
+ await beforeEvent();
37
81
  await page.fill(await getLocator(page, event), event.value);
38
82
  break;
39
83
  case "keypress":
40
- await page.waitForTimeout(delay);
84
+ await beforeEvent();
41
85
  await page.keyboard.press(await getLocator(page, event), event.key);
42
86
  break;
43
87
  case "change":
44
- await page.waitForTimeout(delay);
88
+ await beforeEvent();
45
89
  await page.select(await getLocator(page, event), event.value);
46
90
  break;
47
91
  case "url":
48
- await page.waitForTimeout(delay);
92
+ await beforeEvent();
49
93
  await page.goto(await getLocator(page, event), event.value);
50
94
  break;
51
95
  case "dblclick":
52
- await page.waitForTimeout(delay);
96
+ await beforeEvent();
53
97
  await page.dblclick(await getLocator(page, event));
54
98
  break;
55
99
  case "contextmenu":
56
- await page.waitForTimeout(delay);
100
+ await beforeEvent();
57
101
  await page.contextmenu(await getLocator(page, event));
58
102
  break;
59
103
  case "hover":
60
- await page.waitForTimeout(delay);
104
+ await beforeEvent();
61
105
  await page.hover(await getLocator(page, event));
62
106
  break;
63
107
  case "keydown":
64
- await page.waitForTimeout(delay);
108
+ await beforeEvent();
65
109
  await page.keyboard.down(await getLocator(page, event), event.key);
66
110
  break;
67
111
  case "keyup":
68
- await page.waitForTimeout(delay);
112
+ await beforeEvent();
69
113
  await page.keyboard.up(await getLocator(page, event), event.key);
70
114
  break;
71
115
  default:
@@ -83,8 +127,6 @@ const isValidEvent = (event) => {
83
127
  try {
84
128
  console.log("➡ Validating event", event);
85
129
  switch (event?.type) {
86
- case "url":
87
- return true;
88
130
  case "click":
89
131
  return true;
90
132
  case "input":
@@ -115,9 +157,15 @@ const isValidEvent = (event) => {
115
157
  return false;
116
158
  };
117
159
 
118
- const runEvents = async (page, events, delay = 1000) => {
160
+ const runEvents = async ({
161
+ page,
162
+ events,
163
+ delay = 1000,
164
+ screenshots = false,
165
+ screenshotsDir = null,
166
+ }) => {
119
167
  for (const event of events) {
120
- await runEvent(page, event, delay);
168
+ await runEvent({ page, event, delay, screenshots, screenshotsDir });
121
169
  }
122
170
  };
123
171
 
@@ -128,14 +176,12 @@ const runEventsForTest = async (page, ftmocksConifg, testName) => {
128
176
  `_events.json`
129
177
  );
130
178
  const events = JSON.parse(fs.readFileSync(eventsFile, "utf8"));
131
- await runEvents(page, events, ftmocksConifg.delay || 1000);
132
- };
133
-
134
- const getSelectorPosition = async (page, selector) => {
135
- const element = await page.locator(selector).elementHandle();
136
- const position = await element.boundingBox();
137
- console.log("position", position);
138
- return position;
179
+ await runEvents({
180
+ page,
181
+ events,
182
+ delay: ftmocksConifg.delay || 1000,
183
+ screenshots: false,
184
+ });
139
185
  };
140
186
 
141
187
  const runEventsInPresentationMode = async (page, ftmocksConifg, testName) => {
@@ -152,20 +198,25 @@ const runEventsInPresentationMode = async (page, ftmocksConifg, testName) => {
152
198
  console.log("➡ Next event triggered!");
153
199
  if (currentEventIndex === events.length) {
154
200
  console.log("➡ No more events to run!");
155
- return;
201
+ return false;
156
202
  }
157
- let result = await runEvent(page, events[currentEventIndex]);
203
+ let result = await runEvent({ page, event: events[currentEventIndex] });
158
204
  while (result === "Unsupported event type") {
159
205
  currentEventIndex = currentEventIndex + 1;
160
- result = await runEvent(page, events[currentEventIndex]);
206
+ result = await runEvent({ page, event: events[currentEventIndex] });
161
207
  }
162
208
  currentEventIndex = currentEventIndex + 1;
209
+ return true;
163
210
  });
164
211
 
165
212
  await page.exposeFunction("focusOnBodyForPresentationMode", async () => {
166
213
  await page.bringToFront();
167
214
  });
168
215
 
216
+ await page.exposeFunction("playwrightPageClose", async () => {
217
+ await page.close();
218
+ });
219
+
169
220
  // Inject keyboard listener into browser
170
221
  await page.addInitScript(() => {
171
222
  window.addEventListener("load", async () => {
@@ -174,20 +225,24 @@ const runEventsInPresentationMode = async (page, ftmocksConifg, testName) => {
174
225
  window.focus();
175
226
  document.body.focus();
176
227
  });
177
- window.addEventListener("keydown", (e) => {
228
+ window.addEventListener("keydown", async (e) => {
178
229
  console.log("➡ keydown event triggered!", e);
179
230
  if (e.key === "Shift" && !e.repeat) {
180
231
  e.preventDefault();
181
- window.nextEvent();
232
+ const result = await window.nextEvent();
233
+ if (!result) {
234
+ console.log("➡ No more events to run!");
235
+ await window.playwrightPageClose();
236
+ }
182
237
  }
183
238
  });
184
239
  });
185
240
 
186
- await runEvent(page, events[0]);
241
+ await runEvent({ page, event: events[0] });
187
242
  };
188
243
 
189
244
  const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
190
- let currentEventIndex = 0;
245
+ const executedEvents = [];
191
246
  const eventsFile = path.join(
192
247
  getMockDir(ftmocksConifg),
193
248
  `test_${nameToFolder(testName)}`,
@@ -198,20 +253,31 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
198
253
  // Expose Node function
199
254
  await page.exposeFunction("getNextEvent", async () => {
200
255
  let result = false;
256
+ let nonExecutedEvents = events.filter(
257
+ (event) => !executedEvents.includes(event?.id)
258
+ );
259
+ let currentEventIndex = -1;
201
260
  while (!result) {
202
261
  currentEventIndex = currentEventIndex + 1;
203
- if (currentEventIndex === events.length) {
262
+ if (currentEventIndex === nonExecutedEvents.length) {
204
263
  console.log("➡ No more events to validate!");
205
264
  return;
206
265
  }
207
- result = isValidEvent(events[currentEventIndex]);
266
+ result = isValidEvent(nonExecutedEvents[currentEventIndex]);
208
267
  }
209
- if (events[currentEventIndex]) {
210
- const selector = await getLocator(page, events[currentEventIndex]);
268
+ if (nonExecutedEvents[currentEventIndex]) {
269
+ console.log(
270
+ "➡ Getting locator for event",
271
+ nonExecutedEvents[currentEventIndex]
272
+ );
273
+ const selector = await getLocator(
274
+ page,
275
+ nonExecutedEvents[currentEventIndex]
276
+ );
211
277
  const position = await getSelectorPosition(page, selector);
212
278
  const element = await page.locator(selector).elementHandle();
213
279
  return {
214
- event: events[currentEventIndex],
280
+ event: nonExecutedEvents[currentEventIndex],
215
281
  selector,
216
282
  position,
217
283
  element,
@@ -220,6 +286,14 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
220
286
  return null;
221
287
  });
222
288
 
289
+ await page.exposeFunction("addExecutedEvent", (eventId) => {
290
+ executedEvents.push(eventId);
291
+ });
292
+
293
+ await page.exposeFunction("playwrightPageClose", async () => {
294
+ await page.close();
295
+ });
296
+
223
297
  // Inject keyboard listener into browser
224
298
  await page.addInitScript(async () => {
225
299
  let currentEventInfo = null;
@@ -243,6 +317,44 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
243
317
  popover.style.borderRadius = "8px";
244
318
  popover.style.boxShadow = "0 2px 12px rgba(0,0,0,0.25)";
245
319
 
320
+ // Success Training Popover
321
+ const successPopover = document.createElement("div");
322
+ successPopover.id = "ftmocks-success-popover-training-mode";
323
+ successPopover.style.position = "fixed";
324
+ successPopover.style.top = "32px";
325
+ successPopover.style.left = "50%";
326
+ successPopover.style.transform = "translateX(-50%)";
327
+ successPopover.style.minWidth = "120px";
328
+ successPopover.style.height = "46px";
329
+ successPopover.style.background = "rgba(60,180,75,0.98)";
330
+ successPopover.style.color = "#fff";
331
+ successPopover.style.display = "none";
332
+ successPopover.style.zIndex = "100000";
333
+ successPopover.style.fontFamily = "sans-serif";
334
+ successPopover.style.fontSize = "16px";
335
+ successPopover.style.textAlign = "center";
336
+ successPopover.style.lineHeight = "1.4";
337
+ successPopover.style.padding = "12px 32px";
338
+ successPopover.style.borderRadius = "8px";
339
+ successPopover.style.boxShadow = "0 2px 12px rgba(0,0,0,0.20)";
340
+ successPopover.textContent = "✅ Training Success!";
341
+
342
+ // Utility to show the success popover for a short period
343
+ function showSuccessPopover(message = "✅ Training Success!") {
344
+ successPopover.textContent = message;
345
+ if (!document.getElementById("ftmocks-success-popover-training-mode")) {
346
+ document.body.appendChild(successPopover);
347
+ }
348
+ successPopover.style.display = "block";
349
+ setTimeout(() => {
350
+ successPopover.style.display = "none";
351
+ document
352
+ .getElementById("ftmocks-success-popover-training-mode")
353
+ ?.remove();
354
+ window.playwrightPageClose();
355
+ }, 3000);
356
+ }
357
+
246
358
  const highlighter = document.createElement("div");
247
359
  highlighter.id = "ftmocks-highlighter-training-mode";
248
360
  highlighter.style.position = "absolute";
@@ -254,8 +366,11 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
254
366
  highlighter.style.border = "2px solid #3fa9f5";
255
367
  highlighter.style.display = "none";
256
368
  highlighter.style.pointerEvents = "none";
369
+ highlighter.style.zIndex = "99999";
257
370
 
258
371
  function showPopover(eventInfo) {
372
+ console.log("➡ Showing popover", eventInfo);
373
+ window.addExecutedEvent(eventInfo.event.id);
259
374
  if (!document.getElementById("ftmocks-popover-training-mode")) {
260
375
  document.body.appendChild(popover);
261
376
  }
@@ -279,6 +394,10 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
279
394
 
280
395
  function hidePopover() {
281
396
  popover.style.display = "none";
397
+ highlighter.style.display = "none";
398
+ document.getElementById("ftmocks-popover-training-mode")?.remove();
399
+ document.getElementById("ftmocks-highlighter-training-mode")?.remove();
400
+ showSuccessPopover("✅ Training Success!");
282
401
  }
283
402
 
284
403
  const initialEventRun = async () => {
@@ -288,16 +407,28 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
288
407
  }
289
408
  };
290
409
 
410
+ const showNextEvent = async () => {
411
+ currentEventInfo = await window.getNextEvent();
412
+ if (currentEventInfo) {
413
+ showPopover(currentEventInfo);
414
+ } else {
415
+ hidePopover();
416
+ }
417
+ };
418
+
291
419
  const matchElement = (event, currentEventInfo) => {
292
- console.log(
293
- "➡ Matching element!",
294
- event.target.isEqualNode(currentEventInfo?.element),
295
- currentEventInfo?.element?.contains(event.target)
296
- );
297
- return (
298
- event.target.isEqualNode(currentEventInfo?.element) ||
299
- currentEventInfo?.element?.contains(event.target)
300
- );
420
+ const inBoudingBox =
421
+ currentEventInfo?.position?.x <= event.clientX &&
422
+ currentEventInfo?.position?.x + currentEventInfo?.position?.width >=
423
+ event.clientX &&
424
+ currentEventInfo?.position?.y <= event.clientY &&
425
+ currentEventInfo?.position?.y + currentEventInfo?.position?.height >=
426
+ event.clientY;
427
+ console.log("➡ In bounding box?", inBoudingBox);
428
+ const matchingElement =
429
+ currentEventInfo?.element?.isEqualNode(event.target) ||
430
+ currentEventInfo?.element?.contains(event.target);
431
+ return inBoudingBox || matchingElement;
301
432
  };
302
433
 
303
434
  window.addEventListener("load", async () => {
@@ -305,21 +436,12 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
305
436
  });
306
437
 
307
438
  window.addEventListener("click", async (event) => {
308
- console.log(
309
- "➡ Click event triggered!",
310
- event.target.isEqualNode(currentEventInfo?.element),
311
- currentEventInfo?.element?.contains(event.target)
312
- );
313
439
  if (
314
440
  currentEventInfo?.event?.type === "click" &&
315
441
  matchElement(event, currentEventInfo)
316
442
  ) {
317
- currentEventInfo = await window.getNextEvent();
318
- if (currentEventInfo) {
319
- showPopover(currentEventInfo);
320
- } else {
321
- hidePopover();
322
- }
443
+ console.log("➡ Click event triggered!", event);
444
+ showNextEvent();
323
445
  }
324
446
  });
325
447
  window.addEventListener("dblclick", async (event) => {
@@ -327,12 +449,7 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
327
449
  currentEventInfo?.event?.type === "dblclick" &&
328
450
  matchElement(event, currentEventInfo)
329
451
  ) {
330
- currentEventInfo = await window.getNextEvent();
331
- if (currentEventInfo) {
332
- showPopover(currentEventInfo);
333
- } else {
334
- hidePopover();
335
- }
452
+ showNextEvent();
336
453
  }
337
454
  });
338
455
  window.addEventListener("contextmenu", async (event) => {
@@ -340,12 +457,7 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
340
457
  currentEventInfo?.event?.type === "contextmenu" &&
341
458
  matchElement(event, currentEventInfo)
342
459
  ) {
343
- currentEventInfo = await window.getNextEvent();
344
- if (currentEventInfo) {
345
- showPopover(currentEventInfo);
346
- } else {
347
- hidePopover();
348
- }
460
+ showNextEvent();
349
461
  }
350
462
  });
351
463
  window.addEventListener("input", async (event) => {
@@ -353,10 +465,7 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
353
465
  currentEventInfo?.event?.type === "input" &&
354
466
  matchElement(event, currentEventInfo)
355
467
  ) {
356
- currentEventInfo = await window.getNextEvent();
357
- if (currentEventInfo) {
358
- showPopover(currentEventInfo);
359
- }
468
+ showNextEvent();
360
469
  }
361
470
  });
362
471
  window.addEventListener("keypress", async (event) => {
@@ -364,15 +473,35 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
364
473
  currentEventInfo?.event?.type === "keypress" &&
365
474
  matchElement(event, currentEventInfo)
366
475
  ) {
367
- currentEventInfo = await window.getNextEvent();
368
- if (currentEventInfo) {
369
- showPopover(currentEventInfo);
370
- }
476
+ showNextEvent();
371
477
  }
372
478
  });
373
479
  });
374
480
 
375
- await runEvent(page, events[0]);
481
+ await runEvent({ page, event: events[0] });
482
+ };
483
+
484
+ const runEventsForScreenshots = async (page, ftmocksConifg, testName) => {
485
+ const eventsFile = path.join(
486
+ getMockDir(ftmocksConifg),
487
+ `test_${nameToFolder(testName)}`,
488
+ `_events.json`
489
+ );
490
+ const events = JSON.parse(fs.readFileSync(eventsFile, "utf8"));
491
+ await runEvents({
492
+ page,
493
+ events,
494
+ delay: ftmocksConifg.delay || 1000,
495
+ screenshots: true,
496
+ screenshotsDir: path.join(
497
+ getMockDir(ftmocksConifg),
498
+ `test_${nameToFolder(testName)}`,
499
+ `screenshots`
500
+ ),
501
+ });
502
+ fs.writeFileSync(eventsFile, JSON.stringify(events, null, 2));
503
+ await page.waitForTimeout(1000);
504
+ await page.close();
376
505
  };
377
506
 
378
507
  module.exports = {
@@ -380,5 +509,6 @@ module.exports = {
380
509
  runEventsForTest,
381
510
  runEventsInPresentationMode,
382
511
  runEventsInTrainingMode,
512
+ runEventsForScreenshots,
383
513
  runEvent,
384
514
  };
package/src/index.js CHANGED
@@ -27,6 +27,7 @@ const {
27
27
  runEvent,
28
28
  runEventsInPresentationMode,
29
29
  runEventsInTrainingMode,
30
+ runEventsForScreenshots,
30
31
  } = require("./event-run-utils");
31
32
 
32
33
  // Export functions as a module
@@ -54,4 +55,5 @@ module.exports = {
54
55
  runEvent,
55
56
  runEventsInPresentationMode,
56
57
  runEventsInTrainingMode,
58
+ runEventsForScreenshots,
57
59
  };