ftmocks-utils 1.0.1 → 1.0.3

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.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Util functions for FtMocks",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -5,8 +5,36 @@ export const nameToFolder = name => {
5
5
  return name.replaceAll(' ', '_');
6
6
  };
7
7
 
8
+ const areJsonEqual = (jsonObj1, jsonObj2) => {
9
+ // Check if both are objects and not null
10
+ if (typeof jsonObj1 === 'object' && jsonObj1 !== null &&
11
+ typeof jsonObj2 === 'object' && jsonObj2 !== null) {
12
+
13
+ // Get the keys of both objects
14
+ const keys1 = Object.keys(jsonObj1);
15
+ const keys2 = Object.keys(jsonObj2);
16
+
17
+ // Check if the number of keys is different
18
+ if (keys1.length !== keys2.length) {
19
+ return false;
20
+ }
21
+
22
+ // Recursively check each key-value pair
23
+ for (let key of keys1) {
24
+ if (!keys2.includes(key) || !areJsonEqual(jsonObj1[key], jsonObj2[key])) {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ return true;
30
+ } else {
31
+ // For non-object types, use strict equality comparison
32
+ return jsonObj1 === jsonObj2;
33
+ }
34
+ }
35
+
8
36
  const getDefaultMockDataFromConfig = (testConfig) => {
9
- const defaultPath = path.join(testConfig.MOCK_DIR, testConfig.MOCK_DEFAULT_FILE);
37
+ const defaultPath = path.join(testConfig.MOCK_DIR, 'defaultMocks');
10
38
 
11
39
  try {
12
40
  const defaultData = fs.readFileSync(defaultPath, 'utf8');
@@ -14,7 +42,7 @@ try {
14
42
 
15
43
  // Read and attach mock data for each entry in parsedData
16
44
  parsedData.forEach(entry => {
17
- const mockFilePath = path.join(testConfig.MOCK_DIR, testConfig.MOCK_DEFAULT_DIR, `mock_${entry.id}.json`);;
45
+ const mockFilePath = path.join(testConfig.MOCK_DIR, 'defaultMocks', `mock_${entry.id}.json`);;
18
46
  try {
19
47
  const mockData = fs.readFileSync(mockFilePath, 'utf8');
20
48
  entry.fileContent = JSON.parse(mockData);
@@ -25,7 +53,7 @@ try {
25
53
  });
26
54
  return parsedData;
27
55
  } catch (error) {
28
- console.error(`Error reading or parsing ${testConfig.MOCK_DEFAULT_FILE}:`, error);
56
+ console.error(`Error reading or parsing default.json:`, error);
29
57
  return [];
30
58
  }
31
59
  }
@@ -41,7 +69,7 @@ const loadMockDataFromConfig = (testConfig, _testName) => {
41
69
  const config = JSON.parse(configData);
42
70
  testName = config.testName;
43
71
  }
44
- // Read the tests from testConfig.MOCK_TEST_FILE
72
+ // Read the tests from testConfig
45
73
  const mocksPath = path.join(testConfig.MOCK_DIR, `test_${nameToFolder(testName)}`, '_mock_list.json');
46
74
  const mocksData = fs.readFileSync(mocksPath, 'utf8');
47
75
  const mocks = JSON.parse(mocksData);
@@ -51,9 +79,10 @@ const loadMockDataFromConfig = (testConfig, _testName) => {
51
79
  mock.fileContent = fileContent;
52
80
  });
53
81
 
82
+
54
83
  return mocks;
55
84
  } catch (error) {
56
- console.error('Error loading test data:', error.message);
85
+ console.debug('Error loading test data:', error.message);
57
86
  return [];
58
87
  }
59
88
  };
@@ -104,11 +133,46 @@ function compareMockToFetchRequest(mock, fetchReq) {
104
133
  const postData = mock.fileContent.request?.postData?.text ? JSON.parse(mock.fileContent.request?.postData?.text) : mock.fileContent.request?.postData;
105
134
  return isSameRequest({url: mockURL, method: mock.fileContent.method, postData}, {
106
135
  method: fetchReq.options.method || 'GET',
107
- postData: fetchReq.options.body,
136
+ postData: fetchReq.options.body?.length ? JSON.parse(fetchReq.options.body) : fetchReq.options.body,
108
137
  url: reqURL,
109
138
  });
110
139
  }
111
140
 
141
+ function getMatchingMockData({testMockData, defaultMockData, url, options, testConfig, testName}) {
142
+ let served = false;
143
+ let matchedMocks = testMockData?.filter(mock => {
144
+ if (mock.fileContent.waitForPrevious && !served) {
145
+ return false;
146
+ }
147
+ served = mock.fileContent.served;
148
+ return compareMockToFetchRequest(mock, { url, options });
149
+ }) || [];
150
+ let foundMock = matchedMocks.find(mock => !mock.fileContent.served) ? matchedMocks.find(mock => !mock.fileContent.served) : matchedMocks[0];
151
+ // updating stats to mock file
152
+ if(foundMock) {
153
+ const mockFilePath = path.join(testConfig.MOCK_DIR, `test_${nameToFolder(testName)}`, `mock_${foundMock.id}.json`);
154
+ foundMock.fileContent.served = true;
155
+ fs.writeFileSync(mockFilePath, JSON.stringify(foundMock.fileContent, null, 2));
156
+ }
157
+
158
+ if(!foundMock) {
159
+ foundMock = defaultMockData.find(tm => compareMockToFetchRequest(tm, {
160
+ url,
161
+ options
162
+ }));
163
+ }
164
+ return foundMock ? foundMock.fileContent : null;
165
+ }
166
+
167
+ async function resetAllMockStats({testMockData, testConfig, testName}) {
168
+ for(let i=0; i<testMockData.length; i++) {
169
+ const tmd = testMockData[i];
170
+ const mockFilePath = path.join(testConfig.MOCK_DIR, `test_${nameToFolder(testName)}`, `mock_${tmd.id}.json`);
171
+ tmd.fileContent.served = false;
172
+ await fs.writeFileSync(mockFilePath, JSON.stringify(tmd.fileContent, null, 2));
173
+ }
174
+ }
175
+
112
176
 
113
177
  // Export functions as a module
114
178
  module.exports = {
@@ -118,5 +182,7 @@ module.exports = {
118
182
  loadMockDataFromConfig,
119
183
  getDefaultMockDataFromConfig,
120
184
  nameToFolder,
121
- compareMockToFetchRequest
185
+ compareMockToFetchRequest,
186
+ getMatchingMockData,
187
+ resetAllMockStats
122
188
  };
@@ -0,0 +1,280 @@
1
+ (function () {
2
+ // Intercept Fetch API
3
+ const originalFetch = window.fetch;
4
+ const recordedTracks = [];
5
+
6
+ const addTrack = track => {
7
+ track.id = recordedTracks.length ? recordedTracks[recordedTracks.length - 1].id + 1 : 1;
8
+ track.time = new Date();
9
+ fetch(window.FTMOCKS_CONFIG.record_events_url, {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ body: JSON.stringify(track),
15
+ }).then(response => response.json());
16
+ };
17
+
18
+
19
+ window.fetch = async function (url, options = {}) {
20
+ const method = options.method || 'GET';
21
+ const body = options.body;
22
+ const headers = options.headers || {};
23
+ const queryString = url.includes('?') ? url.split('?')[1] : null;
24
+ const response = await originalFetch(url, options);
25
+ const ftMocksURL = new URL(window.FTMOCKS_CONFIG.record_mocks_url);
26
+ const currentURL = new URL(url.startsWith('http') ? url : `http://something/${url}`);
27
+ const clonedResponse = response.clone();
28
+ clonedResponse.text().then((text) => {
29
+ if (ftMocksURL.hostname !== currentURL.hostname) {
30
+ const mockResponse = {
31
+ url: url,
32
+ time: new Date().toString(),
33
+ method: method,
34
+ request: {
35
+ headers: headers,
36
+ queryString: queryString,
37
+ postData: {
38
+ mimeType: headers['Content-Type'] || null,
39
+ text: body
40
+ }
41
+ },
42
+ response: {
43
+ status: response.status,
44
+ headers: Array.from(clonedResponse.headers.entries()),
45
+ content: text
46
+ },
47
+ };
48
+ fetch(window.FTMOCKS_CONFIG.record_mocks_url, {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ },
53
+ body: JSON.stringify(mockResponse),
54
+ }).then(response => response.json());
55
+ addTrack({
56
+ type: mockResponse.method,
57
+ target: mockResponse.url,
58
+ });
59
+ }
60
+ });
61
+ return response;
62
+ };
63
+
64
+ // Intercept XMLHttpRequest
65
+ const originalXHR = window.XMLHttpRequest;
66
+
67
+ function MockXHR() {
68
+ const xhr = new originalXHR();
69
+ const originalOpen = xhr.open;
70
+ const originalSend = xhr.send;
71
+ const originalSetRequestHeader = xhr.setRequestHeader;
72
+ let requestDetails = {
73
+ headers: {},
74
+ };
75
+
76
+ // Override 'open' method
77
+ xhr.open = function (method, url, async, user, password) {
78
+ requestDetails.method = method;
79
+ requestDetails.url = url;
80
+ requestDetails.async = async;
81
+ requestDetails.user = user;
82
+ requestDetails.password = password;
83
+ requestDetails.queryString = url.includes('?') ? url.split('?')[1] : null;
84
+ originalOpen.apply(xhr, arguments);
85
+ };
86
+
87
+ // Override 'setRequestHeader' to log headers
88
+ xhr.setRequestHeader = function (header, value) {
89
+ requestDetails.headers[header] = value;
90
+ originalSetRequestHeader.apply(xhr, arguments);
91
+ };
92
+
93
+ // Override 'send' method
94
+ xhr.send = function (body) {
95
+ requestDetails.body = body;
96
+ const originalOnReadyStateChange = xhr.onreadystatechange;
97
+ xhr.onreadystatechange = function () {
98
+ if (xhr.readyState === 4) { // Complete
99
+ const ftMocksURL = new URL(window.FTMOCKS_CONFIG.record_mocks_url);
100
+ const currentURL = new URL(requestDetails.url.startsWith('http') ? requestDetails.url : `http://something/${requestDetails.url}`);
101
+ if (ftMocksURL.hostname !== currentURL.hostname) {
102
+ const mockResponse = {
103
+ url: requestDetails.url,
104
+ time: new Date().toString(),
105
+ method: requestDetails.method,
106
+ request: {
107
+ headers: requestDetails.headers,
108
+ queryString: requestDetails.queryString,
109
+ postData: {
110
+ mimeType: requestDetails.headers['Content-Type'] || null,
111
+ text: requestDetails.body
112
+ }
113
+ },
114
+ response: {
115
+ status: xhr.status,
116
+ headers: xhr.getAllResponseHeaders(),
117
+ content: xhr.responseText
118
+ },
119
+ };
120
+ fetch(window.FTMOCKS_CONFIG.record_mocks_url, {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': 'application/json',
124
+ },
125
+ body: JSON.stringify(mockResponse),
126
+ }).then(response => response.json());
127
+
128
+ }
129
+ }
130
+ if (originalOnReadyStateChange) originalOnReadyStateChange.apply(xhr, arguments);
131
+ };
132
+ originalSend.apply(xhr, arguments);
133
+ };
134
+
135
+ return xhr;
136
+ }
137
+
138
+ window.XMLHttpRequest = MockXHR;
139
+
140
+
141
+ const generateXPathWithNearestParentId = (element) => {
142
+ let path = '';
143
+ let nearestParentId = null;
144
+
145
+ // Check if the current element's has an ID
146
+ if (element.id) {
147
+ nearestParentId = element.id;
148
+ }
149
+
150
+ while (!nearestParentId && element !== document.body && element) {
151
+ const tagName = element.tagName.toLowerCase();
152
+ let index = 1;
153
+ let sibling = element.previousElementSibling;
154
+
155
+ while (sibling) {
156
+ if (sibling.tagName.toLowerCase() === tagName) {
157
+ index += 1;
158
+ }
159
+ sibling = sibling.previousElementSibling;
160
+ }
161
+
162
+ if (index === 1) {
163
+ path = `/${tagName}${path}`;
164
+ } else {
165
+ path = `/${tagName}[${index}]${path}`;
166
+ }
167
+
168
+ // Check if the current element's parent has an ID
169
+ if (element.parentElement && element.parentElement.id) {
170
+ nearestParentId = element.parentElement.id;
171
+ break; // Stop searching when we find the nearest parent with an ID
172
+ }
173
+
174
+ element = element.parentElement;
175
+ }
176
+
177
+ if (nearestParentId) {
178
+ path = `//*[@id='${nearestParentId}']${path}`;
179
+ return path;
180
+ }
181
+ return null; // No parent with an ID found
182
+ };
183
+
184
+ const handleMouseEvent = (type, limit) => event => {
185
+ const target = generateXPathWithNearestParentId(event.target);
186
+ const track = {
187
+ id: recordedTracks.length ? recordedTracks[recordedTracks.length - 1].id + 1 : 1,
188
+ type,
189
+ target,
190
+ time: new Date(),
191
+ };
192
+ if(recordedTracks.length > limit + 1) {
193
+ recordedTracks.shift();
194
+ }
195
+ recordedTracks.push(track);
196
+ addTrack(track);
197
+ };
198
+
199
+ const handleChange = limit => event => {
200
+ const prevCommand =
201
+ recordedTracks && recordedTracks.length ? recordedTracks[recordedTracks.length - 1] : null;
202
+ const target = generateXPathWithNearestParentId(event.target);
203
+ const track = {
204
+ id: recordedTracks.length ? recordedTracks[recordedTracks.length - 1].id + 1 : 1,
205
+ type: 'change',
206
+ target,
207
+ value: event.target.value,
208
+ time: new Date(),
209
+ };
210
+ if(recordedTracks.length > limit + 1) {
211
+ recordedTracks.shift();
212
+ }
213
+ if (
214
+ prevCommand &&
215
+ prevCommand.type === 'change' &&
216
+ prevCommand.target === target
217
+ ) {
218
+ recordedTracks.pop();
219
+ }
220
+ recordedTracks.push(track);
221
+ addTrack(track);
222
+ };
223
+
224
+ const handleDocumentLoad = limit => () => {
225
+ let oldHref = document.location.href;
226
+ const body = document.querySelector('body');
227
+ const observer = new MutationObserver(mutations => {
228
+ if (oldHref !== document.location.href) {
229
+ oldHref = document.location.href;
230
+ const track = {
231
+ id: recordedTracks.length ? recordedTracks[recordedTracks.length - 1].id + 1 : 1,
232
+ type: 'url',
233
+ value: oldHref,
234
+ time: new Date(),
235
+ };
236
+ if(recordedTracks.length > limit + 1) {
237
+ recordedTracks.shift();
238
+ }
239
+ recordedTracks.push(track);
240
+ addTrack(track);
241
+ }
242
+ });
243
+ observer.observe(body, { childList: true, subtree: true });
244
+ };
245
+
246
+ const clearTracks = () => {
247
+ recordedTracks = [];
248
+ };
249
+
250
+ const getAllTracks = () => {
251
+ return recordedTracks;
252
+ };
253
+
254
+ const initTracks = (initInfo = {events: ['click', 'change', 'url', 'dblclick', 'contextmenu'], limit: 100}) => {
255
+ const {events, limit} = initInfo;
256
+ const mouseEvents = {
257
+ click: handleMouseEvent('click', limit),
258
+ contextmenu: handleMouseEvent('contextmenu', limit),
259
+ dblclick: handleMouseEvent('dblclick', limit),
260
+ mousedown: handleMouseEvent('mousedown', limit),
261
+ mouseenter: handleMouseEvent('mouseenter', limit),
262
+ mouseleave: handleMouseEvent('mouseleave', limit),
263
+ mousemove: handleMouseEvent('mousemove', limit),
264
+ mouseout: handleMouseEvent('mouseout', limit),
265
+ mouseover: handleMouseEvent('mouseover', limit),
266
+ mouseup: handleMouseEvent('mouseup', limit),
267
+ };
268
+ events.forEach(e => {
269
+ if(e === 'url') {
270
+ window.onload = handleDocumentLoad(limit);
271
+ } else if (e === 'change') {
272
+ document.addEventListener('input', handleChange(limit));
273
+ } else {
274
+ document.addEventListener(e, mouseEvents[e]);
275
+ }
276
+ });
277
+ };
278
+ initTracks();
279
+
280
+ })();