@testomatio/reporter 2.7.9-beta.1-markdown → 2.7.9-beta.2-markdown

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -135,6 +135,7 @@ Bring this reporter on CI and never lose test results again!
135
135
  - [Gitlab](./docs/pipes/gitlab.md)
136
136
  - [CSV](./docs/pipes/csv.md)
137
137
  - [HTML report](./docs/pipes/html.md)
138
+ - [Markdown report](./docs/pipes/markdown.md)
138
139
  - [Bitbucket](./docs/pipes/bitbucket.md)
139
140
  - šŸ”— [Linking Tests](./docs/linking-tests.md)
140
141
  - šŸ““ [JUnit](./docs/junit.md)
package/lib/client.js CHANGED
@@ -199,6 +199,9 @@ class Client {
199
199
  */
200
200
  const { rid, error = null, steps: originalSteps, title, suite_title } = testData;
201
201
  let steps = originalSteps;
202
+ // Capture step artifact paths BEFORE uploadStepArtifacts mutates them to URLs,
203
+ // so we can exclude them from the test-level artifacts list later.
204
+ const stepArtifactPaths = collectStepArtifactPaths(steps);
202
205
  // Upload artifacts from steps
203
206
  try {
204
207
  await this.uploadStepArtifacts(steps, rid);
@@ -208,8 +211,14 @@ class Client {
208
211
  }
209
212
  const uploadedFiles = [];
210
213
  const stackArtifactsEnabled = (0, utils_js_1.transformEnvVarToBoolean)(process.env.TESTOMATIO_STACK_ARTIFACTS);
211
- const { time = 0, example = null, files = [], filesBuffers = [], code = null, file, suite_id, test_id, timestamp, links, manuallyAttachedArtifacts, overwrite, tags, } = testData;
212
- let { message = '', meta = {} } = testData;
214
+ const { time = 0, example = null, filesBuffers = [], code = null, file, suite_id, test_id, timestamp, links, overwrite, tags, } = testData;
215
+ let { files = [], manuallyAttachedArtifacts, message = '', meta = {} } = testData;
216
+ if (stepArtifactPaths.size) {
217
+ files = files.filter(f => !isStepArtifact(f, stepArtifactPaths));
218
+ if (Array.isArray(manuallyAttachedArtifacts)) {
219
+ manuallyAttachedArtifacts = manuallyAttachedArtifacts.filter(a => !isStepArtifact(a, stepArtifactPaths));
220
+ }
221
+ }
213
222
  meta = Object.entries(meta)
214
223
  .filter(([, value]) => value !== null && value !== undefined)
215
224
  .reduce((acc, [key, value]) => {
@@ -371,6 +380,47 @@ class Client {
371
380
  }
372
381
  }
373
382
  exports.Client = Client;
383
+ /**
384
+ * Walks the step tree and returns the set of artifact path/url values
385
+ * referenced by `step.artifacts` at any depth.
386
+ *
387
+ * @param {any} steps
388
+ * @returns {Set<string>}
389
+ */
390
+ function collectStepArtifactPaths(steps) {
391
+ const paths = new Set();
392
+ if (!Array.isArray(steps))
393
+ return paths;
394
+ const walk = arr => {
395
+ for (const step of arr) {
396
+ if (!step)
397
+ continue;
398
+ if (Array.isArray(step.artifacts)) {
399
+ for (const a of step.artifacts) {
400
+ if (typeof a === 'string')
401
+ paths.add(a);
402
+ else if (a && typeof a === 'object' && typeof a.path === 'string')
403
+ paths.add(a.path);
404
+ }
405
+ }
406
+ if (Array.isArray(step.steps))
407
+ walk(step.steps);
408
+ }
409
+ };
410
+ walk(steps);
411
+ return paths;
412
+ }
413
+ /**
414
+ * @param {string|{path?: string}|null|undefined} item
415
+ * @param {Set<string>} paths
416
+ * @returns {boolean}
417
+ */
418
+ function isStepArtifact(item, paths) {
419
+ if (!item)
420
+ return false;
421
+ const p = typeof item === 'object' ? item.path : item;
422
+ return typeof p === 'string' && paths.has(p);
423
+ }
374
424
  /**
375
425
  *
376
426
  * @param {TestData} testData
@@ -111,7 +111,6 @@ class MarkdownPipe {
111
111
  executionDate: getCurrentDateTimeFormatted(),
112
112
  tests: aggregated,
113
113
  stats,
114
- envVars: collectEnvironmentVariables(),
115
114
  };
116
115
  const md = renderDocument(data);
117
116
  fs_1.default.writeFileSync(outputPath, md, 'utf-8');
@@ -136,7 +135,6 @@ function renderDocument(data) {
136
135
  const sections = [];
137
136
  sections.push(renderHeader(data));
138
137
  sections.push(renderRunMetadata(data));
139
- sections.push(renderEnvSection(data.envVars));
140
138
  sections.push(renderTests(data.tests));
141
139
  return sections.filter(Boolean).join('\n\n') + '\n';
142
140
  }
@@ -176,36 +174,6 @@ function renderRunMetadata(data) {
176
174
  }
177
175
  return lines.join('\n');
178
176
  }
179
- function renderEnvSection(envVars) {
180
- if (!envVars)
181
- return '';
182
- const blocks = ['## Environment'];
183
- const groups = [
184
- { title: 'Testomat.io variables', vars: envVars.testomatio || {} },
185
- { title: 'S3 variables', vars: envVars.s3 || {} },
186
- ];
187
- for (const group of groups) {
188
- const entries = Object.entries(group.vars).filter(([, v]) => v && v.isSet);
189
- if (!entries.length)
190
- continue;
191
- entries.sort((a, b) => a[0].localeCompare(b[0]));
192
- const lines = [];
193
- lines.push('<details>');
194
- lines.push(`<summary>${group.title} (${entries.length})</summary>`);
195
- lines.push('');
196
- lines.push('| Variable | Value |');
197
- lines.push('| -------- | ----- |');
198
- for (const [name, info] of entries) {
199
- lines.push(`| \`${name}\` | ${mdTableCell(String(info.value ?? ''))} |`);
200
- }
201
- lines.push('');
202
- lines.push('</details>');
203
- blocks.push(lines.join('\n'));
204
- }
205
- if (blocks.length === 1)
206
- return '';
207
- return blocks.join('\n\n');
208
- }
209
177
  function renderTests(tests) {
210
178
  if (!Array.isArray(tests) || tests.length === 0) {
211
179
  return '## Tests\n\n_No test results recorded._';
@@ -240,22 +208,27 @@ function renderTest(test) {
240
208
  displayStatus = 'todo';
241
209
  }
242
210
  const duration = formatStepDuration(test.run_time);
243
- let header = `#### ${mdInline(title)}`;
244
- if (duration)
245
- header += ` — ${duration}`;
211
+ const header = `#### ${mdInline(title)}`;
246
212
  const lines = [header];
247
- const meta = [`- **Status:** ${displayStatus}`];
213
+ const rows = [['Status', displayStatus]];
248
214
  const retries = computeRetries(test);
249
215
  if (retries.retryCount > 0) {
250
- let retryLine = `- **Retries:** ${retries.retryCount}`;
216
+ let v = String(retries.retryCount);
251
217
  if (retries.flaky)
252
- retryLine += ' (flaky)';
253
- meta.push(retryLine);
218
+ v += ' (flaky)';
219
+ rows.push(['Retries', v]);
220
+ }
221
+ if (duration) {
222
+ rows.push(['Duration', duration]);
254
223
  }
255
224
  if (typeof test.test_id === 'string' && test.test_id) {
256
- meta.push(`- **Test ID:** \`${test.test_id}\``);
225
+ rows.push(['Test ID', `\`${test.test_id}\``]);
257
226
  }
258
- lines.push(meta.join('\n'));
227
+ const metaTable = ['| Key | Value |', '| --- | ----- |'];
228
+ for (const [k, v] of rows) {
229
+ metaTable.push(`| ${k} | ${v} |`);
230
+ }
231
+ lines.push(metaTable.join('\n'));
259
232
  const stepsBlock = renderSteps(test);
260
233
  if (stepsBlock)
261
234
  lines.push(stepsBlock);
@@ -285,10 +258,27 @@ function renderSteps(test) {
285
258
  }
286
259
  if (typeof steps === 'string' && steps.trim()) {
287
260
  const cleaned = steps.replace((0, utils_js_1.ansiRegExp)(), '').trim();
261
+ const parts = cleaned
262
+ .split(/<br\s*\/?>|\r?\n/i)
263
+ .map(s => s.trim())
264
+ .filter(Boolean);
265
+ if (parts.length > 1) {
266
+ const bullets = parts.map(line => formatStringStepBullet(line)).join('\n');
267
+ return `**Steps**\n\n${bullets}`;
268
+ }
288
269
  return `**Steps**\n\n${fence(cleaned)}`;
289
270
  }
290
271
  return '';
291
272
  }
273
+ function formatStringStepBullet(line) {
274
+ const match = line.match(/^(.*?)[\sĀ ]+(\d+)\s*ms\s*$/i);
275
+ if (match) {
276
+ const title = mdInline(match[1].trim());
277
+ const dur = `${match[2]}ms`;
278
+ return `- ${title} _(${dur})_`;
279
+ }
280
+ return `- ${mdInline(line)}`;
281
+ }
292
282
  function renderStepTree(steps, depth) {
293
283
  const indent = ' '.repeat(depth);
294
284
  const lines = [];
@@ -400,7 +390,10 @@ function renderArtifacts(test) {
400
390
  }
401
391
  if (!items.length)
402
392
  return '';
403
- const lines = ['**Artifacts**', ''];
393
+ const lines = [];
394
+ lines.push('<details>');
395
+ lines.push(`<summary><strong>Artifacts</strong> (${items.length})</summary>`);
396
+ lines.push('');
404
397
  for (const item of items) {
405
398
  if (item.isImage) {
406
399
  lines.push(`- ![${mdInline(item.name)}](${item.href})`);
@@ -409,6 +402,8 @@ function renderArtifacts(test) {
409
402
  lines.push(`- [${mdInline(item.name)}](${item.href})`);
410
403
  }
411
404
  }
405
+ lines.push('');
406
+ lines.push('</details>');
412
407
  return lines.join('\n');
413
408
  }
414
409
  function normalizeArtifact(raw) {
@@ -507,11 +502,6 @@ function mdInline(text) {
507
502
  return '';
508
503
  return String(text).replace(/\r?\n/g, ' ').trim();
509
504
  }
510
- function mdTableCell(text) {
511
- if (text == null)
512
- return '';
513
- return String(text).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>').trim();
514
- }
515
505
  function formatStepDuration(value) {
516
506
  if (typeof value !== 'number' || Number.isNaN(value) || value <= 0)
517
507
  return '';
@@ -672,27 +662,4 @@ function aggregateTestRetries(tests) {
672
662
  });
673
663
  return aggregated;
674
664
  }
675
- const SENSITIVE_PATTERNS = [/TOKEN/, /SECRET/, /PASSWORD/, /KEY/, /^TESTOMATIO$/];
676
- function isSensitiveVarName(name) {
677
- return SENSITIVE_PATTERNS.some(re => re.test(name));
678
- }
679
- function collectEnvironmentVariables() {
680
- const groups = { testomatio: {}, s3: {} };
681
- for (const [name, value] of Object.entries(process.env)) {
682
- if (value === undefined)
683
- continue;
684
- let group = null;
685
- if (name === 'TESTOMATIO' || name.startsWith('TESTOMATIO_'))
686
- group = 'testomatio';
687
- else if (name.startsWith('S3_'))
688
- group = 's3';
689
- if (!group)
690
- continue;
691
- let displayValue = value;
692
- if (isSensitiveVarName(name))
693
- displayValue = '***';
694
- groups[group][name] = { value: displayValue, isSet: true };
695
- }
696
- return groups;
697
- }
698
665
  module.exports = MarkdownPipe;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.7.9-beta.1-markdown",
3
+ "version": "2.7.9-beta.2-markdown",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
package/src/client.js CHANGED
@@ -215,6 +215,10 @@ class Client {
215
215
  const { rid, error = null, steps: originalSteps, title, suite_title } = testData;
216
216
  let steps = originalSteps;
217
217
 
218
+ // Capture step artifact paths BEFORE uploadStepArtifacts mutates them to URLs,
219
+ // so we can exclude them from the test-level artifacts list later.
220
+ const stepArtifactPaths = collectStepArtifactPaths(steps);
221
+
218
222
  // Upload artifacts from steps
219
223
  try {
220
224
  await this.uploadStepArtifacts(steps, rid);
@@ -228,7 +232,6 @@ class Client {
228
232
  const {
229
233
  time = 0,
230
234
  example = null,
231
- files = [],
232
235
  filesBuffers = [],
233
236
  code = null,
234
237
  file,
@@ -236,11 +239,17 @@ class Client {
236
239
  test_id,
237
240
  timestamp,
238
241
  links,
239
- manuallyAttachedArtifacts,
240
242
  overwrite,
241
243
  tags,
242
244
  } = testData;
243
- let { message = '', meta = {} } = testData;
245
+ let { files = [], manuallyAttachedArtifacts, message = '', meta = {} } = testData;
246
+
247
+ if (stepArtifactPaths.size) {
248
+ files = files.filter(f => !isStepArtifact(f, stepArtifactPaths));
249
+ if (Array.isArray(manuallyAttachedArtifacts)) {
250
+ manuallyAttachedArtifacts = manuallyAttachedArtifacts.filter(a => !isStepArtifact(a, stepArtifactPaths));
251
+ }
252
+ }
244
253
 
245
254
  meta = Object.entries(meta)
246
255
  .filter(([, value]) => value !== null && value !== undefined)
@@ -450,6 +459,43 @@ class Client {
450
459
  }
451
460
  }
452
461
 
462
+ /**
463
+ * Walks the step tree and returns the set of artifact path/url values
464
+ * referenced by `step.artifacts` at any depth.
465
+ *
466
+ * @param {any} steps
467
+ * @returns {Set<string>}
468
+ */
469
+ function collectStepArtifactPaths(steps) {
470
+ const paths = new Set();
471
+ if (!Array.isArray(steps)) return paths;
472
+ const walk = arr => {
473
+ for (const step of arr) {
474
+ if (!step) continue;
475
+ if (Array.isArray(step.artifacts)) {
476
+ for (const a of step.artifacts) {
477
+ if (typeof a === 'string') paths.add(a);
478
+ else if (a && typeof a === 'object' && typeof a.path === 'string') paths.add(a.path);
479
+ }
480
+ }
481
+ if (Array.isArray(step.steps)) walk(step.steps);
482
+ }
483
+ };
484
+ walk(steps);
485
+ return paths;
486
+ }
487
+
488
+ /**
489
+ * @param {string|{path?: string}|null|undefined} item
490
+ * @param {Set<string>} paths
491
+ * @returns {boolean}
492
+ */
493
+ function isStepArtifact(item, paths) {
494
+ if (!item) return false;
495
+ const p = typeof item === 'object' ? item.path : item;
496
+ return typeof p === 'string' && paths.has(p);
497
+ }
498
+
453
499
  /**
454
500
  *
455
501
  * @param {TestData} testData
@@ -133,7 +133,6 @@ class MarkdownPipe {
133
133
  executionDate: getCurrentDateTimeFormatted(),
134
134
  tests: aggregated,
135
135
  stats,
136
- envVars: collectEnvironmentVariables(),
137
136
  };
138
137
 
139
138
  const md = renderDocument(data);
@@ -163,7 +162,6 @@ function renderDocument(data) {
163
162
  const sections = [];
164
163
  sections.push(renderHeader(data));
165
164
  sections.push(renderRunMetadata(data));
166
- sections.push(renderEnvSection(data.envVars));
167
165
  sections.push(renderTests(data.tests));
168
166
  return sections.filter(Boolean).join('\n\n') + '\n';
169
167
  }
@@ -211,41 +209,6 @@ function renderRunMetadata(data) {
211
209
  return lines.join('\n');
212
210
  }
213
211
 
214
- function renderEnvSection(envVars) {
215
- if (!envVars) return '';
216
-
217
- const blocks = ['## Environment'];
218
-
219
- const groups = [
220
- { title: 'Testomat.io variables', vars: envVars.testomatio || {} },
221
- { title: 'S3 variables', vars: envVars.s3 || {} },
222
- ];
223
-
224
- for (const group of groups) {
225
- const entries = Object.entries(group.vars).filter(([, v]) => v && v.isSet);
226
- if (!entries.length) continue;
227
-
228
- entries.sort((a, b) => a[0].localeCompare(b[0]));
229
-
230
- const lines = [];
231
- lines.push('<details>');
232
- lines.push(`<summary>${group.title} (${entries.length})</summary>`);
233
- lines.push('');
234
- lines.push('| Variable | Value |');
235
- lines.push('| -------- | ----- |');
236
- for (const [name, info] of entries) {
237
- lines.push(`| \`${name}\` | ${mdTableCell(String(info.value ?? ''))} |`);
238
- }
239
- lines.push('');
240
- lines.push('</details>');
241
-
242
- blocks.push(lines.join('\n'));
243
- }
244
-
245
- if (blocks.length === 1) return '';
246
- return blocks.join('\n\n');
247
- }
248
-
249
212
  function renderTests(tests) {
250
213
  if (!Array.isArray(tests) || tests.length === 0) {
251
214
  return '## Tests\n\n_No test results recorded._';
@@ -286,23 +249,30 @@ function renderTest(test) {
286
249
  }
287
250
 
288
251
  const duration = formatStepDuration(test.run_time);
289
- let header = `#### ${mdInline(title)}`;
290
- if (duration) header += ` — ${duration}`;
252
+ const header = `#### ${mdInline(title)}`;
291
253
 
292
254
  const lines = [header];
293
255
 
294
- const meta = [`- **Status:** ${displayStatus}`];
256
+ const rows = [['Status', displayStatus]];
295
257
 
296
258
  const retries = computeRetries(test);
297
259
  if (retries.retryCount > 0) {
298
- let retryLine = `- **Retries:** ${retries.retryCount}`;
299
- if (retries.flaky) retryLine += ' (flaky)';
300
- meta.push(retryLine);
260
+ let v = String(retries.retryCount);
261
+ if (retries.flaky) v += ' (flaky)';
262
+ rows.push(['Retries', v]);
263
+ }
264
+ if (duration) {
265
+ rows.push(['Duration', duration]);
301
266
  }
302
267
  if (typeof test.test_id === 'string' && test.test_id) {
303
- meta.push(`- **Test ID:** \`${test.test_id}\``);
268
+ rows.push(['Test ID', `\`${test.test_id}\``]);
304
269
  }
305
- lines.push(meta.join('\n'));
270
+
271
+ const metaTable = ['| Key | Value |', '| --- | ----- |'];
272
+ for (const [k, v] of rows) {
273
+ metaTable.push(`| ${k} | ${v} |`);
274
+ }
275
+ lines.push(metaTable.join('\n'));
306
276
 
307
277
  const stepsBlock = renderSteps(test);
308
278
  if (stepsBlock) lines.push(stepsBlock);
@@ -335,12 +305,32 @@ function renderSteps(test) {
335
305
 
336
306
  if (typeof steps === 'string' && steps.trim()) {
337
307
  const cleaned = steps.replace(ansiRegExp(), '').trim();
308
+ const parts = cleaned
309
+ .split(/<br\s*\/?>|\r?\n/i)
310
+ .map(s => s.trim())
311
+ .filter(Boolean);
312
+
313
+ if (parts.length > 1) {
314
+ const bullets = parts.map(line => formatStringStepBullet(line)).join('\n');
315
+ return `**Steps**\n\n${bullets}`;
316
+ }
317
+
338
318
  return `**Steps**\n\n${fence(cleaned)}`;
339
319
  }
340
320
 
341
321
  return '';
342
322
  }
343
323
 
324
+ function formatStringStepBullet(line) {
325
+ const match = line.match(/^(.*?)[\sĀ ]+(\d+)\s*ms\s*$/i);
326
+ if (match) {
327
+ const title = mdInline(match[1].trim());
328
+ const dur = `${match[2]}ms`;
329
+ return `- ${title} _(${dur})_`;
330
+ }
331
+ return `- ${mdInline(line)}`;
332
+ }
333
+
344
334
  function renderStepTree(steps, depth) {
345
335
  const indent = ' '.repeat(depth);
346
336
  const lines = [];
@@ -445,7 +435,10 @@ function renderArtifacts(test) {
445
435
 
446
436
  if (!items.length) return '';
447
437
 
448
- const lines = ['**Artifacts**', ''];
438
+ const lines = [];
439
+ lines.push('<details>');
440
+ lines.push(`<summary><strong>Artifacts</strong> (${items.length})</summary>`);
441
+ lines.push('');
449
442
  for (const item of items) {
450
443
  if (item.isImage) {
451
444
  lines.push(`- ![${mdInline(item.name)}](${item.href})`);
@@ -453,6 +446,8 @@ function renderArtifacts(test) {
453
446
  lines.push(`- [${mdInline(item.name)}](${item.href})`);
454
447
  }
455
448
  }
449
+ lines.push('');
450
+ lines.push('</details>');
456
451
  return lines.join('\n');
457
452
  }
458
453
 
@@ -549,11 +544,6 @@ function mdInline(text) {
549
544
  return String(text).replace(/\r?\n/g, ' ').trim();
550
545
  }
551
546
 
552
- function mdTableCell(text) {
553
- if (text == null) return '';
554
- return String(text).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>').trim();
555
- }
556
-
557
547
  function formatStepDuration(value) {
558
548
  if (typeof value !== 'number' || Number.isNaN(value) || value <= 0) return '';
559
549
  if (value < 1000) return `${value}ms`;
@@ -714,30 +704,4 @@ function aggregateTestRetries(tests) {
714
704
  return aggregated;
715
705
  }
716
706
 
717
- const SENSITIVE_PATTERNS = [/TOKEN/, /SECRET/, /PASSWORD/, /KEY/, /^TESTOMATIO$/];
718
-
719
- function isSensitiveVarName(name) {
720
- return SENSITIVE_PATTERNS.some(re => re.test(name));
721
- }
722
-
723
- function collectEnvironmentVariables() {
724
- const groups = { testomatio: {}, s3: {} };
725
-
726
- for (const [name, value] of Object.entries(process.env)) {
727
- if (value === undefined) continue;
728
-
729
- let group = null;
730
- if (name === 'TESTOMATIO' || name.startsWith('TESTOMATIO_')) group = 'testomatio';
731
- else if (name.startsWith('S3_')) group = 's3';
732
- if (!group) continue;
733
-
734
- let displayValue = value;
735
- if (isSensitiveVarName(name)) displayValue = '***';
736
-
737
- groups[group][name] = { value: displayValue, isSet: true };
738
- }
739
-
740
- return groups;
741
- }
742
-
743
707
  export default MarkdownPipe;