artes 1.4.5 → 1.4.7

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.
@@ -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)
@@ -55,7 +55,7 @@ function loadVariables(cliVariables, artesConfigVariables) {
55
55
 
56
56
  if (cliVariables) {
57
57
  try {
58
- cliVariables = JSON.parse(process.env.VARS);
58
+ cliVariables = JSON.parse(cliVariables);
59
59
  } catch (err) {
60
60
  console.error("Invalid JSON in process.env.VARS:", process.env.VARS);
61
61
  envVars = {};
package/executer.js CHANGED
@@ -22,6 +22,11 @@ if (fs.existsSync(artesConfigPath)) {
22
22
 
23
23
  const args = process.argv.slice(2);
24
24
 
25
+ const getArgValue = (flag) => {
26
+ const index = args.indexOf(flag);
27
+ return index >= 0 ? args[index + 1] : undefined;
28
+ };
29
+
25
30
  const flags = {
26
31
  help: args.includes("-h") || args.includes("--help"),
27
32
  version: args.includes("-v") || args.includes("--version"),
@@ -56,23 +61,25 @@ const flags = {
56
61
  slowMo: args.includes("--slowMo"),
57
62
  };
58
63
 
59
- const env = args[args.indexOf("--env") + 1];
60
- const vars = args[args.indexOf("--saveVar") + 1]
61
- const featureFiles = args[args.indexOf("--features") + 1];
64
+
65
+ const env = getArgValue("--env");
66
+ const vars = getArgValue("--saveVar");
67
+ const featureFiles = getArgValue("--features");
62
68
  const features = flags.features && featureFiles;
63
- const stepDef = args[args.indexOf("--stepDef") + 1];
64
- const tags = args[args.indexOf("--tags") + 1];
65
- const parallel = args[args.indexOf("--parallel") + 1];
66
- const retry = args[args.indexOf("--retry") + 1];
67
- const rerun = args[args.indexOf("--rerun") + 1];
68
- const percentage = args[args.indexOf("--percentage") + 1];
69
- const browser = args[args.indexOf("--browser") + 1];
70
- const device = args[args.indexOf("--device") + 1];
71
- const baseURL = args[args.indexOf("--baseURL") + 1];
72
- const width = args[args.indexOf("--width") + 1];
73
- const height = args[args.indexOf("--height") + 1];
74
- const timeout = args[args.indexOf("--timeout") + 1];
75
- 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
+
76
83
 
77
84
  flags.env ? (process.env.ENV = env) : "";
78
85
 
@@ -152,82 +159,243 @@ flags.timeout ? (process.env.TIMEOUT = timeout) : "";
152
159
  flags.slowMo ? (process.env.SLOWMO = slowMo) : "";
153
160
 
154
161
 
155
- function testCoverageCalculation(testStatusDir){
156
- if(fs.existsSync(testStatusDir)){
157
- const files = fs.readdirSync(testStatusDir);
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());
158
172
 
159
- const map = {};
160
- const retriedTests = [];
161
- const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
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');
162
216
 
163
- files.forEach(file => {
164
- const match = file.match(uuidRegex);
165
- if (!match) return;
166
-
167
- const id = match[0];
168
-
169
- const beforeId = file.substring(0, file.indexOf(id) - 1);
170
- const afterId = file.substring(file.indexOf(id) + id.length + 1);
171
-
172
- const status = beforeId.split('-')[0];
173
- const scenario = beforeId.substring(status.length + 1);
174
- const timestamp = afterId;
175
-
176
- if (!map[id]) {
177
- map[id] = {
178
- count: 1,
179
- latest: { status, scenario, timestamp }
180
- };
181
- } else {
182
- map[id].count++;
183
- if (timestamp > map[id].latest.timestamp) {
184
- map[id].latest = { status, scenario, timestamp };
185
- }
186
- }
187
- });
188
-
189
- let total = 0;
190
- let notPassed = 0;
191
-
192
- Object.entries(map).forEach(([id, data]) => {
193
- total++;
194
-
195
- if (data.count > 1) {
196
- retriedTests.push({
197
- scenario: data.latest.scenario,
198
- id,
199
- count: data.count
200
- });
201
- }
202
-
203
- if (data.latest.status !== 'PASSED') {
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') {
204
269
  notPassed++;
205
- }
206
- });
207
-
208
- if (retriedTests.length > 0) {
209
- console.warn('\n\x1b[33mRetried test cases:');
210
- retriedTests.forEach(t => {
211
- console.warn(`- "${t.scenario}" ran ${t.count} times`);
212
- });
213
- console.log("\x1b[0m");
214
- }
215
-
216
- return {
217
- percentage : (total - notPassed)/total*100,
218
- totalTests: total,
219
- notPassed,
220
- passed: total - notPassed,
221
- latestStatuses: Object.fromEntries(
222
- Object.entries(map).map(([id, data]) => [
223
- id,
224
- data.latest.status
225
- ])
226
- )
227
- };
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
+
295
+ function getExecutor() {
296
+ // GitHub Actions
297
+ if (process.env.GITHUB_RUN_ID) {
298
+ return {
299
+ name: "GitHub Actions",
300
+ type: "github",
301
+ buildName: `Workflow #${process.env.GITHUB_RUN_NUMBER}`,
302
+ buildOrder: Number(process.env.GITHUB_RUN_NUMBER),
303
+ buildUrl: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
304
+ };
305
+
306
+ // Jenkins
307
+ } else if (process.env.JENKINS_HOME) {
308
+ return {
309
+ name: "Jenkins",
310
+ type: "jenkins",
311
+ buildName: process.env.JOB_NAME || "Manual Run",
312
+ buildOrder: Number(process.env.BUILD_NUMBER) || 1,
313
+ buildUrl: process.env.BUILD_URL || ""
314
+ };
315
+
316
+ // GitLab CI
317
+ } else if (process.env.CI_PIPELINE_ID) {
318
+ return {
319
+ name: "GitLab CI",
320
+ type: "gitlab",
321
+ buildName: `Pipeline #${process.env.CI_PIPELINE_IID}`,
322
+ buildOrder: Number(process.env.CI_PIPELINE_IID) || 1,
323
+ buildUrl: process.env.CI_PIPELINE_URL || ""
324
+ };
325
+
326
+ // Bitbucket Pipelines
327
+ } else if (process.env.BITBUCKET_BUILD_NUMBER) {
328
+ return {
329
+ name: "Bitbucket Pipelines",
330
+ type: "bitbucket",
331
+ buildName: `Build #${process.env.BITBUCKET_BUILD_NUMBER}`,
332
+ buildOrder: Number(process.env.BITBUCKET_BUILD_NUMBER),
333
+ buildUrl: process.env.BITBUCKET_BUILD_URL || ""
334
+ };
335
+
336
+ // CircleCI
337
+ } else if (process.env.CIRCLE_WORKFLOW_ID) {
338
+ return {
339
+ name: "CircleCI",
340
+ type: "circleci",
341
+ buildName: `Workflow #${process.env.CIRCLE_WORKFLOW_ID}`,
342
+ buildOrder: Number(process.env.CIRCLE_BUILD_NUM) || 1,
343
+ buildUrl: process.env.CIRCLE_BUILD_URL || ""
344
+ };
345
+
346
+ // Azure Pipelines
347
+ } else if (process.env.BUILD_BUILDID) {
348
+ return {
349
+ name: "Azure Pipelines",
350
+ type: "azure",
351
+ buildName: `Build #${process.env.BUILD_BUILDID}`,
352
+ buildOrder: Number(process.env.BUILD_BUILDID) || 1,
353
+ buildUrl: process.env.BUILD_BUILDURI || ""
354
+ };
355
+
356
+ // TeamCity
357
+ } else if (process.env.BUILD_NUMBER && process.env.TEAMCITY_VERSION) {
358
+ return {
359
+ name: "TeamCity",
360
+ type: "teamcity",
361
+ buildName: `Build #${process.env.BUILD_NUMBER}`,
362
+ buildOrder: Number(process.env.BUILD_NUMBER) || 1,
363
+ buildUrl: process.env.BUILD_URL || ""
364
+ };
365
+
366
+ // Travis CI
367
+ } else if (process.env.TRAVIS_BUILD_NUMBER) {
368
+ return {
369
+ name: "Travis CI",
370
+ type: "travis",
371
+ buildName: `Build #${process.env.TRAVIS_BUILD_NUMBER}`,
372
+ buildOrder: Number(process.env.TRAVIS_BUILD_NUMBER) || 1,
373
+ buildUrl: process.env.TRAVIS_BUILD_WEB_URL || ""
374
+ };
375
+
376
+ // Bamboo
377
+ } else if (process.env.bamboo_buildNumber) {
378
+ return {
379
+ name: "Bamboo",
380
+ type: "bamboo",
381
+ buildName: `Build #${process.env.bamboo_buildNumber}`,
382
+ buildOrder: Number(process.env.bamboo_buildNumber) || 1,
383
+ buildUrl: process.env.bamboo_resultsUrl || ""
384
+ };
385
+
386
+ // Default fallback (local/manual)
387
+ } else {
388
+ return {
389
+ name: "Local Run",
390
+ type: "local",
391
+ buildName: "Manual Execution",
392
+ buildOrder: 1,
393
+ buildUrl: ""
394
+ };
228
395
  }
229
396
  }
230
397
 
398
+
231
399
  function main() {
232
400
  if (flags.help) return showHelp();
233
401
  if (flags.version) return showVersion();
@@ -237,7 +405,9 @@ function main() {
237
405
 
238
406
  logPomWarnings();
239
407
 
240
- const testCoverage = testCoverageCalculation(path.join(process.cwd(), "node_modules", "artes" , "testsStatus"))
408
+ findDuplicateTestNames();
409
+
410
+ const testCoverage = testCoverageCalculation()
241
411
 
242
412
  const testPercentage = (process.env.PERCENTAGE ? Number(process.env.PERCENTAGE) : artesConfig.testPercentage || 0)
243
413
 
@@ -266,13 +436,22 @@ if (fs.existsSync(path.join(process.cwd(), "node_modules", "artes" , "@rerun.txt
266
436
  });
267
437
  }
268
438
 
439
+
440
+
269
441
  if (
270
442
  flags.reportWithTrace ||
271
443
  artesConfig.reportWithTrace ||
272
444
  flags.report ||
273
445
  artesConfig.report
274
- )
275
- generateReport();
446
+ ){
447
+ const executor = getExecutor();
448
+
449
+ fs.writeFileSync(
450
+ path.join(process.cwd(), "node_modules", "artes",'allure-result',"executor.json"),
451
+ JSON.stringify(executor, null, 2)
452
+ );
453
+ generateReport();
454
+ }
276
455
 
277
456
 
278
457
  if (!( process.env.TRACE === "true"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artes",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "The simplest way to automate UI and API tests using Cucumber-style steps.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -30,7 +30,6 @@
30
30
  "dayjs": "1.11.13",
31
31
  "deasync": "^0.1.31",
32
32
  "playwright": "^1.58.2",
33
- "proper-lockfile": "^4.1.2",
34
33
  "rimraf": "6.0.1"
35
34
  },
36
35
  "repository": {
@@ -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 pomDuplicateWarnings.json",
38
+ "allure-result allure-results test-results @rerun.txt test-status null pomDuplicateWarnings.json",
39
39
  };
40
40
 
41
41
  module.exports = {
@@ -37,33 +37,10 @@ async function attachResponse(attachFn) {
37
37
  ? `${key}:\n${JSON.stringify(value, null, 2)}`
38
38
  : `${key}:\n${value}`;
39
39
 
40
- await attachFn(key, text, "text/plain");
40
+ await attachFn(key, text, "application/json");
41
41
  }
42
42
  }
43
43
 
44
- function saveTestStatus(result, pickle) {
45
-
46
- fs.mkdirSync(statusDir, { recursive: true });
47
-
48
- const now = new Date();
49
- const YYYY = now.getFullYear();
50
- const MM = String(now.getMonth() + 1).padStart(2, '0');
51
- const DD = String(now.getDate()).padStart(2, '0');
52
- const hh = String(now.getHours()).padStart(2, '0');
53
- const mm = String(now.getMinutes()).padStart(2, '0');
54
- const ss = String(now.getSeconds()).padStart(2, '0');
55
- const ms = String(now.getMilliseconds()).padStart(3, '0');
56
-
57
- const timestamp = `${YYYY}${MM}${DD}-${hh}${mm}${ss}-${ms}`;
58
-
59
- const fileName = `${result.status}-${pickle.name}-${pickle.id}-${timestamp}.txt`;
60
-
61
- const filePath = path.join(statusDir, fileName);
62
-
63
- fs.writeFileSync(filePath, "");
64
-
65
- }
66
-
67
44
  const projectHooksPath = path.resolve(
68
45
  moduleConfig.projectPath,
69
46
  "tests/steps/hooks.js",
@@ -169,6 +146,10 @@ After(async function ({result, pickle}) {
169
146
  await projectHooks.After();
170
147
  }
171
148
 
149
+ await attachResponse(allure.attachment);
150
+ context.response = await {};
151
+ allure.attachment('Variables', JSON.stringify(context.vars, null, 2), 'application/json')
152
+
172
153
  const shouldReport =
173
154
  cucumberConfig.default.successReport || result?.status !== Status.PASSED;
174
155
 
@@ -178,7 +159,6 @@ After(async function ({result, pickle}) {
178
159
  await allure.attachment("Screenshot", screenshotBuffer, "image/png");
179
160
  }
180
161
 
181
- if(cucumberConfig.default.testPercentage>0) saveTestStatus(result, pickle);
182
162
 
183
163
  if (cucumberConfig.default.reportWithTrace || cucumberConfig.default.trace) {
184
164
  var tracePath = path.join(
@@ -202,8 +182,6 @@ After(async function ({result, pickle}) {
202
182
  }
203
183
  }
204
184
 
205
- await attachResponse(allure.attachment);
206
- context.response = await {};
207
185
 
208
186
  await context.page?.close();
209
187
  await context.browserContext?.close();
@@ -24,6 +24,7 @@ When(
24
24
  "User types {string} by hand in {int} th of {string}",
25
25
  async (text, order, elements) => {
26
26
  const nthElement = await frame.nth(elements, order);
27
+ text = resolveVariable(text)
27
28
  await nthElement.pressSequentially(text);
28
29
  },
29
30
  );
@@ -55,6 +56,7 @@ When(
55
56
  "User types {string} in {int} th of {string}",
56
57
  async (text, order, elements) => {
57
58
  const nthElement = await frame.nth(elements, order);
59
+ text = resolveVariable(text)
58
60
  await nthElement.fill(text);
59
61
  },
60
62
  );
@@ -65,6 +67,7 @@ When(
65
67
  const elementCount = await frame.count(selectors);
66
68
  value = await resolveVariable(value);
67
69
  for (let i = 0; i < elementCount; i++) {
70
+ value = resolveVariable(value)
68
71
  await frame.nth(selectors, i).fill(value);
69
72
  }
70
73
  },
@@ -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;