@testomatio/reporter 2.3.7-beta.1-xml-import → 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.
@@ -2,6 +2,7 @@ export function getPackageVersion(): any;
2
2
  export const TEST_ID_REGEX: RegExp;
3
3
  export const SUITE_ID_REGEX: RegExp;
4
4
  export function ansiRegExp(): RegExp;
5
+ export function truncate(s: any, size?: number): any;
5
6
  export function cleanLatestRunId(): any;
6
7
  export function isSameTest(test: any, t: any): boolean;
7
8
  export function fetchSourceCode(contents: any, opts?: {}): string;
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.validateSuiteId = 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.SUITE_ID_REGEX = exports.TEST_ID_REGEX = void 0;
40
40
  exports.getPackageVersion = getPackageVersion;
41
+ exports.truncate = truncate;
41
42
  exports.cleanLatestRunId = cleanLatestRunId;
42
43
  exports.formatStep = formatStep;
43
44
  exports.readLatestRunId = readLatestRunId;
@@ -213,27 +214,56 @@ const fetchSourceCode = (contents, opts = {}) => {
213
214
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
214
215
  }
215
216
  else if (opts.lang === 'csharp') {
216
- // Enhanced C# method detection for NUnit tests
217
- lineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
218
- if (lineIndex === -1) {
219
- lineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
217
+ // Find the method declaration line
218
+ let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
219
+ if (methodLineIndex === -1) {
220
+ methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
220
221
  }
221
- if (lineIndex === -1) {
222
- lineIndex = lines.findIndex(l => l.includes(`${title}(`));
222
+ if (methodLineIndex === -1) {
223
+ methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
223
224
  }
224
- // Look for TestCase or Test attributes above the method
225
- if (lineIndex === -1) {
226
- const testAttributeIndex = lines.findIndex((l, index) => {
227
- if (l.includes('[TestCase') || l.includes('[Test')) {
228
- // Check next few lines for the method
229
- const nextLines = lines.slice(index, Math.min(lines.length, index + 5));
230
- const hasMethod = nextLines.some(nextLine => nextLine.includes(`${title}(`));
231
- return hasMethod;
225
+ // If found, scan upwards to find [TestCase], [Test] attributes and XML comments
226
+ if (methodLineIndex !== -1) {
227
+ lineIndex = methodLineIndex;
228
+ // Scan upwards to find the start of attributes and comments
229
+ for (let i = methodLineIndex - 1; i >= 0; i--) {
230
+ const trimmedLine = lines[i].trim();
231
+ // Include [TestCase], [Test], and other attributes
232
+ if (trimmedLine.startsWith('[')) {
233
+ lineIndex = i;
234
+ continue;
235
+ }
236
+ // Include XML documentation comments
237
+ if (trimmedLine.startsWith('///')) {
238
+ lineIndex = i;
239
+ continue;
240
+ }
241
+ // Stop at empty lines (with some tolerance)
242
+ if (trimmedLine === '') {
243
+ // Check if next non-empty line is an attribute or comment
244
+ let hasMoreAttributes = false;
245
+ for (let j = i - 1; j >= 0; j--) {
246
+ const nextTrimmed = lines[j].trim();
247
+ if (nextTrimmed === '')
248
+ continue;
249
+ if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
250
+ hasMoreAttributes = true;
251
+ lineIndex = j;
252
+ }
253
+ break;
254
+ }
255
+ if (!hasMoreAttributes)
256
+ break;
257
+ continue;
258
+ }
259
+ // Stop at other method declarations or class-level elements
260
+ if (trimmedLine.includes('public ') ||
261
+ trimmedLine.includes('private ') ||
262
+ trimmedLine.includes('protected ') ||
263
+ trimmedLine.includes('internal ')) {
264
+ if (!trimmedLine.startsWith('['))
265
+ break;
232
266
  }
233
- return false;
234
- });
235
- if (testAttributeIndex !== -1) {
236
- lineIndex = testAttributeIndex;
237
267
  }
238
268
  }
239
269
  }
@@ -246,9 +276,26 @@ const fetchSourceCode = (contents, opts = {}) => {
246
276
  }
247
277
  if (lineIndex !== -1 && lineIndex !== undefined) {
248
278
  const result = [];
279
+ let braceDepth = 0; // Track brace depth for C# methods
280
+ let methodStartFound = false; // Flag to indicate we've found the method opening brace
249
281
  for (let i = lineIndex; i < lineIndex + limit; i++) {
250
282
  if (lines[i] === undefined)
251
283
  continue;
284
+ // Track brace depth for C# to stop after method closes
285
+ if (opts.lang === 'csharp') {
286
+ const line = lines[i];
287
+ // Count opening and closing braces
288
+ const openBraces = (line.match(/\{/g) || []).length;
289
+ const closeBraces = (line.match(/\}/g) || []).length;
290
+ if (openBraces > 0)
291
+ methodStartFound = true;
292
+ braceDepth += openBraces - closeBraces;
293
+ // If we've started the method and depth returns to 0, method is complete
294
+ if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
295
+ result.push(lines[i]);
296
+ break;
297
+ }
298
+ }
252
299
  if (i > lineIndex + 2 && !opts.prepend) {
253
300
  // annotation
254
301
  if (opts.lang === 'php' && lines[i].trim().startsWith('#['))
@@ -287,14 +334,22 @@ const fetchSourceCode = (contents, opts = {}) => {
287
334
  break;
288
335
  if (opts.lang === 'java' && lines[i].includes(' class '))
289
336
  break;
290
- if (opts.lang === 'csharp' && lines[i].trim().match(/^\[Test/))
291
- break;
292
- if (opts.lang === 'csharp' && lines[i].includes(' public void '))
293
- break;
294
- if (opts.lang === 'csharp' && lines[i].includes(' public async Task '))
295
- break;
296
- if (opts.lang === 'csharp' && lines[i].includes(' class ') && lines[i].includes('public'))
297
- break;
337
+ // For C#, additional checks if brace tracking didn't stop us
338
+ if (opts.lang === 'csharp') {
339
+ const trimmed = lines[i].trim();
340
+ // Stop at attribute that marks beginning of next test
341
+ if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/))
342
+ break;
343
+ // Stop at XML documentation comments that belong to next method
344
+ if (trimmed.startsWith('///'))
345
+ break;
346
+ // Stop at another method declaration
347
+ if (trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/))
348
+ break;
349
+ // Stop at class declaration
350
+ if (trimmed.includes(' class ') && trimmed.includes('public'))
351
+ break;
352
+ }
298
353
  }
299
354
  result.push(lines[i]);
300
355
  }
@@ -494,9 +549,17 @@ function transformEnvVarToBoolean(value) {
494
549
  // if not recognized, return truthy if any value is set
495
550
  return Boolean(value);
496
551
  }
552
+ function truncate(s, size = 255) {
553
+ if (s.toString().trim().length < size) {
554
+ return s.toString();
555
+ }
556
+ return `${s.toString().substring(0, size)}...`;
557
+ }
497
558
 
498
559
  module.exports.getPackageVersion = getPackageVersion;
499
560
 
561
+ module.exports.truncate = truncate;
562
+
500
563
  module.exports.cleanLatestRunId = cleanLatestRunId;
501
564
 
502
565
  module.exports.formatStep = formatStep;
@@ -52,6 +52,27 @@ declare class XmlReader {
52
52
  failed_count: number;
53
53
  tests: any;
54
54
  };
55
+ _parseTRXTestDefinition(td: any): {
56
+ title: any;
57
+ example: any;
58
+ file: string;
59
+ description: any;
60
+ suite_title: any;
61
+ id: any;
62
+ };
63
+ _parseTRXTestResult(td: any, tests: any): {
64
+ suite_title: any;
65
+ title: any;
66
+ file: any;
67
+ description: any;
68
+ code: any;
69
+ run_time: number;
70
+ stack: any;
71
+ files: any;
72
+ create: boolean;
73
+ overwrite: boolean;
74
+ };
75
+ _mapTRXStatus(outcome: any): string;
55
76
  processXUnit(assemblies: any): {
56
77
  status: string;
57
78
  create_tests: boolean;
package/lib/xmlReader.js CHANGED
@@ -195,61 +195,17 @@ class XmlReader {
195
195
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
196
196
  if (!Array.isArray(defs))
197
197
  defs = [defs].filter(d => !!d);
198
- const tests = defs.map(td => {
199
- const title = td.name.replace(/\(.*?\)/, '').trim();
200
- let example = td.name.match(/\((.*?)\)/);
201
- if (example)
202
- example = { ...example[1].split(',') };
203
- const suite = td.TestMethod.className.split(', ')[0].split('.');
204
- const suite_title = suite.pop();
205
- return {
206
- title,
207
- example,
208
- file: suite.join('/'),
209
- description: td.Description,
210
- suite_title,
211
- id: td.Execution.id,
212
- };
213
- }) || [];
198
+ // Parse test definitions
199
+ const tests = defs.map(td => this._parseTRXTestDefinition(td));
200
+ // Parse test results
214
201
  let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
215
202
  if (!Array.isArray(result))
216
203
  result = [result].filter(d => !!d);
217
- const results = result.map(td => ({
218
- id: td.executionId,
219
- // seconds are used in junit reports, but ms are used by testomatio
220
- run_time: parseFloat(td.duration) * 1000,
221
- status: td.outcome,
222
- stack: td.Output.StdOut,
223
- files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
224
- }));
225
- results.forEach(r => {
226
- const test = tests.find(t => t.id === r.id) || {};
227
- r.suite_title = test.suite_title;
228
- r.title = test.title?.trim();
229
- if (test.code)
230
- r.code = test.code;
231
- if (test.description)
232
- r.description = test.description;
233
- if (test.example)
234
- r.example = test.example;
235
- if (test.file)
236
- r.file = test.file;
237
- r.create = true;
238
- r.overwrite = true;
239
- if (r.status === 'Passed')
240
- r.status = constants_js_1.STATUS.PASSED;
241
- if (r.status === 'Failed')
242
- r.status = constants_js_1.STATUS.FAILED;
243
- if (r.status === 'Skipped')
244
- r.status = constants_js_1.STATUS.SKIPPED;
245
- delete r.id;
246
- });
204
+ const results = result.map(td => this._parseTRXTestResult(td, tests));
247
205
  debug(results);
248
206
  const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
249
207
  const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
250
- let status = constants_js_1.STATUS.PASSED.toString();
251
- if (failed_count > 0)
252
- status = constants_js_1.STATUS.FAILED;
208
+ const status = failed_count > 0 ? constants_js_1.STATUS.FAILED : constants_js_1.STATUS.PASSED.toString();
253
209
  this.tests = results.filter(t => !!t.title);
254
210
  return {
255
211
  status,
@@ -261,6 +217,57 @@ class XmlReader {
261
217
  tests: results,
262
218
  };
263
219
  }
220
+ _parseTRXTestDefinition(td) {
221
+ const title = td.name.replace(/\(.*?\)/, '').trim();
222
+ const exampleMatch = td.name.match(/\((.*?)\)/);
223
+ const example = exampleMatch ? { ...exampleMatch[1].split(',') } : null;
224
+ const suite = td.TestMethod.className.split(', ')[0].split('.');
225
+ const suite_title = suite.pop();
226
+ // Convert namespace to file path for C#
227
+ const file = `${suite.join('/')}.cs`;
228
+ return {
229
+ title, // Base name without parameters for test import
230
+ example, // Parameters object for parameterized tests
231
+ file, // File path with .cs extension
232
+ description: td.Description,
233
+ suite_title,
234
+ id: td.Execution.id,
235
+ };
236
+ }
237
+ _parseTRXTestResult(td, tests) {
238
+ const test = tests.find(t => t.id === td.executionId) || {};
239
+ const result = {
240
+ suite_title: test.suite_title,
241
+ title: test.title?.trim(),
242
+ file: test.file,
243
+ description: test.description,
244
+ code: test.code,
245
+ run_time: parseFloat(td.duration) * 1000,
246
+ stack: td.Output?.StdOut || '',
247
+ files: td?.ResultFiles?.ResultFile?.map(rf => rf.path),
248
+ create: true,
249
+ overwrite: true,
250
+ };
251
+ // Add example for parameterized tests
252
+ if (test.example) {
253
+ result.example = test.example;
254
+ }
255
+ // Map TRX status to Testomat.io status
256
+ result.status = this._mapTRXStatus(td.outcome);
257
+ return result;
258
+ }
259
+ _mapTRXStatus(outcome) {
260
+ switch (outcome) {
261
+ case 'Passed':
262
+ return constants_js_1.STATUS.PASSED;
263
+ case 'Failed':
264
+ return constants_js_1.STATUS.FAILED;
265
+ case 'Skipped':
266
+ return constants_js_1.STATUS.SKIPPED;
267
+ default:
268
+ return constants_js_1.STATUS.PASSED;
269
+ }
270
+ }
264
271
  processXUnit(assemblies) {
265
272
  const tests = [];
266
273
  assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.7-beta.1-xml-import",
3
+ "version": "2.3.7-beta.2-xml-import",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -2,7 +2,7 @@ import createDebugMessages from 'debug';
2
2
  import pc from 'picocolors';
3
3
  import TestomatClient from '../client.js';
4
4
  import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
5
- import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
5
+ import { getTestomatIdFromTestTitle, truncate, fileSystem } from '../utils/utils.js';
6
6
  import { services } from '../services/index.js';
7
7
  import { dataStorage } from '../data-storage.js';
8
8
  import codeceptjs from 'codeceptjs';
@@ -127,6 +127,28 @@ function CodeceptReporter(config) {
127
127
  });
128
128
 
129
129
 
130
+ // mark as failed all tests inside the failed hook
131
+ event.dispatcher.on(event.hook.failed, hook => {
132
+ if (hook.name !== 'BeforeSuiteHook') return;
133
+ const suite = hook.runnable.parent;
134
+
135
+ if (!suite) return;
136
+
137
+ const error = hook?.ctx?.currentTest?.err;
138
+
139
+ for (const test of suite.tests) {
140
+ client.addTestRun('failed', {
141
+ ...stripExampleFromTitle(test.title),
142
+ rid: test.uid,
143
+ test_id: getTestomatIdFromTestTitle(test.title),
144
+ suite_title: stripTagsFromTitle(suite.title),
145
+ error,
146
+ time: hook?.runnable?.duration,
147
+ });
148
+ }
149
+ });
150
+
151
+
130
152
  event.dispatcher.on(event.suite.before, suite => {
131
153
  dataStorage.setContext(suite.fullTitle());
132
154
  });
@@ -441,7 +463,7 @@ function formatCodeceptStep(step) {
441
463
  if (!step) return null;
442
464
 
443
465
  const category = step.constructor.name === 'HelperStep' ? 'framework' : 'user';
444
- const title = step.toString(); // Use built-in toString
466
+ const title = truncate(step); // Use built-in toString
445
467
  const duration = step.duration || 0; // Use built-in duration
446
468
 
447
469
  const formattedStep = {
@@ -469,10 +491,11 @@ function formatHookStep(step) {
469
491
  if (step.actor && step.name) {
470
492
  title = `${step.actor} ${step.name}`;
471
493
  if (step.args && step.args.length > 0) {
472
- const argsStr = step.args.map(arg => JSON.stringify(arg)).join(', ');
494
+ const argsStr = step.args.map(arg => truncate(JSON.stringify(arg))).join(', ');
473
495
  title += ` ${argsStr}`;
474
496
  }
475
497
  }
498
+ title = truncate(title);
476
499
 
477
500
  return {
478
501
  category: 'hook',
@@ -481,5 +504,6 @@ function formatHookStep(step) {
481
504
  };
482
505
  }
483
506
 
507
+
484
508
  export { CodeceptReporter };
485
509
  export default CodeceptReporter;
package/src/bin/cli.js CHANGED
@@ -158,7 +158,7 @@ program
158
158
  .option('--lang <lang>', 'Language used (python, ruby, java)')
159
159
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
160
160
  .action(async (pattern, opts) => {
161
- if (!pattern.endsWith('.xml')) {
161
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
162
162
  pattern += '.xml';
163
163
  }
164
164
  let { javaTests, lang } = opts;
@@ -23,7 +23,7 @@ program
23
23
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
24
24
  .option('--env-file <envfile>', 'Load environment variables from env file')
25
25
  .action(async (pattern, opts) => {
26
- if (!pattern.endsWith('.xml')) {
26
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
27
27
  pattern += '.xml';
28
28
  }
29
29
  let { javaTests, lang } = opts;
@@ -18,7 +18,7 @@ const newArgs = ['run'];
18
18
  let i = 0;
19
19
  while (i < args.length) {
20
20
  const arg = args[i];
21
-
21
+
22
22
  if (arg === '-c' || arg === '--command') {
23
23
  // Map -c/--command to positional argument for run command
24
24
  i++;
@@ -33,7 +33,7 @@ while (i < args.length) {
33
33
  // Map --launch to start command
34
34
  newArgs[0] = 'start';
35
35
  } else if (arg === '--finish') {
36
- // Map --finish to finish command
36
+ // Map --finish to finish command
37
37
  newArgs[0] = 'finish';
38
38
  } else {
39
39
  // Pass through other arguments
@@ -45,9 +45,9 @@ while (i < args.length) {
45
45
  // Execute the main CLI with mapped arguments
46
46
 
47
47
  const child = spawn(process.execPath, [cliPath, ...newArgs], {
48
- stdio: 'inherit'
48
+ stdio: 'inherit',
49
49
  });
50
50
 
51
- child.on('exit', (code) => {
51
+ child.on('exit', code => {
52
52
  process.exit(code);
53
- });
53
+ });
package/src/client.js CHANGED
@@ -10,7 +10,7 @@ import { glob } from 'glob';
10
10
  import path, { sep } from 'path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { S3Uploader } from './uploader.js';
13
- import { formatStep, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
13
+ import { formatStep, truncate, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
14
14
  import { filesize as prettyBytes } from 'filesize';
15
15
 
16
16
  const debug = createDebugMessages('@testomatio/reporter:client');
@@ -37,9 +37,8 @@ class Client {
37
37
  this.runId = '';
38
38
  this.queue = Promise.resolve();
39
39
 
40
- // @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
41
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
42
- const pathToPackageJSON = path.join(__dirname, '../package.json');
40
+ // Get package.json path - use a simple approach that works in both environments
41
+ const pathToPackageJSON = path.join(process.cwd(), 'package.json');
43
42
  try {
44
43
  this.version = JSON.parse(fs.readFileSync(pathToPackageJSON).toString()).version;
45
44
  console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
@@ -387,7 +386,11 @@ class Client {
387
386
  */
388
387
  formatLogs({ error, steps, logs }) {
389
388
  error = error?.trim();
390
- logs = logs?.trim();
389
+ logs = logs
390
+ ?.trim()
391
+ .split('\n')
392
+ .map(l => truncate(l))
393
+ .join('\n');
391
394
 
392
395
  if (Array.isArray(steps)) {
393
396
  steps = steps
@@ -3,18 +3,23 @@ import Adapter from './adapter.js';
3
3
 
4
4
  class CSharpAdapter extends Adapter {
5
5
  formatTest(t) {
6
- // Don't override example if it already exists from NUnit XML processing
7
- // The xmlReader.js already extracts parameters correctly from <arguments>
6
+ // Extract example from title if not already present
8
7
  if (!t.example) {
9
- const title = t.title.replace(/\(.*?\)/, '').trim();
10
8
  const exampleMatch = t.title.match(/\((.*?)\)/);
11
9
  if (exampleMatch) {
12
- // Keep as array for consistency with NUnit XML processing
13
- t.example = exampleMatch[1].split(',').map(param => param.trim());
10
+ // Extract parameters as object with numeric keys for API
11
+ const params = exampleMatch[1].split(',').map(param => param.trim());
12
+ t.example = {};
13
+ params.forEach((param, index) => {
14
+ t.example[index] = param;
15
+ });
14
16
  }
15
- t.title = title.trim();
16
17
  }
17
18
 
19
+ // For runs: keep full title with parameters for display
20
+ // The example field will be used for grouping on import
21
+ // Do NOT remove parameters from title
22
+
18
23
  const suite = t.suite_title.split('.');
19
24
  t.suite_title = suite.pop();
20
25
  t.file = namespaceToFileName(t.file);
@@ -220,8 +220,20 @@ export class NUnitXmlParser {
220
220
  // Build file path from suite path and class name
221
221
  const filePath = this.buildFilePath(suitePath, className, parentSuite);
222
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
+
223
233
  return {
224
- title: isParameterized ? testName : methodName || testName,
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
225
237
  methodName: baseMethodName || methodName || testName,
226
238
  fullName: fullName,
227
239
  suitePath: suitePath,
@@ -235,8 +247,9 @@ export class NUnitXmlParser {
235
247
  create: true,
236
248
  retry: false,
237
249
  // Parameterized test metadata
250
+ example: example, // Parameters as object for API grouping
238
251
  isParameterized: isParameterized,
239
- parameters: parameters,
252
+ parameters: parameters, // Keep original array for reference
240
253
  baseMethodName: baseMethodName,
241
254
  };
242
255
  }
@@ -459,8 +459,6 @@ class TestomatioPipe {
459
459
  },
460
460
  });
461
461
 
462
- debug(APP_PREFIX, '✅ Testrun finished');
463
-
464
462
  if (this.runUrl) {
465
463
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
466
464
  }
@@ -471,7 +469,7 @@ class TestomatioPipe {
471
469
  if (this.runUrl && this.proceed) {
472
470
  const notFinishedMessage = pc.yellow(pc.bold('Run was not finished because of $TESTOMATIO_PROCEED'));
473
471
  console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${pc.magenta(this.runUrl)}`);
474
- 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`);
475
473
  }
476
474
 
477
475
  if (this.hasUnmatchedTests) {