@testomatio/reporter 2.0.0-beta.2-gaxios → 2.0.0-beta.2-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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.0.0-beta.2-gaxios",
3
+ "version": "2.0.0-beta.2-xml",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -16,7 +16,8 @@
16
16
  "@cucumber/cucumber": "^10.9.0",
17
17
  "@octokit/rest": "^21.1.1",
18
18
  "aws-sdk": "^2.1072.0",
19
- "gaxios": "^6.0.0",
19
+ "axios": "^1.6.2",
20
+ "axios-retry": "^3.9.1",
20
21
  "callsite-record": "^4.1.4",
21
22
  "commander": "^12",
22
23
  "cross-spawn": "^7.0.3",
@@ -48,7 +49,6 @@
48
49
  "testcafe"
49
50
  ],
50
51
  "scripts": {
51
- "@cucumber/cucumber": "^10.9.0",
52
52
  "clear-exportdir": "rm -rf export/",
53
53
  "pretty": "npx prettier --check .",
54
54
  "pretty:fix": "prettier --write .",
@@ -67,8 +67,9 @@
67
67
  "test:adapter:playwright:example": "npx playwright test --config='./tests/adapter/examples/playwright/playwright.config.ts'",
68
68
  "test:adapter:vitest:example": "npx vitest --config='./tests/adapter/examples/vitest/vitest.config.ts'",
69
69
  "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/logger-2.test.js && npx mocha tests-storage/reporter-functions.test.js",
70
- "//": "builds code from /src (esm) into /lib (commonjs)",
71
- "build": "rm -rf ./cjs && tsc --module commonjs && npx tsx build/scripts/edit-js-files.js && npx tsx build/scripts/edit-package-json.js && chmod +x ./build/scripts/copy-tesmplate.sh && ./build/scripts/copy-tesmplate.sh"
70
+ "build": "rm -rf ./cjs && tsc --module commonjs && npx tsx build/scripts/edit-js-files.js && npx tsx build/scripts/edit-package-json.js && chmod +x ./build/scripts/copy-tesmplate.sh && ./build/scripts/copy-tesmplate.sh",
71
+ "build:bun": "rm -rf ./cjs && bun build ./src/reporter.js ./src/bin/reportXml.js ./src/bin/startTest.js ./src/bin/uploadArtifacts.js --outdir ./cjs --target node",
72
+ "build:watch:bun": "rm -rf ./cjs && bun build ./src/bin/reportXml.js ./src/bin/startTest.js ./src/bin/uploadArtifacts.js --outdir ./cjs --target node --watch --onSuccess \"build/scripts/post-build.js\""
72
73
  },
73
74
  "devDependencies": {
74
75
  "@playwright/test": "^1.49.1",
@@ -1,6 +1,6 @@
1
1
  import TestomatClient from '../client.js';
2
2
  import { config } from '../config.js';
3
- import { STATUS } from '../constants';
3
+ import { STATUS } from '../constants.js';
4
4
  import { getTestomatIdFromTestTitle } from '../utils/utils.js';
5
5
 
6
6
  const apiKey = config.TESTOMATIO;
@@ -1,4 +1,4 @@
1
- import WDIOReporter, { RunnerStats } from '@wdio/reporter';
1
+ import { default as WDIOReporter, RunnerStats } from '@wdio/reporter';
2
2
  import TestomatClient from '../client.js';
3
3
  import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
4
4
  import { services } from '../services/index.js';
package/src/bin/cli.js CHANGED
@@ -7,7 +7,7 @@ import createDebugMessages from 'debug';
7
7
  import TestomatClient from '../client.js';
8
8
  import XmlReader from '../xmlReader.js';
9
9
  import { APP_PREFIX, STATUS } from '../constants.js';
10
- import { version } from '../../package.json';
10
+ import { getPackageVersion } from '../utils/utils.js';
11
11
  import { config } from '../config.js';
12
12
  import { readLatestRunId } from '../utils/utils.js';
13
13
  import pc from 'picocolors';
@@ -15,6 +15,7 @@ import { filesize as prettyBytes } from 'filesize';
15
15
  import dotenv from 'dotenv';
16
16
 
17
17
  const debug = createDebugMessages('@testomatio/reporter:xml-cli');
18
+ const version = getPackageVersion();
18
19
  console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
19
20
  const program = new Command();
20
21
 
@@ -5,8 +5,11 @@ import { glob } from 'glob';
5
5
  import createDebugMessages from 'debug';
6
6
  import { APP_PREFIX } from '../constants.js';
7
7
  import XmlReader from '../xmlReader.js';
8
- import { version } from '../../package.json';
8
+ import { getPackageVersion } from '../utils/utils.js';
9
9
  import dotenv from 'dotenv';
10
+ import path from 'path';
11
+
12
+ const version = getPackageVersion();
10
13
 
11
14
  const debug = createDebugMessages('@testomatio/reporter:xml-cli');
12
15
  console.log(pc.cyan(pc.bold(` 🤩 Testomat.io XML Reporter v${version}`)));
@@ -4,10 +4,11 @@ import { Command } from 'commander';
4
4
  import pc from 'picocolors';
5
5
  import TestomatClient from '../client.js';
6
6
  import { APP_PREFIX, STATUS } from '../constants.js';
7
- import { version } from '../../package.json';
7
+ import { getPackageVersion } from '../utils/utils.js';
8
8
  import { config } from '../config.js';
9
9
  import dotenv from 'dotenv';
10
10
 
11
+ const version = getPackageVersion();
11
12
  console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
12
13
  const program = new Command();
13
14
 
@@ -5,12 +5,13 @@ import pc from 'picocolors';
5
5
  import createDebugMessages from 'debug';
6
6
  import TestomatClient from '../client.js';
7
7
  import { APP_PREFIX } from '../constants.js';
8
- import { version } from '../../package.json';
8
+ import { getPackageVersion } from '../utils/utils.js';
9
9
  import { config } from '../config.js';
10
10
  import { readLatestRunId } from '../utils/utils.js';
11
11
  import dotenv from 'dotenv';
12
12
 
13
13
  const debug = createDebugMessages('@testomatio/reporter:upload-cli');
14
+ const version = getPackageVersion();
14
15
  console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
15
16
  const program = new Command();
16
17
 
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
  import Adapter from './adapter.js';
2
3
 
3
4
  class CSharpAdapter extends Adapter {
@@ -7,10 +8,21 @@ class CSharpAdapter extends Adapter {
7
8
  if (example) t.example = { ...example[1].split(',') };
8
9
  const suite = t.suite_title.split('.');
9
10
  t.suite_title = suite.pop();
10
- t.file = suite.join('/');
11
+ t.file = namespaceToFileName(t.file);
11
12
  t.title = title.trim();
12
13
  return t;
13
14
  }
15
+
16
+ getFilePath(t) {
17
+ const fileName = namespaceToFileName(t.file);
18
+ return fileName;
19
+ }
14
20
  }
15
21
 
16
22
  export default CSharpAdapter;
23
+
24
+ function namespaceToFileName(fileName) {
25
+ const fileParts = fileName.split('.');
26
+ fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
27
+ return `${fileParts.join(path.sep)}.cs`;
28
+ }
@@ -1,7 +1,7 @@
1
1
  import { APP_PREFIX, testomatLogoURL } from '../constants.js';
2
2
  import { ansiRegExp, isSameTest } from '../utils/utils.js';
3
3
  import { statusEmoji, fullName } from '../utils/pipe_utils.js';
4
- import { Gaxios } from 'gaxios';
4
+ import axios from 'axios';
5
5
  import pc from 'picocolors';
6
6
  import humanizeDuration from 'humanize-duration';
7
7
  import merge from 'lodash.merge';
@@ -40,13 +40,6 @@ export class BitbucketPipe {
40
40
  }
41
41
 
42
42
  this.isEnabled = true;
43
- this.client = new Gaxios({
44
- baseURL: 'https://api.bitbucket.org/2.0',
45
- headers: {
46
- 'Content-Type': 'application/json',
47
- 'Authorization': `Bearer ${this.token}`
48
- }
49
- });
50
43
 
51
44
  debug('Bitbucket Pipe: Enabled');
52
45
  }
@@ -173,21 +166,26 @@ export class BitbucketPipe {
173
166
 
174
167
  // Construct Bitbucket API URL for comments
175
168
  // eslint-disable-next-line max-len
176
- const commentsRequestURL = `/repositories/${this.ENV.BITBUCKET_WORKSPACE}/${this.ENV.BITBUCKET_REPO_SLUG}/pullrequests/${this.ENV.BITBUCKET_PR_ID}/comments`;
169
+ 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`;
177
170
 
178
171
  // Delete previous report
179
- await deletePreviousReport(this.client, commentsRequestURL, this.hiddenCommentData);
172
+ await deletePreviousReport(axios, commentsRequestURL, this.hiddenCommentData, this.token);
180
173
 
181
174
  // Add current report
182
175
  debug(`Adding comment via URL: ${commentsRequestURL}`);
183
176
  debug(`Final Bitbucket API call body: ${body}`);
184
177
 
185
178
  try {
186
- const addCommentResponse = await this.client.request({
187
- method: 'POST',
188
- url: commentsRequestURL,
189
- data: { content: { raw: body } }
190
- });
179
+ const addCommentResponse = await axios.post(
180
+ commentsRequestURL,
181
+ { content: { raw: body } },
182
+ {
183
+ headers: {
184
+ Authorization: `Bearer ${this.token}`,
185
+ 'Content-Type': 'application/json',
186
+ },
187
+ },
188
+ );
191
189
 
192
190
  const commentID = addCommentResponse.data.id;
193
191
  // eslint-disable-next-line max-len
@@ -212,16 +210,18 @@ export class BitbucketPipe {
212
210
  updateRun() {}
213
211
  }
214
212
 
215
- async function deletePreviousReport(client, commentsRequestURL, hiddenCommentData) {
213
+ async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
216
214
  if (process.env.BITBUCKET_KEEP_OUTDATED_REPORTS) return;
217
215
 
218
216
  // Get comments
219
217
  let comments = [];
220
218
 
221
219
  try {
222
- const response = await client.request({
223
- method: 'GET',
224
- url: commentsRequestURL
220
+ const response = await axiosInstance.get(commentsRequestURL, {
221
+ headers: {
222
+ Authorization: `Bearer ${token}`,
223
+ 'Content-Type': 'application/json',
224
+ },
225
225
  });
226
226
  comments = response.data.values;
227
227
  } catch (e) {
@@ -236,9 +236,11 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
236
236
  try {
237
237
  // Delete previous comment
238
238
  const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
239
- await client.request({
240
- method: 'DELETE',
241
- url: deleteCommentURL
239
+ await axiosInstance.delete(deleteCommentURL, {
240
+ headers: {
241
+ Authorization: `Bearer ${token}`,
242
+ 'Content-Type': 'application/json',
243
+ },
242
244
  });
243
245
  } catch (e) {
244
246
  console.warn(`Can't delete previously added comment with testomat.io report. Ignored.`);
@@ -1,5 +1,5 @@
1
1
  import createDebugMessages from 'debug';
2
- import { Gaxios } from 'gaxios';
2
+ import axios from 'axios';
3
3
  import pc from 'picocolors';
4
4
  import humanizeDuration from 'humanize-duration';
5
5
  import merge from 'lodash.merge';
@@ -45,12 +45,6 @@ class GitLabPipe {
45
45
  }
46
46
 
47
47
  this.isEnabled = true;
48
- this.client = new Gaxios({
49
- baseURL: 'https://gitlab.com/api/v4',
50
- headers: {
51
- 'Content-Type': 'application/json',
52
- }
53
- });
54
48
 
55
49
  debug('GitLab Pipe: Enabled');
56
50
  }
@@ -163,21 +157,16 @@ class GitLabPipe {
163
157
  }
164
158
 
165
159
  // eslint-disable-next-line max-len
166
- const commentsRequestURL = `/projects/${this.ENV.CI_PROJECT_ID}/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}/notes`;
160
+ const commentsRequestURL = `https://gitlab.com/api/v4/projects/${this.ENV.CI_PROJECT_ID}/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}/notes`;
167
161
 
168
162
  // delete previous report
169
- await deletePreviousReport(this.client, commentsRequestURL, this.hiddenCommentData, this.token);
163
+ await deletePreviousReport(axios, commentsRequestURL, this.hiddenCommentData, this.token);
170
164
 
171
165
  // add current report
172
166
  debug(`Adding comment via url: ${commentsRequestURL}`);
173
167
 
174
168
  try {
175
- const addCommentResponse = await this.client.request({
176
- method: 'POST',
177
- url: commentsRequestURL,
178
- params: { access_token: this.token },
179
- data: { body }
180
- });
169
+ const addCommentResponse = await axios.post(`${commentsRequestURL}?access_token=${this.token}`, { body });
181
170
 
182
171
  const commentID = addCommentResponse.data.id;
183
172
  // eslint-disable-next-line max-len
@@ -202,18 +191,14 @@ class GitLabPipe {
202
191
  updateRun() {}
203
192
  }
204
193
 
205
- async function deletePreviousReport(client, commentsRequestURL, hiddenCommentData, token) {
194
+ async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
206
195
  if (process.env.GITLAB_KEEP_OUTDATED_REPORTS) return;
207
196
 
208
197
  // get comments
209
198
  let comments = [];
210
199
 
211
200
  try {
212
- const response = await client.request({
213
- method: 'GET',
214
- url: commentsRequestURL,
215
- params: { access_token: token }
216
- });
201
+ const response = await axiosInstance.get(`${commentsRequestURL}?access_token=${token}`);
217
202
  comments = response.data;
218
203
  } catch (e) {
219
204
  console.error('Error while attempt to retrieve comments on GitLab Merge Request:\n', e);
@@ -226,12 +211,8 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
226
211
  if (comment.body.includes(hiddenCommentData)) {
227
212
  try {
228
213
  // delete previous comment
229
- const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
230
- await client.request({
231
- method: 'DELETE',
232
- url: deleteCommentURL,
233
- params: { access_token: token }
234
- });
214
+ const deleteCommentURL = `${commentsRequestURL}/${comment.id}?access_token=${token}`;
215
+ await axiosInstance.delete(deleteCommentURL);
235
216
  } catch (e) {
236
217
  console.warn(`Can't delete previously added comment with testomat.io report. Ignore.`);
237
218
  }
@@ -1,6 +1,12 @@
1
1
  import createDebugMessages from 'debug';
2
2
  import pc from 'picocolors';
3
- import { Gaxios } from 'gaxios';
3
+
4
+ // Retry interceptor function
5
+ import axiosRetry from 'axios-retry';
6
+
7
+ // Default axios instance
8
+ import axios from 'axios';
9
+
4
10
  import JsonCycle from 'json-cycle';
5
11
  import { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
6
12
  import { isValidUrl, foundedTestLog } from '../utils/utils.js';
@@ -51,31 +57,43 @@ class TestomatioPipe {
51
57
  this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
52
58
  this.env = process.env.TESTOMATIO_ENV;
53
59
  this.label = process.env.TESTOMATIO_LABEL;
54
-
55
- // Create a new instance of gaxios with a custom config
56
- this.client = new Gaxios({
60
+ // Create a new instance of axios with a custom config
61
+ this.axios = axios.create({
57
62
  baseURL: `${this.url.trim()}`,
58
63
  timeout: AXIOS_TIMEOUT,
59
- proxy: proxy ? proxy.toString() : undefined,
60
- retry: true,
61
- agent: new (require('https').Agent)({ rejectUnauthorized: false, keepAlive: false }),
62
- retryConfig: {
63
- retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
64
- retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
65
- shouldRetry: (error) => {
66
- if (!error.response) 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;
64
+ proxy: proxy
65
+ ? {
66
+ host: proxy.hostname,
67
+ port: parseInt(proxy.port, 10),
68
+ protocol: proxy.protocol,
75
69
  }
76
- return error.response?.status >= 401; // Retry on 401+ and 5xx
70
+ : false,
71
+ });
72
+
73
+ // Pass the axios instance to the retry function
74
+ axiosRetry(this.axios, {
75
+ // do not use retries for unit tests
76
+ retries: REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
77
+ shouldResetTimeout: true,
78
+ retryCondition: error => {
79
+ if (!error.response) return false;
80
+ switch (error.response?.status) {
81
+ case 400: // Bad request (probably wrong API key)
82
+ case 404: // Test not matched
83
+ case 429: // Rate limit exceeded
84
+ case 500: // Internal server error
85
+ return false;
86
+ default:
87
+ break;
77
88
  }
78
- }
89
+ return error.response?.status >= 401; // Retry on 401+ and 5xx
90
+ },
91
+ retryDelay: () => REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
92
+ onRetry: async (retryCount, error) => {
93
+ this.retriesTimestamps.push(Date.now());
94
+
95
+ debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
96
+ },
79
97
  });
80
98
 
81
99
  this.isEnabled = true;
@@ -116,15 +134,12 @@ class TestomatioPipe {
116
134
  return;
117
135
  }
118
136
 
119
- const resp = await this.client.request({
120
- method: 'GET',
121
- url: '/api/test_grep',
122
- params: q
123
- });
137
+ const resp = await this.axios.get('/api/test_grep', q);
138
+ const { data } = resp;
124
139
 
125
- if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
126
- foundedTestLog(APP_PREFIX, resp.data.tests);
127
- return resp.data.tests;
140
+ if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
141
+ foundedTestLog(APP_PREFIX, data.tests);
142
+ return data.tests;
128
143
  }
129
144
 
130
145
  console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
@@ -148,6 +163,7 @@ class TestomatioPipe {
148
163
 
149
164
  // GitHub Actions Url
150
165
  if (!buildUrl && process.env.GITHUB_RUN_ID) {
166
+ // eslint-disable-next-line max-len
151
167
  buildUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
152
168
  }
153
169
 
@@ -183,23 +199,16 @@ class TestomatioPipe {
183
199
  if (this.runId) {
184
200
  this.store.runId = this.runId;
185
201
  debug(`Run with id ${this.runId} already created, updating...`);
186
- const resp = await this.client.request({
187
- method: 'PUT',
188
- url: `/api/reporter/${this.runId}`,
189
- data: runParams
190
- });
202
+ const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
191
203
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
192
204
  return;
193
205
  }
194
206
 
195
207
  debug('Creating run...');
196
208
  try {
197
- const resp = await this.client.request({
198
- method: 'POST',
199
- url: '/api/reporter',
200
- data: runParams,
209
+ const resp = await this.axios.post(`/api/reporter`, runParams, {
201
210
  maxContentLength: Infinity,
202
- responseType: 'json'
211
+ maxBodyLength: Infinity,
203
212
  });
204
213
 
205
214
  this.runId = resp.data.uid;
@@ -216,7 +225,6 @@ class TestomatioPipe {
216
225
  debug('Run created', this.runId);
217
226
  } catch (err) {
218
227
  const errorText = err.response?.data?.message || err.message;
219
- debug('Error creating run', err);
220
228
  console.log(errorText || err);
221
229
  if (!this.apiKey) console.error('Testomat.io API key is not set');
222
230
  if (!this.apiKey?.startsWith('tstmt')) console.error('Testomat.io API key is invalid');
@@ -263,15 +271,7 @@ class TestomatioPipe {
263
271
 
264
272
  debug('Adding test', json);
265
273
 
266
- return this.client.request({
267
- method: 'POST',
268
- url: `/api/reporter/${this.runId}/testrun`,
269
- data: json,
270
- headers: {
271
- 'Content-Type': 'application/json',
272
- },
273
- maxContentLength: Infinity
274
- }).catch(err => {
274
+ return this.axios.post(`/api/reporter/${this.runId}/testrun`, json, axiosAddTestrunRequestConfig).catch(err => {
275
275
  this.requestFailures++;
276
276
  this.notReportedTestsCount++;
277
277
  if (err.response) {
@@ -323,43 +323,38 @@ class TestomatioPipe {
323
323
  const testsToSend = this.batch.tests.splice(0);
324
324
  debug('📨 Batch upload', testsToSend.length, 'tests');
325
325
 
326
- return this.client.request({
327
- method: 'POST',
328
- url: `/api/reporter/${this.runId}/testrun`,
329
- data: {
330
- api_key: this.apiKey,
331
- tests: testsToSend,
332
- batch_index: this.batch.batchIndex
333
- },
334
- headers: {
335
- 'Content-Type': 'application/json',
336
- },
337
- maxContentLength: Infinity
338
- }).catch(err => {
339
- this.requestFailures++;
340
- this.notReportedTestsCount += testsToSend.length;
341
- if (err.response) {
342
- if (err.response.status >= 400) {
343
- const responseData = err.response.data || { message: '' };
326
+ return this.axios
327
+ .post(
328
+ `/api/reporter/${this.runId}/testrun`,
329
+ { api_key: this.apiKey, tests: testsToSend, batch_index: this.batch.batchIndex },
330
+ axiosAddTestrunRequestConfig,
331
+ )
332
+ .catch(err => {
333
+ this.requestFailures++;
334
+ this.notReportedTestsCount += testsToSend.length;
335
+ if (err.response) {
336
+ if (err.response.status >= 400) {
337
+ const responseData = err.response.data || { message: '' };
338
+ console.log(
339
+ APP_PREFIX,
340
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
341
+ // pc.grey(data?.title || ''),
342
+ );
343
+ if (err.response?.data?.message?.includes('could not be matched')) {
344
+ this.hasUnmatchedTests = true;
345
+ }
346
+ return;
347
+ }
344
348
  console.log(
345
349
  APP_PREFIX,
346
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
350
+ pc.yellow(`Warning: (${err.response?.status})`),
351
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
347
352
  );
348
- if (err.response?.data?.message?.includes('could not be matched')) {
349
- this.hasUnmatchedTests = true;
350
- }
351
- return;
353
+ printCreateIssue(err);
354
+ } else {
355
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
352
356
  }
353
- console.log(
354
- APP_PREFIX,
355
- pc.yellow(`Warning: (${err.response?.status})`),
356
- `Report couldn't be processed: ${err?.response?.data?.message}`,
357
- );
358
- printCreateIssue(err);
359
- } else {
360
- console.log(APP_PREFIX, "Report couldn't be processed", err);
361
- }
362
- });
357
+ });
363
358
  };
364
359
 
365
360
  /**
@@ -418,16 +413,12 @@ class TestomatioPipe {
418
413
 
419
414
  try {
420
415
  if (this.runId && !this.proceed) {
421
- await this.client.request({
422
- method: 'PUT',
423
- url: `/api/reporter/${this.runId}`,
424
- data: {
425
- api_key: this.apiKey,
426
- duration: params.duration,
427
- status_event,
428
- detach: params.detach,
429
- tests: params.tests,
430
- }
416
+ await this.axios.put(`/api/reporter/${this.runId}`, {
417
+ api_key: this.apiKey,
418
+ duration: params.duration,
419
+ status_event,
420
+ detach: params.detach,
421
+ tests: params.tests,
431
422
  });
432
423
  if (this.runUrl) {
433
424
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
@@ -487,11 +478,20 @@ function printCreateIssue(err) {
487
478
  if (!err.config) return;
488
479
 
489
480
  const time = new Date().toUTCString();
490
- const { body, url, baseURL, method } = err?.config || {};
481
+ const { data, url, baseURL, method } = err?.config || {};
491
482
  console.log('```js');
492
- console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
483
+ console.log({ data: data?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
493
484
  console.log('```');
494
485
  });
495
486
  }
496
487
 
488
+ const axiosAddTestrunRequestConfig = {
489
+ maxContentLength: Infinity,
490
+ maxBodyLength: Infinity,
491
+ headers: {
492
+ // Overwrite Axios's automatically set Content-Type
493
+ 'Content-Type': 'application/json',
494
+ },
495
+ };
496
+
497
497
  export default TestomatioPipe;
@@ -5,9 +5,13 @@ import fs from 'fs';
5
5
  import isValid from 'is-valid-path';
6
6
  import createDebugMessages from 'debug';
7
7
  import os from 'os';
8
+ import { fileURLToPath } from 'url';
8
9
 
9
10
  const debug = createDebugMessages('@testomatio/reporter:util');
10
11
 
12
+ // Use __dirname directly since we're compiling to CommonJS
13
+ const __dirname = path.resolve();
14
+
11
15
  /**
12
16
  * @param {String} testTitle - Test title
13
17
  *
@@ -107,7 +111,7 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
107
111
  .join('\n');
108
112
  };
109
113
 
110
- const TEST_ID_REGEX = /@T([\w\d]{8})/;
114
+ export const TEST_ID_REGEX = /@T([\w\d]{8})/;
111
115
 
112
116
  const fetchIdFromCode = (code, opts = {}) => {
113
117
  const comments = code
@@ -150,6 +154,9 @@ const fetchSourceCode = (contents, opts = {}) => {
150
154
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
151
155
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
152
156
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
157
+ } else if (opts.lang === 'csharp') {
158
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
159
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
153
160
  } else {
154
161
  lineIndex = lines.findIndex(l => l.includes(title));
155
162
  }
@@ -353,6 +360,12 @@ function formatStep(step, shift = 0) {
353
360
  return lines;
354
361
  }
355
362
 
363
+ export function getPackageVersion() {
364
+ const packageJsonPath = path.resolve(__dirname, '../../package.json');
365
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
366
+ return packageJson.version;
367
+ }
368
+
356
369
  export {
357
370
  ansiRegExp,
358
371
  isSameTest,
package/src/xmlReader.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  fetchSourceCodeFromStackTrace,
14
14
  fetchIdFromCode,
15
15
  humanize,
16
+ TEST_ID_REGEX,
16
17
  } from './utils/utils.js';
17
18
  import { pipesFactory } from './pipe/index.js';
18
19
  import adapterFactory from './junit-adapter/index.js';
@@ -26,8 +27,9 @@ const debug = createDebugMessages('@testomatio/reporter:xml');
26
27
  const ridRunId = randomUUID();
27
28
 
28
29
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
29
- const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } =
30
- process.env;
30
+ const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE,
31
+ TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV,
32
+ TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } = process.env;
31
33
 
32
34
  const options = {
33
35
  ignoreDeclaration: true,
@@ -37,6 +39,8 @@ const options = {
37
39
  parseTagValue: true,
38
40
  };
39
41
 
42
+ const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
43
+
40
44
  const reduceOptions = {};
41
45
 
42
46
  class XmlReader {
@@ -91,7 +95,7 @@ class XmlReader {
91
95
  ];
92
96
 
93
97
  for (const regex of cutRegexes) {
94
- xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, 5000)}${p3}`);
98
+ xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, MAX_OUTPUT_LENGTH)}${p3}`);
95
99
  }
96
100
 
97
101
  const jsonResult = this.parser.parse(xmlData);
@@ -341,6 +345,7 @@ class XmlReader {
341
345
  if (file.endsWith('.rb')) this.stats.language = 'ruby';
342
346
  if (file.endsWith('.js')) this.stats.language = 'js';
343
347
  if (file.endsWith('.ts')) this.stats.language = 'ts';
348
+ if (file.endsWith('.cs')) this.stats.language = 'csharp';
344
349
  }
345
350
 
346
351
  if (!fs.existsSync(file)) {
@@ -394,13 +399,14 @@ class XmlReader {
394
399
  async uploadArtifacts() {
395
400
  for (const test of this.tests.filter(t => !!t.stack)) {
396
401
  let files = [];
397
- if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f));
398
- files = [...files, ...fetchFilesFromStackTrace(test.stack)];
402
+ if (!test.files?.length) continue;
403
+
404
+ files = test.files.map(f => path.isAbsolute(f) ? f : path.join(process.cwd(), f));
399
405
 
400
406
  if (!files.length) continue;
401
407
 
402
408
  const runId = this.runId || this.store.runId || Date.now().toString();
403
- test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
409
+ test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path.basename(f)])));
404
410
  console.log(APP_PREFIX, `🗄️ Uploaded ${pc.bold(`${files.length} artifacts`)} for test ${test.title}`);
405
411
  }
406
412
  }
@@ -471,7 +477,7 @@ function reduceTestCases(prev, item) {
471
477
  testCases
472
478
  .filter(t => !!t)
473
479
  .forEach(testCaseItem => {
474
- const file = testCaseItem.file || item.filepath || '';
480
+ const file = testCaseItem.file || item.filepath || item.fullname || '';
475
481
 
476
482
  let stack = '';
477
483
  let message = '';
@@ -505,15 +511,38 @@ function reduceTestCases(prev, item) {
505
511
  stack = `${
506
512
  testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''
507
513
  }\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
508
- const testId = fetchIdFromOutput(stack);
514
+ let testId = fetchIdFromOutput(stack);
515
+
516
+ if (tags?.length && !testId) {
517
+ testId = tags.filter(t => t.startsWith('T')).map(t => `@${t}`).find(t => t.match(TEST_ID_REGEX))?.slice(2);
518
+ }
509
519
 
510
520
  let status = STATUS.PASSED.toString();
511
521
  if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
512
522
  if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
523
+ if (testCaseItem.result && Object.values(STATUS).includes(testCaseItem.result.toLowerCase())) {
524
+ status = testCaseItem.result.toLowerCase();
525
+ }
513
526
 
514
527
  let rid = null;
515
528
  if (testCaseItem.id) rid = `${ridRunId}-${testCaseItem.id}`;
516
529
 
530
+ // Extract attachments
531
+ let files = [];
532
+ if (testCaseItem.attachments) {
533
+ const attachments = Array.isArray(testCaseItem.attachments.attachment)
534
+ ? testCaseItem.attachments.attachment
535
+ : [testCaseItem.attachments.attachment];
536
+
537
+ files = attachments
538
+ .filter(a => a && a.filePath)
539
+ .map(a => a.filePath);
540
+ }
541
+
542
+ // Extract files from stack trace using existing utility
543
+ const stackFiles = fetchFilesFromStackTrace(stack);
544
+ files = [...new Set([...files, ...stackFiles])]; // Remove duplicates
545
+
517
546
  prev.push({
518
547
  rid,
519
548
  file,
@@ -528,7 +557,9 @@ function reduceTestCases(prev, item) {
528
557
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
529
558
  status,
530
559
  title,
560
+ root_suite_id: TESTOMATIO_SUITE,
531
561
  suite_title: suiteTitle,
562
+ files,
532
563
  });
533
564
  });
534
565
  return prev;
@@ -555,10 +586,15 @@ function fetchProperties(item) {
555
586
 
556
587
  if (!item.properties) return {};
557
588
 
558
- const prop = [item.properties?.property].flat().find(p => p.name === 'Description');
589
+ // Handle both single property and array of properties
590
+ const properties = Array.isArray(item.properties.property)
591
+ ? item.properties.property
592
+ : [item.properties.property].filter(Boolean);
593
+
594
+ const prop = properties.find(p => p.name === 'Description');
559
595
  if (prop) title = prop.value;
560
- [item.properties?.property]
561
- .flat()
596
+
597
+ properties
562
598
  .filter(p => p.name === 'Category')
563
599
  .forEach(p => tags.push(p.value));
564
600
  return { title, tags };