@dev-blinq/cucumber_client 1.0.1185-dev → 1.0.1185-stage
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/assets/bundled_scripts/recorder.js +220 -0
- package/bin/assets/preload/accessibility.js +1 -1
- package/bin/assets/preload/find_context.js +1 -1
- package/bin/assets/preload/generateSelector.js +24 -0
- package/bin/assets/preload/locators.js +18 -0
- package/bin/assets/preload/recorderv3.js +80 -9
- package/bin/assets/preload/unique_locators.js +24 -3
- package/bin/assets/scripts/aria_snapshot.js +235 -0
- package/bin/assets/scripts/dom_attr.js +372 -0
- package/bin/assets/scripts/dom_element.js +0 -0
- package/bin/assets/scripts/dom_parent.js +185 -0
- package/bin/assets/scripts/event_utils.js +105 -0
- package/bin/assets/scripts/pw.js +7886 -0
- package/bin/assets/scripts/recorder.js +1147 -0
- package/bin/assets/scripts/snapshot_capturer.js +155 -0
- package/bin/assets/scripts/unique_locators.js +844 -0
- package/bin/assets/scripts/yaml.js +4770 -0
- package/bin/assets/templates/page_template.txt +2 -16
- package/bin/assets/templates/utils_template.txt +65 -12
- package/bin/client/cli_helpers.js +0 -1
- package/bin/client/code_cleanup/utils.js +43 -14
- package/bin/client/code_gen/code_inversion.js +44 -12
- package/bin/client/code_gen/index.js +3 -0
- package/bin/client/code_gen/page_reflection.js +37 -20
- package/bin/client/code_gen/playwright_codeget.js +149 -43
- package/bin/client/cucumber/feature.js +96 -42
- package/bin/client/cucumber/project_to_document.js +8 -7
- package/bin/client/cucumber/steps_definitions.js +49 -16
- package/bin/client/local_agent.js +9 -7
- package/bin/client/operations/dump_tree.js +159 -5
- package/bin/client/playground/playground.js +1 -1
- package/bin/client/project.js +6 -2
- package/bin/client/recorderv3/bvt_recorder.js +279 -81
- package/bin/client/recorderv3/cli.js +1 -0
- package/bin/client/recorderv3/implemented_steps.js +111 -11
- package/bin/client/recorderv3/index.js +48 -4
- package/bin/client/recorderv3/network.js +299 -0
- package/bin/client/recorderv3/step_runner.js +183 -13
- package/bin/client/recorderv3/step_utils.js +159 -14
- package/bin/client/recorderv3/update_feature.js +53 -28
- package/bin/client/recording.js +8 -0
- package/bin/client/run_cucumber.js +16 -2
- package/bin/client/scenario_report.js +112 -55
- package/bin/client/test_scenario.js +0 -1
- package/bin/index.js +1 -0
- package/package.json +15 -8
- package/bin/client/code_gen/get_implemented_steps.js +0 -27
|
@@ -1,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,135 @@ 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, config }, options) => {
|
|
82
|
+
const { skipAfter = true, skipBefore = true } = options || {};
|
|
83
|
+
const environment = {
|
|
84
|
+
...process.env,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const { loadConfiguration, loadSupport, runCucumber } = await import("@dev-blinq/cucumber-js/api");
|
|
89
|
+
const { runConfiguration } = await loadConfiguration(
|
|
90
|
+
{
|
|
91
|
+
provided: {
|
|
92
|
+
name: [scenario],
|
|
93
|
+
paths: [feature_file_path],
|
|
94
|
+
import: [path.join(tempFolderPath, "step_definitions", "**", "*.mjs")],
|
|
95
|
+
// format: ["bvt"],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{ cwd: process.cwd(), env: environment }
|
|
99
|
+
);
|
|
100
|
+
// const files = glob.sync(path.join(tempFolderPath, "step_definitions", "**", "*.mjs"));
|
|
101
|
+
// console.log("Files found:", files);
|
|
102
|
+
const support = await loadSupport(runConfiguration, { cwd: process.cwd(), env: environment });
|
|
103
|
+
// console.log("found ", support.stepDefinitions.length, "step definitions");
|
|
104
|
+
// support.stepDefinitions.map((step) => {
|
|
105
|
+
// console.log("step", step.pattern);
|
|
106
|
+
// });
|
|
107
|
+
|
|
108
|
+
support.afterTestRunHookDefinitions = [];
|
|
109
|
+
if (skipAfter) {
|
|
110
|
+
// ignore afterAll/after hooks
|
|
111
|
+
support.afterTestCaseHookDefinitions = [];
|
|
112
|
+
}
|
|
113
|
+
if (skipBefore && !config.legacySyntax) {
|
|
114
|
+
// ignore beforeAll/before hooks
|
|
115
|
+
support.beforeTestCaseHookDefinitions = support.beforeTestCaseHookDefinitions.filter((hook) => {
|
|
116
|
+
return hook.uri.endsWith("utils.mjs");
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
support.beforeTestRunHookDefinitions = [];
|
|
120
|
+
|
|
121
|
+
let errorMesssage = null;
|
|
122
|
+
let info = null;
|
|
123
|
+
let errInfo = null;
|
|
124
|
+
const result = await runCucumber({ ...runConfiguration, support }, environment, (message) => {
|
|
125
|
+
if (message.testStepFinished) {
|
|
126
|
+
const { testStepFinished } = message;
|
|
127
|
+
const { testStepResult } = testStepFinished;
|
|
128
|
+
if (testStepResult.status === "FAILED" || testStepResult.status === "AMBIGUOUS") {
|
|
129
|
+
if (!errorMesssage) {
|
|
130
|
+
errorMesssage = testStepResult.message;
|
|
131
|
+
if (info) {
|
|
132
|
+
errInfo = info;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (testStepResult.status === "UNDEFINED") {
|
|
137
|
+
if (!errorMesssage) {
|
|
138
|
+
errorMesssage = `step ${JSON.stringify(stepText)} is ${testStepResult.status}`;
|
|
139
|
+
if (info) {
|
|
140
|
+
errInfo = info;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (message.attachment) {
|
|
146
|
+
const attachment = message.attachment;
|
|
147
|
+
if (attachment.mediaType === "application/json" && attachment.body) {
|
|
148
|
+
const body = JSON.parse(attachment.body);
|
|
149
|
+
info = body.info;
|
|
150
|
+
const result = body.result;
|
|
151
|
+
|
|
152
|
+
if (result.status === "PASSED") {
|
|
153
|
+
this.sendExecutionStatus({
|
|
154
|
+
type: "cmdExecutionSuccess",
|
|
155
|
+
cmdId: body.cmdId,
|
|
156
|
+
});
|
|
157
|
+
} else {
|
|
158
|
+
this.sendExecutionStatus({
|
|
159
|
+
type: "cmdExecutionError",
|
|
160
|
+
cmdId: body.cmdId,
|
|
161
|
+
error: {
|
|
162
|
+
message: result.message,
|
|
163
|
+
info,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
} else if (attachment.mediaType === "application/json+intercept-results" && attachment.body) {
|
|
168
|
+
const body = JSON.parse(attachment.body);
|
|
169
|
+
if (body) {
|
|
170
|
+
this.sendExecutionStatus({
|
|
171
|
+
type: "interceptResults",
|
|
172
|
+
interceptResults: body,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
if (errorMesssage) {
|
|
179
|
+
const bvtError = new Error(errorMesssage);
|
|
180
|
+
Object.assign(bvtError, { info: errInfo });
|
|
181
|
+
throw bvtError;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
result,
|
|
186
|
+
info,
|
|
187
|
+
};
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error("Error running cucumber-js", error);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
async runStep({ step, parametersMap, envPath, tags, config }, bvtContext, options) {
|
|
195
|
+
let cmdIDs = (step.commands || []).map((cmd) => cmd.cmdId);
|
|
196
|
+
if (bvtContext.web) {
|
|
197
|
+
bvtContext.web.getCmdId = () => {
|
|
198
|
+
if (cmdIDs.length === 0) {
|
|
199
|
+
cmdIDs = (step.commands || []).map((cmd) => cmd.cmdId);
|
|
200
|
+
}
|
|
201
|
+
const cId = cmdIDs.shift();
|
|
202
|
+
this.sendExecutionStatus({
|
|
203
|
+
type: "cmdExecutionStart",
|
|
204
|
+
cmdId: cId,
|
|
205
|
+
});
|
|
206
|
+
return cId;
|
|
207
|
+
};
|
|
208
|
+
}
|
|
71
209
|
let codePage; // = getCodePage();
|
|
72
210
|
// const tempFolderPath = process.env.tempFeaturesFolderPath;
|
|
73
211
|
const __temp_features_FolderName = "__temp_features" + Math.random().toString(36).substring(2, 7);
|
|
@@ -96,7 +234,8 @@ export class BVTStepRunner {
|
|
|
96
234
|
if (!existsSync(stepDefinitionFolderPath)) {
|
|
97
235
|
mkdirSync(stepDefinitionFolderPath, { recursive: true });
|
|
98
236
|
}
|
|
99
|
-
const stepDefsFilePath =
|
|
237
|
+
const stepDefsFilePath = locateDefinitionPath(tempFolderPath, pageName);
|
|
238
|
+
//path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
|
|
100
239
|
codePage = getCodePage(stepDefsFilePath);
|
|
101
240
|
codePage = await saveRecording({ step, cucumberStep, codePage, projectDir: this.projectDir, stepsDefinitions });
|
|
102
241
|
if (codePage) {
|
|
@@ -106,19 +245,49 @@ export class BVTStepRunner {
|
|
|
106
245
|
if (!codePage) {
|
|
107
246
|
codePage = getUtilsCodePage(this.projectDir);
|
|
108
247
|
}
|
|
248
|
+
} else {
|
|
249
|
+
let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
|
|
250
|
+
if (process.env.TEMP_RUN === "true") {
|
|
251
|
+
// console.log("Save routes in temp folder for running:", routesPath);
|
|
252
|
+
if (existsSync(routesPath)) {
|
|
253
|
+
// console.log("Removing existing temp_routes_folder:", routesPath);
|
|
254
|
+
rmSync(routesPath, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
mkdirSync(routesPath, { recursive: true });
|
|
257
|
+
// console.log("Created temp_routes_folder:", routesPath);
|
|
258
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
259
|
+
} else {
|
|
260
|
+
// console.log("Saving routes in project directory:", this.projectDir);
|
|
261
|
+
if (existsSync(routesPath)) {
|
|
262
|
+
// remove the folder
|
|
263
|
+
try {
|
|
264
|
+
rmSync(routesPath, { recursive: true });
|
|
265
|
+
// console.log("Removed temp_routes_folder:", routesPath);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// console.error("Error removing temp_routes folder", error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
routesPath = path.join(this.projectDir, "data", "routes");
|
|
271
|
+
// console.log("Saving routes to:", routesPath);
|
|
272
|
+
if (!existsSync(routesPath)) {
|
|
273
|
+
mkdirSync(routesPath, { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
276
|
+
}
|
|
109
277
|
}
|
|
110
|
-
const feature_file_path = await this.writeTempFeatureFile({ step, parametersMap, tempFolderPath });
|
|
278
|
+
const feature_file_path = await this.writeTempFeatureFile({ step, parametersMap, tempFolderPath, tags });
|
|
111
279
|
// console.log({ feature_file_path, step_text: step.text });
|
|
112
280
|
|
|
113
281
|
const stepExecController = new AbortController();
|
|
114
282
|
this.#currentStepController = stepExecController;
|
|
115
|
-
await withAbort(async () => {
|
|
116
|
-
await
|
|
283
|
+
const { result, info } = await withAbort(async () => {
|
|
284
|
+
return await this.executeStepRemote(
|
|
117
285
|
{
|
|
118
286
|
feature_file_path,
|
|
119
287
|
tempFolderPath,
|
|
120
288
|
stepText: step.text,
|
|
121
289
|
scenario: "Temp Scenario",
|
|
290
|
+
config,
|
|
122
291
|
},
|
|
123
292
|
options
|
|
124
293
|
);
|
|
@@ -128,5 +297,6 @@ export class BVTStepRunner {
|
|
|
128
297
|
fs.rmSync(tempFolderPath, { recursive: true });
|
|
129
298
|
}
|
|
130
299
|
});
|
|
300
|
+
return { result, info };
|
|
131
301
|
}
|
|
132
302
|
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import url from "url";
|
|
4
4
|
import logger from "../../logger.js";
|
|
5
|
-
import { CodePage } from "../code_gen/page_reflection.js";
|
|
5
|
+
import { CodePage, getAiConfig } from "../code_gen/page_reflection.js";
|
|
6
6
|
import { generateCode, generatePageName } from "../code_gen/playwright_codeget.js";
|
|
7
7
|
import { invertCodeToCommand } from "../code_gen/code_inversion.js";
|
|
8
8
|
import { Step } from "../cucumber/feature.js";
|
|
9
|
-
import { StepsDefinitions } from "../cucumber/steps_definitions.js";
|
|
9
|
+
import { locateDefinitionPath, StepsDefinitions } from "../cucumber/steps_definitions.js";
|
|
10
10
|
import { Recording } from "../recording.js";
|
|
11
11
|
import { generateApiCode } from "../code_gen/api_codegen.js";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
import { createHash } from "crypto";
|
|
12
14
|
|
|
13
15
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
14
16
|
|
|
@@ -69,19 +71,72 @@ function makeStepTextUnique(step, stepsDefinitions) {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
export async function saveRecording({ step, cucumberStep, codePage, projectDir, stepsDefinitions }) {
|
|
72
|
-
|
|
74
|
+
let routesPath = path.join(tmpdir(), "blinq_temp_routes");
|
|
75
|
+
|
|
76
|
+
if (process.env.TEMP_RUN) {
|
|
77
|
+
if (existsSync(routesPath)) {
|
|
78
|
+
rmSync(routesPath, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
mkdirSync(routesPath, { recursive: true });
|
|
81
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
82
|
+
} else {
|
|
83
|
+
if (existsSync(routesPath)) {
|
|
84
|
+
// remove the folder
|
|
85
|
+
try {
|
|
86
|
+
rmSync(routesPath, { recursive: true });
|
|
87
|
+
// console.log("Removed temp_routes_folder:", routesPath);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// console.error("Error removing temp_routes folder", error);
|
|
90
|
+
}
|
|
91
|
+
routesPath = path.join(projectDir, "data", "routes");
|
|
92
|
+
if (!existsSync(routesPath)) {
|
|
93
|
+
mkdirSync(routesPath, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
73
99
|
if (step.isImplementedWhileRecording && !process.env.TEMP_RUN) {
|
|
74
100
|
return;
|
|
75
101
|
}
|
|
102
|
+
|
|
76
103
|
if (step.isImplemented && step.shouldOverride) {
|
|
77
104
|
let stepDef = stepsDefinitions.findMatchingStep(step.text);
|
|
78
105
|
codePage = getCodePage(stepDef.file);
|
|
79
106
|
} else {
|
|
80
107
|
const isUtilStep = makeStepTextUnique(step, stepsDefinitions);
|
|
108
|
+
|
|
81
109
|
if (isUtilStep) {
|
|
82
110
|
return;
|
|
83
111
|
}
|
|
84
112
|
}
|
|
113
|
+
if (process.env.TEMP_RUN === "true") {
|
|
114
|
+
// console.log("Save routes in temp folder for running:", routesPath);
|
|
115
|
+
if (existsSync(routesPath)) {
|
|
116
|
+
// console.log("Removing existing temp_routes_folder:", routesPath);
|
|
117
|
+
rmSync(routesPath, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
mkdirSync(routesPath, { recursive: true });
|
|
120
|
+
// console.log("Created temp_routes_folder:", routesPath);
|
|
121
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
122
|
+
} else {
|
|
123
|
+
// console.log("Saving routes in project directory:", projectDir);
|
|
124
|
+
if (existsSync(routesPath)) {
|
|
125
|
+
// remove the folder
|
|
126
|
+
try {
|
|
127
|
+
rmSync(routesPath, { recursive: true });
|
|
128
|
+
// console.log("Removed temp_routes_folder:", routesPath);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// console.error("Error removing temp_routes folder", error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
routesPath = path.join(projectDir, "data", "routes");
|
|
134
|
+
// console.log("Saving routes to:", routesPath);
|
|
135
|
+
if (!existsSync(routesPath)) {
|
|
136
|
+
mkdirSync(routesPath, { recursive: true });
|
|
137
|
+
}
|
|
138
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
139
|
+
}
|
|
85
140
|
cucumberStep.text = step.text;
|
|
86
141
|
const recording = new Recording();
|
|
87
142
|
const steps = step.commands;
|
|
@@ -108,6 +163,7 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
108
163
|
isStaticToken,
|
|
109
164
|
status,
|
|
110
165
|
} = step.commands[0].value;
|
|
166
|
+
|
|
111
167
|
const result = await generateApiCode(
|
|
112
168
|
{
|
|
113
169
|
url,
|
|
@@ -132,11 +188,15 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
132
188
|
step.keyword,
|
|
133
189
|
stepsDefinitions
|
|
134
190
|
);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
191
|
+
|
|
192
|
+
if (!step.isImplemented) {
|
|
193
|
+
stepsDefinitions.addStep({
|
|
194
|
+
name: step.text,
|
|
195
|
+
file: result.codePage.sourceFileName,
|
|
196
|
+
source: "recorder",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
140
200
|
cucumberStep.methodName = result.methodName;
|
|
141
201
|
return result.codePage;
|
|
142
202
|
} else {
|
|
@@ -164,7 +224,13 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
164
224
|
path
|
|
165
225
|
);
|
|
166
226
|
const keyword = (cucumberStep.keywordAlias ?? cucumberStep.keyword).trim();
|
|
167
|
-
const stepResult = codePage.addCucumberStep(
|
|
227
|
+
const stepResult = codePage.addCucumberStep(
|
|
228
|
+
keyword,
|
|
229
|
+
cucumberStep.getTemplate(),
|
|
230
|
+
methodName,
|
|
231
|
+
steps.length,
|
|
232
|
+
step.finalTimeout
|
|
233
|
+
);
|
|
168
234
|
|
|
169
235
|
if (!step.isImplemented) {
|
|
170
236
|
stepsDefinitions.addStep({
|
|
@@ -185,7 +251,17 @@ export async function saveRecording({ step, cucumberStep, codePage, projectDir,
|
|
|
185
251
|
|
|
186
252
|
const getLocatorsJson = (file) => {
|
|
187
253
|
if (!file) return {};
|
|
188
|
-
|
|
254
|
+
let locatorsFilePath = file.replace(".mjs", ".json");
|
|
255
|
+
const originLocatorsFilePath = locatorsFilePath;
|
|
256
|
+
const config = getAiConfig();
|
|
257
|
+
if (config && config.locatorsMetadataDir) {
|
|
258
|
+
// if config.locatorsMetadataDir is set, use it to create the file path
|
|
259
|
+
locatorsFilePath = path.join(config.locatorsMetadataDir, path.basename(locatorsFilePath));
|
|
260
|
+
if (!existsSync(locatorsFilePath)) {
|
|
261
|
+
// if the file does not exist in the config directory, use the original path
|
|
262
|
+
locatorsFilePath = originLocatorsFilePath;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
189
265
|
if (!existsSync(locatorsFilePath)) {
|
|
190
266
|
return {};
|
|
191
267
|
}
|
|
@@ -227,7 +303,7 @@ export const getCommandsForImplementedStep = (stepName, stepsDefinitions, stepPa
|
|
|
227
303
|
|
|
228
304
|
isUtilStep = codePage.sourceFileName.endsWith("utils.mjs");
|
|
229
305
|
for (const { code } of codeCommands) {
|
|
230
|
-
const command = invertCodeToCommand(code, elements, stepParams)[0];
|
|
306
|
+
const command = invertCodeToCommand(code, elements, stepParams, stepsDefinitions, codePage, stepName)[0];
|
|
231
307
|
if (command === undefined || command.type === null) continue;
|
|
232
308
|
if (command.element) {
|
|
233
309
|
const key = command.element.key;
|
|
@@ -297,6 +373,7 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
|
|
|
297
373
|
const steps = scenario.steps;
|
|
298
374
|
|
|
299
375
|
const stepsDefinitions = new StepsDefinitions(projectDir);
|
|
376
|
+
const featureFolder = path.join(projectDir, "features");
|
|
300
377
|
stepsDefinitions.load(false);
|
|
301
378
|
// const parameters = scenario.parameters;
|
|
302
379
|
// await saveRecordings({ steps, parameters, codePage, projectDir });
|
|
@@ -308,13 +385,41 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
|
|
|
308
385
|
}
|
|
309
386
|
}
|
|
310
387
|
if ((step.isImplemented && !step.shouldOverride) || step.commands.length === 0) {
|
|
388
|
+
let routesPath = path.join(tmpdir(), `blinq_temp_routes`);
|
|
389
|
+
if (process.env.TEMP_RUN === "true") {
|
|
390
|
+
// console.log("Save routes in temp folder for running:", routesPath);
|
|
391
|
+
if (existsSync(routesPath)) {
|
|
392
|
+
// console.log("Removing existing temp_routes_folder:", routesPath);
|
|
393
|
+
rmSync(routesPath, { recursive: true });
|
|
394
|
+
}
|
|
395
|
+
mkdirSync(routesPath, { recursive: true });
|
|
396
|
+
// console.log("Created temp_routes_folder:", routesPath);
|
|
397
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
398
|
+
} else {
|
|
399
|
+
// console.log("Saving routes in project directory:", projectDir);
|
|
400
|
+
if (existsSync(routesPath)) {
|
|
401
|
+
// remove the folder
|
|
402
|
+
try {
|
|
403
|
+
rmSync(routesPath, { recursive: true });
|
|
404
|
+
// console.log("Removed temp_routes_folder:", routesPath);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
// console.error("Error removing temp_routes folder", error);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
routesPath = path.join(projectDir, "data", "routes");
|
|
410
|
+
// console.log("Saving routes to:", routesPath);
|
|
411
|
+
if (!existsSync(routesPath)) {
|
|
412
|
+
mkdirSync(routesPath, { recursive: true });
|
|
413
|
+
}
|
|
414
|
+
saveRoutes({ step, folderPath: routesPath });
|
|
415
|
+
}
|
|
311
416
|
continue;
|
|
312
417
|
}
|
|
313
418
|
const cucumberStep = getCucumberStep({ step });
|
|
314
419
|
const pageName = generatePageName(step.startFrame?.url ?? "default");
|
|
315
|
-
const stepDefsFilePath =
|
|
420
|
+
const stepDefsFilePath = locateDefinitionPath(featureFolder, pageName);
|
|
421
|
+
// path.join(stepDefinitionFolderPath, pageName + "_page.mjs");
|
|
316
422
|
let codePage = getCodePage(stepDefsFilePath);
|
|
317
|
-
|
|
318
423
|
codePage = await saveRecording({ step, cucumberStep, codePage, projectDir, stepsDefinitions });
|
|
319
424
|
if (!codePage) {
|
|
320
425
|
continue;
|
|
@@ -326,3 +431,43 @@ export async function updateStepDefinitions({ scenario, featureName, projectDir
|
|
|
326
431
|
}
|
|
327
432
|
writeFileSync(utilsFilePath, utilsContent, "utf8");
|
|
328
433
|
}
|
|
434
|
+
|
|
435
|
+
export function saveRoutes({ step, folderPath }) {
|
|
436
|
+
const routeItems = step.routeItems;
|
|
437
|
+
if (!routeItems || routeItems.length === 0) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const cucumberStep = getCucumberStep({ step });
|
|
441
|
+
const template = cucumberStep.getTemplate();
|
|
442
|
+
const stepNameHash = createHash("sha256").update(template).digest("hex");
|
|
443
|
+
// console.log("Saving routes for step:", step.text, "with hash:", stepNameHash);
|
|
444
|
+
const routeItemsWithFilters = routeItems.map((routeItem) => {
|
|
445
|
+
const oldFilters = routeItem.filters;
|
|
446
|
+
const queryParamsObject = {};
|
|
447
|
+
oldFilters.queryParams.forEach((queryParam) => {
|
|
448
|
+
queryParamsObject[queryParam.key] = queryParam.value;
|
|
449
|
+
});
|
|
450
|
+
const newFilters = { path: oldFilters.path, method: oldFilters.method, queryParams: queryParamsObject };
|
|
451
|
+
return {
|
|
452
|
+
...routeItem,
|
|
453
|
+
filters: newFilters,
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const routesFilePath = path.join(folderPath, stepNameHash + ".json");
|
|
458
|
+
// console.log("Routes file path:", routesFilePath);
|
|
459
|
+
const routesData = {
|
|
460
|
+
template,
|
|
461
|
+
routes: routeItemsWithFilters,
|
|
462
|
+
};
|
|
463
|
+
// console.log("Routes data to save:", routesData);
|
|
464
|
+
if (!existsSync(folderPath)) {
|
|
465
|
+
mkdirSync(folderPath, { recursive: true });
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
writeFileSync(routesFilePath, JSON.stringify(routesData, null, 2), "utf8");
|
|
469
|
+
// console.log("Saved routes to", routesFilePath);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
console.error("Failed to save routes to", routesFilePath, "Error:", error);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
|
|
3
|
+
import { getDefaultPrettierConfig } from "../code_cleanup/utils.js";
|
|
4
|
+
import prettier from "prettier";
|
|
4
5
|
export function containsScenario({ featureFileContent, scenarioName }) {
|
|
5
6
|
const lines = featureFileContent.split("\n");
|
|
6
7
|
for (const line of lines) {
|
|
@@ -209,14 +210,12 @@ const GherkinToObject = (gherkin) => {
|
|
|
209
210
|
steps: [],
|
|
210
211
|
};
|
|
211
212
|
while (idx < lines.length && lines[idx].startsWith("@")) {
|
|
212
|
-
skipEmptyLines();
|
|
213
213
|
const tags = [...lines[idx].matchAll(/@([^@]+)/g)].map((match) => match[1].trim());
|
|
214
214
|
scenario.tags.push(...(tags ?? []));
|
|
215
215
|
idx++;
|
|
216
|
+
skipEmptyLines();
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
skipEmptyLines();
|
|
219
|
-
|
|
220
219
|
if (idx < lines.length && (lines[idx].startsWith("Scenario:") || lines[idx].startsWith("Scenario Outline:"))) {
|
|
221
220
|
scenario.name = lines[idx].substring(lines[idx].indexOf(":") + 1).trim();
|
|
222
221
|
idx++;
|
|
@@ -239,32 +238,32 @@ const GherkinToObject = (gherkin) => {
|
|
|
239
238
|
!lines[idx].startsWith("@")
|
|
240
239
|
) {
|
|
241
240
|
const line = lines[idx++];
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
type: "comment",
|
|
247
|
-
text: comment,
|
|
248
|
-
};
|
|
249
|
-
scenario.steps.push(command);
|
|
250
|
-
}
|
|
251
|
-
} else if (line.startsWith("Examples:")) {
|
|
252
|
-
obj.hasParams = true;
|
|
253
|
-
const command = {
|
|
241
|
+
let command;
|
|
242
|
+
if (line.startsWith("Examples:")) {
|
|
243
|
+
scenario.hasParams = true;
|
|
244
|
+
command = {
|
|
254
245
|
type: "examples",
|
|
255
246
|
lines: [],
|
|
256
247
|
};
|
|
257
|
-
|
|
258
248
|
while (idx < lines.length && lines[idx].startsWith("|")) {
|
|
259
249
|
const line = lines[idx++];
|
|
260
250
|
command.lines.push(line);
|
|
261
251
|
}
|
|
262
252
|
} else {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
253
|
+
if (line.startsWith("#")) {
|
|
254
|
+
command = {
|
|
255
|
+
type: "comment",
|
|
256
|
+
text: line,
|
|
257
|
+
};
|
|
258
|
+
} else {
|
|
259
|
+
command = {
|
|
260
|
+
type: "step",
|
|
261
|
+
text: line,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
267
264
|
}
|
|
265
|
+
scenario.steps.push(command);
|
|
266
|
+
skipEmptyLines();
|
|
268
267
|
}
|
|
269
268
|
|
|
270
269
|
return scenario;
|
|
@@ -273,7 +272,6 @@ const GherkinToObject = (gherkin) => {
|
|
|
273
272
|
while (idx < lines.length) {
|
|
274
273
|
const scenario = getScenario();
|
|
275
274
|
if (scenario === -1) break;
|
|
276
|
-
|
|
277
275
|
if (scenario.error) {
|
|
278
276
|
return {
|
|
279
277
|
error: scenario.error,
|
|
@@ -299,8 +297,7 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
|
|
|
299
297
|
skipScenarioIndex = i;
|
|
300
298
|
continue;
|
|
301
299
|
}
|
|
302
|
-
let scenarioContent = `${
|
|
303
|
-
|
|
300
|
+
let scenarioContent = `${scenario.hasParams ? "Scenario Outline" : "Scenario"}: ${scenario.name}`;
|
|
304
301
|
let tagsLine;
|
|
305
302
|
if (scenario.tags?.length > 0) {
|
|
306
303
|
tagsLine = `${scenario.tags.map((t) => `@${t}`).join(" ")}`;
|
|
@@ -323,7 +320,6 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
|
|
|
323
320
|
if (skipScenarioIndex !== -1) {
|
|
324
321
|
finalContent = results.join("\n") + "\n" + scenarioContent;
|
|
325
322
|
}
|
|
326
|
-
|
|
327
323
|
return finalContent;
|
|
328
324
|
}
|
|
329
325
|
export async function updateFeatureFile({ featureName, scenario, override, projectDir }) {
|
|
@@ -333,6 +329,8 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
|
|
|
333
329
|
{ scenario },
|
|
334
330
|
isFeatureFileExists ? GherkinToObject(readFileSync(featureFilePath, "utf8")) : undefined
|
|
335
331
|
);
|
|
332
|
+
const prettierConfig = getDefaultPrettierConfig();
|
|
333
|
+
// Format the code using Prettier
|
|
336
334
|
|
|
337
335
|
if (isFeatureFileExists) {
|
|
338
336
|
const featureFileContent = readFileSync(featureFilePath, "utf8");
|
|
@@ -341,7 +339,7 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
|
|
|
341
339
|
if (!override) {
|
|
342
340
|
throw new Error(`Scenario "${scenario.name}" already exists in feature "${featureName}"`);
|
|
343
341
|
} else {
|
|
344
|
-
|
|
342
|
+
let updatedFeatureFileContent = updateExistingScenario({
|
|
345
343
|
featureFileContent,
|
|
346
344
|
scenarioName: scenario.name,
|
|
347
345
|
scenarioContent,
|
|
@@ -349,14 +347,41 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
|
|
|
349
347
|
if (updatedFeatureFileContent === "error") {
|
|
350
348
|
throw new Error(`Error while parsing feature file: Invalid gherkin`);
|
|
351
349
|
}
|
|
350
|
+
try {
|
|
351
|
+
updatedFeatureFileContent = await prettier.format(updatedFeatureFileContent, {
|
|
352
|
+
...prettierConfig,
|
|
353
|
+
parser: "gherkin",
|
|
354
|
+
plugins: ["prettier-plugin-gherkin"],
|
|
355
|
+
});
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error("Error formatting feature file content with Prettier:", error);
|
|
358
|
+
}
|
|
352
359
|
writeFileSync(featureFilePath, updatedFeatureFileContent);
|
|
353
360
|
return;
|
|
354
361
|
}
|
|
355
362
|
}
|
|
356
|
-
|
|
363
|
+
let updatedFeatureFileContent = featureFileContent + "\n" + scenarioContent;
|
|
364
|
+
try {
|
|
365
|
+
updatedFeatureFileContent = await prettier.format(updatedFeatureFileContent, {
|
|
366
|
+
...prettierConfig,
|
|
367
|
+
parser: "gherkin",
|
|
368
|
+
plugins: ["prettier-plugin-gherkin"],
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error("Error formatting feature file content with Prettier:", error);
|
|
372
|
+
}
|
|
357
373
|
writeFileSync(featureFilePath, updatedFeatureFileContent);
|
|
358
374
|
} else {
|
|
359
|
-
|
|
375
|
+
let featureFileContent = `Feature: ${featureName}\n${scenarioContent}`;
|
|
376
|
+
try {
|
|
377
|
+
featureFileContent = await prettier.format(featureFileContent, {
|
|
378
|
+
...prettierConfig,
|
|
379
|
+
parser: "gherkin",
|
|
380
|
+
plugins: ["prettier-plugin-gherkin"],
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error("Error formatting feature file content with Prettier:", error);
|
|
384
|
+
}
|
|
360
385
|
writeFileSync(featureFilePath, featureFileContent);
|
|
361
386
|
}
|
|
362
387
|
}
|