@testomatio/reporter 2.3.6 → 2.3.7-beta.2-xml-import

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.
@@ -0,0 +1,404 @@
1
+ import createDebugMessages from 'debug';
2
+ import { STATUS } from '../constants.js';
3
+
4
+ const debug = createDebugMessages('@testomatio/reporter:nunit-parser');
5
+
6
+ /**
7
+ * Enhanced NUnit XML Parser that properly handles test-suite hierarchy
8
+ * and parameterized tests
9
+ */
10
+ export class NUnitXmlParser {
11
+ constructor(options = {}) {
12
+ this.options = options;
13
+ this.tests = [];
14
+ this.stats = {
15
+ total: 0,
16
+ passed: 0,
17
+ failed: 0,
18
+ skipped: 0,
19
+ inconclusive: 0,
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Parse NUnit XML test-run structure
25
+ * @param {Object} testRun - Parsed XML test-run object
26
+ * @returns {Object} - Parsed test results
27
+ */
28
+ parseTestRun(testRun) {
29
+ debug('Parsing NUnit test-run');
30
+
31
+ // Extract run-level statistics
32
+ this.stats = {
33
+ total: parseInt(testRun.total || 0, 10),
34
+ passed: parseInt(testRun.passed || 0, 10),
35
+ failed: parseInt(testRun.failed || 0, 10),
36
+ skipped: parseInt(testRun.skipped || 0, 10),
37
+ inconclusive: parseInt(testRun.inconclusive || 0, 10),
38
+ };
39
+
40
+ // Process the root test-suite
41
+ if (testRun['test-suite']) {
42
+ this.parseTestSuite(testRun['test-suite'], []);
43
+ }
44
+
45
+ debug(`Parsed ${this.tests.length} tests from NUnit XML`);
46
+
47
+ return {
48
+ status: testRun.result?.toLowerCase() || 'unknown',
49
+ create_tests: true,
50
+ tests_count: this.tests.length,
51
+ passed_count: this.tests.filter(t => t.status === STATUS.PASSED).length,
52
+ failed_count: this.tests.filter(t => t.status === STATUS.FAILED).length,
53
+ skipped_count: this.tests.filter(t => t.status === STATUS.SKIPPED).length,
54
+ tests: this.tests,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Recursively parse test-suite elements based on their type
60
+ * @param {Object|Array} testSuite - Test suite object or array
61
+ * @param {Array} parentPath - Current path in the hierarchy
62
+ */
63
+ parseTestSuite(testSuite, parentPath = []) {
64
+ if (!testSuite) return;
65
+
66
+ // Handle arrays of test suites
67
+ if (Array.isArray(testSuite)) {
68
+ testSuite.forEach(suite => this.parseTestSuite(suite, parentPath));
69
+ return;
70
+ }
71
+
72
+ const suiteType = testSuite.type;
73
+ const suiteName = testSuite.name;
74
+ const fullName = testSuite.fullname;
75
+
76
+ debug(`Processing test-suite: type=${suiteType}, name=${suiteName}`);
77
+
78
+ switch (suiteType) {
79
+ case 'Assembly':
80
+ // Assembly level - ignore the name, just process children
81
+ debug('Processing Assembly level - ignoring name, processing children');
82
+ this.processChildren(testSuite, parentPath);
83
+ break;
84
+
85
+ case 'TestSuite':
86
+ // Namespace/grouping level - add to path but don't create test
87
+ debug(`Processing TestSuite level - adding '${suiteName}' to path`);
88
+ const newPath = [...parentPath, suiteName];
89
+ this.processChildren(testSuite, newPath);
90
+ break;
91
+
92
+ case 'TestFixture':
93
+ // Test class level - add to path and process test cases
94
+ debug(`Processing TestFixture level - test class '${suiteName}'`);
95
+ const testFixturePath = [...parentPath, suiteName];
96
+ this.processChildren(testSuite, testFixturePath);
97
+ break;
98
+
99
+ default:
100
+ debug(`Unknown test-suite type: ${suiteType}, treating as TestSuite`);
101
+ const unknownPath = [...parentPath, suiteName];
102
+ this.processChildren(testSuite, unknownPath);
103
+ break;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Process child elements of a test suite
109
+ * @param {Object} testSuite - Test suite object
110
+ * @param {Array} currentPath - Current path in hierarchy
111
+ */
112
+ processChildren(testSuite, currentPath) {
113
+ // Process nested test-suites
114
+ if (testSuite['test-suite']) {
115
+ this.parseTestSuite(testSuite['test-suite'], currentPath);
116
+ }
117
+
118
+ // Process test-cases
119
+ if (testSuite['test-case']) {
120
+ this.parseTestCases(testSuite['test-case'], currentPath, testSuite);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Parse test-case elements (actual tests)
126
+ * @param {Object|Array} testCases - Test case object or array
127
+ * @param {Array} suitePath - Path to the test suite
128
+ * @param {Object} parentSuite - Parent test suite for context
129
+ */
130
+ parseTestCases(testCases, suitePath, parentSuite) {
131
+ if (!testCases) return;
132
+
133
+ // Handle arrays of test cases
134
+ if (!Array.isArray(testCases)) {
135
+ testCases = [testCases];
136
+ }
137
+
138
+ testCases.forEach(testCase => {
139
+ const parsedTest = this.parseTestCase(testCase, suitePath, parentSuite);
140
+ if (parsedTest) {
141
+ this.tests.push(parsedTest);
142
+ }
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Parse individual test case
148
+ * @param {Object} testCase - Test case object
149
+ * @param {Array} suitePath - Path to the test suite
150
+ * @param {Object} parentSuite - Parent test suite for context
151
+ * @returns {Object|null} - Parsed test object
152
+ */
153
+ parseTestCase(testCase, suitePath, parentSuite) {
154
+ if (!testCase || !testCase.name) {
155
+ debug('Skipping test case without name');
156
+ return null;
157
+ }
158
+
159
+ const testName = testCase.name;
160
+ const fullName = testCase.fullname;
161
+ const methodName = testCase.methodname || this.extractMethodName(testName);
162
+ const className = testCase.classname || parentSuite?.name;
163
+
164
+ debug(`Parsing test case: ${testName}`);
165
+
166
+ // Extract parameters if this is a parameterized test
167
+ const { baseMethodName, parameters, isParameterized } = this.extractParameters(testName);
168
+
169
+ // Determine test status
170
+ let status = STATUS.PASSED;
171
+ if (testCase.result) {
172
+ switch (testCase.result.toLowerCase()) {
173
+ case 'passed':
174
+ status = STATUS.PASSED;
175
+ break;
176
+ case 'failed':
177
+ status = STATUS.FAILED;
178
+ break;
179
+ case 'skipped':
180
+ case 'ignored':
181
+ status = STATUS.SKIPPED;
182
+ break;
183
+ case 'inconclusive':
184
+ status = STATUS.SKIPPED; // Treat inconclusive as skipped
185
+ break;
186
+ default:
187
+ status = STATUS.PASSED;
188
+ }
189
+ }
190
+
191
+ // Extract error information
192
+ let message = '';
193
+ let stack = '';
194
+
195
+ if (testCase.failure) {
196
+ message = testCase.failure.message || '';
197
+ stack = testCase.failure['stack-trace'] || testCase.failure['#text'] || '';
198
+ }
199
+
200
+ if (testCase.output && testCase.output['#text']) {
201
+ stack = `${stack}\n\n${testCase.output['#text']}`.trim();
202
+ }
203
+
204
+ // Extract test ID from properties
205
+ let testId = null;
206
+ if (testCase.properties && testCase.properties.property) {
207
+ const properties = Array.isArray(testCase.properties.property)
208
+ ? testCase.properties.property
209
+ : [testCase.properties.property];
210
+
211
+ const idProperty = properties.find(p => p.name === 'ID');
212
+ if (idProperty) {
213
+ testId = idProperty.value;
214
+ // Remove @ and T prefixes if present
215
+ if (testId.startsWith('@')) testId = testId.slice(1);
216
+ if (testId.startsWith('T')) testId = testId.slice(1);
217
+ }
218
+ }
219
+
220
+ // Build file path from suite path and class name
221
+ const filePath = this.buildFilePath(suitePath, className, parentSuite);
222
+
223
+ // For parameterized tests, format example as expected by Testomatio API
224
+ // Convert array of parameters to object with numeric keys
225
+ let example = null;
226
+ if (isParameterized && parameters.length > 0) {
227
+ example = {};
228
+ parameters.forEach((param, index) => {
229
+ example[index] = param;
230
+ });
231
+ }
232
+
233
+ return {
234
+ // For runs: use full test name with parameters (TestBooleanValue(true))
235
+ // For import: API will group by base name using the example field
236
+ title: testName, // Full name with parameters for run display
237
+ methodName: baseMethodName || methodName || testName,
238
+ fullName: fullName,
239
+ suitePath: suitePath,
240
+ suite_title: className || suitePath[suitePath.length - 1] || 'Unknown',
241
+ file: filePath,
242
+ status: status,
243
+ message: message,
244
+ stack: stack,
245
+ run_time: parseFloat(testCase.duration || testCase.time || 0) * 1000,
246
+ test_id: testId,
247
+ create: true,
248
+ retry: false,
249
+ // Parameterized test metadata
250
+ example: example, // Parameters as object for API grouping
251
+ isParameterized: isParameterized,
252
+ parameters: parameters, // Keep original array for reference
253
+ baseMethodName: baseMethodName,
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Extract method name and parameters from test name
259
+ * @param {string} testName - Full test name
260
+ * @returns {Object} - Extracted information
261
+ */
262
+ extractParameters(testName) {
263
+ const paramMatch = testName.match(/^(.+?)\((.+)\)$/);
264
+
265
+ if (paramMatch) {
266
+ const baseMethodName = paramMatch[1].trim();
267
+ const paramString = paramMatch[2];
268
+
269
+ // Parse parameters - handle quoted strings and nested structures
270
+ const parameters = this.parseParameterString(paramString);
271
+
272
+ return {
273
+ baseMethodName: baseMethodName,
274
+ parameters: parameters,
275
+ isParameterized: true,
276
+ };
277
+ }
278
+
279
+ return {
280
+ baseMethodName: testName,
281
+ parameters: [],
282
+ isParameterized: false,
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Parse parameter string into array of parameters
288
+ * @param {string} paramString - Parameter string
289
+ * @returns {Array} - Array of parameters
290
+ */
291
+ parseParameterString(paramString) {
292
+ const parameters = [];
293
+ let current = '';
294
+ let inQuotes = false;
295
+ let quoteChar = null;
296
+ let depth = 0;
297
+
298
+ for (let i = 0; i < paramString.length; i++) {
299
+ const char = paramString[i];
300
+
301
+ if (!inQuotes && (char === '"' || char === "'")) {
302
+ inQuotes = true;
303
+ quoteChar = char;
304
+ current += char;
305
+ } else if (inQuotes && char === quoteChar) {
306
+ inQuotes = false;
307
+ quoteChar = null;
308
+ current += char;
309
+ } else if (!inQuotes && char === '(') {
310
+ depth++;
311
+ current += char;
312
+ } else if (!inQuotes && char === ')') {
313
+ depth--;
314
+ current += char;
315
+ } else if (!inQuotes && char === ',' && depth === 0) {
316
+ parameters.push(current.trim());
317
+ current = '';
318
+ } else {
319
+ current += char;
320
+ }
321
+ }
322
+
323
+ if (current.trim()) {
324
+ parameters.push(current.trim());
325
+ }
326
+
327
+ // Clean up parameters - remove quotes if they wrap the entire parameter
328
+ return parameters.map(param => {
329
+ param = param.trim();
330
+ if ((param.startsWith('"') && param.endsWith('"')) || (param.startsWith("'") && param.endsWith("'"))) {
331
+ return param.slice(1, -1);
332
+ }
333
+ return param;
334
+ });
335
+ }
336
+
337
+ /**
338
+ * Extract method name from test name (fallback)
339
+ * @param {string} testName - Test name
340
+ * @returns {string} - Method name
341
+ */
342
+ extractMethodName(testName) {
343
+ // Remove parameters if present
344
+ const paramMatch = testName.match(/^(.+?)\(/);
345
+ return paramMatch ? paramMatch[1].trim() : testName;
346
+ }
347
+
348
+ /**
349
+ * Build file path from suite path and class name
350
+ * @param {Array} suitePath - Suite path array
351
+ * @param {string} className - Class name
352
+ * @param {Object} parentSuite - Parent suite for context
353
+ * @returns {string} - File path
354
+ */
355
+ buildFilePath(suitePath, className, parentSuite) {
356
+ // Try to get file path from parent suite
357
+ if (parentSuite && parentSuite.filepath) {
358
+ return parentSuite.filepath;
359
+ }
360
+
361
+ // Build path from suite hierarchy
362
+ const pathParts = [...suitePath];
363
+ if (className && !pathParts.includes(className)) {
364
+ pathParts.push(className);
365
+ }
366
+
367
+ // Convert to file path format
368
+ return pathParts.join('/') + '.cs'; // Assume C# for NUnit
369
+ }
370
+
371
+ /**
372
+ * Group parameterized tests by base method name
373
+ * @param {Array} tests - Array of parsed tests
374
+ * @returns {Object} - Grouped tests
375
+ */
376
+ groupParameterizedTests(tests) {
377
+ const grouped = {};
378
+
379
+ tests.forEach(test => {
380
+ const key = test.isParameterized
381
+ ? `${test.suitePath.join('.')}.${test.baseMethodName}`
382
+ : `${test.suitePath.join('.')}.${test.title}`;
383
+
384
+ if (!grouped[key]) {
385
+ grouped[key] = {
386
+ baseTest: {
387
+ name: test.baseMethodName || test.title,
388
+ suitePath: test.suitePath,
389
+ suite_title: test.suite_title,
390
+ file: test.file,
391
+ isParameterized: test.isParameterized,
392
+ },
393
+ variations: [],
394
+ };
395
+ }
396
+
397
+ grouped[key].variations.push(test);
398
+ });
399
+
400
+ return grouped;
401
+ }
402
+ }
403
+
404
+ export default NUnitXmlParser;
package/src/pipe/debug.js CHANGED
@@ -15,7 +15,7 @@ export class DebugPipe {
15
15
  this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
16
16
  if (this.isEnabled) {
17
17
  this.batch = {
18
- isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
18
+ isEnabled: this.params.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : true),
19
19
  intervalFunction: null,
20
20
  intervalTime: 5000,
21
21
  tests: [],
@@ -93,8 +93,7 @@ export class DebugPipe {
93
93
  const logData = { action: 'addTest', testId: data };
94
94
  if (this.store.runId) logData.runId = this.store.runId;
95
95
  this.logToFile(logData);
96
- }
97
- else this.batch.tests.push(data);
96
+ } else this.batch.tests.push(data);
98
97
 
99
98
  if (!this.batch.intervalFunction) await this.batchUpload();
100
99
  }
@@ -20,7 +20,7 @@ if (process.env.TESTOMATIO_RUN) process.env.runId = process.env.TESTOMATIO_RUN;
20
20
  class TestomatioPipe {
21
21
  constructor(params, store) {
22
22
  this.batch = {
23
- isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
23
+ isEnabled: params?.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : true),
24
24
  intervalFunction: null, // will be created in createRun by setInterval function
25
25
  intervalTime: 5000, // how often tests are sent
26
26
  tests: [], // array of tests in batch
@@ -60,8 +60,8 @@ class TestomatioPipe {
60
60
  retryConfig: {
61
61
  retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
62
62
  retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
63
- httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
64
- shouldRetry: (error) => {
63
+ httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
64
+ shouldRetry: error => {
65
65
  if (!error.response) return false;
66
66
  switch (error.response?.status) {
67
67
  case 400: // Bad request (probably wrong API key)
@@ -73,8 +73,8 @@ class TestomatioPipe {
73
73
  break;
74
74
  }
75
75
  return error.response?.status >= 401; // Retry on 401+ and 5xx
76
- }
77
- }
76
+ },
77
+ },
78
78
  });
79
79
 
80
80
  this.isEnabled = true;
@@ -104,7 +104,6 @@ class TestomatioPipe {
104
104
  // add test ID + run ID
105
105
  if (data.rid) data.rid = `${this.runId}-${data.rid}`;
106
106
 
107
-
108
107
  if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
109
108
  data.stack = null;
110
109
  }
@@ -120,7 +119,6 @@ class TestomatioPipe {
120
119
  return data;
121
120
  }
122
121
 
123
-
124
122
  /**
125
123
  * Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
126
124
  * @param {Object} opts - The options for preparing the test grepList.
@@ -215,7 +213,7 @@ class TestomatioPipe {
215
213
  method: 'PUT',
216
214
  url: `/api/reporter/${this.runId}`,
217
215
  data: runParams,
218
- responseType: 'json'
216
+ responseType: 'json',
219
217
  });
220
218
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
221
219
  return;
@@ -228,7 +226,7 @@ class TestomatioPipe {
228
226
  url: '/api/reporter',
229
227
  data: runParams,
230
228
  maxContentLength: Infinity,
231
- responseType: 'json'
229
+ responseType: 'json',
232
230
  });
233
231
 
234
232
  this.runId = resp.data.uid;
@@ -287,44 +285,44 @@ class TestomatioPipe {
287
285
 
288
286
  debug('Adding test', json);
289
287
 
290
- return this.client.request({
291
- method: 'POST',
292
- url: `/api/reporter/${this.runId}/testrun`,
293
- data: json,
294
- headers: {
295
- 'Content-Type': 'application/json',
296
- },
297
- maxContentLength: Infinity
298
- }).catch(err => {
299
- this.requestFailures++;
300
- this.notReportedTestsCount++;
301
- if (err.response) {
302
- if (err.response.status >= 400) {
303
- const responseData = err.response.data || { message: '' };
288
+ return this.client
289
+ .request({
290
+ method: 'POST',
291
+ url: `/api/reporter/${this.runId}/testrun`,
292
+ data: json,
293
+ headers: {
294
+ 'Content-Type': 'application/json',
295
+ },
296
+ maxContentLength: Infinity,
297
+ })
298
+ .catch(err => {
299
+ this.requestFailures++;
300
+ this.notReportedTestsCount++;
301
+ if (err.response) {
302
+ if (err.response.status >= 400) {
303
+ const responseData = err.response.data || { message: '' };
304
+ console.log(
305
+ APP_PREFIX,
306
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
307
+ pc.gray(data?.title || ''),
308
+ );
309
+ if (err.response?.data?.message?.includes('could not be matched')) {
310
+ this.hasUnmatchedTests = true;
311
+ }
312
+ return;
313
+ }
304
314
  console.log(
305
315
  APP_PREFIX,
306
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
307
- pc.gray(data?.title || ''),
316
+ pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
317
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
308
318
  );
309
- if (err.response?.data?.message?.includes('could not be matched')) {
310
- this.hasUnmatchedTests = true;
311
- }
312
- return;
319
+ printCreateIssue(err);
320
+ } else {
321
+ console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
313
322
  }
314
- console.log(
315
- APP_PREFIX,
316
- pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
317
- `Report couldn't be processed: ${err?.response?.data?.message}`,
318
- );
319
- printCreateIssue(err);
320
- } else {
321
- console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
322
- }
323
- });
323
+ });
324
324
  };
325
325
 
326
-
327
-
328
326
  /**
329
327
  * Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
330
328
  */
@@ -349,43 +347,42 @@ class TestomatioPipe {
349
347
  const testsToSend = this.batch.tests.splice(0);
350
348
  debug('📨 Batch upload', testsToSend.length, 'tests');
351
349
 
352
- return this.client.request({
353
- method: 'POST',
354
- url: `/api/reporter/${this.runId}/testrun`,
355
- data: {
356
- api_key: this.apiKey,
357
- tests: testsToSend,
358
- batch_index: this.batch.batchIndex
359
- },
360
- headers: {
361
- 'Content-Type': 'application/json',
362
- },
363
- maxContentLength: Infinity
364
- }).catch(err => {
365
- this.requestFailures++;
366
- this.notReportedTestsCount += testsToSend.length;
367
- if (err.response) {
368
- if (err.response.status >= 400) {
369
- const responseData = err.response.data || { message: '' };
350
+ return this.client
351
+ .request({
352
+ method: 'POST',
353
+ url: `/api/reporter/${this.runId}/testrun`,
354
+ data: {
355
+ api_key: this.apiKey,
356
+ tests: testsToSend,
357
+ batch_index: this.batch.batchIndex,
358
+ },
359
+ headers: {
360
+ 'Content-Type': 'application/json',
361
+ },
362
+ maxContentLength: Infinity,
363
+ })
364
+ .catch(err => {
365
+ this.requestFailures++;
366
+ this.notReportedTestsCount += testsToSend.length;
367
+ if (err.response) {
368
+ if (err.response.status >= 400) {
369
+ const responseData = err.response.data || { message: '' };
370
+ console.log(APP_PREFIX, pc.yellow(`Warning: ${responseData.message} (${err.response.status})`));
371
+ if (err.response?.data?.message?.includes('could not be matched')) {
372
+ this.hasUnmatchedTests = true;
373
+ }
374
+ return;
375
+ }
370
376
  console.log(
371
377
  APP_PREFIX,
372
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
378
+ pc.yellow(`Warning: (${err.response?.status})`),
379
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
373
380
  );
374
- if (err.response?.data?.message?.includes('could not be matched')) {
375
- this.hasUnmatchedTests = true;
376
- }
377
- return;
381
+ printCreateIssue(err);
382
+ } else {
383
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
378
384
  }
379
- console.log(
380
- APP_PREFIX,
381
- pc.yellow(`Warning: (${err.response?.status})`),
382
- `Report couldn't be processed: ${err?.response?.data?.message}`,
383
- );
384
- printCreateIssue(err);
385
- } else {
386
- console.log(APP_PREFIX, "Report couldn't be processed", err);
387
- }
388
- });
385
+ });
389
386
  };
390
387
 
391
388
  /**
@@ -408,9 +405,9 @@ class TestomatioPipe {
408
405
  else this.batch.tests.push(data);
409
406
 
410
407
  // if test is added after run which is already finished
411
- if (!this.batch.intervalFunction) uploading = this.#batchUpload();
408
+ if (!this.batch.intervalFunction) uploading = this.#batchUpload();
412
409
 
413
- // return promise to be able to wait for it
410
+ // return promise to be able to wait for it
414
411
  return uploading;
415
412
  }
416
413
 
@@ -459,7 +456,7 @@ class TestomatioPipe {
459
456
  status_event,
460
457
  detach: params.detach,
461
458
  tests: params.tests,
462
- }
459
+ },
463
460
  });
464
461
 
465
462
  if (this.runUrl) {
@@ -472,7 +469,7 @@ class TestomatioPipe {
472
469
  if (this.runUrl && this.proceed) {
473
470
  const notFinishedMessage = pc.yellow(pc.bold('Run was not finished because of $TESTOMATIO_PROCEED'));
474
471
  console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${pc.magenta(this.runUrl)}`);
475
- console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
472
+ console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx @testomatio/reporter finish`);
476
473
  }
477
474
 
478
475
  if (this.hasUnmatchedTests) {
@@ -525,9 +522,6 @@ function printCreateIssue(err) {
525
522
  console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
526
523
  console.log('```');
527
524
  });
528
-
529
525
  }
530
526
 
531
-
532
-
533
527
  export default TestomatioPipe;