@testomatio/reporter 2.3.9-beta-bin-fix β†’ 2.4.0

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.
Files changed (62) hide show
  1. package/README.md +3 -2
  2. package/lib/adapter/codecept.js +12 -9
  3. package/lib/bin/cli.js +40 -11
  4. package/lib/bin/reportXml.js +5 -2
  5. package/lib/client.d.ts +1 -11
  6. package/lib/client.js +57 -152
  7. package/lib/data-storage.d.ts +1 -1
  8. package/lib/helpers.d.ts +1 -0
  9. package/lib/helpers.js +4 -0
  10. package/lib/junit-adapter/csharp.d.ts +0 -1
  11. package/lib/junit-adapter/csharp.js +43 -7
  12. package/lib/junit-adapter/nunit-parser.d.ts +82 -0
  13. package/lib/junit-adapter/nunit-parser.js +433 -0
  14. package/lib/pipe/bitbucket.js +5 -5
  15. package/lib/pipe/coverage.d.ts +82 -0
  16. package/lib/pipe/coverage.js +373 -0
  17. package/lib/pipe/gitlab.js +4 -4
  18. package/lib/pipe/index.js +2 -0
  19. package/lib/pipe/testomatio.d.ts +3 -2
  20. package/lib/pipe/testomatio.js +44 -18
  21. package/lib/reporter-functions.js +14 -12
  22. package/lib/reporter.d.ts +31 -21
  23. package/lib/reporter.js +40 -5
  24. package/lib/services/artifacts.d.ts +1 -1
  25. package/lib/services/key-values.d.ts +1 -1
  26. package/lib/services/links.d.ts +1 -1
  27. package/lib/services/logger.d.ts +1 -1
  28. package/lib/uploader.js +4 -0
  29. package/lib/utils/log-formatter.d.ts +28 -0
  30. package/lib/utils/log-formatter.js +127 -0
  31. package/lib/utils/pipe_utils.d.ts +15 -0
  32. package/lib/utils/pipe_utils.js +44 -2
  33. package/lib/utils/utils.d.ts +6 -0
  34. package/lib/utils/utils.js +260 -25
  35. package/lib/xmlReader.d.ts +32 -26
  36. package/lib/xmlReader.js +121 -52
  37. package/package.json +12 -7
  38. package/src/adapter/codecept.js +19 -19
  39. package/src/adapter/mocha.js +1 -1
  40. package/src/adapter/playwright.js +2 -2
  41. package/src/bin/cli.js +51 -13
  42. package/src/bin/reportXml.js +5 -2
  43. package/src/client.js +69 -130
  44. package/src/helpers.js +1 -0
  45. package/src/junit-adapter/csharp.js +48 -6
  46. package/src/junit-adapter/nunit-parser.js +474 -0
  47. package/src/pipe/bitbucket.js +5 -5
  48. package/src/pipe/coverage.js +440 -0
  49. package/src/pipe/debug.js +1 -2
  50. package/src/pipe/gitlab.js +4 -4
  51. package/src/pipe/index.js +2 -0
  52. package/src/pipe/testomatio.js +109 -85
  53. package/src/reporter-functions.js +15 -12
  54. package/src/reporter.js +6 -4
  55. package/src/services/links.js +1 -1
  56. package/src/uploader.js +5 -0
  57. package/src/utils/log-formatter.js +113 -0
  58. package/src/utils/pipe_utils.js +52 -3
  59. package/src/utils/utils.js +277 -22
  60. package/src/xmlReader.js +144 -46
  61. package/types/types.d.ts +364 -0
  62. package/types/vitest.types.d.ts +93 -0
package/README.md CHANGED
@@ -13,14 +13,14 @@ Testomat.io Reporter (this npm package) supports:
13
13
  - πŸ”Ž [Stack traces](./docs/stacktrace.md) and error messages
14
14
  - πŸ™ [GitHub](./docs/pipes/github.md), [GitLab](./docs/pipes/gitlab.md) & [Bitbucket](./docs/pipes/bitbucket.md) integration
15
15
  - πŸš… Realtime reports
16
- - πŸ—ƒοΈ Other test frameworks supported via [JUNit XML](./docs/junit.md)
16
+ - πŸ—ƒοΈ Other test frameworks supported via [JUnit XML](./docs/junit.md) with [XML import configuration](./docs/xml-imports.md)
17
17
  - πŸšΆβ€β™€οΈ Steps _(work in progress)_
18
18
  - πŸ“„ [Logger](./docs/logger.md) _(work in progress, supports Jest for now)_
19
19
  - ☁️ Custom properties and metadata _(work in progress)_
20
20
  - πŸ’― Free & open-source.
21
21
  - πŸ“Š Public and private Run reports on cloud via [Testomat.io App](https://testomat.io) πŸ‘‡
22
22
 
23
- ![](./docs/images/app.png)
23
+ <img width="1920" height="1085" alt="image" src="https://github.com/user-attachments/assets/cf823e8b-1305-4ed2-a7c5-712efec12ceb" />
24
24
 
25
25
  ## How It Works
26
26
 
@@ -129,6 +129,7 @@ Bring this reporter on CI and never lose test results again!
129
129
  - [CSV](./docs/pipes/csv.md)
130
130
  - [HTML report](./docs/pipes/html.md)
131
131
  - [Bitbucket](./docs/pipes/bitbucket.md)
132
+ - πŸ”— [Linking Tests](./docs/linking-tests.md)
132
133
  - πŸ““ [JUnit](./docs/junit.md)
133
134
  - πŸ—„οΈ [Artifacts](./docs/artifacts.md)
134
135
  - πŸ”‚ [Workflows](./docs/workflows.md)
@@ -20,11 +20,14 @@ if (!global.codeceptjs) {
20
20
  }
21
21
  // @ts-ignore
22
22
  const { event, recorder, codecept, output } = global.codeceptjs;
23
- const [, MAJOR_VERSION, MINOR_VERSION] = codecept.version().match(/(\d+)\.(\d+)/).map(Number);
23
+ const [, MAJOR_VERSION, MINOR_VERSION] = codecept
24
+ .version()
25
+ .match(/(\d+)\.(\d+)/)
26
+ .map(Number);
24
27
  // Constants for hook execution order
25
28
  const HOOK_EXECUTION_ORDER = {
26
29
  PRE_TEST: ['BeforeSuiteHook', 'BeforeHook'],
27
- POST_TEST: ['AfterHook', 'AfterSuiteHook']
30
+ POST_TEST: ['AfterHook', 'AfterSuiteHook'],
28
31
  };
29
32
  // codeceptjs workers are self-contained
30
33
  data_storage_js_1.dataStorage.isFileStorage = false;
@@ -92,7 +95,7 @@ function CodeceptReporter(config) {
92
95
  global.testomatioDataStore = {};
93
96
  });
94
97
  // Hook event listeners
95
- event.dispatcher.on(event.hook.started, (hook) => {
98
+ event.dispatcher.on(event.hook.started, hook => {
96
99
  output.stepShift = 2;
97
100
  currentHook = hook.name;
98
101
  let title = hook.hookName;
@@ -281,7 +284,7 @@ function captureHookStep(step, currentHook, hookSteps) {
281
284
  status: step.status,
282
285
  startTime,
283
286
  endTime,
284
- helperMethod: step.helperMethod
287
+ helperMethod: step.helperMethod,
285
288
  });
286
289
  hookSteps.set(currentHook, hookStepsArray);
287
290
  }
@@ -369,7 +372,7 @@ function createSectionStep(metaStep) {
369
372
  category: 'user',
370
373
  title: metaStep.toString(), // Use built-in toString method
371
374
  duration: metaStep.duration || 0, // Use built-in duration
372
- steps: []
375
+ steps: [],
373
376
  };
374
377
  }
375
378
  function createHookSection(hookName, steps) {
@@ -379,7 +382,7 @@ function createHookSection(hookName, steps) {
379
382
  category: 'hook',
380
383
  title: formatHookName(hookName),
381
384
  duration: 0,
382
- steps: []
385
+ steps: [],
383
386
  };
384
387
  for (const step of steps) {
385
388
  const formattedStep = formatHookStep(step);
@@ -403,13 +406,13 @@ function formatCodeceptStep(step) {
403
406
  const formattedStep = {
404
407
  category,
405
408
  title,
406
- duration
409
+ duration,
407
410
  };
408
411
  // Add error if step failed
409
412
  if (step.status === 'failed' && step.err) {
410
413
  formattedStep.error = {
411
414
  message: step.err.message || 'Step failed',
412
- stack: step.err.stack || ''
415
+ stack: step.err.stack || '',
413
416
  };
414
417
  }
415
418
  return formattedStep;
@@ -430,7 +433,7 @@ function formatHookStep(step) {
430
433
  return {
431
434
  category: 'hook',
432
435
  title,
433
- duration: step.duration || 0
436
+ duration: step.duration || 0,
434
437
  };
435
438
  }
436
439
  module.exports = CodeceptReporter;
package/lib/bin/cli.js CHANGED
@@ -37,12 +37,17 @@ program
37
37
  program
38
38
  .command('start')
39
39
  .description('Start a new run and return its ID')
40
- .action(async () => {
40
+ .option('--kind <type>', 'Specify run type: automated, manual, or mixed')
41
+ .action(async (opts) => {
41
42
  (0, utils_js_1.cleanLatestRunId)();
42
43
  console.log('Starting a new Run on Testomat.io...');
43
44
  const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
44
45
  const client = new client_js_1.default({ apiKey });
45
- client.createRun().then(() => {
46
+ const createRunParams = {};
47
+ if (opts.kind) {
48
+ createRunParams.kind = opts.kind;
49
+ }
50
+ client.createRun(createRunParams).then(() => {
46
51
  console.log(process.env.runId);
47
52
  process.exit(0);
48
53
  });
@@ -70,6 +75,8 @@ program
70
75
  .description('Run tests with the specified command')
71
76
  .argument('<command>', 'Test runner command')
72
77
  .option('--filter <filter>', 'Additional execution filter')
78
+ .option('--filter-list <filter>', 'Get a list of all tests by filter before running')
79
+ .option('--kind <type>', 'Specify run type: automated, manual, or mixed')
73
80
  .action(async (command, opts) => {
74
81
  const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
75
82
  const title = process.env.TESTOMATIO_TITLE;
@@ -78,17 +85,32 @@ program
78
85
  return process.exit(255);
79
86
  }
80
87
  const client = new client_js_1.default({ apiKey, title });
81
- if (opts.filter) {
82
- const [pipe, ...optsArray] = opts.filter.split(':');
88
+ if (opts.filter || opts.filterList) {
89
+ // Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
90
+ // Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
91
+ // Example of use: npx @testomatio/reporter run "npx jest" --filter-list "coverage:file=coverage.yml"
92
+ const [pipe, ...optsArray] = opts?.filter ? opts?.filter.split(':') : opts?.filterList.split(':');
83
93
  const pipeOptions = optsArray.join(':');
94
+ const prepareRunParams = { pipe, pipeOptions };
84
95
  try {
85
- const tests = await client.prepareRun({ pipe, pipeOptions });
86
- if (tests && tests.length > 0) {
87
- command += ` --grep (${tests.join('|')})`;
96
+ const tests = await client.prepareRun(prepareRunParams);
97
+ if (!tests || tests.length === 0) {
98
+ console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow('No tests found.'));
99
+ return;
88
100
  }
101
+ const pattern = `(${tests.join('|')})`;
102
+ const filteredCommand = (0, utils_js_1.applyFilter)(command, tests);
103
+ debug(`Execution pattern: "${pattern}"`);
104
+ if (opts.filterList) {
105
+ console.log(constants_js_1.APP_PREFIX, picocolors_1.default.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
106
+ console.log(constants_js_1.APP_PREFIX, picocolors_1.default.green(`Full Running Command: ${filteredCommand}`));
107
+ return;
108
+ }
109
+ command = filteredCommand;
89
110
  }
90
111
  catch (err) {
91
- console.log(constants_js_1.APP_PREFIX, err);
112
+ console.log(constants_js_1.APP_PREFIX, err.message || err);
113
+ return;
92
114
  }
93
115
  }
94
116
  console.log(constants_js_1.APP_PREFIX, `πŸš€ Running`, picocolors_1.default.green(command));
@@ -96,7 +118,7 @@ program
96
118
  const testCmds = command.split(' ');
97
119
  const cmd = (0, cross_spawn_1.spawn)(testCmds[0], testCmds.slice(1), {
98
120
  stdio: 'inherit',
99
- env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId },
121
+ env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId, TESTOMATIO_RUN: client.runId },
100
122
  });
101
123
  cmd.on('close', async (code) => {
102
124
  const emoji = code === 0 ? '🟒' : 'πŸ”΄';
@@ -108,8 +130,15 @@ program
108
130
  process.exit(code);
109
131
  });
110
132
  };
133
+ const createRunParams = {};
134
+ if (title) {
135
+ createRunParams.title = title;
136
+ }
137
+ if (opts.kind) {
138
+ createRunParams.kind = opts.kind;
139
+ }
111
140
  if (apiKey) {
112
- await client.createRun().then(runTests);
141
+ await client.createRun(createRunParams).then(runTests);
113
142
  }
114
143
  else {
115
144
  await runTests();
@@ -145,7 +174,7 @@ program
145
174
  .option('--lang <lang>', 'Language used (python, ruby, java)')
146
175
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
147
176
  .action(async (pattern, opts) => {
148
- if (!pattern.endsWith('.xml')) {
177
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
149
178
  pattern += '.xml';
150
179
  }
151
180
  let { javaTests, lang } = opts;
@@ -25,7 +25,7 @@ program
25
25
  .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
26
26
  .option('--env-file <envfile>', 'Load environment variables from env file')
27
27
  .action(async (pattern, opts) => {
28
- if (!pattern.endsWith('.xml')) {
28
+ if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
29
29
  pattern += '.xml';
30
30
  }
31
31
  let { javaTests, lang } = opts;
@@ -37,7 +37,10 @@ program
37
37
  lang = lang?.toLowerCase();
38
38
  if (javaTests === true || (lang === 'java' && !javaTests))
39
39
  javaTests = 'src/test/java';
40
- const runReader = new xmlReader_js_1.default({ javaTests, lang });
40
+ const runReader = new xmlReader_js_1.default({
41
+ javaTests,
42
+ lang,
43
+ });
41
44
  const files = glob_1.glob.sync(pattern, { cwd: opts.dir || process.cwd() });
42
45
  if (!files.length) {
43
46
  console.log(constants_js_1.APP_PREFIX, `Report can't be created. No XML files found πŸ˜₯`);
package/lib/client.d.ts CHANGED
@@ -44,7 +44,7 @@ export class Client {
44
44
  *
45
45
  * @returns {Promise<any>} - resolves to Run id which should be used to update / add test
46
46
  */
47
- createRun(params: any): Promise<any>;
47
+ createRun(params?: {}): Promise<any>;
48
48
  /**
49
49
  * Updates test status and its data
50
50
  *
@@ -61,15 +61,5 @@ export class Client {
61
61
  * @returns {Promise<any>} - A Promise that resolves when finishes the run.
62
62
  */
63
63
  updateRunStatus(status: "passed" | "failed" | "skipped" | "finished"): Promise<any>;
64
- /**
65
- * Returns the formatted stack including the stack trace, steps, and logs.
66
- * @returns {string}
67
- */
68
- formatLogs({ error, steps, logs }: {
69
- error: any;
70
- steps: any;
71
- logs: any;
72
- }): string;
73
- formatError(error: any, message: any): string;
74
64
  }
75
65
  import { S3Uploader } from './uploader.js';
package/lib/client.js CHANGED
@@ -1,56 +1,21 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
38
5
  Object.defineProperty(exports, "__esModule", { value: true });
39
6
  exports.Client = void 0;
40
7
  const debug_1 = __importDefault(require("debug"));
41
- const callsite_record_1 = __importDefault(require("callsite-record"));
42
- const minimatch_1 = require("minimatch");
43
8
  const fs_1 = __importDefault(require("fs"));
44
9
  const picocolors_1 = __importDefault(require("picocolors"));
45
- const crypto_1 = require("crypto");
46
10
  const constants_js_1 = require("./constants.js");
47
11
  const index_js_1 = require("./pipe/index.js");
48
12
  const glob_1 = require("glob");
49
- const path_1 = __importStar(require("path"));
13
+ const path_1 = __importDefault(require("path"));
50
14
  const node_url_1 = require("node:url");
51
15
  const uploader_js_1 = require("./uploader.js");
52
16
  const utils_js_1 = require("./utils/utils.js");
53
17
  const filesize_1 = require("filesize");
18
+ const log_formatter_js_1 = require("./utils/log-formatter.js");
54
19
  const debug = (0, debug_1.default)('@testomatio/reporter:client');
55
20
  // removed __dirname usage, because:
56
21
  // 1. replaced with ESM syntax (import.meta.url), but it throws an error on tsc compilation;
@@ -99,24 +64,32 @@ class Client {
99
64
  * or resolves to undefined if no valid results are found or if all pipes are disabled.
100
65
  */
101
66
  async prepareRun(params) {
102
- this.pipes = await (0, index_js_1.pipesFactory)(params || this.paramsForPipesFactory || {}, this.pipeStore);
103
67
  const { pipe, pipeOptions } = params;
68
+ // ❗ Validation: pipe is required
69
+ if (!pipe || !pipeOptions) {
70
+ console.warn(`❗ No valid pipe found in filter cmd. Expected format: <pipe>:<options>
71
+ Examples:
72
+ --filter "testomatio:tag-name=frontend"
73
+ --filter "coverage:file=coverage.yml"
74
+ --filter-list "coverage:file=coverage.yml"
75
+ Received: "${params}"`);
76
+ return;
77
+ }
78
+ this.pipes = await (0, index_js_1.pipesFactory)(params || this.paramsForPipesFactory || {}, this.pipeStore);
104
79
  // all pipes disabled, skipping
105
80
  if (!this.pipes.some(p => p.isEnabled)) {
106
81
  return Promise.resolve();
107
82
  }
108
83
  try {
109
- const filterPipe = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
110
- if (!filterPipe?.isEnabled) {
111
- // TODO:for the future for the another pipes
112
- console.warn(constants_js_1.APP_PREFIX, `At the moment processing is available only for the "testomatio" key. Example: "testomatio:tag-name=xxx"`);
113
- return;
114
- }
115
- const results = await Promise.all(this.pipes.map(async (p) => ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })));
116
- const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
117
- if (!result || result.length === 0) {
84
+ const p = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
85
+ // const p = this.pipes.find(p => p.id === `${pipe.toLowerCase()}`); TODO: as future updates
86
+ if (!p?.isEnabled) {
87
+ console.warn(constants_js_1.APP_PREFIX, "🚫 No active pipes were found in the system. Execution aborted!");
118
88
  return;
119
89
  }
90
+ // Run only the selected pipe
91
+ const rawResult = await p.prepareRun(pipeOptions);
92
+ const result = Array.isArray(rawResult) ? rawResult : [];
120
93
  debug('Execution tests list', result);
121
94
  return result;
122
95
  }
@@ -129,7 +102,7 @@ class Client {
129
102
  *
130
103
  * @returns {Promise<any>} - resolves to Run id which should be used to update / add test
131
104
  */
132
- async createRun(params) {
105
+ async createRun(params = {}) {
133
106
  if (!this.pipes || !this.pipes.length)
134
107
  this.pipes = await (0, index_js_1.pipesFactory)(params || this.paramsForPipesFactory || {}, this.pipeStore);
135
108
  debug('Creating run...');
@@ -137,7 +110,7 @@ class Client {
137
110
  if (!this.pipes?.filter(p => p.isEnabled).length)
138
111
  return Promise.resolve();
139
112
  this.queue = this.queue
140
- .then(() => Promise.all(this.pipes.map(p => p.createRun())))
113
+ .then(() => Promise.all(this.pipes.map(p => p.createRun(params))))
141
114
  .catch(err => console.log(constants_js_1.APP_PREFIX, err))
142
115
  .then(() => {
143
116
  const runId = this.pipeStore?.runId;
@@ -158,17 +131,6 @@ class Client {
158
131
  * @returns {Promise<PipeResult[]>}
159
132
  */
160
133
  async addTestRun(status, testData) {
161
- if (!this.pipes || !this.pipes.length)
162
- this.pipes = await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
163
- // all pipes disabled, skipping
164
- if (!this.pipes?.filter(p => p.isEnabled).length)
165
- return [];
166
- if (isTestShouldBeExculedFromReport(testData))
167
- return [];
168
- if (status === constants_js_1.STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
169
- debug('Skipping test from report', testData?.title);
170
- return []; // do not log skipped tests
171
- }
172
134
  if (!testData)
173
135
  testData = {
174
136
  title: 'Unknown test',
@@ -181,9 +143,12 @@ class Client {
181
143
  /**
182
144
  * @type {TestData}
183
145
  */
184
- const { rid, error = null, time = 0, example = null, files = [], filesBuffers = [], steps, code = null, title, file, suite_title, suite_id, test_id, timestamp, links, manuallyAttachedArtifacts, overwrite, tags, } = testData;
146
+ const { rid, error = null, steps: originalSteps, title, suite_title } = testData;
147
+ let steps = originalSteps;
148
+ const uploadedFiles = [];
149
+ const stackArtifactsEnabled = (0, utils_js_1.transformEnvVarToBoolean)(process.env.TESTOMATIO_STACK_ARTIFACTS);
150
+ const { time = 0, example = null, files = [], filesBuffers = [], code = null, file, suite_id, test_id, timestamp, links, manuallyAttachedArtifacts, overwrite, tags, } = testData;
185
151
  let { message = '', meta = {} } = testData;
186
- // stringify meta values and limit keys and values length to 255
187
152
  meta = Object.entries(meta)
188
153
  .filter(([, value]) => value !== null && value !== undefined)
189
154
  .reduce((acc, [key, value]) => {
@@ -191,19 +156,38 @@ class Client {
191
156
  acc[key] = value;
192
157
  return acc;
193
158
  }, {});
194
- // Get links from storage using the test context
195
159
  const testContext = suite_title ? `${suite_title} ${title}` : title;
196
160
  let errorFormatted = '';
197
161
  if (error) {
198
- errorFormatted += this.formatError(error) || '';
162
+ errorFormatted += (0, log_formatter_js_1.formatError)(error) || '';
199
163
  message = error?.message;
200
164
  }
201
- // Attach logs
202
- const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
203
- // add artifacts
165
+ let fullLogs = (0, log_formatter_js_1.formatLogs)({ error: errorFormatted, steps, logs: testData.logs });
166
+ if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
167
+ uploadedFiles.push(this.uploader.uploadFileAsBuffer(Buffer.from((0, log_formatter_js_1.stripColors)(fullLogs), 'utf8'), [
168
+ this.runId,
169
+ rid,
170
+ `logs_${+new Date()}.log`,
171
+ ]));
172
+ fullLogs = '';
173
+ steps = null;
174
+ }
175
+ if (!this.pipes || !this.pipes.length)
176
+ this.pipes = await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
177
+ if (!this.pipes?.filter(p => p.isEnabled).length) {
178
+ if (uploadedFiles.length > 0) {
179
+ await Promise.all(uploadedFiles);
180
+ }
181
+ return [];
182
+ }
183
+ if (isTestShouldBeExcludedFromReport(testData))
184
+ return [];
185
+ if (status === constants_js_1.STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
186
+ debug('Skipping test from report', testData?.title);
187
+ return [];
188
+ }
204
189
  if (manuallyAttachedArtifacts?.length)
205
190
  files.push(...manuallyAttachedArtifacts);
206
- const uploadedFiles = [];
207
191
  for (let f of files) {
208
192
  if (!f)
209
193
  continue; // f === null
@@ -285,7 +269,7 @@ class Client {
285
269
  const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
286
270
  relativePath: file.path.replace(process.cwd(), ''),
287
271
  link: file.link,
288
- sizePretty: (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
272
+ sizePretty: file.size == null ? 'unknown' : (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
289
273
  }));
290
274
  uploadedArtifacts.forEach(upload => {
291
275
  debug(`🟒Uploaded artifact`, `${upload.relativePath},`, 'size:', `${upload.sizePretty},`, 'link:', `${upload.link}`);
@@ -295,7 +279,7 @@ class Client {
295
279
  console.log(constants_js_1.APP_PREFIX, `πŸ—„οΈ ${this.uploader.failedUploads.length} artifacts πŸ”΄${picocolors_1.default.bold('failed')} to upload`);
296
280
  const failedUploads = this.uploader.failedUploads.map(file => ({
297
281
  relativePath: file.path.replace(process.cwd(), ''),
298
- sizePretty: (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
282
+ sizePretty: file.size == null ? 'unknown' : (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
299
283
  }));
300
284
  const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
301
285
  failedUploads.forEach(upload => {
@@ -323,93 +307,14 @@ class Client {
323
307
  .catch(err => console.log(constants_js_1.APP_PREFIX, err));
324
308
  return this.queue;
325
309
  }
326
- /**
327
- * Returns the formatted stack including the stack trace, steps, and logs.
328
- * @returns {string}
329
- */
330
- formatLogs({ error, steps, logs }) {
331
- error = error?.trim();
332
- logs = logs?.trim().split('\n').map(l => (0, utils_js_1.truncate)(l)).join('\n');
333
- if (Array.isArray(steps)) {
334
- steps = steps
335
- .map(step => (0, utils_js_1.formatStep)(step))
336
- .flat()
337
- .join('\n');
338
- }
339
- let testLogs = '';
340
- if (steps)
341
- testLogs += `${picocolors_1.default.bold(picocolors_1.default.blue('################[ Steps ]################'))}\n${steps}\n\n`;
342
- if (logs)
343
- testLogs += `${picocolors_1.default.bold(picocolors_1.default.gray('################[ Logs ]################'))}\n${logs}\n\n`;
344
- if (error)
345
- testLogs += `${picocolors_1.default.bold(picocolors_1.default.red('################[ Failure ]################'))}\n${error}`;
346
- return testLogs;
347
- }
348
- formatError(error, message) {
349
- if (!message)
350
- message = error.message;
351
- if (error.inspect)
352
- message = error.inspect() || '';
353
- let stack = '';
354
- if (error.name)
355
- stack += `${picocolors_1.default.red(error.name)}`;
356
- if (error.operator)
357
- stack += ` (${picocolors_1.default.red(error.operator)})`;
358
- // add new line if something was added to stack
359
- if (stack)
360
- stack += ': ';
361
- stack += `${message}\n`;
362
- if (error.diff) {
363
- // diff for vitest
364
- stack += error.diff;
365
- stack += '\n\n';
366
- }
367
- else if (error.actual && error.expected && error.actual !== error.expected) {
368
- // diffs for mocha, cypress, codeceptjs style
369
- stack += `\n\n${picocolors_1.default.bold(picocolors_1.default.green('+ expected'))} ${picocolors_1.default.bold(picocolors_1.default.red('- actual'))}`;
370
- stack += `\n${picocolors_1.default.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
371
- stack += `\n${picocolors_1.default.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
372
- stack += '\n\n';
373
- }
374
- const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
375
- try {
376
- let hasFrame = false;
377
- const record = (0, callsite_record_1.default)({
378
- forError: error,
379
- isCallsiteFrame: frame => {
380
- if (customFilter && (0, minimatch_1.minimatch)(frame.fileName, customFilter))
381
- return false;
382
- if (hasFrame)
383
- return false;
384
- if (isNotInternalFrame(frame))
385
- hasFrame = true;
386
- return hasFrame;
387
- },
388
- });
389
- // @ts-ignore
390
- if (record && !record.filename.startsWith('http')) {
391
- stack += record.renderSync({ stackFilter: isNotInternalFrame });
392
- }
393
- return stack;
394
- }
395
- catch (e) {
396
- console.log(e);
397
- }
398
- }
399
310
  }
400
311
  exports.Client = Client;
401
- function isNotInternalFrame(frame) {
402
- return (frame.getFileName() &&
403
- frame.getFileName().includes(path_1.sep) &&
404
- !frame.getFileName().includes('node_modules') &&
405
- !frame.getFileName().includes('internal'));
406
- }
407
312
  /**
408
313
  *
409
314
  * @param {TestData} testData
410
315
  * @returns boolean
411
316
  */
412
- function isTestShouldBeExculedFromReport(testData) {
317
+ function isTestShouldBeExcludedFromReport(testData) {
413
318
  // const fileName = path.basename(test.location?.file || '');
414
319
  const globExcludeFilesPattern = process.env.TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN;
415
320
  if (!globExcludeFilesPattern)
@@ -418,11 +323,11 @@ function isTestShouldBeExculedFromReport(testData) {
418
323
  debug('No "file" property found for test ', testData.title);
419
324
  return false;
420
325
  }
421
- const excludeParretnsList = globExcludeFilesPattern.split(';');
326
+ const excludePatternsList = globExcludeFilesPattern.split(';');
422
327
  // as scanning files is time consuming operation, just save the result in variable to avoid multiple scans
423
328
  if (!listOfTestFilesToExcludeFromReport) {
424
329
  // list of files with relative paths
425
- listOfTestFilesToExcludeFromReport = glob_1.glob.sync(excludeParretnsList, { ignore: '**/node_modules/**' });
330
+ listOfTestFilesToExcludeFromReport = glob_1.glob.sync(excludePatternsList, { ignore: '**/node_modules/**' });
426
331
  debug('Tests from next files will not be reported:', listOfTestFilesToExcludeFromReport);
427
332
  }
428
333
  const testFileRelativePath = path_1.default.relative(process.cwd(), testData.file);
@@ -1,6 +1,6 @@
1
1
  export const dataStorage: DataStorage;
2
2
  declare class DataStorage {
3
- static "__#12@#instance": any;
3
+ static "__#13@#instance": any;
4
4
  /**
5
5
  *
6
6
  * @returns {DataStorage}
@@ -0,0 +1 @@
1
+ export const isPlaywright: boolean;
package/lib/helpers.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPlaywright = void 0;
4
+ exports.isPlaywright = Boolean(process.env.PLAYWRIGHT_TEST || process.env.PLAYWRIGHT);
@@ -1,5 +1,4 @@
1
1
  export default CSharpAdapter;
2
2
  declare class CSharpAdapter extends Adapter {
3
- getFilePath(t: any): string;
4
3
  }
5
4
  import Adapter from './adapter.js';
@@ -7,24 +7,60 @@ const path_1 = __importDefault(require("path"));
7
7
  const adapter_js_1 = __importDefault(require("./adapter.js"));
8
8
  class CSharpAdapter extends adapter_js_1.default {
9
9
  formatTest(t) {
10
- const title = t.title.replace(/\(.*?\)/, '').trim();
11
- const example = t.title.match(/\((.*?)\)/);
12
- if (example)
13
- t.example = { ...example[1].split(',') };
10
+ // Extract example from title if not already present
11
+ if (!t.example) {
12
+ const exampleMatch = t.title.match(/\((.*?)\)/);
13
+ if (exampleMatch) {
14
+ // Extract parameters as object with numeric keys for API
15
+ const params = exampleMatch[1]
16
+ .split(',')
17
+ .map(param => param.trim())
18
+ .filter(param => param !== '');
19
+ t.example = {};
20
+ params.forEach((param, index) => {
21
+ t.example[index] = param;
22
+ });
23
+ }
24
+ }
25
+ // Remove parameters from title to avoid duplicates in Test Suite
26
+ // The example field will be used for grouping on import
27
+ t.title = t.title.replace(/\(.*?\)/, '').trim();
14
28
  const suite = t.suite_title.split('.');
15
29
  t.suite_title = suite.pop();
16
30
  t.file = namespaceToFileName(t.file);
17
- t.title = title.trim();
18
31
  return t;
19
32
  }
20
33
  getFilePath(t) {
21
- const fileName = namespaceToFileName(t.file);
34
+ if (!t.file)
35
+ return null;
36
+ // Normalize path separators for cross-platform compatibility
37
+ let filePath = t.file.replace(/\\/g, '/');
38
+ // If file already has .cs extension, use it directly
39
+ if (filePath.endsWith('.cs')) {
40
+ // Make relative path if it's absolute
41
+ if (path_1.default.isAbsolute(filePath)) {
42
+ // Try to find project-relative path
43
+ const cwd = process.cwd().replace(/\\/g, '/');
44
+ if (filePath.startsWith(cwd)) {
45
+ filePath = path_1.default.relative(cwd, filePath).replace(/\\/g, '/');
46
+ }
47
+ }
48
+ return filePath;
49
+ }
50
+ // Convert namespace path to file path
51
+ const fileName = namespaceToFileName(filePath);
22
52
  return fileName;
23
53
  }
24
54
  }
25
55
  module.exports = CSharpAdapter;
26
56
  function namespaceToFileName(fileName) {
57
+ if (!fileName)
58
+ return '';
59
+ // If already a .cs file path, clean it up
60
+ if (fileName.endsWith('.cs')) {
61
+ return fileName.replace(/\\/g, '/');
62
+ }
27
63
  const fileParts = fileName.split('.');
28
64
  fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
29
- return `${fileParts.join(path_1.default.sep)}.cs`;
65
+ return `${fileParts.join('/')}.cs`;
30
66
  }