ftmocks-utils 1.1.6 → 1.1.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.
Files changed (2) hide show
  1. package/package.json +6 -2
  2. package/src/index.js +617 -231
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftmocks-utils",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Util functions for FtMocks",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -16,5 +16,9 @@
16
16
  "bugs": {
17
17
  "url": "https://github.com/SodhanaLibrary/ftmocks-utils/issues"
18
18
  },
19
- "homepage": "https://github.com/SodhanaLibrary/ftmocks-utils#readme"
19
+ "homepage": "https://github.com/SodhanaLibrary/ftmocks-utils#readme",
20
+ "dependencies": {
21
+ "uui": "^1.0.7",
22
+ "uuid": "^11.1.0"
23
+ }
20
24
  }
package/src/index.js CHANGED
@@ -1,135 +1,165 @@
1
- // import fs from 'fs';
2
- const fs = require('fs')
3
- // import path from 'path';
4
- const path = require('path')
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { v4: uuidv4 } = require("uuid");
5
4
 
6
- const nameToFolder = name => {
7
- return name.replaceAll(' ', '_');
5
+ const nameToFolder = (name) => {
6
+ return name.replaceAll(" ", "_");
8
7
  };
9
8
 
10
- const getMockDir = config => {
11
- if(!path.isAbsolute(config.MOCK_DIR)) {
12
- return path.resolve( process.cwd(), config.MOCK_DIR);
9
+ const getMockDir = (config) => {
10
+ if (!path.isAbsolute(config.MOCK_DIR)) {
11
+ return path.resolve(process.cwd(), config.MOCK_DIR);
13
12
  }
14
13
  return config.MOCK_DIR;
15
- }
14
+ };
16
15
 
17
- const getFallbackDir = config => {
18
- if(config.FALLBACK_DIR && !path.isAbsolute(config.FALLBACK_DIR)) {
19
- return path.resolve( process.cwd(), config.FALLBACK_DIR);
16
+ const getFallbackDir = (config) => {
17
+ if (config.FALLBACK_DIR && !path.isAbsolute(config.FALLBACK_DIR)) {
18
+ return path.resolve(process.cwd(), config.FALLBACK_DIR);
20
19
  }
21
20
  return config.FALLBACK_DIR;
22
- }
21
+ };
23
22
 
24
- const capitalizeHeader = header => {
23
+ const capitalizeHeader = (header) => {
25
24
  return header
26
- .split('-')
27
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
28
- .join('-');
29
- }
25
+ .split("-")
26
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
27
+ .join("-");
28
+ };
30
29
 
31
- const capitalizeHeaders = headers => {
30
+ const capitalizeHeaders = (headers) => {
32
31
  return Object.fromEntries(
33
- Object.entries(headers).map(([key, value]) => [capitalizeHeader(key), value])
32
+ Object.entries(headers).map(([key, value]) => [
33
+ capitalizeHeader(key),
34
+ value,
35
+ ])
34
36
  );
35
37
  };
36
38
 
37
- const getHeaders = headers => {
39
+ const getHeaders = (headers) => {
38
40
  let res = null;
39
41
  try {
40
- res = new Map([...Object.entries(headers), ...Object.entries(capitalizeHeaders(headers))]);
41
- } catch(e) {
42
- console.debug('error at getHeaders', e);
43
- res = new Map([['Content-Type', 'application/json'], ['content-type', 'application/json']]);
42
+ res = new Map([
43
+ ...Object.entries(headers),
44
+ ...Object.entries(capitalizeHeaders(headers)),
45
+ ]);
46
+ } catch (e) {
47
+ console.debug("error at getHeaders", e);
48
+ res = new Map([
49
+ ["Content-Type", "application/json"],
50
+ ["content-type", "application/json"],
51
+ ]);
44
52
  }
45
53
  return Object.fromEntries(res);
46
- }
54
+ };
47
55
 
48
56
  const areJsonEqual = (jsonObj1, jsonObj2) => {
49
57
  // Check if both are objects and not null
50
- if (typeof jsonObj1 === 'object' && jsonObj1 !== null &&
51
- typeof jsonObj2 === 'object' && jsonObj2 !== null) {
52
-
58
+ if (
59
+ typeof jsonObj1 === "object" &&
60
+ jsonObj1 !== null &&
61
+ typeof jsonObj2 === "object" &&
62
+ jsonObj2 !== null
63
+ ) {
53
64
  // Get the keys of both objects
54
65
  const keys1 = Object.keys(jsonObj1);
55
66
  const keys2 = Object.keys(jsonObj2);
56
-
67
+
57
68
  // Check if the number of keys is different
58
69
  if (keys1.length !== keys2.length) {
59
70
  return false;
60
71
  }
61
-
72
+
62
73
  // Recursively check each key-value pair
63
74
  for (let key of keys1) {
64
75
  if (!keys2.includes(key) || !areJsonEqual(jsonObj1[key], jsonObj2[key])) {
65
76
  return false;
66
77
  }
67
78
  }
68
-
79
+
69
80
  return true;
70
81
  } else {
71
82
  // For non-object types, use strict equality comparison
72
83
  return jsonObj1 === jsonObj2;
73
84
  }
74
- }
85
+ };
75
86
 
76
87
  const getDefaultMockDataFromConfig = (testConfig) => {
77
- const defaultPath = path.join(getMockDir(testConfig), 'default.json');
78
-
79
- try {
80
- const defaultData = fs.readFileSync(defaultPath, 'utf8');
81
- let parsedData = JSON.parse(defaultData);
82
-
83
- // Read and attach mock data for each entry in parsedData
84
- parsedData.forEach(entry => {
85
- const mockFilePath = path.join(getMockDir(testConfig), 'defaultMocks', `mock_${entry.id}.json`);;
86
- try {
87
- const mockData = fs.readFileSync(mockFilePath, 'utf8');
88
- entry.fileContent = JSON.parse(mockData);
89
- } catch (error) {
90
- console.error(`Error reading mock data for ${entry.path}:`, error);
91
- return entry; // Return the original entry if there's an error
92
- }
93
- });
94
- return parsedData;
95
- } catch (error) {
96
- console.error(`Error reading or parsing default.json:`, error);
97
- return [];
98
- }
99
- }
88
+ const defaultPath = path.join(getMockDir(testConfig), "default.json");
89
+
90
+ try {
91
+ const defaultData = fs.readFileSync(defaultPath, "utf8");
92
+ let parsedData = JSON.parse(defaultData);
93
+
94
+ // Read and attach mock data for each entry in parsedData
95
+ parsedData.forEach((entry) => {
96
+ const mockFilePath = path.join(
97
+ getMockDir(testConfig),
98
+ "defaultMocks",
99
+ `mock_${entry.id}.json`
100
+ );
101
+ try {
102
+ const mockData = fs.readFileSync(mockFilePath, "utf8");
103
+ entry.fileContent = JSON.parse(mockData);
104
+ } catch (error) {
105
+ console.error(`Error reading mock data for ${entry.path}:`, error);
106
+ return entry; // Return the original entry if there's an error
107
+ }
108
+ });
109
+ return parsedData;
110
+ } catch (error) {
111
+ console.error(`Error reading or parsing default.json:`, error);
112
+ return [];
113
+ }
114
+ };
100
115
 
101
116
  // src/index.js
102
117
  const loadMockDataFromConfig = (testConfig, _testName) => {
103
118
  try {
104
119
  let testName = _testName;
105
- if(!testName) {
120
+ if (!testName) {
106
121
  // Read the test ID from mockServer.config.json
107
- const configPath = path.join(getMockDir(testConfig), 'mockServer.config.json');
108
- const configData = fs.readFileSync(configPath, 'utf8');
122
+ const configPath = path.join(
123
+ getMockDir(testConfig),
124
+ "mockServer.config.json"
125
+ );
126
+ const configData = fs.readFileSync(configPath, "utf8");
109
127
  const config = JSON.parse(configData);
110
128
  testName = config.testName;
111
129
  }
112
130
  // Read the tests from testConfig
113
- const mocksPath = path.join(getMockDir(testConfig), `test_${nameToFolder(testName)}`, '_mock_list.json');
114
- const mocksData = fs.readFileSync(mocksPath, 'utf8');
131
+ const mocksPath = path.join(
132
+ getMockDir(testConfig),
133
+ `test_${nameToFolder(testName)}`,
134
+ "_mock_list.json"
135
+ );
136
+ const mocksData = fs.readFileSync(mocksPath, "utf8");
115
137
  const mocks = JSON.parse(mocksData);
116
138
 
117
- mocks.forEach(mock => {
118
- const fileContent = JSON.parse(fs.readFileSync(path.join(getMockDir(testConfig), `test_${nameToFolder(testName)}`, `mock_${mock.id}.json`), 'utf8'));
139
+ mocks.forEach((mock) => {
140
+ const fileContent = JSON.parse(
141
+ fs.readFileSync(
142
+ path.join(
143
+ getMockDir(testConfig),
144
+ `test_${nameToFolder(testName)}`,
145
+ `mock_${mock.id}.json`
146
+ ),
147
+ "utf8"
148
+ )
149
+ );
119
150
  mock.fileContent = fileContent;
120
151
  });
121
152
 
122
-
123
153
  return mocks;
124
154
  } catch (error) {
125
- console.debug('Error loading test data:', error.message);
155
+ console.debug("Error loading test data:", error.message);
126
156
  return [];
127
157
  }
128
158
  };
129
159
 
130
- const clearNulls = postData => {
131
- Object.keys(postData || {}).forEach(key => {
132
- if(postData[key] === null) {
160
+ const clearNulls = (postData) => {
161
+ Object.keys(postData || {}).forEach((key) => {
162
+ if (postData[key] === null) {
133
163
  delete postData[key];
134
164
  }
135
165
  });
@@ -139,120 +169,188 @@ const isSameRequest = (req1, req2) => {
139
169
  clearNulls(req1.postData);
140
170
  clearNulls(req2.postData);
141
171
  let matched = true;
142
- if(req1.url !== req2.url) {
172
+ if (req1.url !== req2.url) {
143
173
  matched = false;
144
- } else if(req1.method?.toLowerCase() !== req2.method?.toLowerCase()) {
174
+ } else if (req1.method?.toLowerCase() !== req2.method?.toLowerCase()) {
145
175
  matched = false;
146
- } else if((!req1.postData && req2.postData) || (req1.postData && !req2.postData)) {
147
- matched = areJsonEqual(req1.postData || {} , req2.postData || {});
148
- } else if(req1.postData && req2.postData && !areJsonEqual(req1.postData , req2.postData)) {
176
+ } else if (
177
+ (!req1.postData && req2.postData) ||
178
+ (req1.postData && !req2.postData)
179
+ ) {
180
+ matched = areJsonEqual(req1.postData || {}, req2.postData || {});
181
+ } else if (
182
+ req1.postData &&
183
+ req2.postData &&
184
+ !areJsonEqual(req1.postData, req2.postData)
185
+ ) {
149
186
  matched = false;
150
187
  }
151
188
  return matched;
152
- }
189
+ };
153
190
 
154
- const processURL = (url, ignoreParams=[]) => {
155
- // Remove the hostname from the URL
156
- const urlWithoutHost = url.replace(/^(https?:\/\/)?[^\/]+/, '');
157
- const processedURL = new URL(`http://domain.com${urlWithoutHost}`);
158
- const params = new URLSearchParams(processedURL.search);
159
- if(ignoreParams?.length > 0) {
160
- ignoreParams.forEach(ip => {
161
- params.delete(ip);
162
- });
163
- }
164
- params.sort();
165
- return decodeURIComponent(`${processedURL.pathname}?${params}`);
166
- }
167
-
191
+ const processURL = (url, ignoreParams = []) => {
192
+ // Remove the hostname from the URL
193
+ const urlWithoutHost = url.replace(/^(https?:\/\/)?[^\/]+/, "");
194
+ const processedURL = new URL(`http://domain.com${urlWithoutHost}`);
195
+ const params = new URLSearchParams(processedURL.search);
196
+ if (ignoreParams?.length > 0) {
197
+ ignoreParams.forEach((ip) => {
198
+ params.delete(ip);
199
+ });
200
+ }
201
+ params.sort();
202
+ return decodeURIComponent(`${processedURL.pathname}?${params}`);
203
+ };
168
204
 
169
205
  function compareMockToRequest(mock, req) {
170
- const mockURL = processURL(mock.fileContent.url, mock.fileContent.ignoreParams);
171
- const reqURL = processURL(req.originalUrl, mock.fileContent.ignoreParams);
172
- const postData = mock.fileContent.request?.postData?.text ? JSON.parse(mock.fileContent.request?.postData?.text) : mock.fileContent.request?.postData;
173
- return isSameRequest({url: mockURL, method: mock.fileContent.method, postData}, {
206
+ const mockURL = processURL(
207
+ mock.fileContent.url,
208
+ mock.fileContent.ignoreParams
209
+ );
210
+ const reqURL = processURL(req.originalUrl, mock.fileContent.ignoreParams);
211
+ const postData = mock.fileContent.request?.postData?.text
212
+ ? JSON.parse(mock.fileContent.request?.postData?.text)
213
+ : mock.fileContent.request?.postData;
214
+ return isSameRequest(
215
+ { url: mockURL, method: mock.fileContent.method, postData },
216
+ {
174
217
  method: req.method,
175
218
  postData: req.body,
176
219
  url: reqURL,
177
- });
220
+ }
221
+ );
178
222
  }
179
223
 
180
224
  function compareMockToFetchRequest(mock, fetchReq) {
181
- try{
182
- const mockURL = processURL(mock.fileContent.url, mock.fileContent.ignoreParams);
225
+ try {
226
+ const mockURL = processURL(
227
+ mock.fileContent.url,
228
+ mock.fileContent.ignoreParams
229
+ );
183
230
  const reqURL = processURL(fetchReq.url, mock.fileContent.ignoreParams);
184
- const postData = mock.fileContent.request?.postData?.text ? JSON.parse(mock.fileContent.request?.postData?.text) : mock.fileContent.request?.postData;
185
- return isSameRequest({url: mockURL, method: mock.fileContent.method, postData}, {
186
- method: fetchReq.options.method || 'GET',
187
- postData: fetchReq.options.body?.length ? JSON.parse(fetchReq.options.body) : fetchReq.options.body,
188
- url: reqURL,
189
- });
190
- } catch(e) {
191
- console.debug('error at compareMockToFetchRequest', mock, fetchReq);
231
+ const postData = mock.fileContent.request?.postData?.text
232
+ ? JSON.parse(mock.fileContent.request?.postData?.text)
233
+ : mock.fileContent.request?.postData;
234
+ return isSameRequest(
235
+ { url: mockURL, method: mock.fileContent.method, postData },
236
+ {
237
+ method: fetchReq.options.method || "GET",
238
+ postData: fetchReq.options.body?.length
239
+ ? JSON.parse(fetchReq.options.body)
240
+ : fetchReq.options.body,
241
+ url: reqURL,
242
+ }
243
+ );
244
+ } catch (e) {
245
+ console.debug("error at compareMockToFetchRequest", mock, fetchReq);
192
246
  console.debug(e);
193
247
  }
194
248
  return false;
195
249
  }
196
250
 
197
- function getMatchingMockData({testMockData, defaultMockData, url, options, testConfig, testName}) {
251
+ function getMatchingMockData({
252
+ testMockData,
253
+ defaultMockData,
254
+ url,
255
+ options,
256
+ testConfig,
257
+ testName,
258
+ }) {
198
259
  let served = false;
199
- let matchedMocks = testMockData?.filter(mock => {
200
- if (mock.fileContent.waitForPrevious && !served) {
201
- return false;
202
- }
203
- served = mock.fileContent.served;
204
- return compareMockToFetchRequest(mock, { url, options });
205
- }) || [];
206
- let foundMock = matchedMocks.find(mock => !mock.fileContent.served) ? matchedMocks.find(mock => !mock.fileContent.served) : matchedMocks[matchedMocks.length - 1];
260
+ let matchedMocks =
261
+ testMockData?.filter((mock) => {
262
+ if (mock.fileContent.waitForPrevious && !served) {
263
+ return false;
264
+ }
265
+ served = mock.fileContent.served;
266
+ return compareMockToFetchRequest(mock, { url, options });
267
+ }) || [];
268
+ let foundMock = matchedMocks.find((mock) => !mock.fileContent.served)
269
+ ? matchedMocks.find((mock) => !mock.fileContent.served)
270
+ : matchedMocks[matchedMocks.length - 1];
207
271
  // updating stats to mock file
208
- if(foundMock) {
209
- const mockFilePath = path.join(getMockDir(testConfig), `test_${nameToFolder(testName)}`, `mock_${foundMock.id}.json`);
272
+ if (foundMock) {
273
+ const mockFilePath = path.join(
274
+ getMockDir(testConfig),
275
+ `test_${nameToFolder(testName)}`,
276
+ `mock_${foundMock.id}.json`
277
+ );
210
278
  foundMock.fileContent.served = true;
211
- fs.writeFileSync(mockFilePath, JSON.stringify(foundMock.fileContent, null, 2));
279
+ fs.writeFileSync(
280
+ mockFilePath,
281
+ JSON.stringify(foundMock.fileContent, null, 2)
282
+ );
212
283
  }
213
-
214
- if(!foundMock) {
215
- foundMock = defaultMockData.find(tm => compareMockToFetchRequest(tm, {
216
- url,
217
- options
218
- }));
284
+
285
+ if (!foundMock) {
286
+ foundMock = defaultMockData.find((tm) =>
287
+ compareMockToFetchRequest(tm, {
288
+ url,
289
+ options,
290
+ })
291
+ );
219
292
  }
220
293
  return foundMock ? foundMock.fileContent : null;
221
294
  }
222
295
 
223
- async function resetAllMockStats({testMockData, testConfig, testName}) {
224
- for(let i=0; i<testMockData.length; i++) {
296
+ async function resetAllMockStats({ testMockData, testConfig, testName }) {
297
+ for (let i = 0; i < testMockData.length; i++) {
225
298
  const tmd = testMockData[i];
226
- const mockFilePath = path.join(getMockDir(testConfig), `test_${nameToFolder(testName)}`, `mock_${tmd.id}.json`);
299
+ const mockFilePath = path.join(
300
+ getMockDir(testConfig),
301
+ `test_${nameToFolder(testName)}`,
302
+ `mock_${tmd.id}.json`
303
+ );
227
304
  tmd.fileContent.served = false;
228
- await fs.writeFileSync(mockFilePath, JSON.stringify(tmd.fileContent, null, 2));
305
+ await fs.writeFileSync(
306
+ mockFilePath,
307
+ JSON.stringify(tmd.fileContent, null, 2)
308
+ );
229
309
  }
230
310
  }
231
311
 
232
- async function initiatePlaywrightRoutes (page, ftmocksConifg, testName, mockPath = '**/*', excludeMockPath = null) {
233
- const testMockData = testName ? loadMockDataFromConfig(ftmocksConifg, testName) : [];
234
- resetAllMockStats({testMockData, testConfig: ftmocksConifg, testName});
312
+ async function initiatePlaywrightRoutes(
313
+ page,
314
+ ftmocksConifg,
315
+ testName,
316
+ mockPath = "**/*",
317
+ excludeMockPath = null
318
+ ) {
319
+ const testMockData = testName
320
+ ? loadMockDataFromConfig(ftmocksConifg, testName)
321
+ : [];
322
+ resetAllMockStats({ testMockData, testConfig: ftmocksConifg, testName });
235
323
  const defaultMockData = getDefaultMockDataFromConfig(ftmocksConifg);
236
- console.debug('calling initiatePlaywrightRoutes fetch');
324
+ console.debug("calling initiatePlaywrightRoutes fetch");
237
325
  await page.route(mockPath, async (route, request) => {
238
326
  const url = request.url();
239
327
  const options = {
240
- options: {
241
- url,
242
- method: request.method(),
243
- body: request.postData(),
244
- }
245
- }
328
+ url,
329
+ method: request.method(),
330
+ body: request.postData(),
331
+ };
246
332
  if (excludeMockPath && new RegExp(excludeMockPath).test(url)) {
247
333
  await route.fallback();
248
334
  return;
249
335
  }
250
- console.debug('got fetch request', request.method(), request.url(), request.postData());
251
- let mockData = getMatchingMockData({testMockData, defaultMockData, url, options, testConfig: ftmocksConifg, testName});
336
+ console.debug(
337
+ "got fetch request",
338
+ request.method(),
339
+ request.url(),
340
+ request.postData()
341
+ );
342
+ let mockData = getMatchingMockData({
343
+ testMockData,
344
+ defaultMockData,
345
+ url,
346
+ options,
347
+ testConfig: ftmocksConifg,
348
+ testName,
349
+ });
252
350
  if (mockData) {
253
- console.debug('mocked', url, options);
351
+ console.debug("mocked", url, options);
254
352
  const { content, headers, status } = mockData.response;
255
-
353
+
256
354
  const json = {
257
355
  status,
258
356
  headers: getHeaders(headers),
@@ -261,31 +359,37 @@ async function initiatePlaywrightRoutes (page, ftmocksConifg, testName, mockPath
261
359
 
262
360
  await route.fulfill(json);
263
361
  } else {
264
- console.debug('missing mock data', url, options);
362
+ console.debug("missing mock data", url, options);
265
363
  const fallbackDir = getFallbackDir(ftmocksConifg);
266
- if(!fallbackDir) {
364
+ if (!fallbackDir) {
267
365
  await route.fallback();
268
366
  return;
269
367
  }
270
368
  const urlObj = new URL(route.request().url());
271
- const filePath = path.join(fallbackDir, urlObj.pathname === '/' || urlObj.pathname === '' ? (ftmocksConifg.FALLBACK_DIR_INDEX_FILE || 'index.html') : urlObj.pathname);
272
- console.debug('serving file ', filePath);
369
+ const filePath = path.join(
370
+ fallbackDir,
371
+ urlObj.pathname === "/" || urlObj.pathname === ""
372
+ ? ftmocksConifg.FALLBACK_DIR_INDEX_FILE || "index.html"
373
+ : urlObj.pathname
374
+ );
375
+ console.debug("serving file ", filePath);
273
376
  if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
274
377
  const fileContent = fs.readFileSync(filePath);
275
378
  const ext = path.extname(filePath);
276
- const contentType = {
277
- '.html': 'text/html',
278
- '.css': 'text/css',
279
- '.js': 'application/javascript',
280
- '.json': 'application/json',
281
- '.png': 'image/png',
282
- '.jpg': 'image/jpeg',
283
- }[ext] || 'application/octet-stream';
284
-
285
- console.debug('serving file', filePath);
379
+ const contentType =
380
+ {
381
+ ".html": "text/html",
382
+ ".css": "text/css",
383
+ ".js": "application/javascript",
384
+ ".json": "application/json",
385
+ ".png": "image/png",
386
+ ".jpg": "image/jpeg",
387
+ }[ext] || "application/octet-stream";
388
+
389
+ console.debug("serving file", filePath);
286
390
  await route.fulfill({
287
391
  body: fileContent,
288
- headers: { 'Content-Type': contentType },
392
+ headers: { "Content-Type": contentType },
289
393
  });
290
394
  } else {
291
395
  await route.fallback();
@@ -294,27 +398,36 @@ async function initiatePlaywrightRoutes (page, ftmocksConifg, testName, mockPath
294
398
  });
295
399
  }
296
400
 
297
- async function initiateJestFetch (jest, ftmocksConifg, testName) {
298
- const testMockData = testName ? loadMockDataFromConfig(ftmocksConifg, testName) : [];
299
- resetAllMockStats({testMockData, testConfig: ftmocksConifg, testName});
401
+ async function initiateJestFetch(jest, ftmocksConifg, testName) {
402
+ const testMockData = testName
403
+ ? loadMockDataFromConfig(ftmocksConifg, testName)
404
+ : [];
405
+ resetAllMockStats({ testMockData, testConfig: ftmocksConifg, testName });
300
406
  const defaultMockData = getDefaultMockDataFromConfig(ftmocksConifg);
301
- console.debug('calling initiateJestFetch fetch');
407
+ console.debug("calling initiateJestFetch fetch");
302
408
  global.fetch = jest.fn((url, options = {}) => {
303
- console.debug('got fetch request', url, options);
304
- let mockData = getMatchingMockData({testMockData, defaultMockData, url, options, testConfig: ftmocksConifg, testName});
409
+ console.debug("got fetch request", url, options);
410
+ let mockData = getMatchingMockData({
411
+ testMockData,
412
+ defaultMockData,
413
+ url,
414
+ options,
415
+ testConfig: ftmocksConifg,
416
+ testName,
417
+ });
305
418
  if (mockData) {
306
- console.debug('mocked', url, options);
419
+ console.debug("mocked", url, options);
307
420
  } else {
308
- console.debug('missing mock data', url, options);
421
+ console.debug("missing mock data", url, options);
309
422
  return Promise.resolve({
310
423
  status: 404,
311
- headers: new Map([['Content-Type', 'application/json']]),
312
- json: () => Promise.resolve({ error: 'Mock data not found' }),
424
+ headers: new Map([["Content-Type", "application/json"]]),
425
+ json: () => Promise.resolve({ error: "Mock data not found" }),
313
426
  });
314
427
  }
315
-
428
+
316
429
  const { content, headers, status } = mockData.response;
317
-
430
+
318
431
  return Promise.resolve({
319
432
  status,
320
433
  headers: new Map(Object.entries(headers)),
@@ -322,14 +435,14 @@ async function initiateJestFetch (jest, ftmocksConifg, testName) {
322
435
  });
323
436
  });
324
437
 
325
- console.debug('calling XMLHttpRequest fetch');
438
+ console.debug("calling XMLHttpRequest fetch");
326
439
  global.XMLHttpRequest = jest.fn(function () {
327
440
  const xhrMock = {
328
441
  open: jest.fn(),
329
442
  send: jest.fn(),
330
443
  setRequestHeader: jest.fn(),
331
444
  getAllResponseHeaders: jest.fn(() => {
332
- return '';
445
+ return "";
333
446
  }),
334
447
  getResponseHeader: jest.fn((header) => {
335
448
  return null;
@@ -337,13 +450,13 @@ async function initiateJestFetch (jest, ftmocksConifg, testName) {
337
450
  readyState: 4,
338
451
  status: 0,
339
452
  response: null,
340
- responseText: '',
453
+ responseText: "",
341
454
  headers: new Map(Object.entries(headers)),
342
455
  onreadystatechange: null,
343
456
  onload: null,
344
457
  onerror: null,
345
458
  };
346
-
459
+
347
460
  xhrMock.send.mockImplementation(function () {
348
461
  const mockData = getMatchingMockData({
349
462
  testMockData,
@@ -353,16 +466,16 @@ async function initiateJestFetch (jest, ftmocksConifg, testName) {
353
466
  testConfig: ftmocksConifg,
354
467
  testName,
355
468
  });
356
-
469
+
357
470
  if (mockData) {
358
- console.debug('mocked', xhrMock._url, xhrMock._options);
471
+ console.debug("mocked", xhrMock._url, xhrMock._options);
359
472
  const { content, headers, status } = mockData.response;
360
-
473
+
361
474
  xhrMock.status = status;
362
475
  xhrMock.responseText = content;
363
476
  xhrMock.response = content;
364
477
  xhrMock.headers = new Map(Object.entries(headers));
365
-
478
+
366
479
  if (xhrMock.onreadystatechange) {
367
480
  xhrMock.onreadystatechange();
368
481
  }
@@ -370,12 +483,12 @@ async function initiateJestFetch (jest, ftmocksConifg, testName) {
370
483
  xhrMock.onload();
371
484
  }
372
485
  } else {
373
- console.debug('missing mock data', xhrMock._url, xhrMock._options);
374
-
486
+ console.debug("missing mock data", xhrMock._url, xhrMock._options);
487
+
375
488
  xhrMock.status = 404;
376
- xhrMock.responseText = JSON.stringify({ error: 'Mock data not found' });
489
+ xhrMock.responseText = JSON.stringify({ error: "Mock data not found" });
377
490
  xhrMock.response = xhrMock.responseText;
378
-
491
+
379
492
  if (xhrMock.onreadystatechange) {
380
493
  xhrMock.onreadystatechange();
381
494
  }
@@ -384,59 +497,62 @@ async function initiateJestFetch (jest, ftmocksConifg, testName) {
384
497
  }
385
498
  }
386
499
  });
387
-
500
+
388
501
  xhrMock.open.mockImplementation(function (method, url) {
389
502
  xhrMock._options = { method };
390
503
  xhrMock._url = url;
391
504
  });
392
-
505
+
393
506
  return xhrMock;
394
507
  });
395
-
508
+
396
509
  return;
397
- };
510
+ }
398
511
 
399
512
  function initiateConsoleLogs(jest, ftmocksConifg, testName) {
400
- const logsFile = path.join(getMockDir(ftmocksConifg), `test_${nameToFolder(testName)}`, '_logs.json');
513
+ const logsFile = path.join(
514
+ getMockDir(ftmocksConifg),
515
+ `test_${nameToFolder(testName)}`,
516
+ "_logs.json"
517
+ );
401
518
  let logs = [];
402
- if(!fs.existsSync(logsFile)) {
403
- fs.appendFileSync(logsFile, '[]', 'utf8');
519
+ if (!fs.existsSync(logsFile)) {
520
+ fs.appendFileSync(logsFile, "[]", "utf8");
404
521
  } else {
405
- fs.writeFileSync(logsFile, '[]', 'utf8');
522
+ fs.writeFileSync(logsFile, "[]", "utf8");
406
523
  }
407
524
 
408
525
  const writeToFile = (type, params) => {
409
- const logMessage = params.join(' ') + '\n'; // Combine params into a string with spaces
526
+ const logMessage = params.join(" ") + "\n"; // Combine params into a string with spaces
410
527
  logs.push({
411
528
  type,
412
529
  message: logMessage,
413
- time: Date.now()
530
+ time: Date.now(),
414
531
  });
415
- fs.writeFileSync(logsFile, JSON.stringify(logs, null, 2), 'utf8'); // Append the log message to the file
416
- }
417
-
532
+ fs.writeFileSync(logsFile, JSON.stringify(logs, null, 2), "utf8"); // Append the log message to the file
533
+ };
534
+
418
535
  global.console = {
419
536
  ...console,
420
537
  // uncomment to ignore a specific log level
421
538
  log: jest.fn((...params) => {
422
- writeToFile('log', params);
539
+ writeToFile("log", params);
423
540
  }),
424
541
  debug: jest.fn((...params) => {
425
- writeToFile('debug', params);
542
+ writeToFile("debug", params);
426
543
  }),
427
544
  info: jest.fn((...params) => {
428
- writeToFile('info', params);
545
+ writeToFile("info", params);
429
546
  }),
430
547
  warn: jest.fn((...params) => {
431
- writeToFile('warn', params);
548
+ writeToFile("warn", params);
432
549
  }),
433
550
  error: jest.fn((...params) => {
434
- writeToFile('error', params);
551
+ writeToFile("error", params);
435
552
  }),
436
553
  };
437
554
  }
438
555
 
439
-
440
556
  function countFilesInDirectory(directoryPath) {
441
557
  return new Promise((resolve, reject) => {
442
558
  fs.readdir(directoryPath, (err, files) => {
@@ -445,7 +561,7 @@ function countFilesInDirectory(directoryPath) {
445
561
  }
446
562
 
447
563
  // Filter out directories and only count files
448
- const fileCount = files.filter(file => {
564
+ const fileCount = files.filter((file) => {
449
565
  const filePath = path.join(directoryPath, file);
450
566
  return fs.statSync(filePath).isFile();
451
567
  }).length;
@@ -456,62 +572,332 @@ function countFilesInDirectory(directoryPath) {
456
572
  }
457
573
 
458
574
  const saveSnap = async (html, ftmocksConifg, testName) => {
459
- const snapFolder = path.join(getMockDir(ftmocksConifg), `test_${nameToFolder(testName)}`, '_snaps');
460
- const snapTemplate = path.join(getMockDir(ftmocksConifg), 'snap_template.html');
461
-
575
+ const snapFolder = path.join(
576
+ getMockDir(ftmocksConifg),
577
+ `test_${nameToFolder(testName)}`,
578
+ "_snaps"
579
+ );
580
+ const snapTemplate = path.join(
581
+ getMockDir(ftmocksConifg),
582
+ "snap_template.html"
583
+ );
584
+
462
585
  if (!fs.existsSync(snapFolder)) {
463
586
  fs.mkdirSync(snapFolder);
464
587
  }
465
- const fileCount = await (countFilesInDirectory(snapFolder));
588
+ const fileCount = await countFilesInDirectory(snapFolder);
466
589
  const snapFilePath = path.join(snapFolder, `snap_${fileCount + 1}.html`);
467
590
  let resHtml = html;
468
591
  if (fs.existsSync(snapFolder)) {
469
- const templateHtml = fs.readFileSync(snapTemplate, 'utf8');;
470
- resHtml = templateHtml.replace('<!--FtMocks-Snap-Template-To-Be-Replaced-->', html)
592
+ const templateHtml = fs.readFileSync(snapTemplate, "utf8");
593
+ resHtml = templateHtml.replace(
594
+ "<!--FtMocks-Snap-Template-To-Be-Replaced-->",
595
+ html
596
+ );
471
597
  }
472
- fs.writeFileSync(snapFilePath, resHtml)
598
+ fs.writeFileSync(snapFilePath, resHtml);
473
599
  };
474
600
 
475
601
  const deleteAllSnaps = async (ftmocksConifg, testName) => {
476
- const snapFolder = path.join(getMockDir(ftmocksConifg), `test_${nameToFolder(testName)}`, '_snaps');
602
+ const snapFolder = path.join(
603
+ getMockDir(ftmocksConifg),
604
+ `test_${nameToFolder(testName)}`,
605
+ "_snaps"
606
+ );
477
607
  fs.rmSync(snapFolder, { recursive: true, force: true });
478
608
  };
479
609
 
480
610
  const deleteAllLogs = async (ftmocksConifg, testName) => {
481
- const mockDir = path.join(getMockDir(ftmocksConifg), `test_${nameToFolder(testName)}`);
611
+ const mockDir = path.join(
612
+ getMockDir(ftmocksConifg),
613
+ `test_${nameToFolder(testName)}`
614
+ );
482
615
  const logFilePath = path.join(mockDir, `_logs.json`);
483
616
  fs.rmSync(logFilePath, { recursive: true, force: true });
484
617
  };
485
618
 
486
619
  function initiateJestEventSnaps(jest, ftmocksConifg, testName) {
487
- const mouseEvents = ftmocksConifg.snapEvents || ['click', 'change', 'url', 'dblclick', 'contextmenu'];
488
- mouseEvents.forEach(event => {
489
- jest.spyOn(document, 'addEventListener').mockImplementation((e, callback) => {
490
- if (mouseEvents.includes(e)) {
491
- saveSnap(document.outerHTML, ftmocksConifg, testName);
492
- }
493
- });
620
+ const mouseEvents = ftmocksConifg.snapEvents || [
621
+ "click",
622
+ "change",
623
+ "url",
624
+ "dblclick",
625
+ "contextmenu",
626
+ ];
627
+ mouseEvents.forEach((event) => {
628
+ jest
629
+ .spyOn(document, "addEventListener")
630
+ .mockImplementation((e, callback) => {
631
+ if (mouseEvents.includes(e)) {
632
+ saveSnap(document.outerHTML, ftmocksConifg, testName);
633
+ }
634
+ });
494
635
  });
495
636
  }
496
637
 
638
+ const createTest = async (ftmocksConifg, testName) => {
639
+ const testsPath = path.join(getMockDir(ftmocksConifg), "tests.json");
640
+ let tests = [];
641
+ try {
642
+ // Read existing tests
643
+ const testsData = fs.readFileSync(testsPath, "utf8");
644
+ tests = JSON.parse(testsData);
645
+ const etest = tests.find((tst) => tst.name === testName);
646
+ if (!etest) {
647
+ const newTest = {
648
+ id: uuidv4(),
649
+ name: testName,
650
+ };
651
+ tests.push(newTest);
652
+ fs.writeFileSync(testsPath, JSON.stringify(tests, null, 2));
653
+ const folderPath = path.join(
654
+ getMockDir(ftmocksConifg),
655
+ `test_${nameToFolder(testName)}`
656
+ );
657
+ const mockListFilePath = path.join(folderPath, "_mock_list.json");
658
+ fs.mkdir(folderPath, { recursive: true }, (err) => {
659
+ if (err) {
660
+ console.error("Error creating directory:", err);
661
+ } else {
662
+ console.log("Directory created successfully!");
663
+ }
664
+ });
665
+ await fs.appendFile(mockListFilePath, "[]", () => {
666
+ console.log("mock list file created successfully");
667
+ });
668
+
669
+ return newTest;
670
+ } else {
671
+ throw "Test already exists";
672
+ }
673
+ } catch (error) {
674
+ console.error(`Error reading tests.json:`, error);
675
+ return null;
676
+ }
677
+ };
678
+
679
+ const isSameResponse = (req1, req2) => {
680
+ try {
681
+ let matched = true;
682
+ if (req1.response.status !== req2.response.status) {
683
+ matched = false;
684
+ // console.log('not matched at url', req1.method, req2.method);
685
+ } else if (
686
+ (!req1.response.content && req2.response.content) ||
687
+ (req1.response.content && !req2.response.content)
688
+ ) {
689
+ matched = areJsonEqual(
690
+ JSON.parse(req1.response.content) || {},
691
+ JSON.parse(req2.response.content) || {}
692
+ );
693
+ // console.log('not matched at post Data 0', req1.postData, req2.postData);
694
+ } else if (
695
+ req1.response.content &&
696
+ req2.response.content &&
697
+ !areJsonEqual(
698
+ JSON.parse(req1.response.content) || {},
699
+ JSON.parse(req2.response.content) || {}
700
+ )
701
+ ) {
702
+ matched = false;
703
+ }
704
+ // if (matched) {
705
+ // console.log('matched responses', req1, req2);
706
+ // }
707
+ return matched;
708
+ } catch (error) {
709
+ console.error(error);
710
+ return false;
711
+ }
712
+ };
713
+
714
+ const compareMockToMock = (mock1, mock2, matchResponse) => {
715
+ try {
716
+ if (matchResponse) {
717
+ return isSameRequest(mock1, mock2) && isSameResponse(mock1, mock2);
718
+ } else {
719
+ return isSameRequest(mock1, mock2);
720
+ }
721
+ } catch (error) {
722
+ console.error(error);
723
+ return false;
724
+ }
725
+ };
726
+
727
+ const saveIfItIsFile = async (route, testName, ftmocksConifg) => {
728
+ const urlObj = new URL(route.request().url());
729
+
730
+ // Check if URL contains file extension like .js, .png, .css etc
731
+ const fileExtMatch = urlObj.pathname.match(/\.[a-zA-Z0-9]+$/);
732
+ if (fileExtMatch) {
733
+ const fileExt = fileExtMatch[0];
734
+ // Create directory path matching URL structure
735
+ const dirPath = path.join(
736
+ getMockDir(ftmocksConifg),
737
+ `test_${nameToFolder(testName)}`,
738
+ "_files",
739
+ path.dirname(urlObj.pathname)
740
+ );
741
+
742
+ // Create directories if they don't exist
743
+ fs.mkdirSync(dirPath, { recursive: true });
744
+
745
+ // Save file with original name
746
+ const fileName = path.basename(urlObj.pathname);
747
+ const filePath = path.join(dirPath, fileName);
748
+
749
+ const response = await route.fetch();
750
+ const buffer = await response.body();
751
+ fs.writeFileSync(filePath, buffer);
752
+
753
+ await route.continue();
754
+ return true;
755
+ }
756
+ return false;
757
+ };
758
+
759
+ async function recordPlaywrightRoutes(
760
+ page,
761
+ ftmocksConifg,
762
+ config = {
763
+ testName,
764
+ mockPath: "**/*",
765
+ pattern: "^/api/.*",
766
+ avoidDuplicatesInTheTest: true,
767
+ avoidDuplicatesWithDefaultMocks: true,
768
+ }
769
+ ) {
770
+ await page.route(config.mockPath, async (route) => {
771
+ try {
772
+ const urlObj = new URL(route.request().url());
773
+ if (config.pattern && config.pattern.length > 0) {
774
+ const patternRegex = new RegExp(config.pattern);
775
+ if (!patternRegex.test(urlObj.pathname)) {
776
+ await route.continue();
777
+ return;
778
+ }
779
+ }
497
780
 
781
+ if (await saveIfItIsFile(route, config.testName, ftmocksConifg)) {
782
+ return;
783
+ }
784
+
785
+ const mockData = {
786
+ url: urlObj.pathname + urlObj.search,
787
+ time: new Date().toString(),
788
+ method: route.request().method(),
789
+ request: {
790
+ headers: await route.request().headers(),
791
+ queryString: Array.from(urlObj.searchParams.entries()).map(
792
+ ([name, value]) => ({
793
+ name,
794
+ value,
795
+ })
796
+ ),
797
+ postData: route.request().postData()
798
+ ? {
799
+ mimeType: "application/json",
800
+ text: route.request().postData(),
801
+ }
802
+ : null,
803
+ },
804
+ response: {
805
+ status: (await route.fetch()).status(),
806
+ headers: (await route.fetch()).headers(),
807
+ content: await (await route.fetch()).text(),
808
+ },
809
+ id: crypto.randomUUID(),
810
+ served: false,
811
+ ignoreParams: ftmocksConifg.ignoreParams || [],
812
+ };
813
+
814
+ await createTest(ftmocksConifg, config.testName);
815
+ if (config.avoidDuplicatesInTheTest) {
816
+ // Check if the mock data is a duplicate of a mock data in the test
817
+ const testMockList = loadMockDataFromConfig(
818
+ ftmocksConifg,
819
+ config.testName
820
+ );
821
+ const matchResponse = testMockList.find((mock) =>
822
+ compareMockToMock(mock.fileContent, mockData, true)
823
+ );
824
+ if (matchResponse) {
825
+ console.log("Aborting duplicate mock data in the test");
826
+ await route.continue();
827
+ return;
828
+ }
829
+ }
830
+
831
+ if (config.avoidDuplicatesWithDefaultMocks) {
832
+ // Check if the mock data is a duplicate of a mock data in the test
833
+ const defaultMockList = getDefaultMockDataFromConfig(ftmocksConifg);
834
+ const matchResponse = defaultMockList.find((mock) =>
835
+ compareMockToMock(mock.fileContent, mockData, true)
836
+ );
837
+ if (matchResponse) {
838
+ console.log("Aborting duplicate mock data with default mocks");
839
+ await route.continue();
840
+ return;
841
+ }
842
+ }
843
+
844
+ // Save the mock data to the test
845
+ const mockListPath = path.join(
846
+ getMockDir(ftmocksConifg),
847
+ `test_${nameToFolder(config.testName)}`,
848
+ "_mock_list.json"
849
+ );
850
+ let mockList = [];
851
+ if (fs.existsSync(mockListPath)) {
852
+ mockList = JSON.parse(fs.readFileSync(mockListPath, "utf8"));
853
+ }
854
+ mockList.push({
855
+ id: mockData.id,
856
+ url: mockData.url,
857
+ method: mockData.method,
858
+ time: mockData.time,
859
+ });
860
+
861
+ // Create test directory if it doesn't exist
862
+ const testDir = path.join(
863
+ getMockDir(ftmocksConifg),
864
+ `test_${nameToFolder(config.testName)}`
865
+ );
866
+ if (!fs.existsSync(testDir)) {
867
+ fs.mkdirSync(testDir, { recursive: true });
868
+ }
869
+ fs.writeFileSync(mockListPath, JSON.stringify(mockList, null, 2));
870
+ const mocDataPath = path.join(
871
+ getMockDir(ftmocksConifg),
872
+ `test_${nameToFolder(config.testName)}`,
873
+ `mock_${mockData.id}.json`
874
+ );
875
+ fs.writeFileSync(mocDataPath, JSON.stringify(mockData, null, 2));
876
+ await route.continue();
877
+ } catch (error) {
878
+ console.error(error);
879
+ await route.continue();
880
+ }
881
+ });
882
+ }
498
883
 
499
884
  // Export functions as a module
500
885
  module.exports = {
501
- compareMockToRequest,
502
- processURL,
503
- isSameRequest,
504
- loadMockDataFromConfig,
505
- getDefaultMockDataFromConfig,
506
- nameToFolder,
507
- compareMockToFetchRequest,
508
- getMatchingMockData,
509
- resetAllMockStats,
510
- initiateJestFetch,
511
- saveSnap,
512
- deleteAllSnaps,
513
- deleteAllLogs,
514
- initiateConsoleLogs,
515
- initiatePlaywrightRoutes,
516
- initiateJestEventSnaps
886
+ compareMockToRequest,
887
+ processURL,
888
+ isSameRequest,
889
+ loadMockDataFromConfig,
890
+ getDefaultMockDataFromConfig,
891
+ nameToFolder,
892
+ compareMockToFetchRequest,
893
+ getMatchingMockData,
894
+ resetAllMockStats,
895
+ initiateJestFetch,
896
+ saveSnap,
897
+ deleteAllSnaps,
898
+ deleteAllLogs,
899
+ initiateConsoleLogs,
900
+ initiatePlaywrightRoutes,
901
+ initiateJestEventSnaps,
902
+ recordPlaywrightRoutes,
517
903
  };