@testomatio/reporter 1.5.0-beta-vitest → 1.5.1-beta

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.
@@ -129,8 +129,8 @@ function CodeceptReporter(config) {
129
129
  await Promise.all(reportTestPromises);
130
130
 
131
131
  if (upload.isArtifactsEnabled()) {
132
- uploadAttachments(client, videos, '🎞️ Uploading', 'video');
133
- uploadAttachments(client, traces, '📁 Uploading', 'trace');
132
+ await uploadAttachments(client, videos, '🎞️ Uploading', 'video');
133
+ await uploadAttachments(client, traces, '📁 Uploading', 'trace');
134
134
  }
135
135
 
136
136
  const status = failedTests.length === 0 ? STATUS.PASSED : STATUS.FAILED;
@@ -142,7 +142,6 @@ function CodeceptReporter(config) {
142
142
  if (id && failedTests.includes(id)) {
143
143
  failedTests = failedTests.filter(failed => id !== failed);
144
144
  }
145
- const testId = getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`);
146
145
  const testObj = getTestAndMessage(title);
147
146
 
148
147
  const logs = getTestLogs(test);
@@ -152,11 +151,12 @@ function CodeceptReporter(config) {
152
151
 
153
152
  client.addTestRun(STATUS.PASSED, {
154
153
  ...stripExampleFromTitle(title),
154
+ rid: id,
155
155
  suite_title: test.parent && test.parent.title,
156
156
  message: testObj.message,
157
157
  time: getDuration(test),
158
158
  steps: global.testomatioDataStore.steps.join('\n') || null,
159
- test_id: testId,
159
+ test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
160
160
  logs,
161
161
  manuallyAttachedArtifacts,
162
162
  meta: keyValues,
@@ -179,6 +179,7 @@ function CodeceptReporter(config) {
179
179
  const testId = getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`);
180
180
 
181
181
  client.addTestRun(STATUS.FAILED, {
182
+ rid: id,
182
183
  ...stripExampleFromTitle(title),
183
184
  suite_title: suite.title,
184
185
  test_id: testId,
@@ -194,7 +195,6 @@ function CodeceptReporter(config) {
194
195
  if (test.err) error = test.err;
195
196
  const { id, tags, title, artifacts } = test;
196
197
  failedTests.push(id || title);
197
- let testId = getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`);
198
198
  const testObj = getTestAndMessage(title);
199
199
 
200
200
  const files = [];
@@ -206,32 +206,27 @@ function CodeceptReporter(config) {
206
206
  const keyValues = services.keyValues.get(test.fullTitle());
207
207
  services.setContext(null);
208
208
 
209
- const reportTestPromise = client
210
- .addTestRun(STATUS.FAILED, {
211
- ...stripExampleFromTitle(title),
212
- test_id: testId,
213
- suite_title: test.parent && test.parent.title,
214
- error,
215
- message: testObj.message,
216
- time: getDuration(test),
217
- files,
218
- steps: global.testomatioDataStore?.steps?.join('\n') || null,
219
- logs,
220
- manuallyAttachedArtifacts,
221
- meta: keyValues,
222
- })
223
- .then(pipes => {
224
- testId = pipes.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
225
-
226
- debug('artifacts', artifacts);
227
-
228
- for (const aid in artifacts) {
229
- if (aid.startsWith('video')) videos.push({ testId, title, path: artifacts[aid], type: 'video/webm' });
230
- if (aid.startsWith('trace')) traces.push({ testId, title, path: artifacts[aid], type: 'application/zip' });
231
- }
232
- });
209
+ client.addTestRun(STATUS.FAILED, {
210
+ ...stripExampleFromTitle(title),
211
+ rid: id,
212
+ test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
213
+ suite_title: test.parent && test.parent.title,
214
+ error,
215
+ message: testObj.message,
216
+ time: getDuration(test),
217
+ files,
218
+ steps: global.testomatioDataStore?.steps?.join('\n') || null,
219
+ logs,
220
+ manuallyAttachedArtifacts,
221
+ meta: keyValues,
222
+ });
223
+
224
+ debug('artifacts', artifacts);
233
225
 
234
- reportTestPromises.push(reportTestPromise);
226
+ for (const aid in artifacts) {
227
+ if (aid.startsWith('video')) videos.push({ rid: id, title, path: artifacts[aid], type: 'video/webm' });
228
+ if (aid.startsWith('trace')) traces.push({ rid: id, title, path: artifacts[aid], type: 'application/zip' });
229
+ }
235
230
 
236
231
  // output.stop();
237
232
  });
@@ -240,11 +235,11 @@ function CodeceptReporter(config) {
240
235
  const { id, tags, title } = test;
241
236
  if (failedTests.includes(id || title)) return;
242
237
 
243
- const testId = getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`);
244
238
  const testObj = getTestAndMessage(title);
245
239
  client.addTestRun(STATUS.SKIPPED, {
240
+ rid: id,
246
241
  ...stripExampleFromTitle(title),
247
- test_id: testId,
242
+ test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
248
243
  suite_title: test.parent && test.parent.title,
249
244
  message: testObj.message,
250
245
  time: getDuration(test),
@@ -309,21 +304,21 @@ function CodeceptReporter(config) {
309
304
  }
310
305
 
311
306
  async function uploadAttachments(client, attachments, messagePrefix, attachmentType) {
312
- if (attachments.length > 0) {
313
- console.log(APP_PREFIX, `Attachments: ${messagePrefix} ${attachments.length} ${attachmentType}/-s ...`);
307
+ if (!attachments?.length) return;
314
308
 
315
- const promises = attachments.map(async attachment => {
316
- const { testId, title, path, type } = attachment;
317
- const file = { path, type, title };
318
- return client.addTestRun(undefined, {
319
- ...stripExampleFromTitle(title),
320
- test_id: testId,
321
- files: [file],
322
- });
309
+ console.log(APP_PREFIX, `Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
310
+
311
+ const promises = attachments.map(async attachment => {
312
+ const { rid, title, path, type } = attachment;
313
+ const file = { path, type, title };
314
+ return client.addTestRun(undefined, {
315
+ ...stripExampleFromTitle(title),
316
+ rid,
317
+ files: [file],
323
318
  });
319
+ });
324
320
 
325
- await Promise.all(promises);
326
- }
321
+ await Promise.all(promises);
327
322
  }
328
323
 
329
324
  function getTestAndMessage(title) {
@@ -2,6 +2,7 @@ const chalk = require('chalk');
2
2
  const crypto = require('crypto');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
+ const { v4: uuidv4 } = require('uuid');
5
6
  const fs = require('fs');
6
7
  const { APP_PREFIX, STATUS: Status, TESTOMAT_TMP_STORAGE_DIR } = require('../constants');
7
8
  const TestomatioClient = require('../client');
@@ -38,16 +39,15 @@ class PlaywrightReporter {
38
39
  if (!this.client) return;
39
40
 
40
41
  const { title } = test;
41
-
42
- let testId = getTestomatIdFromTestTitle(`${title} ${test.tags?.join(' ')}`);
43
-
44
42
  const { error, duration } = result;
45
-
46
43
  const suite_title = test.parent ? test.parent?.title : path.basename(test?.location?.file);
47
44
 
48
45
  const steps = [];
49
46
  for (const step of result.steps) {
50
- appendStep(step, steps);
47
+ const appendedStep = appendStep(step);
48
+ if (appendedStep) {
49
+ steps.push(appendedStep);
50
+ }
51
51
  }
52
52
 
53
53
  const fullTestTitle = getTestContextName(test);
@@ -57,33 +57,30 @@ class PlaywrightReporter {
57
57
  }
58
58
  const manuallyAttachedArtifacts = services.artifacts.get(fullTestTitle);
59
59
  const keyValues = services.keyValues.get(fullTestTitle);
60
+ const rid = test.id || test.testId || uuidv4();
61
+
62
+ const reportTestPromise = this.client.addTestRun(checkStatus(result.status), {
63
+ rid,
64
+ error,
65
+ test_id: getTestomatIdFromTestTitle(`${title} ${test.tags?.join(' ')}`),
66
+ suite_title,
67
+ title,
68
+ steps: steps.length ? steps : undefined,
69
+ time: duration,
70
+ logs,
71
+ manuallyAttachedArtifacts,
72
+ meta: keyValues,
73
+ file: test.location?.file,
74
+ });
60
75
 
61
- const reportTestPromise = this.client
62
- .addTestRun(checkStatus(result.status), {
63
- error,
64
- test_id: testId,
65
- suite_title,
66
- title,
67
- steps: steps.join('\n'),
68
- time: duration,
69
- logs,
70
- manuallyAttachedArtifacts,
71
- meta: keyValues,
72
- file: test.location?.file,
73
- })
74
- .then(pipes => {
75
- testId = pipes?.filter(p => p.pipe.includes('Testomatio'))[0]?.result?.data?.test_id;
76
-
77
- this.uploads.push({
78
- testId,
79
- title,
80
- suite_title,
81
- files: result.attachments.filter(a => a.body || a.path),
82
- file: test.location?.file,
83
- });
84
- // remove empty uploads
85
- this.uploads = this.uploads.filter(upload => upload.files.length);
86
- });
76
+ this.uploads.push({
77
+ rid,
78
+ title: test.title,
79
+ files: result.attachments.filter(a => a.body || a.path),
80
+ file: test.location?.file,
81
+ });
82
+ // remove empty uploads
83
+ this.uploads = this.uploads.filter(upload => upload.files.length);
87
84
 
88
85
  reportTestPromises.push(reportTestPromise);
89
86
  }
@@ -115,7 +112,7 @@ class PlaywrightReporter {
115
112
  const promises = [];
116
113
 
117
114
  for (const upload of this.uploads) {
118
- const { title, testId, suite_title } = upload;
115
+ const { rid, file, title } = upload;
119
116
 
120
117
  const files = upload.files.map(attachment => ({
121
118
  path: this.#getArtifactPath(attachment),
@@ -125,11 +122,10 @@ class PlaywrightReporter {
125
122
 
126
123
  promises.push(
127
124
  this.client.addTestRun(undefined, {
128
- test_id: testId,
125
+ rid,
129
126
  title,
130
- suite_title,
131
127
  files,
132
- file: upload.file,
128
+ file,
133
129
  }),
134
130
  );
135
131
  }
@@ -150,18 +146,44 @@ function checkStatus(status) {
150
146
  );
151
147
  }
152
148
 
153
- function appendStep(step, steps = [], shift = 0) {
154
- const prefix = ' '.repeat(shift);
155
-
156
- if (step.error) {
157
- steps.push(`${prefix}${chalk.red(step.title)} ${chalk.gray(`${step.duration}ms`)}`);
158
- } else {
159
- steps.push(`${prefix}${step.title} ${chalk.gray(`${step.duration}ms`)}`);
149
+ function appendStep(step, shift = 0) {
150
+ let newCategory = step.category;
151
+ switch (newCategory) {
152
+ case 'test.step':
153
+ newCategory = 'user';
154
+ break;
155
+ case 'hook':
156
+ newCategory = 'hook';
157
+ break;
158
+ case 'attach':
159
+ return null; // Skip steps with category 'attach'
160
+ default:
161
+ newCategory = 'framework';
160
162
  }
161
163
 
164
+ const formattedSteps = [];
162
165
  for (const child of step.steps || []) {
163
- appendStep(child, steps, shift + 2);
166
+ const appendedChild = appendStep(child, shift + 2);
167
+ if (appendedChild) {
168
+ formattedSteps.push(appendedChild);
169
+ }
170
+ }
171
+
172
+ const resultStep = {
173
+ category: newCategory,
174
+ title: step.title,
175
+ duration: step.duration,
176
+ };
177
+
178
+ if (formattedSteps.length) {
179
+ resultStep.steps = formattedSteps;
164
180
  }
181
+
182
+ if (step.error !== undefined) {
183
+ resultStep.error = step.error;
184
+ }
185
+
186
+ return resultStep;
165
187
  }
166
188
 
167
189
  function tmpFile(prefix = 'tmp.') {
@@ -44,15 +44,10 @@ program
44
44
 
45
45
  let timeoutTimer;
46
46
  if (opts.timelimit) {
47
- timeoutTimer = setTimeout(
48
- () => {
49
- console.log(
50
- `⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
51
- );
52
- process.exit(0);
53
- },
54
- parseInt(opts.timelimit, 10) * 1000,
55
- );
47
+ timeoutTimer = setTimeout(() => {
48
+ console.log(`⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`);
49
+ process.exit(0);
50
+ }, parseInt(opts.timelimit, 10) * 1000);
56
51
  }
57
52
 
58
53
  try {
package/lib/client.js CHANGED
@@ -137,6 +137,7 @@ class Client {
137
137
  * @type {TestData}
138
138
  */
139
139
  const {
140
+ rid,
140
141
  error = null,
141
142
  time = 0,
142
143
  example = null,
@@ -187,6 +188,7 @@ class Client {
187
188
  this.totalUploaded += artifacts.length;
188
189
 
189
190
  const data = {
191
+ rid,
190
192
  files,
191
193
  steps,
192
194
  status,
@@ -225,7 +227,7 @@ class Client {
225
227
  /**
226
228
  *
227
229
  * Updates the status of the current test run and finishes the run.
228
- * @param {'passed' | 'failed' | 'finished'} status - The status of the current test run.
230
+ * @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
229
231
  * Must be one of "passed", "failed", or "finished"
230
232
  * @param {boolean} [isParallel] - Whether the current test run was executed in parallel with other tests.
231
233
  * @returns {Promise<any>} - A Promise that resolves when finishes the run.
@@ -274,9 +276,12 @@ class Client {
274
276
  */
275
277
  formatLogs({ error, steps, logs }) {
276
278
  error = error?.trim();
277
- steps = steps?.trim();
278
279
  logs = logs?.trim();
279
280
 
281
+ if (Array.isArray(steps)) {
282
+ steps = steps.map(step => formatStep(step)).flat().join('\n');
283
+ }
284
+
280
285
  let testLogs = '';
281
286
  if (steps) testLogs += `${chalk.bold.blue('################[ Steps ]################')}\n${steps}\n\n`;
282
287
  if (logs) testLogs += `${chalk.bold.gray('################[ Logs ]################')}\n${logs}\n\n`;
@@ -297,6 +302,7 @@ class Client {
297
302
  stack += `${message}\n`;
298
303
 
299
304
  if (error.diff) {
305
+ // diff for vitest
300
306
  stack += error.diff;
301
307
  stack += '\n\n';
302
308
  } else if (error.actual && error.expected && error.actual !== error.expected) {
@@ -339,6 +345,24 @@ function isNotInternalFrame(frame) {
339
345
  );
340
346
  }
341
347
 
348
+ function formatStep(step, shift = 0) {
349
+ const prefix = ' '.repeat(shift);
350
+
351
+ const lines = [];
352
+
353
+ if (step.error) {
354
+ lines.push(`${prefix}${chalk.red(step.title)} ${chalk.gray(`${step.duration}ms`)}`);
355
+ } else {
356
+ lines.push(`${prefix}${step.title} ${chalk.gray(`${step.duration}ms`)}`);
357
+ }
358
+
359
+ for (const child of step.steps || []) {
360
+ lines.push(...formatStep(child, shift + 2));
361
+ }
362
+
363
+ return lines;
364
+ }
365
+
342
366
  /**
343
367
  *
344
368
  * @param {TestData} testData
@@ -79,13 +79,13 @@ class GitLabPipe {
79
79
  let summary = `${this.hiddenCommentData}
80
80
 
81
81
  | [![Testomat.io Report](${testomatLogoURL})](https://testomat.io) | ${statusEmoji(
82
- runParams.status,
83
- )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
82
+ runParams.status,
83
+ )} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
84
84
  | --- | --- |
85
85
  | Tests | ✔️ **${this.tests.length}** tests run |
86
86
  | Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
87
- 'passed',
88
- )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
87
+ 'passed',
88
+ )} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
89
89
  | Duration | 🕐 **${humanizeDuration(
90
90
  parseInt(
91
91
  this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
@@ -42,6 +42,10 @@ class TestomatioPipe {
42
42
  return;
43
43
  }
44
44
  debug('Testomatio Pipe: Enabled');
45
+
46
+ const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
47
+ const proxy = proxyUrl ? new URL(proxyUrl) : null;
48
+
45
49
  this.parallel = params.parallel;
46
50
  this.store = store || {};
47
51
  this.title = params.title || process.env.TESTOMATIO_TITLE;
@@ -53,6 +57,11 @@ class TestomatioPipe {
53
57
  this.axios = axios.create({
54
58
  baseURL: `${this.url.trim()}`,
55
59
  timeout: AXIOS_TIMEOUT,
60
+ proxy: proxy ? {
61
+ host: proxy.hostname,
62
+ port: proxy.port,
63
+ protocol: proxy.protocol,
64
+ } : false,
56
65
  });
57
66
 
58
67
  // Pass the axios instance to the retry function
@@ -339,6 +348,9 @@ class TestomatioPipe {
339
348
  addTest(data) {
340
349
  if (!this.isEnabled) return;
341
350
  if (!this.runId) return;
351
+
352
+ // add test ID + run ID
353
+ if (data.rid) data.rid = `${this.runId}-${data.rid}`;
342
354
  data.api_key = this.apiKey;
343
355
  data.create = this.createNewTests;
344
356
 
package/lib/xmlReader.js CHANGED
@@ -2,6 +2,7 @@ const debug = require('debug')('@testomatio/reporter:xml');
2
2
  const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const fs = require('fs');
5
+ const { randomUUID } = require('crypto');
5
6
  const { XMLParser } = require('fast-xml-parser');
6
7
  const { APP_PREFIX, STATUS } = require('./constants');
7
8
  const {
@@ -17,6 +18,8 @@ const pipesFactory = require('./pipe');
17
18
  const adapterFactory = require('./junit-adapter');
18
19
  const config = require('./config');
19
20
 
21
+ const ridRunId = randomUUID();
22
+
20
23
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
21
24
  const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
22
25
 
@@ -457,16 +460,20 @@ function reduceTestCases(prev, item) {
457
460
  if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
458
461
  if (!message) message = stack.trim().split('\n')[0];
459
462
 
463
+ const isParametrized = item.type === 'ParameterizedMethod';
464
+ const preferClassname = reduceOptions.preferClassname || isParametrized;
465
+
460
466
  // SpecFlow config
461
- let { title, tags } = fetchProperties(item.type === 'ParameterizedMethod' ? item : testCaseItem);
467
+ let { title, tags } = fetchProperties(isParametrized ? item : testCaseItem);
462
468
  let example = null;
469
+ const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
463
470
 
464
471
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
465
472
  tags ||= [];
466
473
 
467
- const exampleMatches = title.match(/\((.*?)\)/);
474
+ const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
468
475
  if (exampleMatches) {
469
- example = { ...exampleMatches[1].split(',').map(v => v.replace(/^['"]|['"]$/g, '')) };
476
+ example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
470
477
  title = title.replace(/\(.*?\)/, '').trim();
471
478
  }
472
479
 
@@ -480,12 +487,16 @@ function reduceTestCases(prev, item) {
480
487
  if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
481
488
  if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
482
489
 
490
+ let rid = null;
491
+ if (testCaseItem.id) rid = `${ridRunId}-${testCaseItem.id}`;
492
+
483
493
  prev.push({
484
- create: true,
494
+ rid,
485
495
  file,
486
496
  stack,
487
497
  example,
488
498
  tags,
499
+ create: true,
489
500
  test_id: testId,
490
501
  message,
491
502
  line: testCaseItem.lineno,
@@ -493,7 +504,7 @@ function reduceTestCases(prev, item) {
493
504
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
494
505
  status,
495
506
  title,
496
- suite_title: reduceOptions.preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname,
507
+ suite_title: suiteTitle,
497
508
  });
498
509
  });
499
510
  return prev;
@@ -509,9 +520,9 @@ function processTestSuite(testsuite) {
509
520
  suites = [testsuite];
510
521
  }
511
522
 
512
- const res = suites.flat().reduce(reduceTestCases, []);
523
+ const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
513
524
 
514
- return res;
525
+ return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
515
526
  }
516
527
 
517
528
  function fetchProperties(item) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.5.0-beta-vitest",
3
+ "version": "1.5.1-beta",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
@@ -20,7 +20,7 @@
20
20
  "csv-writer": "^1.6.0",
21
21
  "debug": "^4.3.4",
22
22
  "dotenv": "^16.0.1",
23
- "fast-xml-parser": "^4.0.8",
23
+ "fast-xml-parser": "^4.4.1",
24
24
  "file-url": "3.0.0",
25
25
  "glob": "^10.3",
26
26
  "handlebars": "^4.7.8",
@@ -63,7 +63,7 @@
63
63
  "@redocly/cli": "^1.0.0-beta.125",
64
64
  "@wdio/reporter": "^7.16.13",
65
65
  "chai": "^4.3.6",
66
- "codeceptjs": "latest",
66
+ "codeceptjs": "^3.5.11",
67
67
  "cucumber": "^6.0.7",
68
68
  "eslint": "^8.7.0",
69
69
  "eslint-config-airbnb-base": "^15.0.0",
@@ -76,7 +76,7 @@
76
76
  "mock-http-server": "^1.4.5",
77
77
  "pino": "^8.15.0",
78
78
  "prettier": "^3.2.5",
79
- "puppeteer": "^13.1.2"
79
+ "puppeteer": "^22.15.0"
80
80
  },
81
81
  "bin": {
82
82
  "report-xml": "./lib/bin/reportXml.js",