artes 1.4.4 → 1.4.6

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/README.md CHANGED
@@ -119,6 +119,7 @@ npx artes [options]
119
119
  | 📜 `--stepDef` | Specify one or more step definition files' relative paths to use (comma-separated) | `artes --stepDef "tests/steps/login.js,tests/steps/home.js"` |
120
120
  | 🔖 `--tags` | Run tests with specified Cucumber tags | `artes --tags "@smoke or @wip"` |
121
121
  | 🌐 `--env` | Set the environment for the test run | `artes --env "dev"` |
122
+ | `--saveVar` | Set the variables from CLI | `artes --saveVar '{"armud":20,"banana":200}'` |
122
123
  | 🕶️ `--headless` | Run browser in headless mode | `artes --headless` |
123
124
  | ⚡ `--parallel` | Run tests in parallel mode | `artes --parallel 2` |
124
125
  | 🔁 `--retry` | Retry failed tests | `artes --retry 3` |
@@ -482,23 +483,34 @@ You can configure Artes by editing the `artes.config.js` file. Below are the def
482
483
 
483
484
  | **Option** | **Default Value** | **Description** |
484
485
  | ----------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------- |
486
+ | `headless` | `false` | Run browser in headless mode. |
487
+ | `env` | `""` | Environment name for tests. |
488
+ | `variables` | `{}` | Variables for tests. |
489
+ | `baseURL` | `""` | Base URL for API or web tests. |
485
490
  | `timeout` | `30` | Default timeout in seconds. |
486
- | `slowMo` | `0` | Default slow motion in seconds |
491
+ | `slowMo` | `0` | Default slow motion in seconds. |
487
492
  | `paths` | `[moduleConfig.featuresPath]` | Paths to feature files. |
488
493
  | `require` | `[moduleConfig.stepsPath, "src/stepDefinitions/*.js", "src/hooks/hooks.js"]` | Support code paths (CommonJS). |
489
494
  | `pomPath` | `moduleConfig.pomPath` | Path to Page Object Models. |
490
495
  | `import` | `[]` | Support code paths. |
491
496
  | `testPercentage` | `0` | Define test coverage percentage |
492
497
  | `report` | `false` | Generate report |
493
- | `reportSuccess` | `false` | Add screenshots and video records for also success test cases |
494
- | `trace` | `false` | Enable trace |
495
- | `reportWithTrace` | `false` | Add trace to the report |
498
+ | `zip` | `false` | Generate zip of report |
499
+ | `reportSuccess` | `false` | Add screenshots and video records also for success test cases |
500
+ | `trace` | `false` | Enable tracing |
501
+ | `reportWithTrace` | `false` | Include trace in report |
496
502
  | `format` | `["rerun:@rerun.txt", "allure-cucumberjs/reporter"]` | Formatter names/paths. |
497
503
  | `formatOptions` | `{ "resultsDir": "allure-result" }` | Formatter options. |
498
504
  | `parallel` | `1` | Number of parallel workers. |
505
+ | `browser` | `"chrome"` | Browser to use: "chrome", "firefox", "webkit". |
506
+ | `offline` | `false` | Run browser in offline mode. |
507
+ | `device` | `""` | Emulate specific device (e.g., "iPhone 13"). |
508
+ | `width` | `1280` | Browser width in pixels. |
509
+ | `height` | `720` | Browser height in pixels. |
510
+ | `maximizeScreen` | `true` | Maximize browser window on start. |
499
511
  | `dryRun` | `false` | Prepare test run without execution. |
500
512
  | `failFast` | `false` | Stop on first test failure. |
501
- | `forceExit` | `false` | Force `process.exit()` after tests. |
513
+ | `forceExit` | `false` | Force `process.exit()` after tests |
502
514
  | `strict` | `true` | Fail on pending steps. |
503
515
  | `backtrace` | `false` | Show full backtrace for errors. |
504
516
  | `tags` | `""` | Tag expression to filter scenarios. |
@@ -14,7 +14,7 @@ try {
14
14
  console.log("Proceeding with default config.");
15
15
  }
16
16
 
17
- const defaultFormats = ["rerun:@rerun.txt", "progress-bar"];
17
+ const defaultFormats = ["rerun:@rerun.txt", "progress-bar", './status-formatter.js:null'];
18
18
 
19
19
  const userFormatsFromEnv = process.env.REPORT_FORMAT
20
20
  ? JSON.parse(process.env.REPORT_FORMAT)
@@ -51,6 +51,25 @@ function resolveEnv(artesConfig) {
51
51
  }
52
52
  const env = resolveEnv(artesConfig);
53
53
 
54
+ function loadVariables(cliVariables, artesConfigVariables) {
55
+
56
+ if (cliVariables) {
57
+ try {
58
+ cliVariables = JSON.parse(cliVariables);
59
+ } catch (err) {
60
+ console.error("Invalid JSON in process.env.VARS:", process.env.VARS);
61
+ envVars = {};
62
+ }
63
+ }
64
+
65
+ const mergedVars = {
66
+ ...(artesConfigVariables || {}),
67
+ ...cliVariables,
68
+ };
69
+
70
+ return mergedVars;
71
+ }
72
+
54
73
  const resolveFeaturePaths = (basePath, value) => {
55
74
  return value
56
75
  .split(',')
@@ -162,6 +181,7 @@ module.exports = {
162
181
  zip: process.env.ZIP == "true" ? true : artesConfig.zip ? true : false,
163
182
  },
164
183
  env: env,
184
+ variables: loadVariables(process.env.VARS, artesConfig.variables),
165
185
  baseURL: process.env.BASE_URL
166
186
  ? JSON.parse(process.env.BASE_URL)
167
187
  : artesConfig?.baseURL
package/executer.js CHANGED
@@ -10,17 +10,23 @@ const {
10
10
  const { logPomWarnings } = require("./src/helper/controller/pomCollector");
11
11
  const fs = require("fs");
12
12
  const path = require("path");
13
+ const { spawnSync } = require("child_process");
13
14
 
14
- const configPath = path.resolve(process.cwd(), "artes.config.js");
15
+ const artesConfigPath = path.resolve(process.cwd(), "artes.config.js");
15
16
 
16
17
  let artesConfig = {};
17
18
 
18
- if (fs.existsSync(configPath)) {
19
- artesConfig = require(configPath);
19
+ if (fs.existsSync(artesConfigPath)) {
20
+ artesConfig = require(artesConfigPath);
20
21
  }
21
22
 
22
23
  const args = process.argv.slice(2);
23
24
 
25
+ const getArgValue = (flag) => {
26
+ const index = args.indexOf(flag);
27
+ return index >= 0 ? args[index + 1] : undefined;
28
+ };
29
+
24
30
  const flags = {
25
31
  help: args.includes("-h") || args.includes("--help"),
26
32
  version: args.includes("-v") || args.includes("--version"),
@@ -37,6 +43,7 @@ const flags = {
37
43
  stepDef: args.includes("--stepDef"),
38
44
  tags: args.includes("--tags"),
39
45
  env: args.includes("--env"),
46
+ saveVar: args.includes("--saveVar"),
40
47
  headless: args.includes("--headless"),
41
48
  parallel: args.includes("--parallel"),
42
49
  retry: args.includes("--retry"),
@@ -54,25 +61,30 @@ const flags = {
54
61
  slowMo: args.includes("--slowMo"),
55
62
  };
56
63
 
57
- const env = args[args.indexOf("--env") + 1];
58
- const featureFiles = args[args.indexOf("--features") + 1];
64
+
65
+ const env = getArgValue("--env");
66
+ const vars = getArgValue("--saveVar");
67
+ const featureFiles = getArgValue("--features");
59
68
  const features = flags.features && featureFiles;
60
- const stepDef = args[args.indexOf("--stepDef") + 1];
61
- const tags = args[args.indexOf("--tags") + 1];
62
- const parallel = args[args.indexOf("--parallel") + 1];
63
- const retry = args[args.indexOf("--retry") + 1];
64
- const rerun = args[args.indexOf("--rerun") + 1];
65
- const percentage = args[args.indexOf("--percentage") + 1];
66
- const browser = args[args.indexOf("--browser") + 1];
67
- const device = args[args.indexOf("--device") + 1];
68
- const baseURL = args[args.indexOf("--baseURL") + 1];
69
- const width = args[args.indexOf("--width") + 1];
70
- const height = args[args.indexOf("--height") + 1];
71
- const timeout = args[args.indexOf("--timeout") + 1];
72
- const slowMo = args[args.indexOf("--slowMo") + 1];
69
+ const stepDef = getArgValue("--stepDef");
70
+ const tags = getArgValue("--tags");
71
+ const parallel = getArgValue("--parallel");
72
+ const retry = getArgValue("--retry");
73
+ const rerun = getArgValue("--rerun");
74
+ const percentage = getArgValue("--percentage");
75
+ const browser = getArgValue("--browser");
76
+ const device = getArgValue("--device");
77
+ const baseURL = getArgValue("--baseURL");
78
+ const width = getArgValue("--width");
79
+ const height = getArgValue("--height");
80
+ const timeout = getArgValue("--timeout");
81
+ const slowMo = getArgValue("--slowMo");
82
+
73
83
 
74
84
  flags.env ? (process.env.ENV = env) : "";
75
85
 
86
+ vars ? (process.env.VARS = vars) : "";
87
+
76
88
  flags.reportWithTrace ||
77
89
  artesConfig.reportWithTrace ||
78
90
  flags.report ||
@@ -146,6 +158,140 @@ flags.timeout ? (process.env.TIMEOUT = timeout) : "";
146
158
 
147
159
  flags.slowMo ? (process.env.SLOWMO = slowMo) : "";
148
160
 
161
+
162
+ function findDuplicateTestNames() {
163
+ const testStatusFile = path.join(process.cwd(), "node_modules", "artes" , "test-status", 'test-status.txt');
164
+
165
+ if (!fs.existsSync(testStatusFile)) {
166
+ console.error('test-status.txt not found');
167
+ return;
168
+ }
169
+
170
+ const content = fs.readFileSync(testStatusFile, 'utf8');
171
+ const lines = content.split('\n').filter(line => line.trim());
172
+
173
+ const testNameToFiles = {};
174
+
175
+ lines.forEach(line => {
176
+ const parts = line.split(' | ');
177
+ if (parts.length < 5) return;
178
+
179
+ const testName = parts[2].trim();
180
+ const filePath = parts[4].trim();
181
+
182
+ if (!testNameToFiles[testName]) {
183
+ testNameToFiles[testName] = new Set();
184
+ }
185
+
186
+ testNameToFiles[testName].add(filePath);
187
+ });
188
+
189
+ const duplicates = {};
190
+
191
+ Object.entries(testNameToFiles).forEach(([testName, files]) => {
192
+ if (files.size > 1) {
193
+ duplicates[testName] = Array.from(files);
194
+ }
195
+ });
196
+
197
+ if (Object.keys(duplicates).length > 0) {
198
+ console.warn('\n\x1b[33m[WARNING] Duplicate scenarios names found: This will effect your reporting');
199
+ Object.entries(duplicates).forEach(([testName, files]) => {
200
+ console.log(`\x1b[33m"${testName}" exists in:`);
201
+ files.forEach(file => {
202
+ console.log(` - ${file}`);
203
+ });
204
+ console.log('');
205
+ });
206
+ console.log("\x1b[0m");
207
+ }
208
+
209
+ return duplicates;
210
+ }
211
+
212
+
213
+ function testCoverageCalculation() {
214
+
215
+ const testStatusFile = path.join(process.cwd(), "node_modules", "artes" , "test-status", 'test-status.txt');
216
+
217
+ if (!fs.existsSync(testStatusFile)) {
218
+ console.error('test-status.txt not found');
219
+ return null;
220
+ }
221
+
222
+ const content = fs.readFileSync(testStatusFile, 'utf8');
223
+ const lines = content.split('\n').filter(line => line.trim());
224
+
225
+ const map = {};
226
+ const retriedTests = [];
227
+ const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
228
+
229
+ lines.forEach(line => {
230
+ const parts = line.split(' | ');
231
+ if (parts.length < 5) return;
232
+
233
+ const timestamp = parts[0].trim();
234
+ const status = parts[1].trim();
235
+ const scenario = parts[2].trim();
236
+ const id = parts[3].trim();
237
+ const uri = parts[4].trim();
238
+
239
+ if (!uuidRegex.test(id)) return;
240
+
241
+ if (!map[id]) {
242
+ map[id] = {
243
+ count: 1,
244
+ latest: { status, scenario, timestamp, uri }
245
+ };
246
+ } else {
247
+ map[id].count++;
248
+ if (timestamp > map[id].latest.timestamp) {
249
+ map[id].latest = { status, scenario, timestamp, uri };
250
+ }
251
+ }
252
+ });
253
+
254
+ let total = 0;
255
+ let notPassed = 0;
256
+
257
+ Object.entries(map).forEach(([id, data]) => {
258
+ total++;
259
+
260
+ if (data.count > 1) {
261
+ retriedTests.push({
262
+ scenario: data.latest.scenario,
263
+ id,
264
+ count: data.count
265
+ });
266
+ }
267
+
268
+ if (data.latest.status !== 'PASSED') {
269
+ notPassed++;
270
+ }
271
+ });
272
+
273
+ if (retriedTests.length > 0) {
274
+ console.warn('\n\x1b[33mRetried test cases:');
275
+ retriedTests.forEach(t => {
276
+ console.warn(`- "${t.scenario}" ran ${t.count} times`);
277
+ });
278
+ console.log("\x1b[0m");
279
+ }
280
+
281
+ return {
282
+ percentage: (total - notPassed) / total * 100,
283
+ totalTests: total,
284
+ notPassed,
285
+ passed: total - notPassed,
286
+ latestStatuses: Object.fromEntries(
287
+ Object.entries(map).map(([id, data]) => [
288
+ id,
289
+ data.latest.status
290
+ ])
291
+ )
292
+ };
293
+ }
294
+
149
295
  function main() {
150
296
  if (flags.help) return showHelp();
151
297
  if (flags.version) return showVersion();
@@ -155,6 +301,37 @@ function main() {
155
301
 
156
302
  logPomWarnings();
157
303
 
304
+ findDuplicateTestNames();
305
+
306
+ const testCoverage = testCoverageCalculation()
307
+
308
+ const testPercentage = (process.env.PERCENTAGE ? Number(process.env.PERCENTAGE) : artesConfig.testPercentage || 0)
309
+
310
+ if (testPercentage > 0) {
311
+
312
+ const meetsThreshold = testCoverage.percentage >= testPercentage
313
+
314
+ if (meetsThreshold) {
315
+ console.log(
316
+ `✅ Tests passed required ${testPercentage}% success rate with ${testCoverage.percentage.toFixed(2)}%!`,
317
+ );
318
+ process.env.EXIT_CODE = parseInt(0, 10);
319
+ } else {
320
+ console.log(
321
+ `❌ Tests failed required ${testPercentage}% success rate with ${testCoverage.percentage.toFixed(2)}%!`,
322
+ );
323
+ process.env.EXIT_CODE = parseInt(1, 10);
324
+ }
325
+ }
326
+
327
+ if (fs.existsSync(path.join(process.cwd(), "node_modules", "artes" , "@rerun.txt"))) {
328
+ spawnSync("mv", ["@rerun.txt", process.cwd()], {
329
+ cwd: path.join(process.cwd(), "node_modules", "artes"),
330
+ stdio: "inherit",
331
+ shell: true,
332
+ });
333
+ }
334
+
158
335
  if (
159
336
  flags.reportWithTrace ||
160
337
  artesConfig.reportWithTrace ||
@@ -163,18 +340,25 @@ function main() {
163
340
  )
164
341
  generateReport();
165
342
 
166
- if (
167
- fs.existsSync(
168
- path.join(process.cwd(), "node_modules", "artes", "EXIT_CODE.txt"),
169
- )
170
- ) {
171
- const data = fs.readFileSync(
172
- path.join(process.cwd(), "node_modules", "artes", "EXIT_CODE.txt"),
173
- "utf8",
174
- );
175
- process.env.EXIT_CODE = parseInt(data, 10);
176
- }
177
343
 
344
+ if (!( process.env.TRACE === "true"
345
+ ? process.env.TRACE
346
+ : artesConfig.trace || false)) {
347
+ spawnSync(
348
+ "npx",
349
+ [
350
+ "rimraf",
351
+ "--no-glob",
352
+ path.join(process.cwd(), "traces"),
353
+ ],
354
+ {
355
+ cwd: process.cwd(),
356
+ stdio: "inherit",
357
+ shell: false,
358
+ },
359
+ );
360
+ }
361
+
178
362
  cleanUp();
179
363
  process.exit(process.env.EXIT_CODE);
180
364
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artes",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "description": "The simplest way to automate UI and API tests using Cucumber-style steps.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -54,6 +54,9 @@ function showHelp() {
54
54
 
55
55
  🌐 --env Set environment for the test run
56
56
  Usage: artes --env "dev"
57
+
58
+ --saveVar Set variables from cli
59
+ artes --saveVar '{"armud":20,"banana":200}'
57
60
 
58
61
  🕶️ --headless Run browser in headless mode
59
62
  Usage: artes --headless
@@ -47,6 +47,7 @@ function createProject(createYes, noDeps) {
47
47
 
48
48
  // Configuration options:
49
49
  // env: "", // string - Environment name for tests
50
+ // variables: {} // object - Variables for tests
50
51
  // testPercentage: 0, // number - Minimum success rate percentage(Default: 0)
51
52
  // baseURL: "", // string - Base URL for API tests
52
53
  // paths: [], // string[] - Paths to feature files
@@ -56,6 +57,7 @@ function createProject(createYes, noDeps) {
56
57
  // slowMo: 0, // number - Slow down test execution (Default: 0 seconds)
57
58
  // parallel: 0, // number - Number of parallel workers
58
59
  // report: true // boolean - Generate report
60
+ // zip: false // boolean - Generate zip of report
59
61
  // reportSuccess: false, // boolean - Add screenshots and video records to report also for success test cases
60
62
  // trace: false, // boolean - Enable tracing
61
63
  // reportWithTrace: false, // boolean - Include trace in report
@@ -30,7 +30,7 @@ function generateReport() {
30
30
  );
31
31
 
32
32
  if (fs.existsSync(moduleConfig.reportPath) && process.env.ZIP === "true") {
33
- console.log(`🗜️ Zipping report folder`);
33
+ console.log(`🗜️ Zipping report folder...`);
34
34
 
35
35
  const zipPath = path.join(
36
36
  path.dirname(moduleConfig.reportPath),
@@ -35,7 +35,7 @@ const moduleConfig = {
35
35
  stepsPath: path.join(projectPath, "/tests/steps/*.js"),
36
36
  pomPath: path.join(projectPath, "/tests/POMs"),
37
37
  cleanUpPaths:
38
- "allure-result allure-results test-results @rerun.txt testsStatus EXIT_CODE.txt pomDuplicateWarnings.json",
38
+ "allure-result allure-results test-results @rerun.txt test-status null pomDuplicateWarnings.json",
39
39
  };
40
40
 
41
41
  module.exports = {
@@ -8,18 +8,16 @@ const {
8
8
  BeforeStep,
9
9
  AfterAll,
10
10
  } = require("@cucumber/cucumber");
11
- const { spawnSync } = require("child_process");
12
11
  const { invokeBrowser } = require("../helper/contextManager/browserManager");
13
12
  const { invokeRequest } = require("../helper/contextManager/requestManager");
14
13
  const {
15
- pomCollector,
16
- logPomWarnings,
14
+ pomCollector
17
15
  } = require("../helper/controller/pomCollector");
18
16
  const cucumberConfig = require("../../cucumber.config");
19
17
  const { context } = require("./context");
20
18
  const fs = require("fs");
21
19
  const path = require("path");
22
- const { moduleConfig } = require("artes/src/helper/imports/commons");
20
+ const { moduleConfig, saveVar } = require("artes/src/helper/imports/commons");
23
21
  require("allure-cucumberjs");
24
22
  const allure = require("allure-js-commons");
25
23
 
@@ -43,14 +41,6 @@ async function attachResponse(attachFn) {
43
41
  }
44
42
  }
45
43
 
46
- function saveTestStatus(result, pickle) {
47
- fs.mkdirSync(statusDir, { recursive: true });
48
- fs.writeFileSync(
49
- path.join(statusDir, `${result.status}-${pickle.id}.txt`),
50
- "",
51
- );
52
- }
53
-
54
44
  const projectHooksPath = path.resolve(
55
45
  moduleConfig.projectPath,
56
46
  "tests/steps/hooks.js",
@@ -81,6 +71,15 @@ BeforeAll(async () => {
81
71
  Before(async function ({pickle}) {
82
72
  context.vars = {};
83
73
 
74
+ const vars = await cucumberConfig.variables
75
+
76
+ if (vars && typeof vars === "object") {
77
+ for (let [key, value] of Object.entries(vars)) {
78
+
79
+ saveVar(value, key);
80
+ }
81
+ }
82
+
84
83
  const envFilePath = path.join(
85
84
  moduleConfig.projectPath,
86
85
  "tests",
@@ -142,7 +141,7 @@ AfterStep(async function ({ pickleStep }) {
142
141
  }
143
142
  });
144
143
 
145
- After(async function ({ pickle, result }) {
144
+ After(async function ({result, pickle}) {
146
145
  if (typeof projectHooks.After === "function") {
147
146
  await projectHooks.After();
148
147
  }
@@ -156,12 +155,11 @@ After(async function ({ pickle, result }) {
156
155
  await allure.attachment("Screenshot", screenshotBuffer, "image/png");
157
156
  }
158
157
 
159
- saveTestStatus(result, pickle);
160
158
 
161
159
  if (cucumberConfig.default.reportWithTrace || cucumberConfig.default.trace) {
162
160
  var tracePath = path.join(
163
161
  moduleConfig.projectPath,
164
- `./traces/${pickle.name.replaceAll(" ", "_")}.zip`,
162
+ `./traces/${pickle.name.replaceAll(" ", "_")}-${pickle.id}.zip`,
165
163
  );
166
164
  }
167
165
 
@@ -212,55 +210,4 @@ AfterAll(async () => {
212
210
  await projectHooks.AfterAll();
213
211
  }
214
212
 
215
- logPomWarnings();
216
-
217
- if (!cucumberConfig.default.trace) {
218
- spawnSync(
219
- "npx",
220
- [
221
- "rimraf",
222
- "--no-glob",
223
- path.join(moduleConfig.projectPath, "./traces"),
224
- ],
225
- {
226
- cwd: moduleConfig.projectPath,
227
- stdio: "inherit",
228
- shell: false,
229
- },
230
- );
231
- }
232
-
233
- if (!fs.existsSync(statusDir)) return;
234
-
235
- const files = fs.readdirSync(statusDir);
236
- const passedCount = files.filter((f) => f.split("-")[0] === "PASSED").length;
237
- const totalTests = files.length;
238
- const successPercentage = (passedCount / totalTests) * 100;
239
-
240
- const failed = files.filter((f) => f.split("-")[0] === "FAILED").length;
241
-
242
- if (failed > 0) {
243
- spawnSync("mv", ["@rerun.txt", moduleConfig.projectPath], {
244
- cwd: path.join(moduleConfig.projectPath, "node_modules", "artes"),
245
- stdio: "ignore",
246
- shell: true,
247
- });
248
- }
249
-
250
- if (cucumberConfig.default.testPercentage > 0) {
251
- const meetsThreshold =
252
- successPercentage >= cucumberConfig.default.testPercentage;
253
-
254
- if (meetsThreshold) {
255
- console.log(
256
- `✅ Tests passed required ${cucumberConfig.default.testPercentage}% success rate with ${successPercentage.toFixed(2)}%!`,
257
- );
258
- fs.writeFileSync(path.join(process.cwd(), "EXIT_CODE.txt"), "0");
259
- } else {
260
- console.log(
261
- `❌ Tests failed required ${cucumberConfig.default.testPercentage}% success rate with ${successPercentage.toFixed(2)}%!`,
262
- );
263
- fs.writeFileSync(path.join(process.cwd(), "EXIT_CODE.txt"), "1");
264
- }
265
- }
266
213
  });
@@ -0,0 +1,138 @@
1
+ const { Formatter } = require('@cucumber/cucumber');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ class StatusFormatter extends Formatter {
6
+ constructor(options) {
7
+ super(options);
8
+
9
+ const outputDir = './test-status';
10
+ if (!fs.existsSync(outputDir)) {
11
+ fs.mkdirSync(outputDir, { recursive: true });
12
+ }
13
+
14
+ const workerId = process.env.CUCUMBER_WORKER_ID || '0';
15
+ this.workerId = workerId;
16
+ this.outputFile = path.join(outputDir, `test-results-${workerId}.txt`);
17
+ this.outputDir = outputDir;
18
+
19
+ fs.writeFileSync(this.outputFile, '', 'utf8');
20
+
21
+ this.pickles = new Map();
22
+ this.testCases = new Map();
23
+ this.testCaseStartedIdToAttempt = new Map();
24
+
25
+ const { eventBroadcaster } = options;
26
+
27
+ eventBroadcaster.on('envelope', (envelope) => {
28
+ if (envelope.pickle) {
29
+ this.pickles.set(envelope.pickle.id, envelope.pickle);
30
+ }
31
+
32
+ if (envelope.testCase) {
33
+ this.testCases.set(envelope.testCase.id, envelope.testCase);
34
+ }
35
+
36
+ if (envelope.testCaseStarted) {
37
+ this.testCaseStartedIdToAttempt.set(
38
+ envelope.testCaseStarted.id,
39
+ envelope.testCaseStarted.testCaseId
40
+ );
41
+ }
42
+
43
+ if (envelope.testCaseFinished) {
44
+ this.handleTestCaseFinished(envelope.testCaseFinished);
45
+ }
46
+
47
+ if (envelope.testRunFinished) {
48
+ this.handleTestRunFinished();
49
+ }
50
+ });
51
+ }
52
+
53
+ getFormattedTimestamp() {
54
+ const now = new Date();
55
+ const YYYY = now.getFullYear();
56
+ const MM = String(now.getMonth() + 1).padStart(2, '0');
57
+ const DD = String(now.getDate()).padStart(2, '0');
58
+ const hh = String(now.getHours()).padStart(2, '0');
59
+ const mm = String(now.getMinutes()).padStart(2, '0');
60
+ const ss = String(now.getSeconds()).padStart(2, '0');
61
+ const ms = String(now.getMilliseconds()).padStart(3, '0');
62
+
63
+ return `${YYYY}${MM}${DD}-${hh}${mm}${ss}-${ms}`;
64
+ }
65
+
66
+ handleTestCaseFinished(testCaseFinished) {
67
+ try {
68
+ const timestamp = this.getFormattedTimestamp();
69
+
70
+ const testCaseId = this.testCaseStartedIdToAttempt.get(
71
+ testCaseFinished.testCaseStartedId
72
+ );
73
+
74
+ if (!testCaseId) return;
75
+
76
+ const testCase = this.testCases.get(testCaseId);
77
+ if (!testCase) return;
78
+
79
+ const pickle = this.pickles.get(testCase.pickleId);
80
+ if (!pickle) return;
81
+
82
+ const testCaseAttempt = this.eventDataCollector.getTestCaseAttempt(
83
+ testCaseFinished.testCaseStartedId
84
+ );
85
+
86
+ const status = testCaseAttempt?.worstTestStepResult?.status || 'UNKNOWN';
87
+
88
+ const info = {
89
+ name: pickle.name,
90
+ id: pickle.id,
91
+ uri: pickle.uri
92
+ };
93
+
94
+ const line = `${timestamp} | ${status.toUpperCase()} | ${info.name} | ${info.id} | ${info.uri}\n`;
95
+
96
+ fs.appendFileSync(this.outputFile, line, 'utf8');
97
+
98
+ } catch (error) {
99
+ console.error('Error in handleTestCaseFinished:', error);
100
+ }
101
+ }
102
+
103
+ handleTestRunFinished() {
104
+ if (this.workerId !== '0') {
105
+ return;
106
+ }
107
+
108
+ setTimeout(() => {
109
+ this.mergeResults();
110
+ }, 1000);
111
+ }
112
+
113
+ mergeResults() {
114
+ try {
115
+ const testStatusFile = path.join(this.outputDir, 'test-status.txt');
116
+
117
+ const files = fs.readdirSync(this.outputDir)
118
+ .filter(f => f.startsWith('test-results-') && f.endsWith('.txt'))
119
+ .sort();
120
+
121
+ if (files.length === 0) {
122
+ console.log('No result files found to merge');
123
+ return;
124
+ }
125
+
126
+ const combined = files
127
+ .map(f => fs.readFileSync(path.join(this.outputDir, f), 'utf8'))
128
+ .join('');
129
+
130
+ fs.writeFileSync(testStatusFile, combined, 'utf8');
131
+
132
+ } catch (error) {
133
+ console.error('Error merging results:', error);
134
+ }
135
+ }
136
+ }
137
+
138
+ module.exports = StatusFormatter;