@dev-blinq/cucumber_client 1.0.1210-dev → 1.0.1210-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.
Files changed (38) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +220 -0
  2. package/bin/assets/preload/recorderv3.js +74 -4
  3. package/bin/assets/preload/unique_locators.js +24 -3
  4. package/bin/assets/scripts/aria_snapshot.js +235 -0
  5. package/bin/assets/scripts/dom_attr.js +372 -0
  6. package/bin/assets/scripts/dom_element.js +0 -0
  7. package/bin/assets/scripts/dom_parent.js +185 -0
  8. package/bin/assets/scripts/event_utils.js +105 -0
  9. package/bin/assets/scripts/pw.js +7886 -0
  10. package/bin/assets/scripts/recorder.js +1147 -0
  11. package/bin/assets/scripts/snapshot_capturer.js +155 -0
  12. package/bin/assets/scripts/unique_locators.js +841 -0
  13. package/bin/assets/scripts/yaml.js +4770 -0
  14. package/bin/assets/templates/page_template.txt +2 -16
  15. package/bin/assets/templates/utils_template.txt +48 -18
  16. package/bin/client/cli_helpers.js +11 -13
  17. package/bin/client/code_cleanup/utils.js +42 -14
  18. package/bin/client/code_gen/code_inversion.js +48 -11
  19. package/bin/client/code_gen/index.js +3 -0
  20. package/bin/client/code_gen/page_reflection.js +37 -20
  21. package/bin/client/code_gen/playwright_codeget.js +170 -33
  22. package/bin/client/cucumber/feature.js +85 -27
  23. package/bin/client/cucumber/steps_definitions.js +109 -83
  24. package/bin/client/local_agent.js +7 -3
  25. package/bin/client/project.js +6 -2
  26. package/bin/client/recorderv3/bvt_recorder.js +278 -79
  27. package/bin/client/recorderv3/implemented_steps.js +69 -14
  28. package/bin/client/recorderv3/index.js +49 -7
  29. package/bin/client/recorderv3/network.js +299 -0
  30. package/bin/client/recorderv3/step_runner.js +184 -13
  31. package/bin/client/recorderv3/step_utils.js +155 -8
  32. package/bin/client/recorderv3/update_feature.js +58 -30
  33. package/bin/client/recording.js +7 -0
  34. package/bin/client/run_cucumber.js +16 -2
  35. package/bin/client/scenario_report.js +18 -6
  36. package/bin/client/test_scenario.js +0 -1
  37. package/bin/index.js +1 -0
  38. package/package.json +15 -8
@@ -1,9 +1,9 @@
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
8
  import { getCommandsForImplementedStep, loadStepDefinitions } from "./step_utils.js";
9
9
  import { parseStepTextParameters } from "../cucumber/utils.js";
@@ -13,7 +13,6 @@ const uuidFn = () => (++id).toString(16);
13
13
  const builder = new AstBuilder(uuidFn);
14
14
  const matcher = new GherkinClassicTokenMatcher();
15
15
  const parser = new Parser(builder, matcher);
16
-
17
16
  let i = 0;
18
17
  const getImplId = () => {
19
18
  return `I-${i++}`;
@@ -57,6 +56,53 @@ function memorySizeOf(obj) {
57
56
  return sizeOf(obj);
58
57
  }
59
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
+
60
106
  export const getImplementedSteps = async (projectDir) => {
61
107
  const foundErrors = [];
62
108
  try {
@@ -168,7 +214,10 @@ export const getImplementedSteps = async (projectDir) => {
168
214
  }
169
215
  stepLineSet.add(stepLine);
170
216
  step.templateIndex = implementedSteps.length;
171
- implementedSteps.push({
217
+
218
+ parseRouteFiles(projectDir, step);
219
+
220
+ const implementedStep = {
172
221
  keyword: step.keyword.trim(),
173
222
  keywordAlias: step.keywordAlias?.trim(),
174
223
  text: updateStepText(template.pattern, step.parameters),
@@ -179,7 +228,10 @@ export const getImplementedSteps = async (projectDir) => {
179
228
  templateIndex: step.templateIndex,
180
229
  pattern: template.pattern,
181
230
  paths: template.paths,
182
- });
231
+ routeItems: step.routeItems,
232
+ };
233
+
234
+ implementedSteps.push(implementedStep);
183
235
  }
184
236
  }
185
237
 
@@ -233,17 +285,20 @@ export const getImplementedSteps = async (projectDir) => {
233
285
  delete scenario.featureText;
234
286
  delete scenario.scenarioDocument;
235
287
  delete scenario.examples;
236
- delete scenario.steps;
237
288
  for (const tag of scenario.tags) {
238
289
  delete tag.location;
239
290
  }
240
- // const steps = scenario.steps;
241
- // for (const step of steps) {
242
- // const index = implementedSteps.findIndex((istep) => {
243
- // return stepsDefinitions._stepNameToTemplate(step.text) === istep.pattern;
244
- // });
245
- // step.templateIndex = index;
246
- // }
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
+ }
301
+ }
247
302
  }
248
303
  if (foundErrors.length > 0) {
249
304
  console.log("foundErrors", foundErrors);
@@ -253,7 +308,7 @@ export const getImplementedSteps = async (projectDir) => {
253
308
  return { implementedSteps, scenarios };
254
309
  };
255
310
 
256
- export const getStepsAndCommandsForScenario = ({ name, featureName, projectDir }) => {
311
+ export const getStepsAndCommandsForScenario = ({ name, featureName, projectDir, map }) => {
257
312
  const stepsDefinitions = new StepsDefinitions(projectDir);
258
313
  const step_definitions = loadStepDefinitions(projectDir);
259
314
  stepsDefinitions.load();
@@ -16,7 +16,7 @@ class PromisifiedSocketServer {
16
16
  init() {
17
17
  this.socket.on("request", async (data) => {
18
18
  const { event, input, id, roomId, socketId } = data;
19
- // console.log("request", { event, input, id, roomId, socketId });
19
+ console.log("request", { event, input, id, roomId, socketId });
20
20
  try {
21
21
  const handler = this.routes[event];
22
22
  if (!handler) {
@@ -51,14 +51,45 @@ 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);
57
88
  socket.on("connect", () => {
58
- // console.log('connected to server')
89
+ console.log("connected to server");
59
90
  });
60
91
  socket.on("disconnect", () => {
61
- // console.log('disconnected from server')
92
+ console.log("disconnected from server");
62
93
  });
63
94
  socket.emit("joinRoom", { id: roomId, window: "cucumber_client/bvt_recorder" });
64
95
  const recorder = new BVTRecorder({
@@ -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", null, roomId);
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));
@@ -139,9 +172,6 @@ const init = ({ envName, projectDir, roomId, TOKEN }) => {
139
172
  "recorderWindow.runStep": async (input) => {
140
173
  return recorder.runStep(input);
141
174
  },
142
- "recorderWindow.runScenario": async (input) => {
143
- return recorder.runScenario(input);
144
- },
145
175
  "recorderWindow.saveScenario": async (input) => {
146
176
  return recorder.saveScenario(input);
147
177
  },
@@ -242,6 +272,18 @@ const init = ({ envName, projectDir, roomId, TOKEN }) => {
242
272
  "recorderWindow.getStepsAndCommandsForScenario": async (input) => {
243
273
  return await recorder.getStepsAndCommandsForScenario(input);
244
274
  },
275
+ "recorderWindow.getNetworkEvents": async (input) => {
276
+ return await recorder.getNetworkEvents(input);
277
+ },
278
+ "recorderWindow.initExecution": async (input) => {
279
+ return await recorder.initExecution(input);
280
+ },
281
+ "recorderWindow.cleanupExecution": async (input) => {
282
+ return await recorder.cleanupExecution(input);
283
+ },
284
+ "recorderWindow.resetExecution": async (input) => {
285
+ return await recorder.resetExecution(input);
286
+ },
245
287
  });
246
288
  socket.on("targetBrowser.command.event", async (input) => {
247
289
  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;