@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
|
@@ -3,8 +3,7 @@ import { closeContext, initContext, _getDataFile, resetTestData } from "automati
|
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, rmSync } from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import url from "url";
|
|
6
|
-
import
|
|
7
|
-
import { getImplementedSteps } from "./implemented_steps.js";
|
|
6
|
+
import { getImplementedSteps, parseRouteFiles } from "./implemented_steps.js";
|
|
8
7
|
import { NamesService } from "./services.js";
|
|
9
8
|
import { BVTStepRunner } from "./step_runner.js";
|
|
10
9
|
import { readFile, writeFile } from "fs/promises";
|
|
@@ -16,41 +15,22 @@ import chokidar from "chokidar";
|
|
|
16
15
|
import logger from "../../logger.js";
|
|
17
16
|
import { unEscapeNonPrintables } from "../cucumber/utils.js";
|
|
18
17
|
import { findAvailablePort } from "../utils/index.js";
|
|
18
|
+
import NetworkMonitor from "./network.js";
|
|
19
|
+
import { Step } from "../cucumber/feature.js";
|
|
20
|
+
|
|
19
21
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
20
|
-
|
|
22
|
+
|
|
21
23
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
-
export function getInitScript(config) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
window.__bvt_Recorder.setPopupHandlers(${JSON.stringify(popupHandlers)});
|
|
31
|
-
window.__bvt_Recorder.setDisableHighlight(${JSON.stringify(disableHighlight)});
|
|
32
|
-
window.__bvt_Recorder.setImproviseLocators(${JSON.stringify(!disableMultipleLocators)});`;
|
|
33
|
-
return (
|
|
34
|
-
[
|
|
35
|
-
path.join(__dirname, "..", "..", "assets", "preload", "accessibility.js"),
|
|
36
|
-
path.join(__dirname, "..", "..", "assets", "preload", "dom-utils.js"),
|
|
37
|
-
path.join(__dirname, "..", "..", "assets", "preload", "generateSelector.js"),
|
|
38
|
-
path.join(__dirname, "..", "..", "assets", "preload", "locators.js"),
|
|
39
|
-
path.join(__dirname, "..", "..", "assets", "preload", "unique_locators.js"),
|
|
40
|
-
// path.join(__dirname, "..", "..", "assets", "preload", "pw_locators.js"),
|
|
41
|
-
path.join(__dirname, "..", "..", "assets", "preload", "climb.js"),
|
|
42
|
-
path.join(__dirname, "..", "..", "assets", "preload", "text-locator.js"),
|
|
43
|
-
path.join(__dirname, "..", "..", "assets", "preload", "toolbar.js"),
|
|
44
|
-
path.join(__dirname, "..", "..", "assets", "preload", "recording-tool.js"),
|
|
45
|
-
path.join(__dirname, "..", "..", "assets", "preload", "find_context.js"),
|
|
46
|
-
path.join(__dirname, "..", "..", "assets", "preload", "recorderv3.js"),
|
|
47
|
-
path.join(__dirname, "..", "..", "assets", "preload", "findElementText.js"),
|
|
48
|
-
path.join(__dirname, "..", "..", "assets", "preload", "css_gen.js"),
|
|
49
|
-
path.join(__dirname, "..", "..", "assets", "preload", "yaml.js"),
|
|
50
|
-
]
|
|
51
|
-
.map((filePath) => readFileSync(filePath, "utf8"))
|
|
52
|
-
.join("\n") + popupScript
|
|
24
|
+
export function getInitScript(config, options) {
|
|
25
|
+
const preScript = `
|
|
26
|
+
window.__bvt_Recorder_config = ${JSON.stringify(config ?? null)};
|
|
27
|
+
window.__PW_options = ${JSON.stringify(options ?? null)};
|
|
28
|
+
`;
|
|
29
|
+
const recorderScript = readFileSync(
|
|
30
|
+
path.join(__dirname, "..", "..", "assets", "bundled_scripts", "recorder.js"),
|
|
31
|
+
"utf8"
|
|
53
32
|
);
|
|
33
|
+
return preScript + recorderScript;
|
|
54
34
|
}
|
|
55
35
|
|
|
56
36
|
async function evaluate(frame, script) {
|
|
@@ -71,7 +51,7 @@ async function findNestedFrameSelector(frame, obj) {
|
|
|
71
51
|
const frameElement = await frame.frameElement();
|
|
72
52
|
if (!frameElement) return;
|
|
73
53
|
const selectors = await parent.evaluate((element) => {
|
|
74
|
-
return window.
|
|
54
|
+
return window.__bvt_Recorder.locatorGenerator.getElementLocators(element, { excludeText: true }).locators;
|
|
75
55
|
}, frameElement);
|
|
76
56
|
return findNestedFrameSelector(parent, { children: obj, selectors });
|
|
77
57
|
} catch (e) {
|
|
@@ -198,6 +178,7 @@ export class BVTRecorder {
|
|
|
198
178
|
this.logger = logger;
|
|
199
179
|
this.screenshotMap = new Map();
|
|
200
180
|
this.snapshotMap = new Map();
|
|
181
|
+
this.scenariosStepsMap = new Map();
|
|
201
182
|
this.namesService = new NamesService({
|
|
202
183
|
screenshotMap: this.screenshotMap,
|
|
203
184
|
TOKEN: this.TOKEN,
|
|
@@ -206,15 +187,38 @@ export class BVTRecorder {
|
|
|
206
187
|
});
|
|
207
188
|
this.stepRunner = new BVTStepRunner({
|
|
208
189
|
projectDir: this.projectDir,
|
|
190
|
+
sendExecutionStatus: (data) => {
|
|
191
|
+
if (data && data.type) {
|
|
192
|
+
switch (data.type) {
|
|
193
|
+
case "cmdExecutionStart":
|
|
194
|
+
// console.log("Sending cmdExecutionStart event for cmdId:", data);
|
|
195
|
+
this.sendEvent(this.events.cmdExecutionStart, data);
|
|
196
|
+
break;
|
|
197
|
+
case "cmdExecutionSuccess":
|
|
198
|
+
// console.log("Sending cmdExecutionSuccess event for cmdId:", data);
|
|
199
|
+
this.sendEvent(this.events.cmdExecutionSuccess, data);
|
|
200
|
+
break;
|
|
201
|
+
case "cmdExecutionError":
|
|
202
|
+
// console.log("Sending cmdExecutionError event for cmdId:", data);
|
|
203
|
+
this.sendEvent(this.events.cmdExecutionError, data);
|
|
204
|
+
break;
|
|
205
|
+
case "interceptResults":
|
|
206
|
+
// console.log("Sending interceptResults event");
|
|
207
|
+
this.sendEvent(this.events.interceptResults, data);
|
|
208
|
+
break;
|
|
209
|
+
default:
|
|
210
|
+
console.warn("Unknown command execution status type:", data.type);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
209
215
|
});
|
|
210
216
|
this.pageSet = new Set();
|
|
211
|
-
|
|
212
217
|
this.lastKnownUrlPath = "";
|
|
213
218
|
// TODO: what is world?
|
|
214
219
|
this.world = { attach: () => {} };
|
|
215
220
|
this.shouldTakeScreenshot = true;
|
|
216
221
|
this.watcher = null;
|
|
217
|
-
this.sendEvent("BVTRecorder.getTestData", {});
|
|
218
222
|
}
|
|
219
223
|
events = {
|
|
220
224
|
onFrameNavigate: "BVTRecorder.onFrameNavigate",
|
|
@@ -225,6 +229,10 @@ export class BVTRecorder {
|
|
|
225
229
|
onStepDetails: "BVTRecorder.onStepDetails",
|
|
226
230
|
getTestData: "BVTRecorder.getTestData",
|
|
227
231
|
onGoto: "BVTRecorder.onGoto",
|
|
232
|
+
cmdExecutionStart: "BVTRecorder.cmdExecutionStart",
|
|
233
|
+
cmdExecutionSuccess: "BVTRecorder.cmdExecutionSuccess",
|
|
234
|
+
cmdExecutionError: "BVTRecorder.cmdExecutionError",
|
|
235
|
+
interceptResults: "BVTRecorder.interceptResults",
|
|
228
236
|
};
|
|
229
237
|
bindings = {
|
|
230
238
|
__bvt_recordCommand: async ({ frame, page, context }, event) => {
|
|
@@ -300,22 +308,15 @@ export class BVTRecorder {
|
|
|
300
308
|
return result;
|
|
301
309
|
}
|
|
302
310
|
getInitScripts(config) {
|
|
303
|
-
return getInitScript(config
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// const inlineScript = `
|
|
313
|
-
// window.__bvt_Recorder.setPopupHandlers(${JSON.stringify(popupHandlers)});
|
|
314
|
-
// window.__bvt_Recorder.setDisableHighlight(${JSON.stringify(disableHighlight)});
|
|
315
|
-
// window.__bvt_Recorder.setImproviseLocators(${JSON.stringify(!disableMultipleLocators)});
|
|
316
|
-
// `
|
|
317
|
-
// scripts.push(inlineScript);
|
|
318
|
-
// return scripts;
|
|
311
|
+
return getInitScript(config, {
|
|
312
|
+
sdkLanguage: "javascript",
|
|
313
|
+
testIdAttributeName: "blinq-test-id",
|
|
314
|
+
stableRafCount: 0,
|
|
315
|
+
browserName: this.browser?.browserType().name(),
|
|
316
|
+
inputFileRoleTextbox: false,
|
|
317
|
+
customEngines: [],
|
|
318
|
+
isUnderTest: true,
|
|
319
|
+
});
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
async _initBrowser({ url }) {
|
|
@@ -335,7 +336,7 @@ export class BVTRecorder {
|
|
|
335
336
|
}
|
|
336
337
|
}
|
|
337
338
|
const initScripts = {
|
|
338
|
-
recorderCjs: injectedScriptSource,
|
|
339
|
+
// recorderCjs: injectedScriptSource,
|
|
339
340
|
scripts: [
|
|
340
341
|
this.getInitScripts(ai_config),
|
|
341
342
|
`\ndelete Object.getPrototypeOf(navigator).webdriver;${process.env.WINDOW_DEBUGGER ? "window.debug=true;\n" : ""}`,
|
|
@@ -462,6 +463,75 @@ export class BVTRecorder {
|
|
|
462
463
|
}
|
|
463
464
|
});
|
|
464
465
|
}
|
|
466
|
+
|
|
467
|
+
hasHistoryReplacementAtIndex(previousEntries, currentEntries, index) {
|
|
468
|
+
if (!previousEntries || !currentEntries) return false;
|
|
469
|
+
if (index >= previousEntries.length || index >= currentEntries.length) return false;
|
|
470
|
+
|
|
471
|
+
const prevEntry = previousEntries[index];
|
|
472
|
+
// console.log("prevEntry", prevEntry);
|
|
473
|
+
const currEntry = currentEntries[index];
|
|
474
|
+
// console.log("currEntry", currEntry);
|
|
475
|
+
|
|
476
|
+
// Check if the entry at this index has been replaced
|
|
477
|
+
return prevEntry.id !== currEntry.id;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Even simpler approach for your specific case
|
|
481
|
+
analyzeTransitionType(entries, currentIndex, currentEntry) {
|
|
482
|
+
// console.log("Analyzing transition type");
|
|
483
|
+
// console.log("===========================");
|
|
484
|
+
// console.log("Current Index:", currentIndex);
|
|
485
|
+
// console.log("Current Entry:", currentEntry);
|
|
486
|
+
// console.log("Current Entries:", entries);
|
|
487
|
+
// console.log("Current entries length:", entries.length);
|
|
488
|
+
// console.log("===========================");
|
|
489
|
+
// console.log("Previous Index:", this.previousIndex);
|
|
490
|
+
// // console.log("Previous Entry:", this.previousEntries[this.previousIndex]);
|
|
491
|
+
// console.log("Previous Entries:", this.previousEntries);
|
|
492
|
+
// console.log("Previous entries length:", this.previousHistoryLength);
|
|
493
|
+
|
|
494
|
+
if (this.previousIndex === null || this.previousHistoryLength === null || !this.previousEntries) {
|
|
495
|
+
return {
|
|
496
|
+
action: "initial",
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const indexDiff = currentIndex - this.previousIndex;
|
|
501
|
+
const lengthDiff = entries.length - this.previousHistoryLength;
|
|
502
|
+
|
|
503
|
+
// Backward navigation
|
|
504
|
+
if (indexDiff < 0) {
|
|
505
|
+
return { action: "back" };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Forward navigation
|
|
509
|
+
if (indexDiff > 0 && lengthDiff === 0) {
|
|
510
|
+
// Check if the entry at current index is the same as before
|
|
511
|
+
const entryReplaced = this.hasHistoryReplacementAtIndex(this.previousEntries, entries, currentIndex);
|
|
512
|
+
|
|
513
|
+
if (entryReplaced) {
|
|
514
|
+
return { action: "navigate" }; // New navigation that replaced forward history
|
|
515
|
+
} else {
|
|
516
|
+
return { action: "forward" }; // True forward navigation
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// New navigation (history grew)
|
|
521
|
+
if (lengthDiff > 0) {
|
|
522
|
+
return { action: "navigate" };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Same position, same length
|
|
526
|
+
if (lengthDiff <= 0) {
|
|
527
|
+
const entryReplaced = this.hasHistoryReplacementAtIndex(this.previousEntries, entries, currentIndex);
|
|
528
|
+
|
|
529
|
+
return entryReplaced ? { action: "navigate" } : { action: "reload" };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return { action: "unknown" };
|
|
533
|
+
}
|
|
534
|
+
|
|
465
535
|
async getCurrentTransition() {
|
|
466
536
|
if (this?.web?.browser?._name !== "chromium") {
|
|
467
537
|
return;
|
|
@@ -470,31 +540,82 @@ export class BVTRecorder {
|
|
|
470
540
|
|
|
471
541
|
try {
|
|
472
542
|
const result = await client.send("Page.getNavigationHistory");
|
|
473
|
-
// console.log("
|
|
543
|
+
// console.log("Navigation History:", result);
|
|
474
544
|
const entries = result.entries;
|
|
475
545
|
const currentIndex = result.currentIndex;
|
|
476
546
|
|
|
477
547
|
// ignore if currentIndex is not the last entry
|
|
478
|
-
if (currentIndex !== entries.length - 1) return;
|
|
548
|
+
// if (currentIndex !== entries.length - 1) return;
|
|
479
549
|
|
|
480
550
|
const currentEntry = entries[currentIndex];
|
|
481
|
-
|
|
551
|
+
const transitionInfo = this.analyzeTransitionType(entries, currentIndex, currentEntry);
|
|
552
|
+
this.previousIndex = currentIndex;
|
|
553
|
+
this.previousHistoryLength = entries.length;
|
|
554
|
+
this.previousUrl = currentEntry.url;
|
|
555
|
+
this.previousEntries = [...entries]; // Store a copy of current entries
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
currentEntry,
|
|
559
|
+
navigationAction: transitionInfo.action,
|
|
560
|
+
};
|
|
482
561
|
} catch (error) {
|
|
483
|
-
console.error("Error in getTransistionType event");
|
|
562
|
+
console.error("Error in getTransistionType event", error);
|
|
484
563
|
} finally {
|
|
485
564
|
await client.detach();
|
|
486
565
|
}
|
|
487
566
|
}
|
|
488
|
-
|
|
567
|
+
userInitiatedTransitionTypes = ["typed", "address_bar"];
|
|
489
568
|
async handlePageTransition() {
|
|
490
569
|
const transition = await this.getCurrentTransition();
|
|
491
570
|
if (!transition) return;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
571
|
+
|
|
572
|
+
const { currentEntry, navigationAction } = transition;
|
|
573
|
+
|
|
574
|
+
switch (navigationAction) {
|
|
575
|
+
case "initial":
|
|
576
|
+
// console.log("Initial navigation, no action taken");
|
|
577
|
+
return;
|
|
578
|
+
case "navigate":
|
|
579
|
+
// console.log("transitionType", transition.transitionType);
|
|
580
|
+
// console.log("sending onGoto event", { url: currentEntry.url,
|
|
581
|
+
// type: "navigate", });
|
|
582
|
+
if (this.userInitiatedTransitionTypes.includes(currentEntry.transitionType)) {
|
|
583
|
+
const env = JSON.parse(readFileSync(this.envName), "utf8");
|
|
584
|
+
const baseUrl = env.baseUrl;
|
|
585
|
+
let url = currentEntry.userTypedURL;
|
|
586
|
+
if (baseUrl && url.startsWith(baseUrl)) {
|
|
587
|
+
url = url.replace(baseUrl, "{{env.baseUrl}}");
|
|
588
|
+
}
|
|
589
|
+
// console.log("User initiated transition");
|
|
590
|
+
this.sendEvent(this.events.onGoto, { url, type: "navigate" });
|
|
591
|
+
}
|
|
592
|
+
return;
|
|
593
|
+
case "back":
|
|
594
|
+
// console.log("User navigated back");
|
|
595
|
+
// console.log("sending onGoto event", {
|
|
596
|
+
// type: "back",
|
|
597
|
+
// });
|
|
598
|
+
this.sendEvent(this.events.onGoto, { type: "back" });
|
|
599
|
+
return;
|
|
600
|
+
case "forward":
|
|
601
|
+
// console.log("User navigated forward"); console.log("sending onGoto event", { type: "forward", });
|
|
602
|
+
this.sendEvent(this.events.onGoto, { type: "forward" });
|
|
603
|
+
return;
|
|
604
|
+
default:
|
|
605
|
+
this.sendEvent(this.events.onGoto, { type: "unknown" });
|
|
606
|
+
return;
|
|
496
607
|
}
|
|
497
608
|
}
|
|
609
|
+
|
|
610
|
+
async getCurrentPageTitle() {
|
|
611
|
+
const title = await this.page.title();
|
|
612
|
+
return title;
|
|
613
|
+
}
|
|
614
|
+
async getCurrentPageUrl() {
|
|
615
|
+
const url = await this.page.url();
|
|
616
|
+
return url;
|
|
617
|
+
}
|
|
618
|
+
|
|
498
619
|
_addPagelisteners(context) {
|
|
499
620
|
context.on("page", async (page) => {
|
|
500
621
|
try {
|
|
@@ -609,10 +730,13 @@ export class BVTRecorder {
|
|
|
609
730
|
}
|
|
610
731
|
async closeBrowser() {
|
|
611
732
|
delete process.env.TEMP_RUN;
|
|
612
|
-
await this.watcher.close().then(() => {
|
|
613
|
-
this.logger.info(`Closed current testData file`);
|
|
614
|
-
});
|
|
733
|
+
await this.watcher.close().then(() => {});
|
|
615
734
|
this.watcher = null;
|
|
735
|
+
this.previousIndex = null;
|
|
736
|
+
this.previousHistoryLength = null;
|
|
737
|
+
this.previousUrl = null;
|
|
738
|
+
this.previousEntries = null;
|
|
739
|
+
|
|
616
740
|
await closeContext();
|
|
617
741
|
this.pageSet.clear();
|
|
618
742
|
}
|
|
@@ -695,34 +819,41 @@ export class BVTRecorder {
|
|
|
695
819
|
}, 100);
|
|
696
820
|
this.timerId = timerId;
|
|
697
821
|
}
|
|
698
|
-
async runStep({ step, parametersMap }, options) {
|
|
822
|
+
async runStep({ step, parametersMap, tags, isFirstStep, listenNetwork }, options) {
|
|
699
823
|
const _env = {
|
|
700
824
|
TOKEN: this.TOKEN,
|
|
701
825
|
TEMP_RUN: true,
|
|
702
826
|
REPORT_FOLDER: this.bvtContext.reportFolder,
|
|
703
827
|
BLINQ_ENV: this.envName,
|
|
828
|
+
STORE_DETAILED_NETWORK_DATA: listenNetwork ? "true" : "false",
|
|
829
|
+
CURRENT_STEP_ID: step.id,
|
|
704
830
|
};
|
|
705
831
|
|
|
706
832
|
this.bvtContext.navigate = true;
|
|
833
|
+
this.bvtContext.loadedRoutes = null;
|
|
707
834
|
for (const [key, value] of Object.entries(_env)) {
|
|
708
835
|
process.env[key] = value;
|
|
709
836
|
}
|
|
837
|
+
|
|
710
838
|
if (this.timerId) {
|
|
711
839
|
clearTimeout(this.timerId);
|
|
712
840
|
this.timerId = null;
|
|
713
841
|
}
|
|
714
842
|
await this.setMode("running");
|
|
843
|
+
|
|
715
844
|
try {
|
|
716
|
-
await this.stepRunner.runStep(
|
|
845
|
+
const { result, info } = await this.stepRunner.runStep(
|
|
717
846
|
{
|
|
718
847
|
step,
|
|
719
848
|
parametersMap,
|
|
720
849
|
envPath: this.envName,
|
|
850
|
+
tags,
|
|
721
851
|
},
|
|
722
852
|
this.bvtContext,
|
|
723
|
-
options
|
|
853
|
+
options ? { ...options, skipBefore: !isFirstStep } : { skipBefore: !isFirstStep }
|
|
724
854
|
);
|
|
725
855
|
await this.revertMode();
|
|
856
|
+
return { info };
|
|
726
857
|
} catch (error) {
|
|
727
858
|
await this.revertMode();
|
|
728
859
|
throw error;
|
|
@@ -733,19 +864,42 @@ export class BVTRecorder {
|
|
|
733
864
|
this.bvtContext.navigate = false;
|
|
734
865
|
}
|
|
735
866
|
}
|
|
736
|
-
async runScenario({ steps, parametersMap }) {
|
|
737
|
-
for (const step of steps) {
|
|
738
|
-
await this.runStep({ step, parametersMap });
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
867
|
async saveScenario({ scenario, featureName, override, isSingleStep }) {
|
|
742
868
|
await updateStepDefinitions({ scenario, featureName, projectDir: this.projectDir }); // updates mjs files
|
|
743
869
|
if (!isSingleStep) await updateFeatureFile({ featureName, scenario, override, projectDir: this.projectDir }); // updates gherkin files
|
|
744
|
-
await this.cleanup();
|
|
870
|
+
await this.cleanup({ tags: scenario.tags });
|
|
745
871
|
}
|
|
746
872
|
async getImplementedSteps() {
|
|
747
|
-
|
|
873
|
+
const stepsAndScenarios = await getImplementedSteps(this.projectDir);
|
|
874
|
+
const implementedSteps = stepsAndScenarios.implementedSteps;
|
|
875
|
+
const scenarios = stepsAndScenarios.scenarios;
|
|
876
|
+
for (const scenario of scenarios) {
|
|
877
|
+
this.scenariosStepsMap.set(scenario.name, scenario.steps);
|
|
878
|
+
delete scenario.steps;
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
implementedSteps,
|
|
882
|
+
scenarios,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
async getStepsAndCommandsForScenario({ name, featureName }) {
|
|
886
|
+
const steps = this.scenariosStepsMap.get(name) || [];
|
|
887
|
+
for (const step of steps) {
|
|
888
|
+
if (step.isImplemented) {
|
|
889
|
+
step.commands = this.getCommandsForImplementedStep({ stepName: step.text });
|
|
890
|
+
} else {
|
|
891
|
+
step.commands = [];
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return steps;
|
|
895
|
+
// return getStepsAndCommandsForScenario({
|
|
896
|
+
// name,
|
|
897
|
+
// featureName,
|
|
898
|
+
// projectDir: this.projectDir,
|
|
899
|
+
// map: this.scenariosStepsMap,
|
|
900
|
+
// });
|
|
748
901
|
}
|
|
902
|
+
|
|
749
903
|
async generateStepName({ commands, stepsNames, parameters, map }) {
|
|
750
904
|
return await this.namesService.generateStepName({ commands, stepsNames, parameters, map });
|
|
751
905
|
}
|
|
@@ -828,9 +982,9 @@ export class BVTRecorder {
|
|
|
828
982
|
}
|
|
829
983
|
}
|
|
830
984
|
|
|
831
|
-
async discardTestData() {
|
|
985
|
+
async discardTestData({ tags }) {
|
|
832
986
|
resetTestData(this.envName, this.world);
|
|
833
|
-
await this.cleanup();
|
|
987
|
+
await this.cleanup({ tags });
|
|
834
988
|
}
|
|
835
989
|
async addToTestData(obj) {
|
|
836
990
|
if (!existsSync(_getDataFile(this.world, this.bvtContext, this.web))) {
|
|
@@ -869,6 +1023,7 @@ export class BVTRecorder {
|
|
|
869
1023
|
const stepParams = parseStepTextParameters(stepName);
|
|
870
1024
|
return getCommandsForImplementedStep(stepName, step_definitions, stepParams).commands;
|
|
871
1025
|
}
|
|
1026
|
+
|
|
872
1027
|
loadExistingScenario({ featureName, scenarioName }) {
|
|
873
1028
|
const step_definitions = loadStepDefinitions(this.projectDir);
|
|
874
1029
|
const featureFilePath = path.join(this.projectDir, "features", featureName);
|
|
@@ -897,6 +1052,7 @@ export class BVTRecorder {
|
|
|
897
1052
|
..._s,
|
|
898
1053
|
keyword: step.keyword.trim(),
|
|
899
1054
|
};
|
|
1055
|
+
parseRouteFiles(this.projectDir, _step);
|
|
900
1056
|
steps.push(_step);
|
|
901
1057
|
}
|
|
902
1058
|
return {
|
|
@@ -936,7 +1092,7 @@ export class BVTRecorder {
|
|
|
936
1092
|
}
|
|
937
1093
|
return result;
|
|
938
1094
|
}
|
|
939
|
-
async cleanup() {
|
|
1095
|
+
async cleanup({ tags }) {
|
|
940
1096
|
const noopStep = {
|
|
941
1097
|
text: "Noop",
|
|
942
1098
|
isImplemented: true,
|
|
@@ -950,6 +1106,7 @@ export class BVTRecorder {
|
|
|
950
1106
|
{
|
|
951
1107
|
step: noopStep,
|
|
952
1108
|
parametersMap: {},
|
|
1109
|
+
tags: tags || [],
|
|
953
1110
|
},
|
|
954
1111
|
{
|
|
955
1112
|
skipAfter: false,
|
|
@@ -34,6 +34,7 @@ let recorder = null;
|
|
|
34
34
|
const init = async ({ envName, projectDir, roomId }) => {
|
|
35
35
|
console.log("connecting to " + WS_URL);
|
|
36
36
|
const socket = io(WS_URL);
|
|
37
|
+
// Disconnect from the server when the process exits
|
|
37
38
|
socket.on("connect", () => {
|
|
38
39
|
// console.log('connected to server')
|
|
39
40
|
});
|
|
@@ -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
|
+
paramKey: key,
|
|
95
|
+
paramValue: 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
|
+
};
|