@testomatio/reporter 1.1.1-beta-codecept-logger → 1.1.1-beta.1.fix-logger

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.
@@ -1,6 +1,6 @@
1
1
  const chalk = require('chalk');
2
- const debug = require('debug')('@testomatio/reporter:logger');
3
- const DataStorage = require('./data-storage');
2
+ const debug = require('debug')('@testomatio/reporter:services-logger');
3
+ const { dataStorage } = require('../data-storage');
4
4
 
5
5
  const LOG_METHODS = ['assert', 'debug', 'error', 'info', 'log', 'trace', 'warn'];
6
6
  const LEVELS = {
@@ -26,9 +26,7 @@ class Logger {
26
26
  // set default logger to be used in log, warn, error, etc methods
27
27
  #originalUserLogger = { ...console };
28
28
 
29
- #dataStorage;
30
-
31
- #context = null;
29
+ #isConsoleIntercepted = false;
32
30
 
33
31
  static #instance;
34
32
 
@@ -46,25 +44,7 @@ class Logger {
46
44
  logLevel = process?.env?.LOG_LEVEL?.toUpperCase() || 'ALL';
47
45
 
48
46
  constructor() {
49
- this.#dataStorage = new DataStorage('log');
50
-
51
- // intercept console by default
52
- this.intercept(console);
53
- }
54
-
55
- /**
56
- * @param { 'file' | 'global' } storageType
57
- */
58
- setStorageType(storageType) {
59
- this.isFileStorage = storageType === 'file';
60
- this.#dataStorage.isFileStorage = this.isFileStorage;
61
- }
62
-
63
- /**
64
- * @param {string} context - suite title + test title
65
- */
66
- setContext(context) {
67
- this.#context = context;
47
+ if (!dataStorage.isFileStorage || process.env.TESTOMATIO_INTERCEPT_CONSOLE_LOGS) this.intercept(console);
68
48
  }
69
49
 
70
50
  /**
@@ -82,7 +62,7 @@ class Logger {
82
62
  }
83
63
  }
84
64
  logs = chalk.blue(`> ${logs}`);
85
- this.#dataStorage.putData(logs, this.#context);
65
+ dataStorage.putData('log', logs);
86
66
  }
87
67
 
88
68
  /**
@@ -91,7 +71,8 @@ class Logger {
91
71
  * @returns {string[]}
92
72
  */
93
73
  getLogs(context) {
94
- const logs = this.#dataStorage.getData(context);
74
+ const logs = dataStorage.getData('log', context);
75
+ if (!logs) return [];
95
76
  return logs;
96
77
  }
97
78
 
@@ -124,7 +105,7 @@ class Logger {
124
105
  * 2. Standard: log(`text ${someVar}`)
125
106
  * 3. Standard with multiple arguments: log('text', someVar)
126
107
  */
127
- templateLiteralLog(strings, ...args) {
108
+ _templateLiteralLog(strings, ...args) {
128
109
  if (Array.isArray(strings)) strings = strings.filter(item => item !== '').map(item => item.trim());
129
110
  if (Array.isArray(args)) args = args.filter(item => item !== '');
130
111
 
@@ -151,7 +132,7 @@ class Logger {
151
132
  logs = this.#stringifyLogs(strings, ...args);
152
133
  }
153
134
  this.#originalUserLogger.log(logs);
154
- this.#dataStorage.putData(logs, this.#context);
135
+ dataStorage.putData('log', logs);
155
136
  }
156
137
 
157
138
  /**
@@ -168,11 +149,12 @@ class Logger {
168
149
 
169
150
  const logs = this.#stringifyLogs(...argsArray);
170
151
 
171
- // skip logs from testomatio reporter itself
172
- if (logs.includes('[TESTOMATIO]')) return;
173
-
174
152
  const colorizedLogs = chalk[LEVELS[level].color](logs);
175
- this.#dataStorage.putData(colorizedLogs, this.#context);
153
+ // do not attach logs from testomatio reporter itself
154
+ if (!logs.includes('[TESTOMATIO]')) {
155
+ dataStorage.putData('log', colorizedLogs);
156
+ }
157
+
176
158
  try {
177
159
  // level.toLowerCase() represents method name (log, warn, error, etc)
178
160
  this.#originalUserLogger[level.toLowerCase()](colorizedLogs);
@@ -218,7 +200,7 @@ class Logger {
218
200
  /* prevent multiple console interceptions (cause of infinite loop)
219
201
  actual only for "console", because its used as default output and is intercepted by default */
220
202
  const isUserLoggerConsole = userLogger.toString?.().toLowerCase() === '[object console]';
221
- if (isUserLoggerConsole && global.TESTOMATIO_LOGGER_CONSOLE_INTERCEPTED) {
203
+ if (isUserLoggerConsole && this.#isConsoleIntercepted) {
222
204
  debug(`Try to intercept console, but it is already intercepted`);
223
205
  return;
224
206
  }
@@ -141,7 +141,15 @@ const fetchSourceCode = (contents, opts = {}) => {
141
141
  // remove special chars from title
142
142
  if (!lineIndex && opts.title) {
143
143
  const title = opts.title.replace(/[([@].*/g, '');
144
- lineIndex = lines.findIndex(l => l.includes(title));
144
+
145
+ if (opts.lang === 'java') {
146
+ lineIndex = lines.findIndex(l => l.includes(`test${title}`));
147
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
148
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
149
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
150
+ } else {
151
+ lineIndex = lines.findIndex(l => l.includes(title));
152
+ }
145
153
  }
146
154
 
147
155
  if (opts.prepend) {
@@ -290,6 +298,20 @@ function removeColorCodes(input) {
290
298
  return input.replace(/\x1b\[[0-9;]*m/g, '');
291
299
  }
292
300
 
301
+ const jestHelpers = {
302
+ getIdOfCurrentlyRunningTest: () => {
303
+ if (!process.env.JEST_WORKER_ID) return null;
304
+ try {
305
+ // TODO: expect?.getState()?.testPath + ' ' + expect?.getState()?.currentTestName
306
+ // @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
307
+ // eslint-disable-next-line no-undef
308
+ return expect?.getState()?.currentTestName;
309
+ } catch (e) {
310
+ return null;
311
+ }
312
+ },
313
+ };
314
+
293
315
  module.exports = {
294
316
  isSameTest,
295
317
  fetchSourceCode,
@@ -306,5 +328,6 @@ module.exports = {
306
328
  parseSuite,
307
329
  humanize,
308
330
  removeColorCodes,
309
- foundedTestLog
331
+ foundedTestLog,
332
+ jestHelpers,
310
333
  };
package/lib/xmlReader.js CHANGED
@@ -1,35 +1,36 @@
1
1
  const debug = require('debug')('@testomatio/reporter:xml');
2
- const path = require("path");
2
+ const path = require('path');
3
3
  const chalk = require('chalk');
4
- const fs = require("fs");
5
- const { XMLParser } = require("fast-xml-parser");
4
+ const fs = require('fs');
5
+ const { XMLParser } = require('fast-xml-parser');
6
6
  const { APP_PREFIX, STATUS } = require('./constants');
7
- const { fetchFilesFromStackTrace,
8
- fetchIdFromOutput,
9
- fetchSourceCode,
10
- fetchSourceCodeFromStackTrace,
11
- fetchIdFromCode,
12
- humanize
13
- } = require('./utils/utils');
7
+ const {
8
+ fetchFilesFromStackTrace,
9
+ fetchIdFromOutput,
10
+ fetchSourceCode,
11
+ fetchSourceCodeFromStackTrace,
12
+ fetchIdFromCode,
13
+ humanize,
14
+ } = require('./utils/utils');
14
15
  const upload = require('./fileUploader');
15
16
  const pipesFactory = require('./pipe');
16
17
  const adapterFactory = require('./junit-adapter');
18
+ const { TESTOMATIO } = require('./config');
17
19
 
18
- const TESTOMATIO_URL = process.env.TESTOMATIO_URL || "https://app.testomat.io";
19
- const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO } = process.env;
20
+ const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
21
+ const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
20
22
 
21
23
  const options = {
22
24
  ignoreDeclaration: true,
23
25
  ignoreAttributes: false,
24
26
  alwaysCreateTextNode: false,
25
- attributeNamePrefix: "",
27
+ attributeNamePrefix: '',
26
28
  parseTagValue: true,
27
29
  };
28
30
 
29
31
  const reduceOptions = {};
30
32
 
31
33
  class XmlReader {
32
-
33
34
  constructor(opts = {}) {
34
35
  this.requestParams = {
35
36
  apiKey: opts.apiKey || TESTOMATIO,
@@ -39,18 +40,18 @@ class XmlReader {
39
40
  group_title: TESTOMATIO_RUNGROUP_TITLE,
40
41
  };
41
42
  this.runId = opts.runId || TESTOMATIO_RUN;
42
- this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts)
43
+ this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts);
43
44
  if (!this.adapter) throw new Error('XML adapter for this format not found');
44
45
 
45
46
  this.opts = opts || {};
46
- this.store = {}
47
+ this.store = {};
47
48
  this.pipes = pipesFactory(opts, this.store);
48
49
 
49
50
  this.parser = new XMLParser(options);
50
- this.tests = []
51
- this.stats = {}
51
+ this.tests = [];
52
+ this.stats = {};
52
53
  this.stats.language = opts.lang?.toLowerCase();
53
- this.filesToUpload = {}
54
+ this.filesToUpload = {};
54
55
 
55
56
  this.version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString()).version;
56
57
  console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
@@ -81,8 +82,8 @@ class XmlReader {
81
82
  } else if (jsonResult.assemblies) {
82
83
  return this.processXUnit(jsonResult.assemblies);
83
84
  } else {
84
- console.log(jsonResult)
85
- throw new Error("Format can't be parsed")
85
+ console.log(jsonResult);
86
+ throw new Error("Format can't be parsed");
86
87
  }
87
88
 
88
89
  return this.processJUnit(jsonSuite);
@@ -95,7 +96,7 @@ class XmlReader {
95
96
  const resultTests = processTestSuite(testsuite);
96
97
 
97
98
  const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
98
- const status = (failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
99
+ const status = failures > 0 || errors > 0 || hasFailures ? 'failed' : 'passed';
99
100
 
100
101
  this.tests = this.tests.concat(resultTests);
101
102
 
@@ -134,21 +135,22 @@ class XmlReader {
134
135
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
135
136
  if (!Array.isArray(defs)) defs = [defs].filter(d => !!d);
136
137
 
137
- const tests = defs.map(td => {
138
- const title = td.name.replace(/\(.*?\)/, '').trim();
139
- let example = td.name.match(/\((.*?)\)/);
140
- if (example) example = { ...example[1].split(',') };
141
- const suite = td.TestMethod.className.split(', ')[0].split('.');
142
- const suite_title = suite.pop();
143
- return {
144
- title,
145
- example,
146
- file: suite.join('/'),
147
- description: td.Description,
148
- suite_title,
149
- id: td.Execution.id,
150
- }
151
- }) || [];
138
+ const tests =
139
+ defs.map(td => {
140
+ const title = td.name.replace(/\(.*?\)/, '').trim();
141
+ let example = td.name.match(/\((.*?)\)/);
142
+ if (example) example = { ...example[1].split(',') };
143
+ const suite = td.TestMethod.className.split(', ')[0].split('.');
144
+ const suite_title = suite.pop();
145
+ return {
146
+ title,
147
+ example,
148
+ file: suite.join('/'),
149
+ description: td.Description,
150
+ suite_title,
151
+ id: td.Execution.id,
152
+ };
153
+ }) || [];
152
154
 
153
155
  let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
154
156
  if (!Array.isArray(result)) result = [result].filter(d => !!d);
@@ -159,10 +161,9 @@ class XmlReader {
159
161
  run_time: parseFloat(td.duration) * 1000,
160
162
  status: td.outcome,
161
163
  stack: td.Output.StdOut,
162
- files: td?.ResultFiles?.ResultFile?.map(rf => rf.path)
164
+ files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
163
165
  }));
164
166
 
165
-
166
167
  results.forEach(r => {
167
168
  const test = tests.find(t => t.id === r.id) || {};
168
169
  r.suite_title = test.suite_title;
@@ -222,7 +223,7 @@ class XmlReader {
222
223
 
223
224
  if (testCase.failure) {
224
225
  message = testCase.failure.message;
225
- stack = testCase.failure['stack-trace']
226
+ stack = testCase.failure['stack-trace'];
226
227
  }
227
228
  if (testCase.reason) {
228
229
  message = testCase.reason.message;
@@ -249,7 +250,6 @@ class XmlReader {
249
250
  suite_title,
250
251
  run_time,
251
252
  });
252
-
253
253
  });
254
254
  });
255
255
  });
@@ -282,12 +282,12 @@ class XmlReader {
282
282
  passed_count: 0,
283
283
  failed_count: 0,
284
284
  skipped_count: 0,
285
- }
285
+ };
286
286
  this.tests.forEach(t => {
287
287
  this.stats.tests_count++;
288
288
  if (t.status === 'passed') this.stats.passed_count++;
289
289
  if (t.status === 'failed') this.stats.failed_count++;
290
- })
290
+ });
291
291
  if (this.stats.failed_count) this.stats.status = 'failed';
292
292
 
293
293
  return this.stats;
@@ -296,7 +296,7 @@ class XmlReader {
296
296
  fetchSourceCode() {
297
297
  this.tests.forEach(t => {
298
298
  try {
299
- const file = this.adapter.getFilePath(t)
299
+ const file = this.adapter.getFilePath(t);
300
300
  if (!file) return;
301
301
 
302
302
  if (!this.stats.language) {
@@ -309,16 +309,16 @@ class XmlReader {
309
309
  }
310
310
 
311
311
  if (!fs.existsSync(file)) {
312
- debug('Failed to open file with the source code', file)
312
+ debug('Failed to open file with the source code', file);
313
313
  return;
314
314
  }
315
315
  const contents = fs.readFileSync(file).toString();
316
- t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language })
316
+ t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language });
317
317
  if (t.code) debug('Fetched code for test %s', t.title);
318
- t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language })
318
+ t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language });
319
319
  if (t.test_id) debug('Fetched test id %s for test %s', t.test_id, t.title);
320
320
  } catch (err) {
321
- debug(err)
321
+ debug(err);
322
322
  }
323
323
  });
324
324
  }
@@ -326,21 +326,22 @@ class XmlReader {
326
326
  formatTests() {
327
327
  this.tests.forEach(t => {
328
328
  if (t.file) {
329
- t.file = t.file.replace(process.cwd() + path.sep, '')
329
+ t.file = t.file.replace(process.cwd() + path.sep, '');
330
330
  }
331
331
 
332
- this.adapter.formatTest(t)
332
+ this.adapter.formatTest(t);
333
333
 
334
334
  t.title = humanize(t.title);
335
335
  });
336
336
  }
337
337
 
338
338
  formatErrors() {
339
- this.tests.filter(t => !!t.stack).forEach(t => {
340
- t.stack = this.formatStack(t)
341
- t.message = this.adapter.formatMessage(t);
342
- });
343
-
339
+ this.tests
340
+ .filter(t => !!t.stack)
341
+ .forEach(t => {
342
+ t.stack = this.formatStack(t);
343
+ t.message = this.adapter.formatMessage(t);
344
+ });
344
345
  }
345
346
 
346
347
  formatStack(t) {
@@ -358,7 +359,7 @@ class XmlReader {
358
359
  async uploadArtifacts() {
359
360
  for (const test of this.tests.filter(t => !!t.stack)) {
360
361
  let files = [];
361
- if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f))
362
+ if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f));
362
363
  files = [...files, ...fetchFilesFromStackTrace(test.stack)];
363
364
 
364
365
  if (!files.length) continue;
@@ -377,7 +378,7 @@ class XmlReader {
377
378
  group_title: this.requestParams.group_title,
378
379
  };
379
380
 
380
- debug("Run", runParams);
381
+ debug('Run', runParams);
381
382
 
382
383
  return Promise.all(this.pipes.map(p => p.createRun(runParams)));
383
384
  }
@@ -390,12 +391,10 @@ class XmlReader {
390
391
  this.formatErrors();
391
392
  this.formatTests();
392
393
 
393
- debug(
394
- 'Uploading data',
395
- {
396
- ...this.stats,
397
- tests: this.tests,
398
- })
394
+ debug('Uploading data', {
395
+ ...this.stats,
396
+ tests: this.tests,
397
+ });
399
398
 
400
399
  const dataString = {
401
400
  ...this.stats,
@@ -410,59 +409,62 @@ class XmlReader {
410
409
 
411
410
  module.exports = XmlReader;
412
411
 
413
-
414
412
  function reduceTestCases(prev, item) {
415
413
  let testCases = item.testcase;
416
414
  if (!testCases) testCases = item['test-case'];
417
415
  if (!Array.isArray(testCases)) {
418
- testCases = [testCases]
416
+ testCases = [testCases];
419
417
  }
420
418
  const suiteOutput = item['system-out'] || item.output || item.log || '';
421
419
  const suiteErr = item['system-err'] || item.output || item.log || '';
422
- testCases.filter(t => !!t).forEach(testCaseItem => {
423
- const file = testCaseItem.file || item.filepath || '';
424
-
425
- let stack = '';
426
- let message = '';
427
- if (testCaseItem.error) stack = testCaseItem.error;
428
- if (testCaseItem.failure) stack = testCaseItem.failure;
429
- if (testCaseItem?.failure?.['stack-trace']) stack = testCaseItem.failure['stack-trace'];
430
- if (testCaseItem?.failure?.message) message = testCaseItem.failure.message;
431
- if (testCaseItem?.error?.message) message = testCaseItem.error.message;
432
-
433
- if (testCaseItem.failure && testCaseItem.failure['#text']) stack = testCaseItem.failure['#text'];
434
- if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
435
- if (!message) message = stack.trim().split('\n')[0];
436
-
437
- // eslint-disable-next-line
438
- stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim()
439
- const testId = fetchIdFromOutput(stack);
440
-
441
- let status = STATUS.PASSED.toString();
442
- if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
443
- if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
444
-
445
- prev.push({
446
- create: true,
447
- file,
448
- stack,
449
- test_id: testId,
450
- message,
451
- line: testCaseItem.lineno,
452
- // seconds are used in junit reports, but ms are used by testomatio
453
- run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
454
- status,
455
- title: testCaseItem.name,
456
- suite_title: reduceOptions.preferClassname ? testCaseItem.classname : (item.name || testCaseItem.classname),
457
- })
458
- });
420
+ testCases
421
+ .filter(t => !!t)
422
+ .forEach(testCaseItem => {
423
+ const file = testCaseItem.file || item.filepath || '';
424
+
425
+ let stack = '';
426
+ let message = '';
427
+ if (testCaseItem.error) stack = testCaseItem.error;
428
+ if (testCaseItem.failure) stack = testCaseItem.failure;
429
+ if (testCaseItem?.failure?.['stack-trace']) stack = testCaseItem.failure['stack-trace'];
430
+ if (testCaseItem?.failure?.message) message = testCaseItem.failure.message;
431
+ if (testCaseItem?.error?.message) message = testCaseItem.error.message;
432
+
433
+ if (testCaseItem.failure && testCaseItem.failure['#text']) stack = testCaseItem.failure['#text'];
434
+ if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
435
+ if (!message) message = stack.trim().split('\n')[0];
436
+
437
+ // eslint-disable-next-line
438
+ stack = `${
439
+ testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''
440
+ }\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
441
+ const testId = fetchIdFromOutput(stack);
442
+
443
+ let status = STATUS.PASSED.toString();
444
+ if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
445
+ if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
446
+
447
+ prev.push({
448
+ create: true,
449
+ file,
450
+ stack,
451
+ test_id: testId,
452
+ message,
453
+ line: testCaseItem.lineno,
454
+ // seconds are used in junit reports, but ms are used by testomatio
455
+ run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
456
+ status,
457
+ title: testCaseItem.name,
458
+ suite_title: reduceOptions.preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname,
459
+ });
460
+ });
459
461
  return prev;
460
462
  }
461
463
 
462
464
  function processTestSuite(testsuite) {
463
465
  if (!testsuite) return [];
464
- if (testsuite.testsuite) return processTestSuite(testsuite.testsuite)
465
- if (testsuite['test-suite']) return processTestSuite(testsuite['test-suite'])
466
+ if (testsuite.testsuite) return processTestSuite(testsuite.testsuite);
467
+ if (testsuite['test-suite']) return processTestSuite(testsuite['test-suite']);
466
468
 
467
469
  let suites = testsuite;
468
470
  if (!Array.isArray(testsuite)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.1.1-beta-codecept-logger",
3
+ "version": "1.1.1-beta.1.fix-logger",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
@@ -52,7 +52,7 @@
52
52
  "test:adapter:jasmine:example": "./tests/adapter/examples/jasmine/passReporterOpts.sh && jasmine './tests/adapter/examples/jasmine/index.test.js' --reporter=./../../../lib/adapter/jasmine.js",
53
53
  "test:adapter:codecept:example": "codeceptjs run --config='./tests/adapter/examples/codecept/codecept.conf.js'",
54
54
  "test:adapter:cucumber:example": "cd ./tests/adapter/examples/cucumber && npx cucumber-js",
55
- "test:storage": "npx mocha ./tests-storage/**"
55
+ "test:storage": "npx mocha tests-storage/artifact-storage.test.js && npx mocha tests-storage/data-storage.test.js && TESTOMATIO_INTERCEPT_CONSOLE_LOGS=true npx mocha tests-storage/logger.test.js && npx mocha tests-storage/reporter-functions.test.js"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@cucumber/cucumber": "^9.3.0",