@dev-blinq/cucumber_client 1.0.1312-dev → 1.0.1313-dev
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 +50 -50
- package/bin/assets/scripts/unique_locators.js +8 -2
- package/bin/client/code_gen/playwright_codeget.js +23 -4
- package/bin/client/recorderv3/bvt_recorder.js +36 -7
- package/bin/client/recorderv3/index.js +37 -1
- package/bin/client/recorderv3/network.js +299 -0
- package/bin/client/recorderv3/step_runner.js +116 -2
- package/bin/client/recorderv3/step_utils.js +70 -2
- package/package.json +2 -2
|
@@ -34,6 +34,11 @@ function cssEscapeCharacter(s, i) {
|
|
|
34
34
|
return s.charAt(i);
|
|
35
35
|
return "\\" + s.charAt(i);
|
|
36
36
|
}
|
|
37
|
+
function escapeRegexForSelector(re) {
|
|
38
|
+
if (re.unicode || re.unicodeSets)
|
|
39
|
+
return String(re);
|
|
40
|
+
return String(re).replace(/(^|[^\\])(\\\\)*(["'`])/g, "$1$2\\$3").replace(/>>/g, "\\>\\>");
|
|
41
|
+
}
|
|
37
42
|
class LocatorGenerator {
|
|
38
43
|
constructor(injectedScript, options = {}) {
|
|
39
44
|
this.locatorStrategies = {
|
|
@@ -356,8 +361,9 @@ class LocatorGenerator {
|
|
|
356
361
|
hasDigitsInText = digitsRegex.test(text);
|
|
357
362
|
|
|
358
363
|
let pattern = this.PW.stringUtils.escapeRegExp(text.substring(1, text.length - 2));
|
|
359
|
-
|
|
360
|
-
|
|
364
|
+
const re = new RegExp("^" + pattern + "$");
|
|
365
|
+
|
|
366
|
+
finalSelector += `internal:text=${escapeRegexForSelector(re)} >> `;
|
|
361
367
|
}
|
|
362
368
|
if (!hasDigitsInText) {
|
|
363
369
|
continue;
|
|
@@ -674,6 +674,22 @@ const _generateCodeFromCommand = (step, elements, userData) => {
|
|
|
674
674
|
return { codeLines, elements: elementsChanged ? elements : null, elementIdentifier, allStrategyLocators };
|
|
675
675
|
};
|
|
676
676
|
|
|
677
|
+
/**
|
|
678
|
+
* Generates a report command based on the given position.
|
|
679
|
+
* @param {"start"|"end"} position
|
|
680
|
+
*/
|
|
681
|
+
const generateReportCommand = (position, cmdId) => {
|
|
682
|
+
const codeLines = [];
|
|
683
|
+
if (position === "start") {
|
|
684
|
+
const line = `await context.web.addCommandToReport("${cmdId}", "PASSED", '{"status":"start","cmdId":"${cmdId}"}', {type:"cmdReport"},this);`;
|
|
685
|
+
codeLines.push(line);
|
|
686
|
+
} else if (position === "end") {
|
|
687
|
+
const line = `await context.web.addCommandToReport("${cmdId}", "PASSED", '{"status":"end","cmdId":"${cmdId}"}', {type:"cmdReport"},this);`;
|
|
688
|
+
codeLines.push(line);
|
|
689
|
+
}
|
|
690
|
+
return codeLines;
|
|
691
|
+
};
|
|
692
|
+
|
|
677
693
|
const generateCode = (recording, codePage, userData, projectDir, methodName) => {
|
|
678
694
|
const stepsDefinitions = new StepsDefinitions(projectDir);
|
|
679
695
|
stepsDefinitions.load(false);
|
|
@@ -714,12 +730,15 @@ const generateCode = (recording, codePage, userData, projectDir, methodName) =>
|
|
|
714
730
|
if (recordingStep.type === Types.COMPLETE) {
|
|
715
731
|
return;
|
|
716
732
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
733
|
+
|
|
734
|
+
if (process.env.TEMP_RUN === "true") {
|
|
735
|
+
codeLines.push(...generateReportCommand("start", recordingStep.cmdId));
|
|
736
|
+
}
|
|
721
737
|
const result = _generateCodeFromCommand(recordingStep, elements, userData);
|
|
722
738
|
codeLines.push(...result.codeLines);
|
|
739
|
+
if (process.env.TEMP_RUN === "true") {
|
|
740
|
+
codeLines.push(...generateReportCommand("end", recordingStep.cmdId));
|
|
741
|
+
}
|
|
723
742
|
if (result.elements) {
|
|
724
743
|
elements = result.elements;
|
|
725
744
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// define the jsdoc type for the input
|
|
2
2
|
import { closeContext, initContext, _getDataFile, resetTestData } from "automation_model";
|
|
3
|
-
import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
|
|
3
|
+
import { existsSync, mkdir, mkdirSync, readdirSync, readFileSync, rmSync } from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import url from "url";
|
|
6
6
|
import { getImplementedSteps, getStepsAndCommandsForScenario } from "./implemented_steps.js";
|
|
@@ -15,6 +15,7 @@ import chokidar from "chokidar";
|
|
|
15
15
|
import logger from "../../logger.js";
|
|
16
16
|
import { unEscapeNonPrintables } from "../cucumber/utils.js";
|
|
17
17
|
import { findAvailablePort } from "../utils/index.js";
|
|
18
|
+
import NetworkMonitor from "./network.js";
|
|
18
19
|
|
|
19
20
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
20
21
|
|
|
@@ -185,12 +186,33 @@ export class BVTRecorder {
|
|
|
185
186
|
});
|
|
186
187
|
this.stepRunner = new BVTStepRunner({
|
|
187
188
|
projectDir: this.projectDir,
|
|
189
|
+
sendExecutionStatus: (data) => {
|
|
190
|
+
if (data && data.type) {
|
|
191
|
+
switch (data.type) {
|
|
192
|
+
case "cmdExecutionStart":
|
|
193
|
+
console.log("Sending cmdExecutionStart event for cmdId:", data.cmdId);
|
|
194
|
+
this.sendEvent(this.events.cmdExecutionStart, data.cmdId);
|
|
195
|
+
break;
|
|
196
|
+
case "cmdExecutionSuccess":
|
|
197
|
+
console.log("Sending cmdExecutionSuccess event for cmdId:", data.cmdId);
|
|
198
|
+
this.sendEvent(this.events.cmdExecutionSuccess, data.cmdId);
|
|
199
|
+
break;
|
|
200
|
+
case "cmdExecutionFailure":
|
|
201
|
+
console.log("Sending cmdExecutionFailure event for cmdId:", data.cmdId);
|
|
202
|
+
this.sendEvent(this.events.cmdExecutionFailure, data.cmdId);
|
|
203
|
+
break;
|
|
204
|
+
default:
|
|
205
|
+
console.warn("Unknown command execution status type:", data.type);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
},
|
|
188
210
|
});
|
|
189
211
|
this.pageSet = new Set();
|
|
190
|
-
|
|
212
|
+
this.networkMonitor = new NetworkMonitor();
|
|
191
213
|
this.lastKnownUrlPath = "";
|
|
192
214
|
// TODO: what is world?
|
|
193
|
-
this.world = { attach: () => {
|
|
215
|
+
this.world = { attach: () => {} };
|
|
194
216
|
this.shouldTakeScreenshot = true;
|
|
195
217
|
this.watcher = null;
|
|
196
218
|
}
|
|
@@ -203,6 +225,9 @@ export class BVTRecorder {
|
|
|
203
225
|
onStepDetails: "BVTRecorder.onStepDetails",
|
|
204
226
|
getTestData: "BVTRecorder.getTestData",
|
|
205
227
|
onGoto: "BVTRecorder.onGoto",
|
|
228
|
+
cmdExecutionStart: "BVTRecorder.cmdExecutionStart",
|
|
229
|
+
cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
|
|
230
|
+
cmdExecutionFailure: "BVTRecorder.cmdExecutionFailure",
|
|
206
231
|
};
|
|
207
232
|
bindings = {
|
|
208
233
|
__bvt_recordCommand: async ({ frame, page, context }, event) => {
|
|
@@ -294,7 +319,7 @@ export class BVTRecorder {
|
|
|
294
319
|
process.env.CDP_LISTEN_PORT = this.#remoteDebuggerPort;
|
|
295
320
|
|
|
296
321
|
this.stepRunner.setRemoteDebugPort(this.#remoteDebuggerPort);
|
|
297
|
-
this.world = { attach: () => {
|
|
322
|
+
this.world = { attach: () => {} };
|
|
298
323
|
|
|
299
324
|
const ai_config_file = path.join(this.projectDir, "ai_config.json");
|
|
300
325
|
let ai_config = {};
|
|
@@ -700,7 +725,7 @@ export class BVTRecorder {
|
|
|
700
725
|
}
|
|
701
726
|
async closeBrowser() {
|
|
702
727
|
delete process.env.TEMP_RUN;
|
|
703
|
-
await this.watcher.close().then(() => {
|
|
728
|
+
await this.watcher.close().then(() => {});
|
|
704
729
|
this.watcher = null;
|
|
705
730
|
this.previousIndex = null;
|
|
706
731
|
this.previousHistoryLength = null;
|
|
@@ -789,30 +814,34 @@ export class BVTRecorder {
|
|
|
789
814
|
}, 100);
|
|
790
815
|
this.timerId = timerId;
|
|
791
816
|
}
|
|
792
|
-
async runStep({ step, parametersMap, tags, isFirstStep }, options) {
|
|
817
|
+
async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
|
|
793
818
|
const _env = {
|
|
794
819
|
TOKEN: this.TOKEN,
|
|
795
820
|
TEMP_RUN: true,
|
|
796
821
|
REPORT_FOLDER: this.bvtContext.reportFolder,
|
|
797
822
|
BLINQ_ENV: this.envName,
|
|
823
|
+
STORE_DETAILED_NETWORK_DATA: listenNetwork ? "true" : "false",
|
|
824
|
+
CURRENT_STEP_ID: step.id,
|
|
798
825
|
};
|
|
799
826
|
|
|
800
827
|
this.bvtContext.navigate = true;
|
|
801
828
|
for (const [key, value] of Object.entries(_env)) {
|
|
802
829
|
process.env[key] = value;
|
|
803
830
|
}
|
|
831
|
+
|
|
804
832
|
if (this.timerId) {
|
|
805
833
|
clearTimeout(this.timerId);
|
|
806
834
|
this.timerId = null;
|
|
807
835
|
}
|
|
808
836
|
await this.setMode("running");
|
|
837
|
+
|
|
809
838
|
try {
|
|
810
839
|
const { result, info } = await this.stepRunner.runStep(
|
|
811
840
|
{
|
|
812
841
|
step,
|
|
813
842
|
parametersMap,
|
|
814
843
|
envPath: this.envName,
|
|
815
|
-
tags
|
|
844
|
+
tags,
|
|
816
845
|
},
|
|
817
846
|
this.bvtContext,
|
|
818
847
|
options ? { ...options, skipBefore: !isFirstStep } : { skipBefore: !isFirstStep }
|
|
@@ -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,8 +97,10 @@ 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);
|
|
103
|
+
console.log("Successfully sent event", event, "to room", roomId);
|
|
71
104
|
},
|
|
72
105
|
});
|
|
73
106
|
recorder
|
|
@@ -77,7 +110,7 @@ const init = ({ envName, projectDir, roomId, TOKEN }) => {
|
|
|
77
110
|
socket.emit("BVTRecorder.browserOpened", null, roomId);
|
|
78
111
|
})
|
|
79
112
|
.catch((e) => {
|
|
80
|
-
socket.emit("BVTRecorder.browserLaunchFailed",
|
|
113
|
+
socket.emit("BVTRecorder.browserLaunchFailed", e, roomId);
|
|
81
114
|
});
|
|
82
115
|
const timeOutForFunction = async (promise, timeout = 5000) => {
|
|
83
116
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(), timeout));
|
|
@@ -239,6 +272,9 @@ const init = ({ envName, projectDir, roomId, TOKEN }) => {
|
|
|
239
272
|
"recorderWindow.getStepsAndCommandsForScenario": async (input) => {
|
|
240
273
|
return await recorder.getStepsAndCommandsForScenario(input);
|
|
241
274
|
},
|
|
275
|
+
"recorderWindow.getNetworkEvents": async (input) => {
|
|
276
|
+
return await recorder.getNetworkEvents(input);
|
|
277
|
+
},
|
|
242
278
|
});
|
|
243
279
|
socket.on("targetBrowser.command.event", async (input) => {
|
|
244
280
|
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;
|
|
@@ -33,8 +33,9 @@ async function withAbort(fn, signal) {
|
|
|
33
33
|
export class BVTStepRunner {
|
|
34
34
|
#currentStepController;
|
|
35
35
|
#port;
|
|
36
|
-
constructor({ projectDir }) {
|
|
36
|
+
constructor({ projectDir, sendExecutionStatus }) {
|
|
37
37
|
this.projectDir = projectDir;
|
|
38
|
+
this.sendExecutionStatus = sendExecutionStatus;
|
|
38
39
|
}
|
|
39
40
|
setRemoteDebugPort(port) {
|
|
40
41
|
this.#port = port;
|
|
@@ -74,6 +75,119 @@ export class BVTStepRunner {
|
|
|
74
75
|
writeFileSync(tFilePath, tFileContent);
|
|
75
76
|
return tFilePath;
|
|
76
77
|
}
|
|
78
|
+
|
|
79
|
+
executeStepRemote = async ({ feature_file_path, scenario, tempFolderPath, stepText }, options) => {
|
|
80
|
+
if (!options) {
|
|
81
|
+
options = {
|
|
82
|
+
skipAfter: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const environment = {
|
|
86
|
+
...process.env,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const { loadConfiguration, loadSupport, runCucumber } = await import("@dev-blinq/cucumber-js/api");
|
|
91
|
+
const { runConfiguration } = await loadConfiguration(
|
|
92
|
+
{
|
|
93
|
+
provided: {
|
|
94
|
+
name: [scenario],
|
|
95
|
+
paths: [feature_file_path],
|
|
96
|
+
import: [path.join(tempFolderPath, "step_definitions", "**", "*.mjs")],
|
|
97
|
+
// format: ["bvt"],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{ cwd: process.cwd(), env: environment }
|
|
101
|
+
);
|
|
102
|
+
// const files = glob.sync(path.join(tempFolderPath, "step_definitions", "**", "*.mjs"));
|
|
103
|
+
// console.log("Files found:", files);
|
|
104
|
+
const support = await loadSupport(runConfiguration, { cwd: process.cwd(), env: environment });
|
|
105
|
+
// console.log("found ", support.stepDefinitions.length, "step definitions");
|
|
106
|
+
// support.stepDefinitions.map((step) => {
|
|
107
|
+
// console.log("step", step.pattern);
|
|
108
|
+
// });
|
|
109
|
+
|
|
110
|
+
if (options.skipAfter) {
|
|
111
|
+
// ignore afterAll/after hooks
|
|
112
|
+
support.afterTestCaseHookDefinitions = [];
|
|
113
|
+
support.afterTestRunHookDefinitions = [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let errorMesssage = null;
|
|
117
|
+
let info = null;
|
|
118
|
+
let errInfo = null;
|
|
119
|
+
const result = await runCucumber({ ...runConfiguration, support }, environment, (message) => {
|
|
120
|
+
if (message.testStepFinished) {
|
|
121
|
+
const { testStepFinished } = message;
|
|
122
|
+
const { testStepResult } = testStepFinished;
|
|
123
|
+
if (testStepResult.status === "FAILED" || testStepResult.status === "AMBIGUOUS") {
|
|
124
|
+
if (!errorMesssage) {
|
|
125
|
+
errorMesssage = testStepResult.message;
|
|
126
|
+
if (info) {
|
|
127
|
+
errInfo = info;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (testStepResult.status === "UNDEFINED") {
|
|
132
|
+
if (!errorMesssage) {
|
|
133
|
+
errorMesssage = `step ${JSON.stringify(stepText)} is ${testStepResult.status}`;
|
|
134
|
+
if (info) {
|
|
135
|
+
errInfo = info;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (message.attachment) {
|
|
141
|
+
const attachment = message.attachment;
|
|
142
|
+
if (attachment.mediaType === "application/json" && attachment.body) {
|
|
143
|
+
const body = JSON.parse(attachment.body);
|
|
144
|
+
info = body.info;
|
|
145
|
+
|
|
146
|
+
if (body && body.payload) {
|
|
147
|
+
const payload = body.payload;
|
|
148
|
+
const content = payload.content;
|
|
149
|
+
const type = payload.type;
|
|
150
|
+
if (type === "cmdReport") {
|
|
151
|
+
if (content) {
|
|
152
|
+
const report = JSON.parse(content);
|
|
153
|
+
switch (report.status) {
|
|
154
|
+
case "start":
|
|
155
|
+
this.sendExecutionStatus({
|
|
156
|
+
type: "cmdExecutionStart",
|
|
157
|
+
cmdId: report.cmdId,
|
|
158
|
+
});
|
|
159
|
+
break;
|
|
160
|
+
case "end":
|
|
161
|
+
this.sendExecutionStatus({
|
|
162
|
+
type: "cmdExecutionSuccess",
|
|
163
|
+
cmdId: report.cmdId,
|
|
164
|
+
});
|
|
165
|
+
break;
|
|
166
|
+
default:
|
|
167
|
+
console.warn("Unknown command report status:", report.status);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (errorMesssage) {
|
|
176
|
+
const bvtError = new Error(errorMesssage);
|
|
177
|
+
Object.assign(bvtError, { info: errInfo });
|
|
178
|
+
throw bvtError;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
result,
|
|
183
|
+
info,
|
|
184
|
+
};
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error("Error running cucumber-js", error);
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
77
191
|
async runStep({ step, parametersMap, envPath, tags }, bvtContext, options) {
|
|
78
192
|
let codePage; // = getCodePage();
|
|
79
193
|
// const tempFolderPath = process.env.tempFeaturesFolderPath;
|
|
@@ -121,7 +235,7 @@ export class BVTStepRunner {
|
|
|
121
235
|
const stepExecController = new AbortController();
|
|
122
236
|
this.#currentStepController = stepExecController;
|
|
123
237
|
const { result, info } = await withAbort(async () => {
|
|
124
|
-
return await
|
|
238
|
+
return await this.executeStepRemote(
|
|
125
239
|
{
|
|
126
240
|
feature_file_path,
|
|
127
241
|
tempFolderPath,
|