@testomatio/reporter 2.0.0-beta.2-xml → 2.0.0-beta.3-xml

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.
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const client_js_1 = __importDefault(require("../client.js"));
7
7
  const config_js_1 = require("../config.js");
8
- const constants_1 = require("../constants");
8
+ const constants_js_1 = require("../constants.js");
9
9
  const utils_js_1 = require("../utils/utils.js");
10
10
  const apiKey = config_js_1.config.TESTOMATIO;
11
11
  const client = new client_js_1.default({ apiKey });
@@ -29,14 +29,14 @@ module.exports = {
29
29
  let status;
30
30
  switch (test.status) {
31
31
  case 'pass':
32
- status = constants_1.STATUS.PASSED;
32
+ status = constants_js_1.STATUS.PASSED;
33
33
  break;
34
34
  case 'fail':
35
- status = constants_1.STATUS.FAILED;
35
+ status = constants_js_1.STATUS.FAILED;
36
36
  break;
37
37
  // probably not required (because skipped tests are in separate array), but just in case
38
38
  case 'skip':
39
- status = constants_1.STATUS.SKIPPED;
39
+ status = constants_js_1.STATUS.SKIPPED;
40
40
  console.info('Skipped test is in completed tests array:', test, 'Not expected behavior.');
41
41
  break;
42
42
  default:
@@ -58,7 +58,7 @@ module.exports = {
58
58
  }
59
59
  // just array with skipped tests titles, no any other info
60
60
  for (const testTitle of skippedTests) {
61
- client.addTestRun(constants_1.STATUS.SKIPPED, {
61
+ client.addTestRun(constants_js_1.STATUS.SKIPPED, {
62
62
  suite_title: suiteTitle,
63
63
  tags,
64
64
  rid: `${testModule.uuid || ''}_${testTitle || ''}`,
@@ -19,6 +19,6 @@ declare class WebdriverReporter extends WDIOReporter {
19
19
  */
20
20
  addBddScenario(scenario: import("../../types/types.js").WebdriverIOScenario): Promise<import("../../types/types.js").PipeResult[]>;
21
21
  }
22
- import WDIOReporter from '@wdio/reporter';
22
+ import { default as WDIOReporter } from '@wdio/reporter';
23
23
  import TestomatClient from '../client.js';
24
24
  import { RunnerStats } from '@wdio/reporter';
package/lib/bin/cli.js CHANGED
@@ -11,17 +11,18 @@ const debug_1 = __importDefault(require("debug"));
11
11
  const client_js_1 = __importDefault(require("../client.js"));
12
12
  const xmlReader_js_1 = __importDefault(require("../xmlReader.js"));
13
13
  const constants_js_1 = require("../constants.js");
14
- const package_json_1 = require("../../package.json");
15
- const config_js_1 = require("../config.js");
16
14
  const utils_js_1 = require("../utils/utils.js");
15
+ const config_js_1 = require("../config.js");
16
+ const utils_js_2 = require("../utils/utils.js");
17
17
  const picocolors_1 = __importDefault(require("picocolors"));
18
18
  const filesize_1 = require("filesize");
19
19
  const dotenv_1 = __importDefault(require("dotenv"));
20
20
  const debug = (0, debug_1.default)('@testomatio/reporter:xml-cli');
21
- console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${package_json_1.version}`)));
21
+ const version = (0, utils_js_1.getPackageVersion)();
22
+ console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${version}`)));
22
23
  const program = new commander_1.Command();
23
24
  program
24
- .version(package_json_1.version)
25
+ .version(version)
25
26
  .option('--env-file <envfile>', 'Load environment variables from env file')
26
27
  .hook('preAction', thisCommand => {
27
28
  const opts = thisCommand.opts();
@@ -48,7 +49,7 @@ program
48
49
  .command('finish')
49
50
  .description('Finish Run by its ID')
50
51
  .action(async () => {
51
- process.env.TESTOMATIO_RUN ||= (0, utils_js_1.readLatestRunId)();
52
+ process.env.TESTOMATIO_RUN ||= (0, utils_js_2.readLatestRunId)();
52
53
  if (!process.env.TESTOMATIO_RUN) {
53
54
  console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
54
55
  return process.exit(1);
@@ -180,7 +181,7 @@ program
180
181
  .action(async (opts) => {
181
182
  const apiKey = config_js_1.config.TESTOMATIO;
182
183
  process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
183
- const runId = process.env.TESTOMATIO_RUN || process.env.runId || (0, utils_js_1.readLatestRunId)();
184
+ const runId = process.env.TESTOMATIO_RUN || process.env.runId || (0, utils_js_2.readLatestRunId)();
184
185
  if (!runId) {
185
186
  console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
186
187
  return process.exit(1);
@@ -10,10 +10,12 @@ const glob_1 = require("glob");
10
10
  const debug_1 = __importDefault(require("debug"));
11
11
  const constants_js_1 = require("../constants.js");
12
12
  const xmlReader_js_1 = __importDefault(require("../xmlReader.js"));
13
- const package_json_1 = require("../../package.json");
13
+ const utils_js_1 = require("../utils/utils.js");
14
14
  const dotenv_1 = __importDefault(require("dotenv"));
15
+ const path_1 = __importDefault(require("path"));
16
+ const version = (0, utils_js_1.getPackageVersion)();
15
17
  const debug = (0, debug_1.default)('@testomatio/reporter:xml-cli');
16
- console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io XML Reporter v${package_json_1.version}`)));
18
+ console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io XML Reporter v${version}`)));
17
19
  const program = new commander_1.Command();
18
20
  program
19
21
  .arguments('<pattern>')
@@ -9,10 +9,11 @@ const commander_1 = require("commander");
9
9
  const picocolors_1 = __importDefault(require("picocolors"));
10
10
  const client_js_1 = __importDefault(require("../client.js"));
11
11
  const constants_js_1 = require("../constants.js");
12
- const package_json_1 = require("../../package.json");
12
+ const utils_js_1 = require("../utils/utils.js");
13
13
  const config_js_1 = require("../config.js");
14
14
  const dotenv_1 = __importDefault(require("dotenv"));
15
- console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${package_json_1.version}`)));
15
+ const version = (0, utils_js_1.getPackageVersion)();
16
+ console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${version}`)));
16
17
  const program = new commander_1.Command();
17
18
  program
18
19
  .option('-c, --command <cmd>', 'Test runner command')
@@ -9,12 +9,13 @@ const picocolors_1 = __importDefault(require("picocolors"));
9
9
  const debug_1 = __importDefault(require("debug"));
10
10
  const client_js_1 = __importDefault(require("../client.js"));
11
11
  const constants_js_1 = require("../constants.js");
12
- const package_json_1 = require("../../package.json");
13
- const config_js_1 = require("../config.js");
14
12
  const utils_js_1 = require("../utils/utils.js");
13
+ const config_js_1 = require("../config.js");
14
+ const utils_js_2 = require("../utils/utils.js");
15
15
  const dotenv_1 = __importDefault(require("dotenv"));
16
16
  const debug = (0, debug_1.default)('@testomatio/reporter:upload-cli');
17
- console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${package_json_1.version}`)));
17
+ const version = (0, utils_js_1.getPackageVersion)();
18
+ console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${version}`)));
18
19
  const program = new commander_1.Command();
19
20
  program
20
21
  .option('--env-file <envfile>', 'Load environment variables from env file')
@@ -28,7 +29,7 @@ program
28
29
  }
29
30
  const apiKey = config_js_1.config.TESTOMATIO;
30
31
  process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
31
- const runId = process.env.TESTOMATIO_RUN || process.env.runId || (0, utils_js_1.readLatestRunId)();
32
+ const runId = process.env.TESTOMATIO_RUN || process.env.runId || (0, utils_js_2.readLatestRunId)();
32
33
  if (!runId) {
33
34
  console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
34
35
  return process.exit(1);
@@ -1,6 +1,6 @@
1
1
  export const dataStorage: DataStorage;
2
2
  declare class DataStorage {
3
- static "__#12@#instance": any;
3
+ static "__#11@#instance": any;
4
4
  /**
5
5
  *
6
6
  * @returns {DataStorage}
@@ -1,4 +1,5 @@
1
1
  export default CSharpAdapter;
2
2
  declare class CSharpAdapter extends Adapter {
3
+ getFilePath(t: any): string;
3
4
  }
4
5
  import Adapter from './adapter.js';
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const path_1 = __importDefault(require("path"));
6
7
  const adapter_js_1 = __importDefault(require("./adapter.js"));
7
8
  class CSharpAdapter extends adapter_js_1.default {
8
9
  formatTest(t) {
@@ -12,9 +13,18 @@ class CSharpAdapter extends adapter_js_1.default {
12
13
  t.example = { ...example[1].split(',') };
13
14
  const suite = t.suite_title.split('.');
14
15
  t.suite_title = suite.pop();
15
- t.file = suite.join('/');
16
+ t.file = namespaceToFileName(t.file);
16
17
  t.title = title.trim();
17
18
  return t;
18
19
  }
20
+ getFilePath(t) {
21
+ const fileName = namespaceToFileName(t.file);
22
+ return fileName;
23
+ }
19
24
  }
20
25
  module.exports = CSharpAdapter;
26
+ function namespaceToFileName(fileName) {
27
+ const fileParts = fileName.split('.');
28
+ fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
29
+ return `${fileParts.join(path_1.default.sep)}.cs`;
30
+ }
@@ -11,7 +11,6 @@ export class BitbucketPipe {
11
11
  tests: any[];
12
12
  token: any;
13
13
  hiddenCommentData: string;
14
- client: Gaxios;
15
14
  cleanLog(log: any): Promise<string>;
16
15
  prepareRun(): Promise<void>;
17
16
  createRun(): Promise<void>;
@@ -22,4 +21,3 @@ export class BitbucketPipe {
22
21
  }
23
22
  export type Pipe = import("../../types/types.js").Pipe;
24
23
  export type TestData = import("../../types/types.js").TestData;
25
- import { Gaxios } from 'gaxios';
@@ -40,7 +40,7 @@ exports.BitbucketPipe = void 0;
40
40
  const constants_js_1 = require("../constants.js");
41
41
  const utils_js_1 = require("../utils/utils.js");
42
42
  const pipe_utils_js_1 = require("../utils/pipe_utils.js");
43
- const gaxios_1 = require("gaxios");
43
+ const axios_1 = __importDefault(require("axios"));
44
44
  const picocolors_1 = __importDefault(require("picocolors"));
45
45
  const humanize_duration_1 = __importDefault(require("humanize-duration"));
46
46
  const lodash_merge_1 = __importDefault(require("lodash.merge"));
@@ -69,13 +69,6 @@ class BitbucketPipe {
69
69
  return;
70
70
  }
71
71
  this.isEnabled = true;
72
- this.client = new gaxios_1.Gaxios({
73
- baseURL: 'https://api.bitbucket.org/2.0',
74
- headers: {
75
- 'Content-Type': 'application/json',
76
- 'Authorization': `Bearer ${this.token}`
77
- }
78
- });
79
72
  debug('Bitbucket Pipe: Enabled');
80
73
  }
81
74
  async cleanLog(log) {
@@ -175,17 +168,18 @@ class BitbucketPipe {
175
168
  }
176
169
  // Construct Bitbucket API URL for comments
177
170
  // eslint-disable-next-line max-len
178
- const commentsRequestURL = `/repositories/${this.ENV.BITBUCKET_WORKSPACE}/${this.ENV.BITBUCKET_REPO_SLUG}/pullrequests/${this.ENV.BITBUCKET_PR_ID}/comments`;
171
+ const commentsRequestURL = `https://api.bitbucket.org/2.0/repositories/${this.ENV.BITBUCKET_WORKSPACE}/${this.ENV.BITBUCKET_REPO_SLUG}/pullrequests/${this.ENV.BITBUCKET_PR_ID}/comments`;
179
172
  // Delete previous report
180
- await deletePreviousReport(this.client, commentsRequestURL, this.hiddenCommentData);
173
+ await deletePreviousReport(axios_1.default, commentsRequestURL, this.hiddenCommentData, this.token);
181
174
  // Add current report
182
175
  debug(`Adding comment via URL: ${commentsRequestURL}`);
183
176
  debug(`Final Bitbucket API call body: ${body}`);
184
177
  try {
185
- const addCommentResponse = await this.client.request({
186
- method: 'POST',
187
- url: commentsRequestURL,
188
- data: { content: { raw: body } }
178
+ const addCommentResponse = await axios_1.default.post(commentsRequestURL, { content: { raw: body } }, {
179
+ headers: {
180
+ Authorization: `Bearer ${this.token}`,
181
+ 'Content-Type': 'application/json',
182
+ },
189
183
  });
190
184
  const commentID = addCommentResponse.data.id;
191
185
  // eslint-disable-next-line max-len
@@ -204,15 +198,17 @@ class BitbucketPipe {
204
198
  updateRun() { }
205
199
  }
206
200
  exports.BitbucketPipe = BitbucketPipe;
207
- async function deletePreviousReport(client, commentsRequestURL, hiddenCommentData) {
201
+ async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
208
202
  if (process.env.BITBUCKET_KEEP_OUTDATED_REPORTS)
209
203
  return;
210
204
  // Get comments
211
205
  let comments = [];
212
206
  try {
213
- const response = await client.request({
214
- method: 'GET',
215
- url: commentsRequestURL
207
+ const response = await axiosInstance.get(commentsRequestURL, {
208
+ headers: {
209
+ Authorization: `Bearer ${token}`,
210
+ 'Content-Type': 'application/json',
211
+ },
216
212
  });
217
213
  comments = response.data.values;
218
214
  }
@@ -227,9 +223,11 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
227
223
  try {
228
224
  // Delete previous comment
229
225
  const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
230
- await client.request({
231
- method: 'DELETE',
232
- url: deleteCommentURL
226
+ await axiosInstance.delete(deleteCommentURL, {
227
+ headers: {
228
+ Authorization: `Bearer ${token}`,
229
+ 'Content-Type': 'application/json',
230
+ },
233
231
  });
234
232
  }
235
233
  catch (e) {
@@ -14,7 +14,6 @@ declare class GitLabPipe {
14
14
  tests: any[];
15
15
  token: any;
16
16
  hiddenCommentData: string;
17
- client: Gaxios;
18
17
  prepareRun(): Promise<void>;
19
18
  createRun(): Promise<void>;
20
19
  addTest(test: any): void;
@@ -22,4 +21,3 @@ declare class GitLabPipe {
22
21
  toString(): string;
23
22
  updateRun(): void;
24
23
  }
25
- import { Gaxios } from 'gaxios';
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const debug_1 = __importDefault(require("debug"));
7
- const gaxios_1 = require("gaxios");
7
+ const axios_1 = __importDefault(require("axios"));
8
8
  const picocolors_1 = __importDefault(require("picocolors"));
9
9
  const humanize_duration_1 = __importDefault(require("humanize-duration"));
10
10
  const lodash_merge_1 = __importDefault(require("lodash.merge"));
@@ -39,12 +39,6 @@ class GitLabPipe {
39
39
  return;
40
40
  }
41
41
  this.isEnabled = true;
42
- this.client = new gaxios_1.Gaxios({
43
- baseURL: 'https://gitlab.com/api/v4',
44
- headers: {
45
- 'Content-Type': 'application/json',
46
- }
47
- });
48
42
  debug('GitLab Pipe: Enabled');
49
43
  }
50
44
  // TODO: to using SET opts as argument => prepareRun(opts)
@@ -132,18 +126,13 @@ class GitLabPipe {
132
126
  body += '\n</details>';
133
127
  }
134
128
  // eslint-disable-next-line max-len
135
- const commentsRequestURL = `/projects/${this.ENV.CI_PROJECT_ID}/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}/notes`;
129
+ const commentsRequestURL = `https://gitlab.com/api/v4/projects/${this.ENV.CI_PROJECT_ID}/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}/notes`;
136
130
  // delete previous report
137
- await deletePreviousReport(this.client, commentsRequestURL, this.hiddenCommentData, this.token);
131
+ await deletePreviousReport(axios_1.default, commentsRequestURL, this.hiddenCommentData, this.token);
138
132
  // add current report
139
133
  debug(`Adding comment via url: ${commentsRequestURL}`);
140
134
  try {
141
- const addCommentResponse = await this.client.request({
142
- method: 'POST',
143
- url: commentsRequestURL,
144
- params: { access_token: this.token },
145
- data: { body }
146
- });
135
+ const addCommentResponse = await axios_1.default.post(`${commentsRequestURL}?access_token=${this.token}`, { body });
147
136
  const commentID = addCommentResponse.data.id;
148
137
  // eslint-disable-next-line max-len
149
138
  const commentURL = `${this.ENV.CI_PROJECT_URL}/-/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}#note_${commentID}`;
@@ -160,17 +149,13 @@ class GitLabPipe {
160
149
  }
161
150
  updateRun() { }
162
151
  }
163
- async function deletePreviousReport(client, commentsRequestURL, hiddenCommentData, token) {
152
+ async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
164
153
  if (process.env.GITLAB_KEEP_OUTDATED_REPORTS)
165
154
  return;
166
155
  // get comments
167
156
  let comments = [];
168
157
  try {
169
- const response = await client.request({
170
- method: 'GET',
171
- url: commentsRequestURL,
172
- params: { access_token: token }
173
- });
158
+ const response = await axiosInstance.get(`${commentsRequestURL}?access_token=${token}`);
174
159
  comments = response.data;
175
160
  }
176
161
  catch (e) {
@@ -183,12 +168,8 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
183
168
  if (comment.body.includes(hiddenCommentData)) {
184
169
  try {
185
170
  // delete previous comment
186
- const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
187
- await client.request({
188
- method: 'DELETE',
189
- url: deleteCommentURL,
190
- params: { access_token: token }
191
- });
171
+ const deleteCommentURL = `${commentsRequestURL}/${comment.id}?access_token=${token}`;
172
+ await axiosInstance.delete(deleteCommentURL);
192
173
  }
193
174
  catch (e) {
194
175
  console.warn(`Can't delete previously added comment with testomat.io report. Ignore.`);
@@ -31,7 +31,7 @@ declare class TestomatioPipe implements Pipe {
31
31
  groupTitle: any;
32
32
  env: string;
33
33
  label: string;
34
- client: Gaxios;
34
+ axios: import("axios").AxiosInstance;
35
35
  proceed: string;
36
36
  jiraId: string;
37
37
  runId: any;
@@ -68,4 +68,3 @@ declare class TestomatioPipe implements Pipe {
68
68
  toString(): string;
69
69
  #private;
70
70
  }
71
- import { Gaxios } from 'gaxios';
@@ -5,7 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const debug_1 = __importDefault(require("debug"));
7
7
  const picocolors_1 = __importDefault(require("picocolors"));
8
- const gaxios_1 = require("gaxios");
8
+ // Retry interceptor function
9
+ const axios_retry_1 = __importDefault(require("axios-retry"));
10
+ // Default axios instance
11
+ const axios_1 = __importDefault(require("axios"));
9
12
  const json_cycle_1 = __importDefault(require("json-cycle"));
10
13
  const constants_js_1 = require("../constants.js");
11
14
  const utils_js_1 = require("../utils/utils.js");
@@ -51,31 +54,42 @@ class TestomatioPipe {
51
54
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
52
55
  this.env = process.env.TESTOMATIO_ENV;
53
56
  this.label = process.env.TESTOMATIO_LABEL;
54
- // Create a new instance of gaxios with a custom config
55
- this.client = new gaxios_1.Gaxios({
57
+ // Create a new instance of axios with a custom config
58
+ this.axios = axios_1.default.create({
56
59
  baseURL: `${this.url.trim()}`,
57
60
  timeout: constants_js_1.AXIOS_TIMEOUT,
58
- proxy: proxy ? proxy.toString() : undefined,
59
- retry: true,
60
- agent: new (require('https').Agent)({ rejectUnauthorized: false, keepAlive: false }),
61
- retryConfig: {
62
- retry: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest,
63
- retryDelay: constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout,
64
- shouldRetry: (error) => {
65
- if (!error.response)
61
+ proxy: proxy
62
+ ? {
63
+ host: proxy.hostname,
64
+ port: parseInt(proxy.port, 10),
65
+ protocol: proxy.protocol,
66
+ }
67
+ : false,
68
+ });
69
+ // Pass the axios instance to the retry function
70
+ (0, axios_retry_1.default)(this.axios, {
71
+ // do not use retries for unit tests
72
+ retries: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
73
+ shouldResetTimeout: true,
74
+ retryCondition: error => {
75
+ if (!error.response)
76
+ return false;
77
+ switch (error.response?.status) {
78
+ case 400: // Bad request (probably wrong API key)
79
+ case 404: // Test not matched
80
+ case 429: // Rate limit exceeded
81
+ case 500: // Internal server error
66
82
  return false;
67
- switch (error.response?.status) {
68
- case 400: // Bad request (probably wrong API key)
69
- case 404: // Test not matched
70
- case 429: // Rate limit exceeded
71
- case 500: // Internal server error
72
- return false;
73
- default:
74
- break;
75
- }
76
- return error.response?.status >= 401; // Retry on 401+ and 5xx
83
+ default:
84
+ break;
77
85
  }
78
- }
86
+ return error.response?.status >= 401; // Retry on 401+ and 5xx
87
+ },
88
+ retryDelay: () => constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
89
+ onRetry: async (retryCount, error) => {
90
+ this.retriesTimestamps.push(Date.now());
91
+ debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
92
+ },
79
93
  });
80
94
  this.isEnabled = true;
81
95
  // do not finish this run (for parallel testing)
@@ -110,14 +124,11 @@ class TestomatioPipe {
110
124
  if (!q) {
111
125
  return;
112
126
  }
113
- const resp = await this.client.request({
114
- method: 'GET',
115
- url: '/api/test_grep',
116
- params: q
117
- });
118
- if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
119
- (0, utils_js_1.foundedTestLog)(constants_js_1.APP_PREFIX, resp.data.tests);
120
- return resp.data.tests;
127
+ const resp = await this.axios.get('/api/test_grep', q);
128
+ const { data } = resp;
129
+ if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
130
+ (0, utils_js_1.foundedTestLog)(constants_js_1.APP_PREFIX, data.tests);
131
+ return data.tests;
121
132
  }
122
133
  console.log(constants_js_1.APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
123
134
  }
@@ -139,6 +150,7 @@ class TestomatioPipe {
139
150
  let buildUrl = process.env.BUILD_URL || process.env.CI_JOB_URL || process.env.CIRCLE_BUILD_URL;
140
151
  // GitHub Actions Url
141
152
  if (!buildUrl && process.env.GITHUB_RUN_ID) {
153
+ // eslint-disable-next-line max-len
142
154
  buildUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
143
155
  }
144
156
  // Azure DevOps Url
@@ -168,23 +180,16 @@ class TestomatioPipe {
168
180
  if (this.runId) {
169
181
  this.store.runId = this.runId;
170
182
  debug(`Run with id ${this.runId} already created, updating...`);
171
- const resp = await this.client.request({
172
- method: 'PUT',
173
- url: `/api/reporter/${this.runId}`,
174
- data: runParams
175
- });
183
+ const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
176
184
  if (resp.data.artifacts)
177
185
  (0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
178
186
  return;
179
187
  }
180
188
  debug('Creating run...');
181
189
  try {
182
- const resp = await this.client.request({
183
- method: 'POST',
184
- url: '/api/reporter',
185
- data: runParams,
190
+ const resp = await this.axios.post(`/api/reporter`, runParams, {
186
191
  maxContentLength: Infinity,
187
- responseType: 'json'
192
+ maxBodyLength: Infinity,
188
193
  });
189
194
  this.runId = resp.data.uid;
190
195
  this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
@@ -200,7 +205,6 @@ class TestomatioPipe {
200
205
  }
201
206
  catch (err) {
202
207
  const errorText = err.response?.data?.message || err.message;
203
- debug('Error creating run', err);
204
208
  console.log(errorText || err);
205
209
  if (!this.apiKey)
206
210
  console.error('Testomat.io API key is not set');
@@ -241,15 +245,7 @@ class TestomatioPipe {
241
245
  }
242
246
  const json = json_cycle_1.default.stringify(data);
243
247
  debug('Adding test', json);
244
- return this.client.request({
245
- method: 'POST',
246
- url: `/api/reporter/${this.runId}/testrun`,
247
- data: json,
248
- headers: {
249
- 'Content-Type': 'application/json',
250
- },
251
- maxContentLength: Infinity
252
- }).catch(err => {
248
+ return this.axios.post(`/api/reporter/${this.runId}/testrun`, json, axiosAddTestrunRequestConfig).catch(err => {
253
249
  this.requestFailures++;
254
250
  this.notReportedTestsCount++;
255
251
  if (err.response) {
@@ -294,19 +290,9 @@ class TestomatioPipe {
294
290
  // get tests from batch and clear batch
295
291
  const testsToSend = this.batch.tests.splice(0);
296
292
  debug('📨 Batch upload', testsToSend.length, 'tests');
297
- return this.client.request({
298
- method: 'POST',
299
- url: `/api/reporter/${this.runId}/testrun`,
300
- data: {
301
- api_key: this.apiKey,
302
- tests: testsToSend,
303
- batch_index: this.batch.batchIndex
304
- },
305
- headers: {
306
- 'Content-Type': 'application/json',
307
- },
308
- maxContentLength: Infinity
309
- }).catch(err => {
293
+ return this.axios
294
+ .post(`/api/reporter/${this.runId}/testrun`, { api_key: this.apiKey, tests: testsToSend, batch_index: this.batch.batchIndex }, axiosAddTestrunRequestConfig)
295
+ .catch(err => {
310
296
  this.requestFailures++;
311
297
  this.notReportedTestsCount += testsToSend.length;
312
298
  if (err.response) {
@@ -380,16 +366,12 @@ class TestomatioPipe {
380
366
  status_event += '_parallel';
381
367
  try {
382
368
  if (this.runId && !this.proceed) {
383
- await this.client.request({
384
- method: 'PUT',
385
- url: `/api/reporter/${this.runId}`,
386
- data: {
387
- api_key: this.apiKey,
388
- duration: params.duration,
389
- status_event,
390
- detach: params.detach,
391
- tests: params.tests,
392
- }
369
+ await this.axios.put(`/api/reporter/${this.runId}`, {
370
+ api_key: this.apiKey,
371
+ duration: params.duration,
372
+ status_event,
373
+ detach: params.detach,
374
+ tests: params.tests,
393
375
  });
394
376
  if (this.runUrl) {
395
377
  console.log(constants_js_1.APP_PREFIX, '📊 Report Saved. Report URL:', picocolors_1.default.magenta(this.runUrl));
@@ -439,10 +421,18 @@ function printCreateIssue(err) {
439
421
  if (!err.config)
440
422
  return;
441
423
  const time = new Date().toUTCString();
442
- const { body, url, baseURL, method } = err?.config || {};
424
+ const { data, url, baseURL, method } = err?.config || {};
443
425
  console.log('```js');
444
- console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
426
+ console.log({ data: data?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
445
427
  console.log('```');
446
428
  });
447
429
  }
430
+ const axiosAddTestrunRequestConfig = {
431
+ maxContentLength: Infinity,
432
+ maxBodyLength: Infinity,
433
+ headers: {
434
+ // Overwrite Axios's automatically set Content-Type
435
+ 'Content-Type': 'application/json',
436
+ },
437
+ };
448
438
  module.exports = TestomatioPipe;
package/lib/reporter.d.ts CHANGED
@@ -8,7 +8,7 @@ export type log = typeof import("./reporter-functions.js");
8
8
  export const log: (...args: any[]) => void;
9
9
  export type logger = typeof import("./services/index.js");
10
10
  export const logger: {
11
- "__#13@#originalUserLogger": {
11
+ "__#12@#originalUserLogger": {
12
12
  assert(condition?: boolean, ...data: any[]): void;
13
13
  assert(value: any, message?: string, ...optionalParams: any[]): void;
14
14
  clear(): void;
@@ -53,13 +53,13 @@ export const logger: {
53
53
  profile(label?: string): void;
54
54
  profileEnd(label?: string): void;
55
55
  };
56
- "__#13@#userLoggerWithOverridenMethods": any;
56
+ "__#12@#userLoggerWithOverridenMethods": any;
57
57
  logLevel: string;
58
58
  step(strings: any, ...values: any[]): void;
59
59
  getLogs(context: string): string[];
60
- "__#13@#stringifyLogs"(...args: any[]): string;
60
+ "__#12@#stringifyLogs"(...args: any[]): string;
61
61
  _templateLiteralLog(strings: any, ...args: any[]): void;
62
- "__#13@#logWrapper"(argsArray: any, level: any): void;
62
+ "__#12@#logWrapper"(argsArray: any, level: any): void;
63
63
  assert(...args: any[]): void;
64
64
  debug(...args: any[]): void;
65
65
  error(...args: any[]): void;
@@ -83,7 +83,7 @@ export type step = typeof import("./reporter-functions.js");
83
83
  export const step: (message: string) => void;
84
84
  declare namespace _default {
85
85
  let testomatioLogger: {
86
- "__#13@#originalUserLogger": {
86
+ "__#12@#originalUserLogger": {
87
87
  assert(condition?: boolean, ...data: any[]): void;
88
88
  assert(value: any, message?: string, ...optionalParams: any[]): void;
89
89
  clear(): void;
@@ -128,13 +128,13 @@ declare namespace _default {
128
128
  profile(label?: string): void;
129
129
  profileEnd(label?: string): void;
130
130
  };
131
- "__#13@#userLoggerWithOverridenMethods": any;
131
+ "__#12@#userLoggerWithOverridenMethods": any;
132
132
  logLevel: string;
133
133
  step(strings: any, ...values: any[]): void;
134
134
  getLogs(context: string): string[];
135
- "__#13@#stringifyLogs"(...args: any[]): string;
135
+ "__#12@#stringifyLogs"(...args: any[]): string;
136
136
  _templateLiteralLog(strings: any, ...args: any[]): void;
137
- "__#13@#logWrapper"(argsArray: any, level: any): void;
137
+ "__#12@#logWrapper"(argsArray: any, level: any): void;
138
138
  assert(...args: any[]): void;
139
139
  debug(...args: any[]): void;
140
140
  error(...args: any[]): void;
@@ -157,7 +157,7 @@ declare namespace _default {
157
157
  }, context?: any) => void;
158
158
  let log: (...args: any[]) => void;
159
159
  let logger: {
160
- "__#13@#originalUserLogger": {
160
+ "__#12@#originalUserLogger": {
161
161
  assert(condition?: boolean, ...data: any[]): void;
162
162
  assert(value: any, message?: string, ...optionalParams: any[]): void;
163
163
  clear(): void;
@@ -202,13 +202,13 @@ declare namespace _default {
202
202
  profile(label?: string): void;
203
203
  profileEnd(label?: string): void;
204
204
  };
205
- "__#13@#userLoggerWithOverridenMethods": any;
205
+ "__#12@#userLoggerWithOverridenMethods": any;
206
206
  logLevel: string;
207
207
  step(strings: any, ...values: any[]): void;
208
208
  getLogs(context: string): string[];
209
- "__#13@#stringifyLogs"(...args: any[]): string;
209
+ "__#12@#stringifyLogs"(...args: any[]): string;
210
210
  _templateLiteralLog(strings: any, ...args: any[]): void;
211
- "__#13@#logWrapper"(argsArray: any, level: any): void;
211
+ "__#12@#logWrapper"(argsArray: any, level: any): void;
212
212
  assert(...args: any[]): void;
213
213
  debug(...args: any[]): void;
214
214
  error(...args: any[]): void;
@@ -3,7 +3,7 @@ export const artifactStorage: ArtifactStorage;
3
3
  * Artifact storage is supposed to store file paths
4
4
  */
5
5
  declare class ArtifactStorage {
6
- static "__#14@#instance": any;
6
+ static "__#13@#instance": any;
7
7
  /**
8
8
  * Singleton
9
9
  * @returns {ArtifactStorage}
@@ -1,6 +1,6 @@
1
1
  export const keyValueStorage: KeyValueStorage;
2
2
  declare class KeyValueStorage {
3
- static "__#15@#instance": any;
3
+ static "__#14@#instance": any;
4
4
  /**
5
5
  *
6
6
  * @returns {KeyValueStorage}
@@ -5,7 +5,7 @@ export const logger: Logger;
5
5
  * Supports different syntaxes to satisfy any user preferences.
6
6
  */
7
7
  declare class Logger {
8
- static "__#13@#instance": any;
8
+ static "__#12@#instance": any;
9
9
  /**
10
10
  *
11
11
  * @returns {Logger}
@@ -1,3 +1,5 @@
1
+ export function getPackageVersion(): any;
2
+ export const TEST_ID_REGEX: RegExp;
1
3
  export function ansiRegExp(): RegExp;
2
4
  export function isSameTest(test: any, t: any): boolean;
3
5
  export function fetchSourceCode(contents: any, opts?: {}): string;
@@ -36,7 +36,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = void 0;
39
+ exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.TEST_ID_REGEX = void 0;
40
+ exports.getPackageVersion = getPackageVersion;
40
41
  exports.formatStep = formatStep;
41
42
  exports.readLatestRunId = readLatestRunId;
42
43
  exports.removeColorCodes = removeColorCodes;
@@ -48,7 +49,9 @@ const fs_1 = __importDefault(require("fs"));
48
49
  const is_valid_path_1 = __importDefault(require("is-valid-path"));
49
50
  const debug_1 = __importDefault(require("debug"));
50
51
  const os_1 = __importDefault(require("os"));
52
+ const url_2 = require("url");
51
53
  const debug = (0, debug_1.default)('@testomatio/reporter:util');
54
+ // Use __dirname directly since we're compiling to CommonJS
52
55
  /**
53
56
  * @param {String} testTitle - Test title
54
57
  *
@@ -140,7 +143,7 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
140
143
  .join('\n');
141
144
  };
142
145
  exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
143
- const TEST_ID_REGEX = /@T([\w\d]{8})/;
146
+ exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
144
147
  const fetchIdFromCode = (code, opts = {}) => {
145
148
  const comments = code
146
149
  .split('\n')
@@ -154,11 +157,11 @@ const fetchIdFromCode = (code, opts = {}) => {
154
157
  return l.startsWith('// ');
155
158
  }
156
159
  });
157
- return comments.find(c => c.match(TEST_ID_REGEX))?.match(TEST_ID_REGEX)?.[1];
160
+ return comments.find(c => c.match(exports.TEST_ID_REGEX))?.match(exports.TEST_ID_REGEX)?.[1];
158
161
  };
159
162
  exports.fetchIdFromCode = fetchIdFromCode;
160
163
  const fetchIdFromOutput = output => {
161
- const TID_FULL_PATTERN = new RegExp(`tid:\\/\\/.*?(${TEST_ID_REGEX.source})`);
164
+ const TID_FULL_PATTERN = new RegExp(`tid:\\/\\/.*?(${exports.TEST_ID_REGEX.source})`);
162
165
  return output.match(TID_FULL_PATTERN)?.[2];
163
166
  };
164
167
  exports.fetchIdFromOutput = fetchIdFromOutput;
@@ -183,6 +186,12 @@ const fetchSourceCode = (contents, opts = {}) => {
183
186
  if (lineIndex === -1)
184
187
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
185
188
  }
189
+ else if (opts.lang === 'csharp') {
190
+ if (lineIndex === -1)
191
+ lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
192
+ if (lineIndex === -1)
193
+ lineIndex = lines.findIndex(l => l.includes(`${title}(`));
194
+ }
186
195
  else {
187
196
  lineIndex = lines.findIndex(l => l.includes(title));
188
197
  }
@@ -384,6 +393,13 @@ function formatStep(step, shift = 0) {
384
393
  }
385
394
  return lines;
386
395
  }
396
+ function getPackageVersion() {
397
+ const packageJsonPath = path_1.default.resolve(__dirname, '../../package.json');
398
+ const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
399
+ return packageJson.version;
400
+ }
401
+
402
+ module.exports.getPackageVersion = getPackageVersion;
387
403
 
388
404
  module.exports.formatStep = formatStep;
389
405
 
package/lib/xmlReader.js CHANGED
@@ -20,7 +20,7 @@ const uploader_js_1 = require("./uploader.js");
20
20
  const debug = (0, debug_1.default)('@testomatio/reporter:xml');
21
21
  const ridRunId = (0, crypto_1.randomUUID)();
22
22
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
23
- const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } = process.env;
23
+ const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } = process.env;
24
24
  const options = {
25
25
  ignoreDeclaration: true,
26
26
  ignoreAttributes: false,
@@ -28,6 +28,7 @@ const options = {
28
28
  attributeNamePrefix: '',
29
29
  parseTagValue: true,
30
30
  };
31
+ const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
31
32
  const reduceOptions = {};
32
33
  class XmlReader {
33
34
  constructor(opts = {}) {
@@ -75,7 +76,7 @@ class XmlReader {
75
76
  /(<system-out><!\[CDATA\[)([\s\S]*?)(\]\]><\/system-out>)/g,
76
77
  ];
77
78
  for (const regex of cutRegexes) {
78
- xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, 5000)}${p3}`);
79
+ xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, MAX_OUTPUT_LENGTH)}${p3}`);
79
80
  }
80
81
  const jsonResult = this.parser.parse(xmlData);
81
82
  let jsonSuite;
@@ -313,6 +314,8 @@ class XmlReader {
313
314
  this.stats.language = 'js';
314
315
  if (file.endsWith('.ts'))
315
316
  this.stats.language = 'ts';
317
+ if (file.endsWith('.cs'))
318
+ this.stats.language = 'csharp';
316
319
  }
317
320
  if (!fs_1.default.existsSync(file)) {
318
321
  debug('Failed to open file with the source code', file);
@@ -359,13 +362,13 @@ class XmlReader {
359
362
  async uploadArtifacts() {
360
363
  for (const test of this.tests.filter(t => !!t.stack)) {
361
364
  let files = [];
362
- if (test.files?.length)
363
- files = test.files.map(f => path_1.default.join(process.cwd(), f));
364
- files = [...files, ...(0, utils_js_1.fetchFilesFromStackTrace)(test.stack)];
365
+ if (!test.files?.length)
366
+ continue;
367
+ files = test.files.map(f => path_1.default.isAbsolute(f) ? f : path_1.default.join(process.cwd(), f));
365
368
  if (!files.length)
366
369
  continue;
367
370
  const runId = this.runId || this.store.runId || Date.now().toString();
368
- test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
371
+ test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path_1.default.basename(f)])));
369
372
  console.log(constants_js_1.APP_PREFIX, `🗄️ Uploaded ${picocolors_1.default.bold(`${files.length} artifacts`)} for test ${test.title}`);
370
373
  }
371
374
  }
@@ -425,7 +428,7 @@ function reduceTestCases(prev, item) {
425
428
  testCases
426
429
  .filter(t => !!t)
427
430
  .forEach(testCaseItem => {
428
- const file = testCaseItem.file || item.filepath || '';
431
+ const file = testCaseItem.file || item.filepath || item.fullname || '';
429
432
  let stack = '';
430
433
  let message = '';
431
434
  if (testCaseItem.error)
@@ -458,15 +461,34 @@ function reduceTestCases(prev, item) {
458
461
  title = title.replace(/\(.*?\)/, '').trim();
459
462
  }
460
463
  stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
461
- const testId = (0, utils_js_1.fetchIdFromOutput)(stack);
464
+ let testId = (0, utils_js_1.fetchIdFromOutput)(stack);
465
+ if (tags?.length && !testId) {
466
+ testId = tags.filter(t => t.startsWith('T')).map(t => `@${t}`).find(t => t.match(utils_js_1.TEST_ID_REGEX))?.slice(2);
467
+ }
462
468
  let status = constants_js_1.STATUS.PASSED.toString();
463
469
  if ('failure' in testCaseItem || 'error' in testCaseItem)
464
470
  status = constants_js_1.STATUS.FAILED;
465
471
  if ('skipped' in testCaseItem)
466
472
  status = constants_js_1.STATUS.SKIPPED;
473
+ if (testCaseItem.result && Object.values(constants_js_1.STATUS).includes(testCaseItem.result.toLowerCase())) {
474
+ status = testCaseItem.result.toLowerCase();
475
+ }
467
476
  let rid = null;
468
477
  if (testCaseItem.id)
469
478
  rid = `${ridRunId}-${testCaseItem.id}`;
479
+ // Extract attachments
480
+ let files = [];
481
+ if (testCaseItem.attachments) {
482
+ const attachments = Array.isArray(testCaseItem.attachments.attachment)
483
+ ? testCaseItem.attachments.attachment
484
+ : [testCaseItem.attachments.attachment];
485
+ files = attachments
486
+ .filter(a => a && a.filePath)
487
+ .map(a => a.filePath);
488
+ }
489
+ // Extract files from stack trace using existing utility
490
+ const stackFiles = (0, utils_js_1.fetchFilesFromStackTrace)(stack);
491
+ files = [...new Set([...files, ...stackFiles])]; // Remove duplicates
470
492
  prev.push({
471
493
  rid,
472
494
  file,
@@ -481,7 +503,9 @@ function reduceTestCases(prev, item) {
481
503
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
482
504
  status,
483
505
  title,
506
+ root_suite_id: TESTOMATIO_SUITE,
484
507
  suite_title: suiteTitle,
508
+ files,
485
509
  });
486
510
  });
487
511
  return prev;
@@ -505,11 +529,14 @@ function fetchProperties(item) {
505
529
  let title = '';
506
530
  if (!item.properties)
507
531
  return {};
508
- const prop = [item.properties?.property].flat().find(p => p.name === 'Description');
532
+ // Handle both single property and array of properties
533
+ const properties = Array.isArray(item.properties.property)
534
+ ? item.properties.property
535
+ : [item.properties.property].filter(Boolean);
536
+ const prop = properties.find(p => p.name === 'Description');
509
537
  if (prop)
510
538
  title = prop.value;
511
- [item.properties?.property]
512
- .flat()
539
+ properties
513
540
  .filter(p => p.name === 'Category')
514
541
  .forEach(p => tags.push(p.value));
515
542
  return { title, tags };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.0.0-beta.2-xml",
3
+ "version": "2.0.0-beta.3-xml",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"