@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 +1 -0
- package/lib/client.js +52 -2
- package/lib/pipe/markdown.js +37 -70
- package/package.json +1 -1
- package/src/client.js +49 -3
- package/src/pipe/markdown.js +41 -77
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,
|
|
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
|
package/lib/pipe/markdown.js
CHANGED
|
@@ -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
|
-
|
|
244
|
-
if (duration)
|
|
245
|
-
header += ` ā ${duration}`;
|
|
211
|
+
const header = `#### ${mdInline(title)}`;
|
|
246
212
|
const lines = [header];
|
|
247
|
-
const
|
|
213
|
+
const rows = [['Status', displayStatus]];
|
|
248
214
|
const retries = computeRetries(test);
|
|
249
215
|
if (retries.retryCount > 0) {
|
|
250
|
-
let
|
|
216
|
+
let v = String(retries.retryCount);
|
|
251
217
|
if (retries.flaky)
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
225
|
+
rows.push(['Test ID', `\`${test.test_id}\``]);
|
|
257
226
|
}
|
|
258
|
-
|
|
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 = [
|
|
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(`- `);
|
|
@@ -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
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
|
package/src/pipe/markdown.js
CHANGED
|
@@ -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
|
-
|
|
290
|
-
if (duration) header += ` ā ${duration}`;
|
|
252
|
+
const header = `#### ${mdInline(title)}`;
|
|
291
253
|
|
|
292
254
|
const lines = [header];
|
|
293
255
|
|
|
294
|
-
const
|
|
256
|
+
const rows = [['Status', displayStatus]];
|
|
295
257
|
|
|
296
258
|
const retries = computeRetries(test);
|
|
297
259
|
if (retries.retryCount > 0) {
|
|
298
|
-
let
|
|
299
|
-
if (retries.flaky)
|
|
300
|
-
|
|
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
|
-
|
|
268
|
+
rows.push(['Test ID', `\`${test.test_id}\``]);
|
|
304
269
|
}
|
|
305
|
-
|
|
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 = [
|
|
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(`- `);
|
|
@@ -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;
|