ftmocks-utils 1.5.4 → 1.5.6

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.5.4",
3
+ "version": "1.5.6",
4
4
  "description": "Util functions for FtMocks",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -1,6 +1,70 @@
1
1
  const { clearNulls, processURL } = require("./common-utils");
2
2
  const { FtJSON } = require("./json-utils");
3
3
 
4
+ /** @param {Record<string, string>|undefined|null} headers */
5
+ function getHeaderValueCaseInsensitive(headers, headerName) {
6
+ if (!headers || !headerName) {
7
+ return undefined;
8
+ }
9
+ const lower = headerName.toLowerCase();
10
+ for (const key of Object.keys(headers)) {
11
+ if (key.toLowerCase() === lower) {
12
+ return headers[key];
13
+ }
14
+ }
15
+ return undefined;
16
+ }
17
+
18
+ function parseMatchHeadersList(testConfig) {
19
+ const raw = testConfig?.MATCH_HEADERS;
20
+ if (!raw || typeof raw !== "string") {
21
+ return [];
22
+ }
23
+ return raw
24
+ .split(",")
25
+ .map((h) => h.trim())
26
+ .filter(Boolean);
27
+ }
28
+
29
+ /** Normalize fetch `options.headers` (plain object or Headers) to a plain object. */
30
+ function normalizeIncomingHeaders(headers) {
31
+ if (!headers) {
32
+ return {};
33
+ }
34
+ if (typeof headers.forEach === "function") {
35
+ const out = {};
36
+ headers.forEach((value, key) => {
37
+ out[key] = value;
38
+ });
39
+ return out;
40
+ }
41
+ if (typeof headers === "object") {
42
+ return { ...headers };
43
+ }
44
+ return {};
45
+ }
46
+
47
+ /**
48
+ * When MATCH_HEADERS is set, each listed header must match between the mock
49
+ * recording and the incoming request (case-insensitive name, value compared as strings).
50
+ */
51
+ function incomingHeadersMatchMock(mock, incomingHeaders, testConfig) {
52
+ const names = parseMatchHeadersList(testConfig);
53
+ if (names.length === 0) {
54
+ return true;
55
+ }
56
+ const mockHeaders = mock.fileContent.request?.headers || {};
57
+ const incoming = incomingHeaders || {};
58
+ for (const name of names) {
59
+ const mockVal = getHeaderValueCaseInsensitive(mockHeaders, name);
60
+ const reqVal = getHeaderValueCaseInsensitive(incoming, name);
61
+ if (String(mockVal ?? "") !== String(reqVal ?? "")) {
62
+ return false;
63
+ }
64
+ }
65
+ return true;
66
+ }
67
+
4
68
  const isUrlAndMethodSame = (req1, req2) => {
5
69
  const url1 = new URL(`http://domain.com${req1.url}`);
6
70
  const url2 = new URL(`http://domain.com${req2.url}`);
@@ -45,7 +109,7 @@ const isSameResponse = (req1, req2) => {
45
109
  ) {
46
110
  matched = FtJSON.areJsonEqual(
47
111
  FtJSON.parse(req1.response.content) || {},
48
- FtJSON.parse(req2.response.content) || {}
112
+ FtJSON.parse(req2.response.content) || {},
49
113
  );
50
114
  // console.log('not matched at post Data 0', req1.postData, req2.postData);
51
115
  } else if (
@@ -53,7 +117,7 @@ const isSameResponse = (req1, req2) => {
53
117
  req2.response.content &&
54
118
  !FtJSON.areJsonEqual(
55
119
  FtJSON.parse(req1.response.content) || {},
56
- FtJSON.parse(req2.response.content) || {}
120
+ FtJSON.parse(req2.response.content) || {},
57
121
  )
58
122
  ) {
59
123
  matched = false;
@@ -71,12 +135,12 @@ const isSameResponse = (req1, req2) => {
71
135
  function compareMockToRequest(mock, req) {
72
136
  const mockURL = processURL(
73
137
  mock.fileContent.url,
74
- mock.fileContent.ignoreParams
138
+ mock.fileContent.ignoreParams,
75
139
  );
76
140
  const reqURL = processURL(req.originalUrl, mock.fileContent.ignoreParams);
77
141
  const isSameUrlAndMethod = isUrlAndMethodSame(
78
142
  { url: mockURL, method: mock.fileContent.method },
79
- { url: reqURL, method: req.method }
143
+ { url: reqURL, method: req.method },
80
144
  );
81
145
  if (!isSameUrlAndMethod) {
82
146
  return false;
@@ -90,20 +154,20 @@ function compareMockToRequest(mock, req) {
90
154
  method: req.method,
91
155
  postData: req.body,
92
156
  url: reqURL,
93
- }
157
+ },
94
158
  );
95
159
  }
96
160
 
97
- function compareMockToFetchRequest(mock, fetchReq) {
161
+ function compareMockToFetchRequest(mock, fetchReq, testConfig) {
98
162
  try {
99
163
  const mockURL = processURL(
100
164
  mock.fileContent.url,
101
- mock.fileContent.ignoreParams
165
+ mock.fileContent.ignoreParams,
102
166
  );
103
167
  const reqURL = processURL(fetchReq.url, mock.fileContent.ignoreParams);
104
168
  const isSameUrlAndMethod = isUrlAndMethodSame(
105
169
  { url: mockURL, method: mock.fileContent.method },
106
- { url: reqURL, method: fetchReq.options.method || "GET" }
170
+ { url: reqURL, method: fetchReq.options.method || "GET" },
107
171
  );
108
172
  if (!isSameUrlAndMethod) {
109
173
  return false;
@@ -111,7 +175,7 @@ function compareMockToFetchRequest(mock, fetchReq) {
111
175
  const postData = mock.fileContent.request?.postData?.text
112
176
  ? FtJSON.parse(mock.fileContent.request?.postData?.text)
113
177
  : mock.fileContent.request?.postData;
114
- return isSameRequest(
178
+ const sameBody = isSameRequest(
115
179
  { url: mockURL, method: mock.fileContent.method, postData },
116
180
  {
117
181
  method: fetchReq.options.method || "GET",
@@ -119,8 +183,13 @@ function compareMockToFetchRequest(mock, fetchReq) {
119
183
  ? FtJSON.parse(fetchReq.options.body)
120
184
  : fetchReq.options.body,
121
185
  url: reqURL,
122
- }
186
+ },
123
187
  );
188
+ if (!sameBody) {
189
+ return false;
190
+ }
191
+ const incomingHeaders = normalizeIncomingHeaders(fetchReq.options?.headers);
192
+ return incomingHeadersMatchMock(mock, incomingHeaders, testConfig);
124
193
  } catch (e) {
125
194
  console.error("error at compareMockToFetchRequest", mock, fetchReq);
126
195
  console.error(e);
@@ -148,4 +217,8 @@ module.exports = {
148
217
  compareMockToRequest,
149
218
  compareMockToFetchRequest,
150
219
  compareMockToMock,
220
+ getHeaderValueCaseInsensitive,
221
+ parseMatchHeadersList,
222
+ normalizeIncomingHeaders,
223
+ incomingHeadersMatchMock,
151
224
  };
@@ -20,7 +20,7 @@ const createDiffImage = async (img1, img2, diffPath) => {
20
20
  threshold: 0.1, // sensitivity
21
21
  diffColor: [255, 0, 0], // highlight color (red)
22
22
  diffMask: false,
23
- }
23
+ },
24
24
  );
25
25
 
26
26
  fs.writeFileSync(diffPath, PNG.sync.write(diff));
@@ -58,7 +58,7 @@ const matchAndReplaceScreenshot = async (page, event, screenshotsDir) => {
58
58
  await createDiffImage(
59
59
  img1,
60
60
  img2,
61
- path.join(screenshotsDir, `diff_${event.id}.png`)
61
+ path.join(screenshotsDir, `diff_${event.id}.png`),
62
62
  );
63
63
  return {
64
64
  replaced: true,
@@ -170,6 +170,8 @@ const runEvent = async ({
170
170
  screenshots = false,
171
171
  screenshotsDir = null,
172
172
  healSelectors = false,
173
+ eventsFile = null,
174
+ allEvents = null,
173
175
  }) => {
174
176
  try {
175
177
  const beforeEvent = async () => {
@@ -178,7 +180,7 @@ const runEvent = async ({
178
180
  const res = await matchAndReplaceScreenshot(
179
181
  page,
180
182
  event,
181
- screenshotsDir
183
+ screenshotsDir,
182
184
  );
183
185
  if (res.replaced) {
184
186
  const locator = await getLocator(page, event);
@@ -200,7 +202,7 @@ const runEvent = async ({
200
202
  page,
201
203
  event,
202
204
  event.target,
203
- position
205
+ position,
204
206
  );
205
207
  event.target = healedTarget.value;
206
208
  const selectors = [];
@@ -210,7 +212,7 @@ const runEvent = async ({
210
212
  page,
211
213
  event,
212
214
  event.selectors[i].value,
213
- position
215
+ position,
214
216
  );
215
217
  event.selectors[i].value = healedSelector.value;
216
218
  if (healedSelector.count !== 1) {
@@ -273,6 +275,10 @@ const runEvent = async ({
273
275
  default:
274
276
  return "Unsupported event type";
275
277
  }
278
+ if (eventsFile && allEvents) {
279
+ allEvents.find((e) => e.id === event.id).executed = true;
280
+ fs.writeFileSync(eventsFile, JSON.stringify(allEvents, null, 2));
281
+ }
276
282
  } catch (error) {
277
283
  console.error("Error running event", {
278
284
  error: error.message,
@@ -321,6 +327,8 @@ const runEvents = async ({
321
327
  screenshots = false,
322
328
  screenshotsDir = null,
323
329
  healSelectors = false,
330
+ eventsFile = null,
331
+ allEvents = null,
324
332
  }) => {
325
333
  for (const event of events) {
326
334
  await runEvent({
@@ -330,6 +338,8 @@ const runEvents = async ({
330
338
  screenshots,
331
339
  screenshotsDir,
332
340
  healSelectors,
341
+ eventsFile,
342
+ allEvents,
333
343
  });
334
344
  }
335
345
  };
@@ -338,14 +348,19 @@ const runEventsForTest = async (page, ftmocksConifg, testName) => {
338
348
  const eventsFile = path.join(
339
349
  getMockDir(ftmocksConifg),
340
350
  `test_${nameToFolder(testName)}`,
341
- `_events.json`
351
+ `_events.json`,
342
352
  );
343
353
  const events = JSON.parse(fs.readFileSync(eventsFile, "utf8"));
354
+ events.forEach((event) => {
355
+ event.executed = false;
356
+ });
344
357
  await runEvents({
345
358
  page,
346
359
  events,
347
360
  delay: ftmocksConifg.delay || 1000,
348
361
  screenshots: false,
362
+ eventsFile,
363
+ allEvents: events,
349
364
  });
350
365
  };
351
366
 
@@ -354,19 +369,32 @@ const runEventsInPresentationMode = async (page, ftmocksConifg, testName) => {
354
369
  const eventsFile = path.join(
355
370
  getMockDir(ftmocksConifg),
356
371
  `test_${nameToFolder(testName)}`,
357
- `_events.json`
372
+ `_events.json`,
358
373
  );
359
374
  const events = JSON.parse(fs.readFileSync(eventsFile, "utf8"));
375
+ events.forEach((event) => {
376
+ event.executed = false;
377
+ });
360
378
 
361
379
  // Expose Node function
362
380
  await page.exposeFunction("nextEvent", async () => {
363
381
  if (currentEventIndex === events.length) {
364
382
  return false;
365
383
  }
366
- let result = await runEvent({ page, event: events[currentEventIndex] });
384
+ let result = await runEvent({
385
+ page,
386
+ event: events[currentEventIndex],
387
+ eventsFile,
388
+ allEvents: events,
389
+ });
367
390
  while (result === "Unsupported event type") {
368
391
  currentEventIndex = currentEventIndex + 1;
369
- result = await runEvent({ page, event: events[currentEventIndex] });
392
+ result = await runEvent({
393
+ page,
394
+ event: events[currentEventIndex],
395
+ eventsFile,
396
+ allEvents: events,
397
+ });
370
398
  }
371
399
  currentEventIndex = currentEventIndex + 1;
372
400
  return true;
@@ -401,7 +429,7 @@ const runEventsInPresentationMode = async (page, ftmocksConifg, testName) => {
401
429
  });
402
430
  });
403
431
 
404
- await runEvent({ page, event: events[0] });
432
+ await runEvent({ page, event: events[0], eventsFile, allEvents: events });
405
433
  };
406
434
 
407
435
  const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
@@ -409,15 +437,17 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
409
437
  const eventsFile = path.join(
410
438
  getMockDir(ftmocksConifg),
411
439
  `test_${nameToFolder(testName)}`,
412
- `_events.json`
440
+ `_events.json`,
413
441
  );
414
442
  const events = JSON.parse(fs.readFileSync(eventsFile, "utf8"));
415
-
443
+ events.forEach((event) => {
444
+ event.executed = false;
445
+ });
416
446
  // Expose Node function
417
447
  await page.exposeFunction("getNextEvent", async () => {
418
448
  let result = false;
419
449
  let nonExecutedEvents = events.filter(
420
- (event) => !executedEvents.includes(event?.id)
450
+ (event) => !executedEvents.includes(event?.id),
421
451
  );
422
452
  let currentEventIndex = -1;
423
453
  while (!result) {
@@ -431,11 +461,11 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
431
461
  if (nonExecutedEvents[currentEventIndex]) {
432
462
  console.log(
433
463
  "➡ Getting locator for event",
434
- nonExecutedEvents[currentEventIndex]
464
+ nonExecutedEvents[currentEventIndex],
435
465
  );
436
466
  const selector = await getLocator(
437
467
  page,
438
- nonExecutedEvents[currentEventIndex]
468
+ nonExecutedEvents[currentEventIndex],
439
469
  );
440
470
  const position = await getSelectorPosition(page, selector);
441
471
  const element = await page.locator(selector).elementHandle();
@@ -641,16 +671,19 @@ const runEventsInTrainingMode = async (page, ftmocksConifg, testName) => {
641
671
  });
642
672
  });
643
673
 
644
- await runEvent({ page, event: events[0] });
674
+ await runEvent({ page, event: events[0], eventsFile, allEvents: events });
645
675
  };
646
676
 
647
677
  const runEventsForScreenshots = async (page, ftmocksConifg, testName) => {
648
678
  const eventsFile = path.join(
649
679
  getMockDir(ftmocksConifg),
650
680
  `test_${nameToFolder(testName)}`,
651
- `_events.json`
681
+ `_events.json`,
652
682
  );
653
683
  const events = JSON.parse(fs.readFileSync(eventsFile, "utf8"));
684
+ events.forEach((event) => {
685
+ event.executed = false;
686
+ });
654
687
  await runEvents({
655
688
  page,
656
689
  events,
@@ -659,8 +692,10 @@ const runEventsForScreenshots = async (page, ftmocksConifg, testName) => {
659
692
  screenshotsDir: path.join(
660
693
  getMockDir(ftmocksConifg),
661
694
  `test_${nameToFolder(testName)}`,
662
- `screenshots`
695
+ `screenshots`,
663
696
  ),
697
+ eventsFile,
698
+ allEvents: events,
664
699
  });
665
700
  fs.writeFileSync(eventsFile, JSON.stringify(events, null, 2));
666
701
  await page.waitForTimeout(1000);
@@ -671,14 +706,19 @@ const runEventsForHealingSelectors = async (page, ftmocksConifg, testName) => {
671
706
  const eventsFile = path.join(
672
707
  getMockDir(ftmocksConifg),
673
708
  `test_${nameToFolder(testName)}`,
674
- `_events.json`
709
+ `_events.json`,
675
710
  );
676
711
  const events = JSON.parse(fs.readFileSync(eventsFile, "utf8"));
712
+ events.forEach((event) => {
713
+ event.executed = false;
714
+ });
677
715
  await runEvents({
678
716
  page,
679
717
  events,
680
718
  delay: ftmocksConifg.delay || 1000,
681
719
  healSelectors: true,
720
+ eventsFile,
721
+ allEvents: events,
682
722
  });
683
723
  fs.writeFileSync(eventsFile, JSON.stringify(events, null, 2));
684
724
  await page.waitForTimeout(1000);
@@ -38,7 +38,7 @@ function getMatchingMockData({
38
38
  }
39
39
  }
40
40
  served = mock.fileContent.served;
41
- return compareMockToFetchRequest(mock, { url, options });
41
+ return compareMockToFetchRequest(mock, { url, options }, testConfig);
42
42
  }) || [];
43
43
  let foundMock = matchedMocks.find((mock) => !mock.fileContent.served)
44
44
  ? matchedMocks.find((mock) => !mock.fileContent.served)
@@ -46,29 +46,41 @@ function getMatchingMockData({
46
46
 
47
47
  if (!foundMock) {
48
48
  foundMock = defaultMockData.find((tm) =>
49
- compareMockToFetchRequest(tm, {
50
- url,
51
- options,
52
- })
49
+ compareMockToFetchRequest(
50
+ tm,
51
+ {
52
+ url,
53
+ options,
54
+ },
55
+ testConfig
56
+ )
53
57
  );
54
58
  }
55
59
 
56
60
  if (!foundMock && mode !== "strict") {
57
61
  const mockRanks = {};
58
62
  testMockData.forEach((tm) => {
59
- const rank = getCompareRankMockToFetchRequest(tm, {
60
- url,
61
- options,
62
- });
63
+ const rank = getCompareRankMockToFetchRequest(
64
+ tm,
65
+ {
66
+ url,
67
+ options,
68
+ },
69
+ testConfig
70
+ );
63
71
  if (rank > 0) {
64
72
  mockRanks[tm.id] = rank;
65
73
  }
66
74
  });
67
75
  defaultMockData.forEach((tm) => {
68
- const rank = getCompareRankMockToFetchRequest(tm, {
69
- url,
70
- options,
71
- });
76
+ const rank = getCompareRankMockToFetchRequest(
77
+ tm,
78
+ {
79
+ url,
80
+ options,
81
+ },
82
+ testConfig
83
+ );
72
84
  if (rank > 0) {
73
85
  mockRanks[tm.id] = rank;
74
86
  }
@@ -157,20 +169,28 @@ function getMatchingMockDataV2({
157
169
  const mockRanks = {};
158
170
  matchedMocks.forEach((mockId) => {
159
171
  const mock = testMockIdMap[mockId];
160
- const rank = getCompareRankMockToFetchRequest(mock, {
161
- url,
162
- options,
163
- });
172
+ const rank = getCompareRankMockToFetchRequest(
173
+ mock,
174
+ {
175
+ url,
176
+ options,
177
+ },
178
+ testConfig
179
+ );
164
180
  if (rank > 0) {
165
181
  mockRanks[mock.id] = rank;
166
182
  }
167
183
  });
168
184
  defaultMatchedMocks.forEach((mockId) => {
169
185
  const mock = defaultMockIdMap[mockId];
170
- const rank = getCompareRankMockToFetchRequest(mock, {
171
- url,
172
- options,
173
- });
186
+ const rank = getCompareRankMockToFetchRequest(
187
+ mock,
188
+ {
189
+ url,
190
+ options,
191
+ },
192
+ testConfig
193
+ );
174
194
  if (rank > 0) {
175
195
  mockRanks[mock.id] = rank;
176
196
  }
@@ -46,6 +46,7 @@ async function initiatePlaywrightRoutes(
46
46
  url,
47
47
  method: request.method(),
48
48
  body: request.postData(),
49
+ headers: request.headers(),
49
50
  };
50
51
  if (excludeMockPath && new RegExp(excludeMockPath).test(url)) {
51
52
  await route.fallback();
@@ -1,6 +1,10 @@
1
1
  const { clearNulls, charDifference, processURL } = require("./common-utils");
2
2
  const { FtJSON } = require("./json-utils");
3
- const { isUrlAndMethodSame } = require("./compare-utils");
3
+ const {
4
+ isUrlAndMethodSame,
5
+ incomingHeadersMatchMock,
6
+ normalizeIncomingHeaders,
7
+ } = require("./compare-utils");
4
8
 
5
9
  const getSameRequestRank = (req1, req2) => {
6
10
  let rank = 1;
@@ -21,7 +25,7 @@ const getSameRequestRank = (req1, req2) => {
21
25
  return rank;
22
26
  };
23
27
 
24
- function getCompareRankMockToFetchRequest(mock, fetchReq) {
28
+ function getCompareRankMockToFetchRequest(mock, fetchReq, testConfig) {
25
29
  try {
26
30
  const mockURL = processURL(
27
31
  mock.fileContent.url,
@@ -36,6 +40,10 @@ function getCompareRankMockToFetchRequest(mock, fetchReq) {
36
40
  ) {
37
41
  return 0;
38
42
  }
43
+ const incomingHeaders = normalizeIncomingHeaders(fetchReq.options?.headers);
44
+ if (!incomingHeadersMatchMock(mock, incomingHeaders, testConfig)) {
45
+ return 0;
46
+ }
39
47
  const postData = mock.fileContent.request?.postData?.text
40
48
  ? FtJSON.parse(mock.fileContent.request?.postData?.text)
41
49
  : mock.fileContent.request?.postData;