@dev-blinq/cucumber_client 1.0.1184-dev → 1.0.1184-stage
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/bin/assets/bundled_scripts/recorder.js +220 -0
- package/bin/assets/preload/accessibility.js +1 -1
- package/bin/assets/preload/find_context.js +1 -1
- package/bin/assets/preload/generateSelector.js +24 -0
- package/bin/assets/preload/locators.js +18 -0
- package/bin/assets/preload/recorderv3.js +80 -9
- package/bin/assets/preload/unique_locators.js +24 -3
- package/bin/assets/scripts/aria_snapshot.js +235 -0
- package/bin/assets/scripts/dom_attr.js +372 -0
- package/bin/assets/scripts/dom_element.js +0 -0
- package/bin/assets/scripts/dom_parent.js +185 -0
- package/bin/assets/scripts/event_utils.js +105 -0
- package/bin/assets/scripts/pw.js +7886 -0
- package/bin/assets/scripts/recorder.js +1147 -0
- package/bin/assets/scripts/snapshot_capturer.js +155 -0
- package/bin/assets/scripts/unique_locators.js +844 -0
- package/bin/assets/scripts/yaml.js +4770 -0
- package/bin/assets/templates/page_template.txt +2 -16
- package/bin/assets/templates/utils_template.txt +65 -12
- package/bin/client/cli_helpers.js +0 -1
- package/bin/client/code_cleanup/utils.js +43 -14
- package/bin/client/code_gen/code_inversion.js +44 -12
- package/bin/client/code_gen/index.js +3 -0
- package/bin/client/code_gen/page_reflection.js +37 -20
- package/bin/client/code_gen/playwright_codeget.js +149 -43
- package/bin/client/cucumber/feature.js +96 -42
- package/bin/client/cucumber/project_to_document.js +8 -7
- package/bin/client/cucumber/steps_definitions.js +49 -16
- package/bin/client/local_agent.js +9 -7
- package/bin/client/operations/dump_tree.js +159 -5
- package/bin/client/playground/playground.js +1 -1
- package/bin/client/project.js +6 -2
- package/bin/client/recorderv3/bvt_recorder.js +279 -81
- package/bin/client/recorderv3/cli.js +1 -0
- package/bin/client/recorderv3/implemented_steps.js +111 -11
- package/bin/client/recorderv3/index.js +48 -4
- package/bin/client/recorderv3/network.js +299 -0
- package/bin/client/recorderv3/step_runner.js +183 -13
- package/bin/client/recorderv3/step_utils.js +159 -14
- package/bin/client/recorderv3/update_feature.js +53 -28
- package/bin/client/recording.js +8 -0
- package/bin/client/run_cucumber.js +16 -2
- package/bin/client/scenario_report.js +112 -55
- package/bin/client/test_scenario.js +0 -1
- package/bin/index.js +1 -0
- package/package.json +15 -8
- package/bin/client/code_gen/get_implemented_steps.js +0 -27
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import url from "url";
|
|
5
5
|
import { findFilesWithExtension, StepsDefinitions } from "../cucumber/steps_definitions.js";
|
|
6
|
-
import { Feature } from "../cucumber/feature.js";
|
|
6
|
+
import { Feature, Step } from "../cucumber/feature.js";
|
|
7
7
|
import { CodePage } from "../code_gen/page_reflection.js";
|
|
8
|
+
import { getCommandsForImplementedStep, loadStepDefinitions } from "./step_utils.js";
|
|
9
|
+
import { parseStepTextParameters } from "../cucumber/utils.js";
|
|
8
10
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
9
11
|
let id = 0;
|
|
10
12
|
const uuidFn = () => (++id).toString(16);
|
|
11
13
|
const builder = new AstBuilder(uuidFn);
|
|
12
14
|
const matcher = new GherkinClassicTokenMatcher();
|
|
13
15
|
const parser = new Parser(builder, matcher);
|
|
14
|
-
|
|
15
16
|
let i = 0;
|
|
16
17
|
const getImplId = () => {
|
|
17
18
|
return `I-${i++}`;
|
|
@@ -55,6 +56,53 @@ function memorySizeOf(obj) {
|
|
|
55
56
|
return sizeOf(obj);
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
export function parseRouteFiles(projectDir, step) {
|
|
60
|
+
const routeFolder = path.join(projectDir, "data", "routes");
|
|
61
|
+
const templateRouteMap = new Map();
|
|
62
|
+
|
|
63
|
+
if (!existsSync(routeFolder)) {
|
|
64
|
+
step.routeItems = null;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Go over all the files in the route folder and parse them
|
|
69
|
+
const routeFiles = readdirSync(routeFolder).filter((file) => file.endsWith(".json"));
|
|
70
|
+
for (const file of routeFiles) {
|
|
71
|
+
const filePath = path.join(routeFolder, file);
|
|
72
|
+
const routeData = JSON.parse(readFileSync(filePath, "utf8"));
|
|
73
|
+
if (routeData && routeData.template) {
|
|
74
|
+
const template = routeData.template;
|
|
75
|
+
const routes = routeData.routes;
|
|
76
|
+
|
|
77
|
+
templateRouteMap.set(template, routes);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!existsSync(routeFolder)) {
|
|
82
|
+
return null;
|
|
83
|
+
} else if (step && step.text) {
|
|
84
|
+
// Convert the step text to cucumber template
|
|
85
|
+
const cucumberStep = new Step();
|
|
86
|
+
cucumberStep.text = step.text;
|
|
87
|
+
const template = cucumberStep.getTemplate();
|
|
88
|
+
if (templateRouteMap.has(template)) {
|
|
89
|
+
const routeItems = templateRouteMap.get(template);
|
|
90
|
+
routeItems.forEach((item) => {
|
|
91
|
+
const filters = item.filters || {};
|
|
92
|
+
const queryParams = filters?.queryParams || {};
|
|
93
|
+
const queryParamsArray = Object.keys(queryParams).map((key) => ({
|
|
94
|
+
key: key,
|
|
95
|
+
value: queryParams[key],
|
|
96
|
+
}));
|
|
97
|
+
filters.queryParams = queryParamsArray || [];
|
|
98
|
+
});
|
|
99
|
+
step.routeItems = routeItems;
|
|
100
|
+
} else {
|
|
101
|
+
step.routeItems = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
58
106
|
export const getImplementedSteps = async (projectDir) => {
|
|
59
107
|
const foundErrors = [];
|
|
60
108
|
try {
|
|
@@ -166,7 +214,10 @@ export const getImplementedSteps = async (projectDir) => {
|
|
|
166
214
|
}
|
|
167
215
|
stepLineSet.add(stepLine);
|
|
168
216
|
step.templateIndex = implementedSteps.length;
|
|
169
|
-
|
|
217
|
+
|
|
218
|
+
parseRouteFiles(projectDir, step);
|
|
219
|
+
|
|
220
|
+
const implementedStep = {
|
|
170
221
|
keyword: step.keyword.trim(),
|
|
171
222
|
keywordAlias: step.keywordAlias?.trim(),
|
|
172
223
|
text: updateStepText(template.pattern, step.parameters),
|
|
@@ -177,7 +228,10 @@ export const getImplementedSteps = async (projectDir) => {
|
|
|
177
228
|
templateIndex: step.templateIndex,
|
|
178
229
|
pattern: template.pattern,
|
|
179
230
|
paths: template.paths,
|
|
180
|
-
|
|
231
|
+
routeItems: step.routeItems,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
implementedSteps.push(implementedStep);
|
|
181
235
|
}
|
|
182
236
|
}
|
|
183
237
|
|
|
@@ -231,12 +285,19 @@ export const getImplementedSteps = async (projectDir) => {
|
|
|
231
285
|
delete scenario.featureText;
|
|
232
286
|
delete scenario.scenarioDocument;
|
|
233
287
|
delete scenario.examples;
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
288
|
+
for (const tag of scenario.tags) {
|
|
289
|
+
delete tag.location;
|
|
290
|
+
}
|
|
291
|
+
for (const scenario of scenarios) {
|
|
292
|
+
for (const step of scenario.steps) {
|
|
293
|
+
if (step.templateIndex === undefined) {
|
|
294
|
+
const cleanStepName = stepsDefinitions._stepNameToTemplate(step.text);
|
|
295
|
+
const index = implementedSteps.findIndex((istep) => {
|
|
296
|
+
return cleanStepName === istep.pattern;
|
|
297
|
+
});
|
|
298
|
+
step.templateIndex = index;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
240
301
|
}
|
|
241
302
|
}
|
|
242
303
|
if (foundErrors.length > 0) {
|
|
@@ -246,3 +307,42 @@ export const getImplementedSteps = async (projectDir) => {
|
|
|
246
307
|
console.log("Size of scenarios", memorySizeOf(scenarios));
|
|
247
308
|
return { implementedSteps, scenarios };
|
|
248
309
|
};
|
|
310
|
+
|
|
311
|
+
export const getStepsAndCommandsForScenario = ({ name, featureName, projectDir, map }) => {
|
|
312
|
+
const stepsDefinitions = new StepsDefinitions(projectDir);
|
|
313
|
+
const step_definitions = loadStepDefinitions(projectDir);
|
|
314
|
+
stepsDefinitions.load();
|
|
315
|
+
const featureFilePath = path.join(projectDir, "features", featureName.trim() + ".feature");
|
|
316
|
+
if (!existsSync(featureFilePath)) {
|
|
317
|
+
throw new Error(`Feature file ${featureFilePath} not found`);
|
|
318
|
+
}
|
|
319
|
+
const content = readFileSync(featureFilePath, "utf8");
|
|
320
|
+
const doc = parser.parse(content);
|
|
321
|
+
const feature = new Feature(doc, content);
|
|
322
|
+
const scenario = feature.getScenario(name);
|
|
323
|
+
if (!scenario) {
|
|
324
|
+
throw new Error(`Scenario ${name} not found in feature ${featureName}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (const step of scenario.steps) {
|
|
328
|
+
const stepName = step.text;
|
|
329
|
+
const stepParams = parseStepTextParameters(stepName);
|
|
330
|
+
step.commands = [];
|
|
331
|
+
try {
|
|
332
|
+
const stepDefinition = stepsDefinitions.findMatchingStep(stepName);
|
|
333
|
+
if (stepDefinition) {
|
|
334
|
+
step.isImplemented = true;
|
|
335
|
+
const commands = getCommandsForImplementedStep(stepName, step_definitions, stepParams).commands;
|
|
336
|
+
step.commands = commands;
|
|
337
|
+
} else {
|
|
338
|
+
step.isImplemented = false;
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error(`Error getting step definition for ${stepName}`, error);
|
|
342
|
+
step.isImplemented = false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
console.log("Scenario steps size", memorySizeOf(scenario.steps));
|
|
346
|
+
|
|
347
|
+
return { steps: scenario.steps };
|
|
348
|
+
};
|
|
@@ -51,6 +51,37 @@ class PromisifiedSocketServer {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function memorySizeOf(obj) {
|
|
55
|
+
var bytes = 0;
|
|
56
|
+
|
|
57
|
+
function sizeOf(obj) {
|
|
58
|
+
if (obj !== null && obj !== undefined) {
|
|
59
|
+
switch (typeof obj) {
|
|
60
|
+
case "number":
|
|
61
|
+
bytes += 8;
|
|
62
|
+
break;
|
|
63
|
+
case "string":
|
|
64
|
+
bytes += obj.length * 2;
|
|
65
|
+
break;
|
|
66
|
+
case "boolean":
|
|
67
|
+
bytes += 4;
|
|
68
|
+
break;
|
|
69
|
+
case "object":
|
|
70
|
+
var objClass = Object.prototype.toString.call(obj).slice(8, -1);
|
|
71
|
+
if (objClass === "Object" || objClass === "Array") {
|
|
72
|
+
for (var key in obj) {
|
|
73
|
+
if (!obj.hasOwnProperty(key)) continue;
|
|
74
|
+
sizeOf(obj[key]);
|
|
75
|
+
}
|
|
76
|
+
} else bytes += obj.toString().length * 2;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return bytes;
|
|
81
|
+
}
|
|
82
|
+
return sizeOf(obj);
|
|
83
|
+
}
|
|
84
|
+
|
|
54
85
|
const init = ({ envName, projectDir, roomId, TOKEN }) => {
|
|
55
86
|
console.log("connecting to " + WS_URL);
|
|
56
87
|
const socket = io(WS_URL);
|
|
@@ -66,6 +97,7 @@ const init = ({ envName, projectDir, roomId, TOKEN }) => {
|
|
|
66
97
|
projectDir,
|
|
67
98
|
TOKEN,
|
|
68
99
|
sendEvent: (event, data) => {
|
|
100
|
+
console.log("Size of data", memorySizeOf(data), "bytes");
|
|
69
101
|
console.log("----", event, data, "roomId", roomId);
|
|
70
102
|
socket.emit(event, data, roomId);
|
|
71
103
|
},
|
|
@@ -77,7 +109,7 @@ const init = ({ envName, projectDir, roomId, TOKEN }) => {
|
|
|
77
109
|
socket.emit("BVTRecorder.browserOpened", null, roomId);
|
|
78
110
|
})
|
|
79
111
|
.catch((e) => {
|
|
80
|
-
socket.emit("BVTRecorder.browserLaunchFailed",
|
|
112
|
+
socket.emit("BVTRecorder.browserLaunchFailed", e, roomId);
|
|
81
113
|
});
|
|
82
114
|
const timeOutForFunction = async (promise, timeout = 5000) => {
|
|
83
115
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(), timeout));
|
|
@@ -139,9 +171,6 @@ const init = ({ envName, projectDir, roomId, TOKEN }) => {
|
|
|
139
171
|
"recorderWindow.runStep": async (input) => {
|
|
140
172
|
return recorder.runStep(input);
|
|
141
173
|
},
|
|
142
|
-
"recorderWindow.runScenario": async (input) => {
|
|
143
|
-
return recorder.runScenario(input);
|
|
144
|
-
},
|
|
145
174
|
"recorderWindow.saveScenario": async (input) => {
|
|
146
175
|
return recorder.saveScenario(input);
|
|
147
176
|
},
|
|
@@ -239,6 +268,21 @@ const init = ({ envName, projectDir, roomId, TOKEN }) => {
|
|
|
239
268
|
const mode = input?.mode;
|
|
240
269
|
return recorder.setMode(mode);
|
|
241
270
|
},
|
|
271
|
+
"recorderWindow.getStepsAndCommandsForScenario": async (input) => {
|
|
272
|
+
return await recorder.getStepsAndCommandsForScenario(input);
|
|
273
|
+
},
|
|
274
|
+
"recorderWindow.getNetworkEvents": async (input) => {
|
|
275
|
+
return await recorder.getNetworkEvents(input);
|
|
276
|
+
},
|
|
277
|
+
"recorderWindow.initExecution": async (input) => {
|
|
278
|
+
return await recorder.initExecution(input);
|
|
279
|
+
},
|
|
280
|
+
"recorderWindow.cleanupExecution": async (input) => {
|
|
281
|
+
return await recorder.cleanupExecution(input);
|
|
282
|
+
},
|
|
283
|
+
"recorderWindow.resetExecution": async (input) => {
|
|
284
|
+
return await recorder.resetExecution(input);
|
|
285
|
+
},
|
|
242
286
|
});
|
|
243
287
|
socket.on("targetBrowser.command.event", async (input) => {
|
|
244
288
|
return recorder.onAction(input);
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} NetworkEvent
|
|
3
|
+
* @property {import('playwright').Request} request
|
|
4
|
+
* @property {import('playwright').Response|null} response
|
|
5
|
+
* @property {string} id
|
|
6
|
+
* @property {number} timestamp
|
|
7
|
+
* @property {string} status - 'completed', 'failed'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class NetworkMonitor {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.networkId = 0;
|
|
13
|
+
/** @type {Map<string, NetworkEvent>} */
|
|
14
|
+
this.requestIdMap = new Map();
|
|
15
|
+
this.networkEvents = new Map();
|
|
16
|
+
/** @type {Map<import('playwright').Page, {responseListener: Function, requestFailedListener: Function}>} */
|
|
17
|
+
this.pageListeners = new Map();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Add network event listeners to a page
|
|
22
|
+
* @param {import('playwright').Page} page
|
|
23
|
+
*/
|
|
24
|
+
addNetworkEventListener(page) {
|
|
25
|
+
// Create the listener functions
|
|
26
|
+
const responseListener = async (response) => {
|
|
27
|
+
const request = response.request();
|
|
28
|
+
let eventId = this.requestIdMap.get(request);
|
|
29
|
+
|
|
30
|
+
if (!eventId) {
|
|
31
|
+
this.networkId++;
|
|
32
|
+
eventId = this.networkId.toString();
|
|
33
|
+
this.requestIdMap.set(request, eventId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const networkEvent = {
|
|
37
|
+
id: eventId,
|
|
38
|
+
request,
|
|
39
|
+
response,
|
|
40
|
+
timestamp: Date.now(),
|
|
41
|
+
status: "completed",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
this.networkEvents.set(eventId, networkEvent);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const requestFailedListener = (request) => {
|
|
48
|
+
let eventId = this.requestIdMap.get(request);
|
|
49
|
+
|
|
50
|
+
if (!eventId) {
|
|
51
|
+
this.networkId++;
|
|
52
|
+
eventId = this.networkId.toString();
|
|
53
|
+
this.requestIdMap.set(request, eventId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const networkEvent = {
|
|
57
|
+
id: eventId,
|
|
58
|
+
request,
|
|
59
|
+
response: null,
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
status: "failed",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
this.networkEvents.set(eventId, networkEvent);
|
|
65
|
+
};
|
|
66
|
+
// Store the listeners for later removal
|
|
67
|
+
this.pageListeners.set(page, {
|
|
68
|
+
responseListener,
|
|
69
|
+
requestFailedListener,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Add the listeners to the page
|
|
73
|
+
page.on("response", responseListener);
|
|
74
|
+
page.on("requestfailed", requestFailedListener);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Remove network event listeners from a specific page
|
|
79
|
+
* @param {import('playwright').Page} page
|
|
80
|
+
*/
|
|
81
|
+
removeNetworkEventListener(page) {
|
|
82
|
+
const listeners = this.pageListeners.get(page);
|
|
83
|
+
if (listeners) {
|
|
84
|
+
page.off("response", listeners.responseListener);
|
|
85
|
+
page.off("requestfailed", listeners.requestFailedListener);
|
|
86
|
+
this.pageListeners.delete(page);
|
|
87
|
+
console.log("Network event listeners removed from page");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Remove network event listeners from all pages
|
|
93
|
+
*/
|
|
94
|
+
removeAllNetworkEventListeners() {
|
|
95
|
+
for (const [page, listeners] of this.pageListeners) {
|
|
96
|
+
page.off("response", listeners.responseListener);
|
|
97
|
+
page.off("requestfailed", listeners.requestFailedListener);
|
|
98
|
+
}
|
|
99
|
+
this.pageListeners.clear();
|
|
100
|
+
console.log("All network event listeners removed");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if a page has active listeners
|
|
105
|
+
* @param {import('playwright').Page} page
|
|
106
|
+
* @returns {boolean}
|
|
107
|
+
*/
|
|
108
|
+
hasListeners(page) {
|
|
109
|
+
return this.pageListeners.has(page);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the number of pages with active listeners
|
|
114
|
+
* @returns {number}
|
|
115
|
+
*/
|
|
116
|
+
getActiveListenersCount() {
|
|
117
|
+
return this.pageListeners.size;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get all network events until now
|
|
122
|
+
*/
|
|
123
|
+
getAllNetworkEvents() {
|
|
124
|
+
return Array.from(this.networkEvents.values());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get network events within a range
|
|
129
|
+
* @param {number} startId
|
|
130
|
+
* @param {number} endId
|
|
131
|
+
* @returns {NetworkEvent[]}
|
|
132
|
+
*/
|
|
133
|
+
getNetworkEventsInRange(startId, endId) {
|
|
134
|
+
const events = [];
|
|
135
|
+
for (let i = startId; i <= endId; i++) {
|
|
136
|
+
const event = this.networkEvents.get(i.toString());
|
|
137
|
+
if (event) {
|
|
138
|
+
events.push(event);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return events;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get network events since a specific ID
|
|
146
|
+
* @param {number} sinceId
|
|
147
|
+
* @returns {NetworkEvent[]}
|
|
148
|
+
*/
|
|
149
|
+
getNetworkEventsSince(sinceId) {
|
|
150
|
+
return this.getNetworkEventsInRange(sinceId + 1, this.networkId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get events by status
|
|
155
|
+
* @param {string} status - 'completed', 'failed'
|
|
156
|
+
* @returns {NetworkEvent[]}
|
|
157
|
+
*/
|
|
158
|
+
getEventsByStatus(status) {
|
|
159
|
+
return Array.from(this.networkEvents.values()).filter((event) => event.status === status);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get current network ID (latest)
|
|
164
|
+
* @returns {number}
|
|
165
|
+
*/
|
|
166
|
+
getCurrentNetworkId() {
|
|
167
|
+
return this.networkId;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
getCurrentNetworkEventsLength() {
|
|
171
|
+
return this.networkEvents.size;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get statistics about stored events
|
|
176
|
+
* @returns {Object}
|
|
177
|
+
*/
|
|
178
|
+
getStats() {
|
|
179
|
+
const events = Array.from(this.networkEvents.values());
|
|
180
|
+
return {
|
|
181
|
+
total: events.length,
|
|
182
|
+
completed: events.filter((e) => e.status === "completed").length,
|
|
183
|
+
failed: events.filter((e) => e.status === "failed").length,
|
|
184
|
+
oldestTimestamp: events.length > 0 ? Math.min(...events.map((e) => e.timestamp)) : null,
|
|
185
|
+
newestTimestamp: events.length > 0 ? Math.max(...events.map((e) => e.timestamp)) : null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Marshall network event for serialization
|
|
191
|
+
* @param {NetworkEvent} networkEvent
|
|
192
|
+
* @returns {Promise<Object>}
|
|
193
|
+
*/
|
|
194
|
+
async marshallNetworkEvent(networkEvent) {
|
|
195
|
+
const { request, response } = networkEvent;
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const url = new URL(request.url());
|
|
199
|
+
const marshalledEvent = {
|
|
200
|
+
id: networkEvent.id,
|
|
201
|
+
timestamp: networkEvent.timestamp,
|
|
202
|
+
status: networkEvent.status,
|
|
203
|
+
url: url.href,
|
|
204
|
+
method: request.method(),
|
|
205
|
+
statusCode: response?.status() ?? null,
|
|
206
|
+
statusText: response?.statusText() ?? null,
|
|
207
|
+
queryParams: Object.fromEntries(url.searchParams.entries()),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Try to get response body safely (only for successful responses)
|
|
211
|
+
if (response && networkEvent.status === "completed") {
|
|
212
|
+
try {
|
|
213
|
+
const isBinary =
|
|
214
|
+
!response.headers()["content-type"]?.includes("application/json") &&
|
|
215
|
+
!response.headers()["content-type"]?.includes("text");
|
|
216
|
+
let body;
|
|
217
|
+
if (isBinary) {
|
|
218
|
+
body = await response.body();
|
|
219
|
+
} else {
|
|
220
|
+
body = await response.text();
|
|
221
|
+
}
|
|
222
|
+
let json;
|
|
223
|
+
try {
|
|
224
|
+
if (typeof body === "string") {
|
|
225
|
+
json = JSON.parse(body); // Check if body is valid JSON
|
|
226
|
+
}
|
|
227
|
+
} catch (_) {
|
|
228
|
+
//Ignore
|
|
229
|
+
}
|
|
230
|
+
const responseBody = isBinary ? body : json ? JSON.stringify(json) : body;
|
|
231
|
+
marshalledEvent.contentType = isBinary ? "binary" : json ? "json" : "text";
|
|
232
|
+
marshalledEvent.responseBody = responseBody;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
marshalledEvent.responseBody = `[Error reading response: ${error.message}]`;
|
|
235
|
+
}
|
|
236
|
+
} else if (networkEvent.status === "failed") {
|
|
237
|
+
marshalledEvent.failureReason = request.failure()?.errorText || "Unknown error";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log("Marshalled network event:", marshalledEvent);
|
|
241
|
+
|
|
242
|
+
return marshalledEvent;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error("Error marshalling network event:", error);
|
|
245
|
+
return {
|
|
246
|
+
id: networkEvent.id,
|
|
247
|
+
timestamp: networkEvent.timestamp,
|
|
248
|
+
status: networkEvent.status,
|
|
249
|
+
url: request.url(),
|
|
250
|
+
method: request.method(),
|
|
251
|
+
error: error.message,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
*@returns {Promise<Object[]>}
|
|
258
|
+
* Get all marshalled network events
|
|
259
|
+
* This is useful for sending to the server or saving to a file.
|
|
260
|
+
* */
|
|
261
|
+
async getAllMarshalledNetworkEvents() {
|
|
262
|
+
const events = this.getAllNetworkEvents();
|
|
263
|
+
const marshalledEvents = await Promise.all(events.map((event) => this.marshallNetworkEvent(event)));
|
|
264
|
+
return marshalledEvents;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get marshalled network events since this ID
|
|
269
|
+
* @returns {Promise<Object[]>}
|
|
270
|
+
* @param {number} sinceId
|
|
271
|
+
*/
|
|
272
|
+
async getMarshalledNetworkEvents(sinceId) {
|
|
273
|
+
const events = this.getNetworkEventsSince(sinceId);
|
|
274
|
+
const marshalledEvents = await Promise.all(events.map((event) => this.marshallNetworkEvent(event)));
|
|
275
|
+
return marshalledEvents;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get marshalled network events in a range
|
|
280
|
+
* @param {number} startId
|
|
281
|
+
* @param {number} endId
|
|
282
|
+
* @returns {Promise<Object[]>}
|
|
283
|
+
*/
|
|
284
|
+
async getMarshalledNetworkEventsInRange(startId, endId) {
|
|
285
|
+
const events = this.getNetworkEventsInRange(startId, endId);
|
|
286
|
+
const marshalledEvents = await Promise.all(events.map((event) => this.marshallNetworkEvent(event)));
|
|
287
|
+
return marshalledEvents;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Clear all network events
|
|
292
|
+
*/
|
|
293
|
+
clearNetworkEvents() {
|
|
294
|
+
this.networkEvents.clear();
|
|
295
|
+
this.networkId = 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export default NetworkMonitor;
|