@dev-blinq/cucumber_client 1.0.1175-dev → 1.0.1175-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 +85 -11
- 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 +112 -18
- 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 +152 -48
- 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 +59 -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 +236 -79
- package/bin/client/recorderv3/cli.js +1 -0
- package/bin/client/recorderv3/implemented_steps.js +111 -11
- package/bin/client/recorderv3/index.js +45 -4
- package/bin/client/recorderv3/network.js +299 -0
- package/bin/client/recorderv3/step_runner.js +179 -13
- package/bin/client/recorderv3/step_utils.js +159 -14
- package/bin/client/recorderv3/update_feature.js +55 -30
- package/bin/client/recording.js +8 -0
- package/bin/client/run_cucumber.js +116 -4
- package/bin/client/scenario_report.js +112 -50
- 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
|
@@ -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
|
},
|
|
@@ -216,6 +245,12 @@ const init = ({ envName, projectDir, roomId, TOKEN }) => {
|
|
|
216
245
|
return { folder: snapshotFolder, files: ymlFiles };
|
|
217
246
|
} else return { folder: null, files: [] };
|
|
218
247
|
},
|
|
248
|
+
"recorderWindow.getCurrentPageTitle": async (input) => {
|
|
249
|
+
return await recorder.getCurrentPageTitle();
|
|
250
|
+
},
|
|
251
|
+
"recorderWindow.getCurrentPageUrl": async (input) => {
|
|
252
|
+
return await recorder.getCurrentPageUrl();
|
|
253
|
+
},
|
|
219
254
|
"recorderWindow.sendAriaSnapshot": async (input) => {
|
|
220
255
|
const snapshot = input?.snapshot;
|
|
221
256
|
const deselect = input?.deselect;
|
|
@@ -233,6 +268,12 @@ const init = ({ envName, projectDir, roomId, TOKEN }) => {
|
|
|
233
268
|
const mode = input?.mode;
|
|
234
269
|
return recorder.setMode(mode);
|
|
235
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
|
+
},
|
|
236
277
|
});
|
|
237
278
|
socket.on("targetBrowser.command.event", async (input) => {
|
|
238
279
|
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;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import fs from "fs";
|
|
4
3
|
import { generatePageName } from "../code_gen/playwright_codeget.js";
|
|
5
4
|
import {
|
|
6
5
|
executeStep,
|
|
@@ -9,26 +8,36 @@ import {
|
|
|
9
8
|
getUtilsCodePage,
|
|
10
9
|
loadStepDefinitions,
|
|
11
10
|
saveRecording,
|
|
11
|
+
saveRoutes,
|
|
12
12
|
} from "./step_utils.js";
|
|
13
13
|
import { escapeString, getExamplesContent } from "./update_feature.js";
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import { locateDefinitionPath } from "../cucumber/steps_definitions.js";
|
|
16
|
+
import { tmpdir } from "os";
|
|
14
17
|
|
|
15
18
|
// let copiedCodeToTemp = false;
|
|
16
19
|
async function withAbort(fn, signal) {
|
|
17
20
|
if (!signal) {
|
|
18
21
|
return await fn();
|
|
19
22
|
}
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const abortHandler = () => reject(new Error("Aborted"));
|
|
25
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
fn()
|
|
28
|
+
.then(resolve)
|
|
29
|
+
.catch(reject)
|
|
30
|
+
.finally(() => {
|
|
31
|
+
signal.removeEventListener("abort", abortHandler);
|
|
32
|
+
});
|
|
23
33
|
});
|
|
24
|
-
|
|
25
|
-
return await Promise.race([fn(), abortPromise]);
|
|
26
34
|
}
|
|
27
35
|
export class BVTStepRunner {
|
|
28
36
|
#currentStepController;
|
|
29
37
|
#port;
|
|
30
|
-
constructor({ projectDir }) {
|
|
38
|
+
constructor({ projectDir, sendExecutionStatus }) {
|
|
31
39
|
this.projectDir = projectDir;
|
|
40
|
+
this.sendExecutionStatus = sendExecutionStatus;
|
|
32
41
|
}
|
|
33
42
|
setRemoteDebugPort(port) {
|
|
34
43
|
this.#port = port;
|
|
@@ -55,11 +64,12 @@ export class BVTStepRunner {
|
|
|
55
64
|
// copiedCodeToTemp = true;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
async writeTempFeatureFile({ step, parametersMap, tempFolderPath }) {
|
|
67
|
+
async writeTempFeatureFile({ step, parametersMap, tempFolderPath, tags }) {
|
|
59
68
|
const tFilePath = path.join(tempFolderPath, "__temp.feature");
|
|
60
69
|
// console.log(tFilePath);
|
|
61
70
|
let tFileContent = `# temp feature file
|
|
62
71
|
Feature: Temp feature
|
|
72
|
+
${tags ? tags.join(" ") : ""}
|
|
63
73
|
Scenario Outline: Temp Scenario
|
|
64
74
|
Given ${escapeString(step.text)}
|
|
65
75
|
`;
|
|
@@ -67,7 +77,132 @@ export class BVTStepRunner {
|
|
|
67
77
|
writeFileSync(tFilePath, tFileContent);
|
|
68
78
|
return tFilePath;
|
|
69
79
|
}
|
|
70
|
-
|
|
80
|
+
|
|
81
|
+
executeStepRemote = async ({ feature_file_path, scenario, tempFolderPath, stepText }, options) => {
|
|
82
|
+
if (!options) {
|
|
83
|
+
options = {
|
|
84
|
+
skipAfter: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const environment = {
|
|
88
|
+
...process.env,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const { loadConfiguration, loadSupport, runCucumber } = await import("@dev-blinq/cucumber-js/api");
|
|
93
|
+
const { runConfiguration } = await loadConfiguration(
|
|
94
|
+
{
|
|
95
|
+
provided: {
|
|
96
|
+
name: [scenario],
|
|
97
|
+
paths: [feature_file_path],
|
|
98
|
+
import: [path.join(tempFolderPath, "step_definitions", "**", "*.mjs")],
|
|
99
|
+
// format: ["bvt"],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{ cwd: process.cwd(), env: environment }
|
|
103
|
+
);
|
|
104
|
+
// const files = glob.sync(path.join(tempFolderPath, "step_definitions", "**", "*.mjs"));
|
|
105
|
+
// console.log("Files found:", files);
|
|
106
|
+
const support = await loadSupport(runConfiguration, { cwd: process.cwd(), env: environment });
|
|
107
|
+
// console.log("found ", support.stepDefinitions.length, "step definitions");
|
|
108
|
+
// support.stepDefinitions.map((step) => {
|
|
109
|
+
// console.log("step", step.pattern);
|
|
110
|
+
// });
|
|
111
|
+
|
|
112
|
+
if (options.skipAfter) {
|
|
113
|
+
// ignore afterAll/after hooks
|
|
114
|
+
support.afterTestCaseHookDefinitions = [];
|
|
115
|
+
support.afterTestRunHookDefinitions = [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let errorMesssage = null;
|
|
119
|
+
let info = null;
|
|
120
|
+
let errInfo = null;
|
|
121
|
+
const result = await runCucumber({ ...runConfiguration, support }, environment, (message) => {
|
|
122
|
+
if (message.testStepFinished) {
|
|
123
|
+
const { testStepFinished } = message;
|
|
124
|
+
const { testStepResult } = testStepFinished;
|
|
125
|
+
if (testStepResult.status === "FAILED" || testStepResult.status === "AMBIGUOUS") {
|
|
126
|
+
if (!errorMesssage) {
|
|
127
|
+
errorMesssage = testStepResult.message;
|
|
128
|
+
if (info) {
|
|
129
|
+
errInfo = info;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (testStepResult.status === "UNDEFINED") {
|
|
134
|
+
if (!errorMesssage) {
|
|
135
|
+
errorMesssage = `step ${JSON.stringify(stepText)} is ${testStepResult.status}`;
|
|
136
|
+
if (info) {
|
|
137
|
+
errInfo = info;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (message.attachment) {
|
|
143
|
+
const attachment = message.attachment;
|
|
144
|
+
if (attachment.mediaType === "application/json" && attachment.body) {
|
|
145
|
+
const body = JSON.parse(attachment.body);
|
|
146
|
+
info = body.info;
|
|
147
|
+
const result = body.result;
|
|
148
|
+
|
|
149
|
+
if (result.status === "PASSED") {
|
|
150
|
+
this.sendExecutionStatus({
|
|
151
|
+
type: "cmdExecutionSuccess",
|
|
152
|
+
cmdId: body.cmdId,
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
this.sendExecutionStatus({
|
|
156
|
+
type: "cmdExecutionError",
|
|
157
|
+
cmdId: body.cmdId,
|
|
158
|
+
error: {
|
|
159
|
+
message: result.message,
|
|
160
|
+
info,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
} else if (attachment.mediaType === "application/json+intercept-results" && attachment.body) {
|
|
165
|
+
const body = JSON.parse(attachment.body);
|
|
166
|
+
if (body) {
|
|
167
|
+
this.sendExecutionStatus({
|
|
168
|
+
type: "interceptResults",
|
|
169
|
+
interceptResults: body,
|
|
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
|
+
|
|
191
|
+
async runStep({ step, parametersMap, envPath, tags }, bvtContext, options) {
|
|
192
|
+
let cmdIDs = (step.commands || []).map((cmd) => cmd.cmdId);
|
|
193
|
+
if (bvtContext.web) {
|
|
194
|
+
bvtContext.web.getCmdId = () => {
|
|
195
|
+
if (cmdIDs.length === 0) {
|
|
196
|
+
cmdIDs = (step.commands || []).map((cmd) => cmd.cmdId);
|
|
197
|
+
}
|
|
198
|
+
const cId = cmdIDs.shift();
|
|
199
|
+
this.sendExecutionStatus({
|
|
200
|
+
type: "cmdExecutionStart",
|
|
201
|
+
cmdId: cId,
|
|
202
|
+
});
|
|
203
|
+
return cId;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
71
206
|
let codePage; // = getCodePage();
|
|
72
207
|
// const tempFolderPath = process.env.tempFeaturesFolderPath;
|
|
73
208
|
const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
|
|
@@ -96,7 +231,8 @@ export class BVTStepRunner {
|
|
|
96
231
|
if (!existsSync(stepDefinitionFolderPath)) {
|
|
97
232
|
mkdirSync(stepDefinitionFolderPath, { recursive: true });
|
|
98
233
|
}
|
|
99
|
-
const stepDefsFilePath =
|
|
234
|
+
const stepDefsFilePath = locateDefinitionPath(tempFolderPath, pageName);
|
|
235
|
+
//path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
|
|
100
236
|
codePage = getCodePage(stepDefsFilePath);
|
|
101
237
|
codePage = await saveRecording({ step, cucumberStep, codePage, projectDir: this.projectDir, stepsDefinitions });
|
|
102
238
|
if (codePage) {
|
|
@@ -106,14 +242,43 @@ export class BVTStepRunner {
|
|
|
106
242
|
if (!codePage) {
|
|
107
243
|
codePage = getUtilsCodePage(this.projectDir);
|
|
108
244
|
}
|
|
245
|
+
} else {
|
|
246
|
+
let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
|
|
247
|
+
if (process.env.TEMP_RUN === "true") {
|
|
248
|
+
// console.log("Save routes in temp folder for running:", routesPath);
|
|
249
|
+
if (existsSync(routesPath)) {
|
|
250
|
+
// console.log("Removing existing temp_routes_folder:", routesPath);
|
|
251
|
+
rmSync(routesPath, { recursive: true });
|
|
252
|
+
}
|
|
253
|
+
mkdirSync(routesPath, { recursive: true });
|
|
254
|
+
// console.log("Created temp_routes_folder:", routesPath);
|
|
255
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
256
|
+
} else {
|
|
257
|
+
// console.log("Saving routes in project directory:", this.projectDir);
|
|
258
|
+
if (existsSync(routesPath)) {
|
|
259
|
+
// remove the folder
|
|
260
|
+
try {
|
|
261
|
+
rmSync(routesPath, { recursive: true });
|
|
262
|
+
// console.log("Removed temp_routes_folder:", routesPath);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
// console.error("Error removing temp_routes folder", error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
routesPath = path.join(this.projectDir, "data", "routes");
|
|
268
|
+
// console.log("Saving routes to:", routesPath);
|
|
269
|
+
if (!existsSync(routesPath)) {
|
|
270
|
+
mkdirSync(routesPath, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
273
|
+
}
|
|
109
274
|
}
|
|
110
|
-
const feature_file_path = await this.writeTempFeatureFile({ step, parametersMap, tempFolderPath });
|
|
275
|
+
const feature_file_path = await this.writeTempFeatureFile({ step, parametersMap, tempFolderPath, tags });
|
|
111
276
|
// console.log({ feature_file_path, step_text: step.text });
|
|
112
277
|
|
|
113
278
|
const stepExecController = new AbortController();
|
|
114
279
|
this.#currentStepController = stepExecController;
|
|
115
|
-
await withAbort(async () => {
|
|
116
|
-
await
|
|
280
|
+
const { result, info } = await withAbort(async () => {
|
|
281
|
+
return await this.executeStepRemote(
|
|
117
282
|
{
|
|
118
283
|
feature_file_path,
|
|
119
284
|
tempFolderPath,
|
|
@@ -128,5 +293,6 @@ export class BVTStepRunner {
|
|
|
128
293
|
fs.rmSync(tempFolderPath, { recursive: true });
|
|
129
294
|
}
|
|
130
295
|
});
|
|
296
|
+
return { result, info };
|
|
131
297
|
}
|
|
132
298
|
}
|