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 +1 -1
- package/src/index.js +73 -7
- package/src/recorder.js +280 -0
package/package.json
CHANGED
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,
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
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
|
};
|
package/src/recorder.js
ADDED
|
@@ -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
|
+
})();
|