@testomatio/reporter 2.7.9-beta.1-markdown → 2.7.9-beta.3-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/html.d.ts +2 -1
- package/lib/pipe/html.js +35 -2
- package/lib/pipe/markdown.d.ts +2 -1
- package/lib/pipe/markdown.js +81 -72
- package/lib/pipe/testomatio.d.ts +2 -1
- package/lib/pipe/testomatio.js +9 -1
- package/lib/template/testomatio.hbs +153 -0
- package/package.json +2 -1
- package/src/client.js +49 -3
- package/src/pipe/html.js +35 -2
- package/src/pipe/markdown.js +82 -79
- package/src/pipe/testomatio.js +10 -1
- package/src/template/testomatio.hbs +153 -0
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/html.d.ts
CHANGED
|
@@ -9,11 +9,12 @@ declare class HtmlPipe {
|
|
|
9
9
|
htmlOutputPath: string;
|
|
10
10
|
filenameMsg: string;
|
|
11
11
|
tests: any[];
|
|
12
|
+
configuration: any;
|
|
12
13
|
htmlReportDir: string;
|
|
13
14
|
htmlReportName: string;
|
|
14
15
|
templateFolderPath: string;
|
|
15
16
|
templateHtmlPath: string;
|
|
16
|
-
createRun(): Promise<void>;
|
|
17
|
+
createRun(params?: {}): Promise<void>;
|
|
17
18
|
prepareRun(): Promise<void>;
|
|
18
19
|
updateRun(): void;
|
|
19
20
|
/**
|
package/lib/pipe/html.js
CHANGED
|
@@ -9,6 +9,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
11
11
|
const handlebars_1 = __importDefault(require("handlebars"));
|
|
12
|
+
const marked_1 = require("marked");
|
|
12
13
|
const file_url_1 = __importDefault(require("file-url"));
|
|
13
14
|
const utils_js_1 = require("../utils/utils.js");
|
|
14
15
|
const constants_js_1 = require("../constants.js");
|
|
@@ -26,6 +27,7 @@ class HtmlPipe {
|
|
|
26
27
|
this.htmlOutputPath = '';
|
|
27
28
|
this.filenameMsg = '';
|
|
28
29
|
this.tests = [];
|
|
30
|
+
this.configuration = null;
|
|
29
31
|
if (this.isHtml) {
|
|
30
32
|
this.isEnabled = true;
|
|
31
33
|
this.htmlReportDir = process.env.TESTOMATIO_HTML_REPORT_FOLDER || constants_js_1.HTML_REPORT.FOLDER;
|
|
@@ -49,8 +51,10 @@ class HtmlPipe {
|
|
|
49
51
|
debug(picocolors_1.default.yellow('HTML Pipe:'), `Save HTML report: ${this.isEnabled}`, `HTML report folder: ${this.htmlReportDir}, report name: ${this.htmlReportName}`);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
|
-
async createRun() {
|
|
53
|
-
|
|
54
|
+
async createRun(params = {}) {
|
|
55
|
+
if (params?.configuration && typeof params.configuration === 'object') {
|
|
56
|
+
this.configuration = { ...(this.configuration || {}), ...params.configuration };
|
|
57
|
+
}
|
|
54
58
|
}
|
|
55
59
|
async prepareRun() { }
|
|
56
60
|
updateRun() {
|
|
@@ -200,6 +204,8 @@ class HtmlPipe {
|
|
|
200
204
|
runUrl: this.store.runUrl || '',
|
|
201
205
|
executionTime: testExecutionSumTime(aggregatedTests),
|
|
202
206
|
executionDate: getCurrentDateTimeFormatted(),
|
|
207
|
+
description: runParams.description || this.store.coverageDescription || this.store.description || '',
|
|
208
|
+
configuration: buildDisplayConfiguration(this.configuration || this.store.configuration || runParams.configuration || null),
|
|
203
209
|
tests: aggregatedTests,
|
|
204
210
|
envVars: collectEnvironmentVariables(),
|
|
205
211
|
};
|
|
@@ -245,6 +251,11 @@ class HtmlPipe {
|
|
|
245
251
|
}
|
|
246
252
|
#loadReportHelpers() {
|
|
247
253
|
handlebars_1.default.registerHelper('getTestsByStatus', (tests, status) => tests.filter(test => test.status.toLowerCase() === status.toLowerCase()).length);
|
|
254
|
+
handlebars_1.default.registerHelper('markdown', value => {
|
|
255
|
+
if (typeof value !== 'string' || !value.trim())
|
|
256
|
+
return '';
|
|
257
|
+
return new handlebars_1.default.SafeString(marked_1.marked.parse(value, { async: false }));
|
|
258
|
+
});
|
|
248
259
|
handlebars_1.default.registerHelper('formatDuration', milliseconds => {
|
|
249
260
|
if (!milliseconds || milliseconds === 0)
|
|
250
261
|
return '0ms';
|
|
@@ -948,4 +959,26 @@ function loadTracesFromFiles(test) {
|
|
|
948
959
|
}
|
|
949
960
|
}
|
|
950
961
|
}
|
|
962
|
+
function buildDisplayConfiguration(configuration) {
|
|
963
|
+
if (!configuration || typeof configuration !== 'object')
|
|
964
|
+
return null;
|
|
965
|
+
const entries = Object.entries(configuration).filter(([k]) => k !== 'tests' && k !== 'suites');
|
|
966
|
+
if (!entries.length)
|
|
967
|
+
return null;
|
|
968
|
+
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
969
|
+
return entries.map(([key, value]) => ({ key, value: formatConfigDisplayValue(value) }));
|
|
970
|
+
}
|
|
971
|
+
function formatConfigDisplayValue(value) {
|
|
972
|
+
if (value == null)
|
|
973
|
+
return '';
|
|
974
|
+
if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
|
|
975
|
+
return String(value);
|
|
976
|
+
}
|
|
977
|
+
try {
|
|
978
|
+
return JSON.stringify(value);
|
|
979
|
+
}
|
|
980
|
+
catch (_) {
|
|
981
|
+
return String(value);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
951
984
|
module.exports = HtmlPipe;
|
package/lib/pipe/markdown.d.ts
CHANGED
|
@@ -9,9 +9,10 @@ declare class MarkdownPipe {
|
|
|
9
9
|
markdownOutputPath: string;
|
|
10
10
|
filenameMsg: string;
|
|
11
11
|
tests: any[];
|
|
12
|
+
configuration: any;
|
|
12
13
|
markdownReportDir: string;
|
|
13
14
|
markdownReportName: string;
|
|
14
|
-
createRun(): Promise<void>;
|
|
15
|
+
createRun(params?: {}): Promise<void>;
|
|
15
16
|
prepareRun(): Promise<void>;
|
|
16
17
|
updateRun(): void;
|
|
17
18
|
/**
|
package/lib/pipe/markdown.js
CHANGED
|
@@ -24,6 +24,7 @@ class MarkdownPipe {
|
|
|
24
24
|
this.markdownOutputPath = '';
|
|
25
25
|
this.filenameMsg = '';
|
|
26
26
|
this.tests = [];
|
|
27
|
+
this.configuration = null;
|
|
27
28
|
if (!this.isMarkdown)
|
|
28
29
|
return;
|
|
29
30
|
this.isEnabled = true;
|
|
@@ -45,8 +46,10 @@ class MarkdownPipe {
|
|
|
45
46
|
utils_js_1.fileSystem.createDir(this.markdownReportDir);
|
|
46
47
|
debug(picocolors_1.default.yellow('Markdown Pipe:'), `Save Markdown report: ${this.isEnabled}`, `Markdown report folder: ${this.markdownReportDir}, report name: ${this.markdownReportName}`);
|
|
47
48
|
}
|
|
48
|
-
async createRun() {
|
|
49
|
-
|
|
49
|
+
async createRun(params = {}) {
|
|
50
|
+
if (params?.configuration && typeof params.configuration === 'object') {
|
|
51
|
+
this.configuration = { ...(this.configuration || {}), ...params.configuration };
|
|
52
|
+
}
|
|
50
53
|
}
|
|
51
54
|
async prepareRun() { }
|
|
52
55
|
updateRun() {
|
|
@@ -109,9 +112,10 @@ class MarkdownPipe {
|
|
|
109
112
|
isParallel: runParams?.isParallel,
|
|
110
113
|
executionTime: testExecutionSumTime(aggregated),
|
|
111
114
|
executionDate: getCurrentDateTimeFormatted(),
|
|
115
|
+
description: runParams?.description || this.store.coverageDescription || this.store.description || '',
|
|
116
|
+
configuration: this.configuration || this.store.configuration || runParams?.configuration || null,
|
|
112
117
|
tests: aggregated,
|
|
113
118
|
stats,
|
|
114
|
-
envVars: collectEnvironmentVariables(),
|
|
115
119
|
};
|
|
116
120
|
const md = renderDocument(data);
|
|
117
121
|
fs_1.default.writeFileSync(outputPath, md, 'utf-8');
|
|
@@ -136,10 +140,46 @@ function renderDocument(data) {
|
|
|
136
140
|
const sections = [];
|
|
137
141
|
sections.push(renderHeader(data));
|
|
138
142
|
sections.push(renderRunMetadata(data));
|
|
139
|
-
sections.push(
|
|
143
|
+
sections.push(renderDescription(data.description));
|
|
144
|
+
sections.push(renderConfiguration(data.configuration));
|
|
140
145
|
sections.push(renderTests(data.tests));
|
|
141
146
|
return sections.filter(Boolean).join('\n\n') + '\n';
|
|
142
147
|
}
|
|
148
|
+
function renderDescription(description) {
|
|
149
|
+
if (typeof description !== 'string')
|
|
150
|
+
return '';
|
|
151
|
+
const trimmed = description.trim();
|
|
152
|
+
if (!trimmed)
|
|
153
|
+
return '';
|
|
154
|
+
return `## Description\n\n${trimmed}`;
|
|
155
|
+
}
|
|
156
|
+
function renderConfiguration(configuration) {
|
|
157
|
+
if (!configuration || typeof configuration !== 'object')
|
|
158
|
+
return '';
|
|
159
|
+
const entries = Object.entries(configuration).filter(([k]) => k !== 'tests' && k !== 'suites');
|
|
160
|
+
if (!entries.length)
|
|
161
|
+
return '';
|
|
162
|
+
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
163
|
+
const lines = ['## Configuration', '', '| Key | Value |', '| --- | ----- |'];
|
|
164
|
+
for (const [k, v] of entries) {
|
|
165
|
+
lines.push(`| \`${k}\` | ${formatConfigValue(v)} |`);
|
|
166
|
+
}
|
|
167
|
+
return lines.join('\n');
|
|
168
|
+
}
|
|
169
|
+
function formatConfigValue(value) {
|
|
170
|
+
if (value == null)
|
|
171
|
+
return '';
|
|
172
|
+
if (typeof value === 'boolean' || typeof value === 'number')
|
|
173
|
+
return String(value);
|
|
174
|
+
if (typeof value === 'string')
|
|
175
|
+
return mdInline(value).replace(/\|/g, '\\|');
|
|
176
|
+
try {
|
|
177
|
+
return `\`${JSON.stringify(value)}\``;
|
|
178
|
+
}
|
|
179
|
+
catch (_) {
|
|
180
|
+
return String(value);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
143
183
|
function renderHeader(data) {
|
|
144
184
|
const overall = String(data.status || 'unknown').toLowerCase();
|
|
145
185
|
const headline = `# ${data.title} — ${overall.toUpperCase()}`;
|
|
@@ -176,36 +216,6 @@ function renderRunMetadata(data) {
|
|
|
176
216
|
}
|
|
177
217
|
return lines.join('\n');
|
|
178
218
|
}
|
|
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
219
|
function renderTests(tests) {
|
|
210
220
|
if (!Array.isArray(tests) || tests.length === 0) {
|
|
211
221
|
return '## Tests\n\n_No test results recorded._';
|
|
@@ -240,22 +250,27 @@ function renderTest(test) {
|
|
|
240
250
|
displayStatus = 'todo';
|
|
241
251
|
}
|
|
242
252
|
const duration = formatStepDuration(test.run_time);
|
|
243
|
-
|
|
244
|
-
if (duration)
|
|
245
|
-
header += ` — ${duration}`;
|
|
253
|
+
const header = `#### ${mdInline(title)}`;
|
|
246
254
|
const lines = [header];
|
|
247
|
-
const
|
|
255
|
+
const rows = [['Status', displayStatus]];
|
|
248
256
|
const retries = computeRetries(test);
|
|
249
257
|
if (retries.retryCount > 0) {
|
|
250
|
-
let
|
|
258
|
+
let v = String(retries.retryCount);
|
|
251
259
|
if (retries.flaky)
|
|
252
|
-
|
|
253
|
-
|
|
260
|
+
v += ' (flaky)';
|
|
261
|
+
rows.push(['Retries', v]);
|
|
262
|
+
}
|
|
263
|
+
if (duration) {
|
|
264
|
+
rows.push(['Duration', duration]);
|
|
254
265
|
}
|
|
255
266
|
if (typeof test.test_id === 'string' && test.test_id) {
|
|
256
|
-
|
|
267
|
+
rows.push(['Test ID', `\`${test.test_id}\``]);
|
|
257
268
|
}
|
|
258
|
-
|
|
269
|
+
const metaTable = ['| Key | Value |', '| --- | ----- |'];
|
|
270
|
+
for (const [k, v] of rows) {
|
|
271
|
+
metaTable.push(`| ${k} | ${v} |`);
|
|
272
|
+
}
|
|
273
|
+
lines.push(metaTable.join('\n'));
|
|
259
274
|
const stepsBlock = renderSteps(test);
|
|
260
275
|
if (stepsBlock)
|
|
261
276
|
lines.push(stepsBlock);
|
|
@@ -285,10 +300,27 @@ function renderSteps(test) {
|
|
|
285
300
|
}
|
|
286
301
|
if (typeof steps === 'string' && steps.trim()) {
|
|
287
302
|
const cleaned = steps.replace((0, utils_js_1.ansiRegExp)(), '').trim();
|
|
303
|
+
const parts = cleaned
|
|
304
|
+
.split(/<br\s*\/?>|\r?\n/i)
|
|
305
|
+
.map(s => s.trim())
|
|
306
|
+
.filter(Boolean);
|
|
307
|
+
if (parts.length > 1) {
|
|
308
|
+
const bullets = parts.map(line => formatStringStepBullet(line)).join('\n');
|
|
309
|
+
return `**Steps**\n\n${bullets}`;
|
|
310
|
+
}
|
|
288
311
|
return `**Steps**\n\n${fence(cleaned)}`;
|
|
289
312
|
}
|
|
290
313
|
return '';
|
|
291
314
|
}
|
|
315
|
+
function formatStringStepBullet(line) {
|
|
316
|
+
const match = line.match(/^(.*?)[\s ]+(\d+)\s*ms\s*$/i);
|
|
317
|
+
if (match) {
|
|
318
|
+
const title = mdInline(match[1].trim());
|
|
319
|
+
const dur = `${match[2]}ms`;
|
|
320
|
+
return `- ${title} _(${dur})_`;
|
|
321
|
+
}
|
|
322
|
+
return `- ${mdInline(line)}`;
|
|
323
|
+
}
|
|
292
324
|
function renderStepTree(steps, depth) {
|
|
293
325
|
const indent = ' '.repeat(depth);
|
|
294
326
|
const lines = [];
|
|
@@ -400,7 +432,10 @@ function renderArtifacts(test) {
|
|
|
400
432
|
}
|
|
401
433
|
if (!items.length)
|
|
402
434
|
return '';
|
|
403
|
-
const lines = [
|
|
435
|
+
const lines = [];
|
|
436
|
+
lines.push('<details>');
|
|
437
|
+
lines.push(`<summary><strong>Artifacts</strong> (${items.length})</summary>`);
|
|
438
|
+
lines.push('');
|
|
404
439
|
for (const item of items) {
|
|
405
440
|
if (item.isImage) {
|
|
406
441
|
lines.push(`- `);
|
|
@@ -409,6 +444,8 @@ function renderArtifacts(test) {
|
|
|
409
444
|
lines.push(`- [${mdInline(item.name)}](${item.href})`);
|
|
410
445
|
}
|
|
411
446
|
}
|
|
447
|
+
lines.push('');
|
|
448
|
+
lines.push('</details>');
|
|
412
449
|
return lines.join('\n');
|
|
413
450
|
}
|
|
414
451
|
function normalizeArtifact(raw) {
|
|
@@ -507,11 +544,6 @@ function mdInline(text) {
|
|
|
507
544
|
return '';
|
|
508
545
|
return String(text).replace(/\r?\n/g, ' ').trim();
|
|
509
546
|
}
|
|
510
|
-
function mdTableCell(text) {
|
|
511
|
-
if (text == null)
|
|
512
|
-
return '';
|
|
513
|
-
return String(text).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>').trim();
|
|
514
|
-
}
|
|
515
547
|
function formatStepDuration(value) {
|
|
516
548
|
if (typeof value !== 'number' || Number.isNaN(value) || value <= 0)
|
|
517
549
|
return '';
|
|
@@ -672,27 +704,4 @@ function aggregateTestRetries(tests) {
|
|
|
672
704
|
});
|
|
673
705
|
return aggregated;
|
|
674
706
|
}
|
|
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
707
|
module.exports = MarkdownPipe;
|
package/lib/pipe/testomatio.d.ts
CHANGED
|
@@ -47,12 +47,13 @@ declare class TestomatioPipe implements Pipe {
|
|
|
47
47
|
prepareRun(opts: any): Promise<string[]>;
|
|
48
48
|
/**
|
|
49
49
|
* Creates a new run on Testomat.io
|
|
50
|
-
* @param {{isBatchEnabled?: boolean, kind?: string}} params
|
|
50
|
+
* @param {{isBatchEnabled?: boolean, kind?: string, configuration?: Record<string, any>}} params
|
|
51
51
|
* @returns Promise<void>
|
|
52
52
|
*/
|
|
53
53
|
createRun(params?: {
|
|
54
54
|
isBatchEnabled?: boolean;
|
|
55
55
|
kind?: string;
|
|
56
|
+
configuration?: Record<string, any>;
|
|
56
57
|
}): Promise<void>;
|
|
57
58
|
runUrl: string;
|
|
58
59
|
runPublicUrl: any;
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -164,7 +164,7 @@ class TestomatioPipe {
|
|
|
164
164
|
}
|
|
165
165
|
/**
|
|
166
166
|
* Creates a new run on Testomat.io
|
|
167
|
-
* @param {{isBatchEnabled?: boolean, kind?: string}} params
|
|
167
|
+
* @param {{isBatchEnabled?: boolean, kind?: string, configuration?: Record<string, any>}} params
|
|
168
168
|
* @returns Promise<void>
|
|
169
169
|
*/
|
|
170
170
|
async createRun(params = {}) {
|
|
@@ -202,6 +202,14 @@ class TestomatioPipe {
|
|
|
202
202
|
suites: coverageConfiguration.suites?.map(id => id.replace(/^S/, '')) || [],
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
|
+
// Merge caller-supplied configuration (e.g. { exploratory: true }) into runParams.configuration.
|
|
206
|
+
// Caller values win on key conflict; coverage-derived tests/suites lists are preserved when not overridden.
|
|
207
|
+
if (params.configuration && typeof params.configuration === 'object') {
|
|
208
|
+
configuration = { ...(configuration || {}), ...params.configuration };
|
|
209
|
+
if (this.store) {
|
|
210
|
+
this.store.configuration = { ...(this.store.configuration || {}), ...params.configuration };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
205
213
|
const runParams = Object.fromEntries(Object.entries({
|
|
206
214
|
ci_build_url: buildUrl,
|
|
207
215
|
api_key: this.apiKey.trim(),
|
|
@@ -163,6 +163,126 @@
|
|
|
163
163
|
transform: none;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
.description-section {
|
|
167
|
+
padding: 24px 30px 0;
|
|
168
|
+
background: var(--gray-50);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.description-card {
|
|
172
|
+
background: white;
|
|
173
|
+
border: 1px solid var(--gray-200);
|
|
174
|
+
border-radius: var(--border-radius);
|
|
175
|
+
padding: 20px 24px;
|
|
176
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.description-title {
|
|
180
|
+
font-size: 16px;
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
color: var(--gray-700);
|
|
183
|
+
margin: 0 0 12px;
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
gap: 8px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.description-title i { color: var(--gray-500); }
|
|
190
|
+
|
|
191
|
+
.markdown-body { color: var(--gray-700); line-height: 1.55; font-size: 14px; }
|
|
192
|
+
.markdown-body p { margin: 0 0 10px; }
|
|
193
|
+
.markdown-body p:last-child { margin-bottom: 0; }
|
|
194
|
+
.markdown-body h1,
|
|
195
|
+
.markdown-body h2,
|
|
196
|
+
.markdown-body h3 {
|
|
197
|
+
margin: 14px 0 8px;
|
|
198
|
+
color: var(--gray-800);
|
|
199
|
+
line-height: 1.3;
|
|
200
|
+
}
|
|
201
|
+
.markdown-body h1 { font-size: 20px; }
|
|
202
|
+
.markdown-body h2 { font-size: 17px; }
|
|
203
|
+
.markdown-body h3 { font-size: 15px; }
|
|
204
|
+
.markdown-body ul,
|
|
205
|
+
.markdown-body ol { margin: 6px 0 10px 22px; padding: 0; }
|
|
206
|
+
.markdown-body li { margin: 2px 0; }
|
|
207
|
+
.markdown-body code {
|
|
208
|
+
background: var(--gray-100);
|
|
209
|
+
border-radius: 4px;
|
|
210
|
+
padding: 1px 5px;
|
|
211
|
+
font-size: 12.5px;
|
|
212
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
213
|
+
}
|
|
214
|
+
.markdown-body pre {
|
|
215
|
+
background: var(--gray-100);
|
|
216
|
+
border-radius: 6px;
|
|
217
|
+
padding: 10px 12px;
|
|
218
|
+
overflow-x: auto;
|
|
219
|
+
margin: 8px 0;
|
|
220
|
+
}
|
|
221
|
+
.markdown-body pre code { background: transparent; padding: 0; }
|
|
222
|
+
.markdown-body a { color: var(--primary, #2563eb); text-decoration: underline; }
|
|
223
|
+
.markdown-body blockquote {
|
|
224
|
+
margin: 8px 0;
|
|
225
|
+
padding: 4px 12px;
|
|
226
|
+
border-left: 3px solid var(--gray-300);
|
|
227
|
+
color: var(--gray-600);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.configuration-section {
|
|
231
|
+
padding: 16px 30px 0;
|
|
232
|
+
background: var(--gray-50);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.configuration-card {
|
|
236
|
+
background: white;
|
|
237
|
+
border: 1px solid var(--gray-200);
|
|
238
|
+
border-radius: var(--border-radius);
|
|
239
|
+
padding: 20px 24px;
|
|
240
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.configuration-title {
|
|
244
|
+
font-size: 16px;
|
|
245
|
+
font-weight: 600;
|
|
246
|
+
color: var(--gray-700);
|
|
247
|
+
margin: 0 0 12px;
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
gap: 8px;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.configuration-title i { color: var(--gray-500); }
|
|
254
|
+
|
|
255
|
+
.configuration-table {
|
|
256
|
+
width: 100%;
|
|
257
|
+
border-collapse: collapse;
|
|
258
|
+
font-size: 14px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.configuration-table th,
|
|
262
|
+
.configuration-table td {
|
|
263
|
+
padding: 6px 12px;
|
|
264
|
+
text-align: left;
|
|
265
|
+
border-bottom: 1px solid var(--gray-100);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.configuration-table tr:last-child th,
|
|
269
|
+
.configuration-table tr:last-child td { border-bottom: 0; }
|
|
270
|
+
|
|
271
|
+
.configuration-table th {
|
|
272
|
+
font-weight: 600;
|
|
273
|
+
color: var(--gray-600);
|
|
274
|
+
width: 30%;
|
|
275
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
276
|
+
font-size: 12.5px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.configuration-table td {
|
|
280
|
+
color: var(--gray-800);
|
|
281
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
282
|
+
font-size: 12.5px;
|
|
283
|
+
word-break: break-word;
|
|
284
|
+
}
|
|
285
|
+
|
|
166
286
|
.stats-section {
|
|
167
287
|
padding: 30px;
|
|
168
288
|
background: var(--gray-50);
|
|
@@ -2078,6 +2198,39 @@
|
|
|
2078
2198
|
</div>
|
|
2079
2199
|
</header>
|
|
2080
2200
|
|
|
2201
|
+
{{#if description}}
|
|
2202
|
+
<section class='description-section'>
|
|
2203
|
+
<div class='description-card'>
|
|
2204
|
+
<h2 class='description-title'>
|
|
2205
|
+
<i class='fas fa-info-circle'></i>
|
|
2206
|
+
Description
|
|
2207
|
+
</h2>
|
|
2208
|
+
<div class='description-body markdown-body'>{{markdown description}}</div>
|
|
2209
|
+
</div>
|
|
2210
|
+
</section>
|
|
2211
|
+
{{/if}}
|
|
2212
|
+
|
|
2213
|
+
{{#if configuration}}
|
|
2214
|
+
<section class='configuration-section'>
|
|
2215
|
+
<div class='configuration-card'>
|
|
2216
|
+
<h2 class='configuration-title'>
|
|
2217
|
+
<i class='fas fa-sliders-h'></i>
|
|
2218
|
+
Configuration
|
|
2219
|
+
</h2>
|
|
2220
|
+
<table class='configuration-table'>
|
|
2221
|
+
<tbody>
|
|
2222
|
+
{{#each configuration}}
|
|
2223
|
+
<tr>
|
|
2224
|
+
<th>{{this.key}}</th>
|
|
2225
|
+
<td>{{this.value}}</td>
|
|
2226
|
+
</tr>
|
|
2227
|
+
{{/each}}
|
|
2228
|
+
</tbody>
|
|
2229
|
+
</table>
|
|
2230
|
+
</div>
|
|
2231
|
+
</section>
|
|
2232
|
+
{{/if}}
|
|
2233
|
+
|
|
2081
2234
|
<section class='stats-section'>
|
|
2082
2235
|
<div class='stats-grid'>
|
|
2083
2236
|
<div class='chart-container'>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.7.9-beta.
|
|
3
|
+
"version": "2.7.9-beta.3-markdown",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"json-cycle": "^1.3.0",
|
|
37
37
|
"lodash.memoize": "^4.1.2",
|
|
38
38
|
"lodash.merge": "^4.6.2",
|
|
39
|
+
"marked": "^14.1.4",
|
|
39
40
|
"minimatch": "^10.2.4",
|
|
40
41
|
"picocolors": "^1.0.1",
|
|
41
42
|
"pretty-ms": "^7.0.1",
|
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/html.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import handlebars from 'handlebars';
|
|
7
|
+
import { marked } from 'marked';
|
|
7
8
|
import fileUrl from 'file-url';
|
|
8
9
|
import { fileSystem, isSameTest, ansiRegExp, formatStep } from '../utils/utils.js';
|
|
9
10
|
import { HTML_REPORT } from '../constants.js';
|
|
@@ -27,6 +28,7 @@ class HtmlPipe {
|
|
|
27
28
|
this.htmlOutputPath = '';
|
|
28
29
|
this.filenameMsg = '';
|
|
29
30
|
this.tests = [];
|
|
31
|
+
this.configuration = null;
|
|
30
32
|
|
|
31
33
|
if (this.isHtml) {
|
|
32
34
|
this.isEnabled = true;
|
|
@@ -61,8 +63,10 @@ class HtmlPipe {
|
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
async createRun() {
|
|
65
|
-
|
|
66
|
+
async createRun(params = {}) {
|
|
67
|
+
if (params?.configuration && typeof params.configuration === 'object') {
|
|
68
|
+
this.configuration = { ...(this.configuration || {}), ...params.configuration };
|
|
69
|
+
}
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
async prepareRun() {}
|
|
@@ -250,6 +254,10 @@ class HtmlPipe {
|
|
|
250
254
|
runUrl: this.store.runUrl || '',
|
|
251
255
|
executionTime: testExecutionSumTime(aggregatedTests),
|
|
252
256
|
executionDate: getCurrentDateTimeFormatted(),
|
|
257
|
+
description: runParams.description || this.store.coverageDescription || this.store.description || '',
|
|
258
|
+
configuration: buildDisplayConfiguration(
|
|
259
|
+
this.configuration || this.store.configuration || runParams.configuration || null,
|
|
260
|
+
),
|
|
253
261
|
tests: aggregatedTests,
|
|
254
262
|
envVars: collectEnvironmentVariables(),
|
|
255
263
|
};
|
|
@@ -304,6 +312,11 @@ class HtmlPipe {
|
|
|
304
312
|
(tests, status) => tests.filter(test => test.status.toLowerCase() === status.toLowerCase()).length,
|
|
305
313
|
);
|
|
306
314
|
|
|
315
|
+
handlebars.registerHelper('markdown', value => {
|
|
316
|
+
if (typeof value !== 'string' || !value.trim()) return '';
|
|
317
|
+
return new handlebars.SafeString(marked.parse(value, { async: false }));
|
|
318
|
+
});
|
|
319
|
+
|
|
307
320
|
handlebars.registerHelper('formatDuration', milliseconds => {
|
|
308
321
|
if (!milliseconds || milliseconds === 0) return '0ms';
|
|
309
322
|
|
|
@@ -1110,4 +1123,24 @@ function loadTracesFromFiles(test) {
|
|
|
1110
1123
|
}
|
|
1111
1124
|
}
|
|
1112
1125
|
|
|
1126
|
+
function buildDisplayConfiguration(configuration) {
|
|
1127
|
+
if (!configuration || typeof configuration !== 'object') return null;
|
|
1128
|
+
const entries = Object.entries(configuration).filter(([k]) => k !== 'tests' && k !== 'suites');
|
|
1129
|
+
if (!entries.length) return null;
|
|
1130
|
+
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
1131
|
+
return entries.map(([key, value]) => ({ key, value: formatConfigDisplayValue(value) }));
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function formatConfigDisplayValue(value) {
|
|
1135
|
+
if (value == null) return '';
|
|
1136
|
+
if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
|
|
1137
|
+
return String(value);
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
return JSON.stringify(value);
|
|
1141
|
+
} catch (_) {
|
|
1142
|
+
return String(value);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1113
1146
|
export default HtmlPipe;
|
package/src/pipe/markdown.js
CHANGED
|
@@ -24,6 +24,7 @@ class MarkdownPipe {
|
|
|
24
24
|
this.markdownOutputPath = '';
|
|
25
25
|
this.filenameMsg = '';
|
|
26
26
|
this.tests = [];
|
|
27
|
+
this.configuration = null;
|
|
27
28
|
|
|
28
29
|
if (!this.isMarkdown) return;
|
|
29
30
|
|
|
@@ -52,8 +53,10 @@ class MarkdownPipe {
|
|
|
52
53
|
);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
async createRun() {
|
|
56
|
-
|
|
56
|
+
async createRun(params = {}) {
|
|
57
|
+
if (params?.configuration && typeof params.configuration === 'object') {
|
|
58
|
+
this.configuration = { ...(this.configuration || {}), ...params.configuration };
|
|
59
|
+
}
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
async prepareRun() {}
|
|
@@ -131,9 +134,10 @@ class MarkdownPipe {
|
|
|
131
134
|
isParallel: runParams?.isParallel,
|
|
132
135
|
executionTime: testExecutionSumTime(aggregated),
|
|
133
136
|
executionDate: getCurrentDateTimeFormatted(),
|
|
137
|
+
description: runParams?.description || this.store.coverageDescription || this.store.description || '',
|
|
138
|
+
configuration: this.configuration || this.store.configuration || runParams?.configuration || null,
|
|
134
139
|
tests: aggregated,
|
|
135
140
|
stats,
|
|
136
|
-
envVars: collectEnvironmentVariables(),
|
|
137
141
|
};
|
|
138
142
|
|
|
139
143
|
const md = renderDocument(data);
|
|
@@ -163,11 +167,44 @@ function renderDocument(data) {
|
|
|
163
167
|
const sections = [];
|
|
164
168
|
sections.push(renderHeader(data));
|
|
165
169
|
sections.push(renderRunMetadata(data));
|
|
166
|
-
sections.push(
|
|
170
|
+
sections.push(renderDescription(data.description));
|
|
171
|
+
sections.push(renderConfiguration(data.configuration));
|
|
167
172
|
sections.push(renderTests(data.tests));
|
|
168
173
|
return sections.filter(Boolean).join('\n\n') + '\n';
|
|
169
174
|
}
|
|
170
175
|
|
|
176
|
+
function renderDescription(description) {
|
|
177
|
+
if (typeof description !== 'string') return '';
|
|
178
|
+
const trimmed = description.trim();
|
|
179
|
+
if (!trimmed) return '';
|
|
180
|
+
return `## Description\n\n${trimmed}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderConfiguration(configuration) {
|
|
184
|
+
if (!configuration || typeof configuration !== 'object') return '';
|
|
185
|
+
const entries = Object.entries(configuration).filter(([k]) => k !== 'tests' && k !== 'suites');
|
|
186
|
+
if (!entries.length) return '';
|
|
187
|
+
|
|
188
|
+
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
189
|
+
|
|
190
|
+
const lines = ['## Configuration', '', '| Key | Value |', '| --- | ----- |'];
|
|
191
|
+
for (const [k, v] of entries) {
|
|
192
|
+
lines.push(`| \`${k}\` | ${formatConfigValue(v)} |`);
|
|
193
|
+
}
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatConfigValue(value) {
|
|
198
|
+
if (value == null) return '';
|
|
199
|
+
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
200
|
+
if (typeof value === 'string') return mdInline(value).replace(/\|/g, '\\|');
|
|
201
|
+
try {
|
|
202
|
+
return `\`${JSON.stringify(value)}\``;
|
|
203
|
+
} catch (_) {
|
|
204
|
+
return String(value);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
171
208
|
function renderHeader(data) {
|
|
172
209
|
const overall = String(data.status || 'unknown').toLowerCase();
|
|
173
210
|
const headline = `# ${data.title} — ${overall.toUpperCase()}`;
|
|
@@ -211,41 +248,6 @@ function renderRunMetadata(data) {
|
|
|
211
248
|
return lines.join('\n');
|
|
212
249
|
}
|
|
213
250
|
|
|
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
251
|
function renderTests(tests) {
|
|
250
252
|
if (!Array.isArray(tests) || tests.length === 0) {
|
|
251
253
|
return '## Tests\n\n_No test results recorded._';
|
|
@@ -286,23 +288,30 @@ function renderTest(test) {
|
|
|
286
288
|
}
|
|
287
289
|
|
|
288
290
|
const duration = formatStepDuration(test.run_time);
|
|
289
|
-
|
|
290
|
-
if (duration) header += ` — ${duration}`;
|
|
291
|
+
const header = `#### ${mdInline(title)}`;
|
|
291
292
|
|
|
292
293
|
const lines = [header];
|
|
293
294
|
|
|
294
|
-
const
|
|
295
|
+
const rows = [['Status', displayStatus]];
|
|
295
296
|
|
|
296
297
|
const retries = computeRetries(test);
|
|
297
298
|
if (retries.retryCount > 0) {
|
|
298
|
-
let
|
|
299
|
-
if (retries.flaky)
|
|
300
|
-
|
|
299
|
+
let v = String(retries.retryCount);
|
|
300
|
+
if (retries.flaky) v += ' (flaky)';
|
|
301
|
+
rows.push(['Retries', v]);
|
|
302
|
+
}
|
|
303
|
+
if (duration) {
|
|
304
|
+
rows.push(['Duration', duration]);
|
|
301
305
|
}
|
|
302
306
|
if (typeof test.test_id === 'string' && test.test_id) {
|
|
303
|
-
|
|
307
|
+
rows.push(['Test ID', `\`${test.test_id}\``]);
|
|
304
308
|
}
|
|
305
|
-
|
|
309
|
+
|
|
310
|
+
const metaTable = ['| Key | Value |', '| --- | ----- |'];
|
|
311
|
+
for (const [k, v] of rows) {
|
|
312
|
+
metaTable.push(`| ${k} | ${v} |`);
|
|
313
|
+
}
|
|
314
|
+
lines.push(metaTable.join('\n'));
|
|
306
315
|
|
|
307
316
|
const stepsBlock = renderSteps(test);
|
|
308
317
|
if (stepsBlock) lines.push(stepsBlock);
|
|
@@ -335,12 +344,32 @@ function renderSteps(test) {
|
|
|
335
344
|
|
|
336
345
|
if (typeof steps === 'string' && steps.trim()) {
|
|
337
346
|
const cleaned = steps.replace(ansiRegExp(), '').trim();
|
|
347
|
+
const parts = cleaned
|
|
348
|
+
.split(/<br\s*\/?>|\r?\n/i)
|
|
349
|
+
.map(s => s.trim())
|
|
350
|
+
.filter(Boolean);
|
|
351
|
+
|
|
352
|
+
if (parts.length > 1) {
|
|
353
|
+
const bullets = parts.map(line => formatStringStepBullet(line)).join('\n');
|
|
354
|
+
return `**Steps**\n\n${bullets}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
338
357
|
return `**Steps**\n\n${fence(cleaned)}`;
|
|
339
358
|
}
|
|
340
359
|
|
|
341
360
|
return '';
|
|
342
361
|
}
|
|
343
362
|
|
|
363
|
+
function formatStringStepBullet(line) {
|
|
364
|
+
const match = line.match(/^(.*?)[\s ]+(\d+)\s*ms\s*$/i);
|
|
365
|
+
if (match) {
|
|
366
|
+
const title = mdInline(match[1].trim());
|
|
367
|
+
const dur = `${match[2]}ms`;
|
|
368
|
+
return `- ${title} _(${dur})_`;
|
|
369
|
+
}
|
|
370
|
+
return `- ${mdInline(line)}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
344
373
|
function renderStepTree(steps, depth) {
|
|
345
374
|
const indent = ' '.repeat(depth);
|
|
346
375
|
const lines = [];
|
|
@@ -445,7 +474,10 @@ function renderArtifacts(test) {
|
|
|
445
474
|
|
|
446
475
|
if (!items.length) return '';
|
|
447
476
|
|
|
448
|
-
const lines = [
|
|
477
|
+
const lines = [];
|
|
478
|
+
lines.push('<details>');
|
|
479
|
+
lines.push(`<summary><strong>Artifacts</strong> (${items.length})</summary>`);
|
|
480
|
+
lines.push('');
|
|
449
481
|
for (const item of items) {
|
|
450
482
|
if (item.isImage) {
|
|
451
483
|
lines.push(`- `);
|
|
@@ -453,6 +485,8 @@ function renderArtifacts(test) {
|
|
|
453
485
|
lines.push(`- [${mdInline(item.name)}](${item.href})`);
|
|
454
486
|
}
|
|
455
487
|
}
|
|
488
|
+
lines.push('');
|
|
489
|
+
lines.push('</details>');
|
|
456
490
|
return lines.join('\n');
|
|
457
491
|
}
|
|
458
492
|
|
|
@@ -549,11 +583,6 @@ function mdInline(text) {
|
|
|
549
583
|
return String(text).replace(/\r?\n/g, ' ').trim();
|
|
550
584
|
}
|
|
551
585
|
|
|
552
|
-
function mdTableCell(text) {
|
|
553
|
-
if (text == null) return '';
|
|
554
|
-
return String(text).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>').trim();
|
|
555
|
-
}
|
|
556
|
-
|
|
557
586
|
function formatStepDuration(value) {
|
|
558
587
|
if (typeof value !== 'number' || Number.isNaN(value) || value <= 0) return '';
|
|
559
588
|
if (value < 1000) return `${value}ms`;
|
|
@@ -714,30 +743,4 @@ function aggregateTestRetries(tests) {
|
|
|
714
743
|
return aggregated;
|
|
715
744
|
}
|
|
716
745
|
|
|
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
746
|
export default MarkdownPipe;
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -194,7 +194,7 @@ class TestomatioPipe {
|
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
196
|
* Creates a new run on Testomat.io
|
|
197
|
-
* @param {{isBatchEnabled?: boolean, kind?: string}} params
|
|
197
|
+
* @param {{isBatchEnabled?: boolean, kind?: string, configuration?: Record<string, any>}} params
|
|
198
198
|
* @returns Promise<void>
|
|
199
199
|
*/
|
|
200
200
|
async createRun(params = {}) {
|
|
@@ -236,6 +236,15 @@ class TestomatioPipe {
|
|
|
236
236
|
suites: coverageConfiguration.suites?.map(id => id.replace(/^S/, '')) || [],
|
|
237
237
|
};
|
|
238
238
|
}
|
|
239
|
+
|
|
240
|
+
// Merge caller-supplied configuration (e.g. { exploratory: true }) into runParams.configuration.
|
|
241
|
+
// Caller values win on key conflict; coverage-derived tests/suites lists are preserved when not overridden.
|
|
242
|
+
if (params.configuration && typeof params.configuration === 'object') {
|
|
243
|
+
configuration = { ...(configuration || {}), ...params.configuration };
|
|
244
|
+
if (this.store) {
|
|
245
|
+
this.store.configuration = { ...(this.store.configuration || {}), ...params.configuration };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
239
248
|
const runParams = Object.fromEntries(
|
|
240
249
|
Object.entries({
|
|
241
250
|
ci_build_url: buildUrl,
|
|
@@ -163,6 +163,126 @@
|
|
|
163
163
|
transform: none;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
.description-section {
|
|
167
|
+
padding: 24px 30px 0;
|
|
168
|
+
background: var(--gray-50);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.description-card {
|
|
172
|
+
background: white;
|
|
173
|
+
border: 1px solid var(--gray-200);
|
|
174
|
+
border-radius: var(--border-radius);
|
|
175
|
+
padding: 20px 24px;
|
|
176
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.description-title {
|
|
180
|
+
font-size: 16px;
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
color: var(--gray-700);
|
|
183
|
+
margin: 0 0 12px;
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
gap: 8px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.description-title i { color: var(--gray-500); }
|
|
190
|
+
|
|
191
|
+
.markdown-body { color: var(--gray-700); line-height: 1.55; font-size: 14px; }
|
|
192
|
+
.markdown-body p { margin: 0 0 10px; }
|
|
193
|
+
.markdown-body p:last-child { margin-bottom: 0; }
|
|
194
|
+
.markdown-body h1,
|
|
195
|
+
.markdown-body h2,
|
|
196
|
+
.markdown-body h3 {
|
|
197
|
+
margin: 14px 0 8px;
|
|
198
|
+
color: var(--gray-800);
|
|
199
|
+
line-height: 1.3;
|
|
200
|
+
}
|
|
201
|
+
.markdown-body h1 { font-size: 20px; }
|
|
202
|
+
.markdown-body h2 { font-size: 17px; }
|
|
203
|
+
.markdown-body h3 { font-size: 15px; }
|
|
204
|
+
.markdown-body ul,
|
|
205
|
+
.markdown-body ol { margin: 6px 0 10px 22px; padding: 0; }
|
|
206
|
+
.markdown-body li { margin: 2px 0; }
|
|
207
|
+
.markdown-body code {
|
|
208
|
+
background: var(--gray-100);
|
|
209
|
+
border-radius: 4px;
|
|
210
|
+
padding: 1px 5px;
|
|
211
|
+
font-size: 12.5px;
|
|
212
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
213
|
+
}
|
|
214
|
+
.markdown-body pre {
|
|
215
|
+
background: var(--gray-100);
|
|
216
|
+
border-radius: 6px;
|
|
217
|
+
padding: 10px 12px;
|
|
218
|
+
overflow-x: auto;
|
|
219
|
+
margin: 8px 0;
|
|
220
|
+
}
|
|
221
|
+
.markdown-body pre code { background: transparent; padding: 0; }
|
|
222
|
+
.markdown-body a { color: var(--primary, #2563eb); text-decoration: underline; }
|
|
223
|
+
.markdown-body blockquote {
|
|
224
|
+
margin: 8px 0;
|
|
225
|
+
padding: 4px 12px;
|
|
226
|
+
border-left: 3px solid var(--gray-300);
|
|
227
|
+
color: var(--gray-600);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.configuration-section {
|
|
231
|
+
padding: 16px 30px 0;
|
|
232
|
+
background: var(--gray-50);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.configuration-card {
|
|
236
|
+
background: white;
|
|
237
|
+
border: 1px solid var(--gray-200);
|
|
238
|
+
border-radius: var(--border-radius);
|
|
239
|
+
padding: 20px 24px;
|
|
240
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.configuration-title {
|
|
244
|
+
font-size: 16px;
|
|
245
|
+
font-weight: 600;
|
|
246
|
+
color: var(--gray-700);
|
|
247
|
+
margin: 0 0 12px;
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
gap: 8px;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.configuration-title i { color: var(--gray-500); }
|
|
254
|
+
|
|
255
|
+
.configuration-table {
|
|
256
|
+
width: 100%;
|
|
257
|
+
border-collapse: collapse;
|
|
258
|
+
font-size: 14px;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.configuration-table th,
|
|
262
|
+
.configuration-table td {
|
|
263
|
+
padding: 6px 12px;
|
|
264
|
+
text-align: left;
|
|
265
|
+
border-bottom: 1px solid var(--gray-100);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.configuration-table tr:last-child th,
|
|
269
|
+
.configuration-table tr:last-child td { border-bottom: 0; }
|
|
270
|
+
|
|
271
|
+
.configuration-table th {
|
|
272
|
+
font-weight: 600;
|
|
273
|
+
color: var(--gray-600);
|
|
274
|
+
width: 30%;
|
|
275
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
276
|
+
font-size: 12.5px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.configuration-table td {
|
|
280
|
+
color: var(--gray-800);
|
|
281
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
282
|
+
font-size: 12.5px;
|
|
283
|
+
word-break: break-word;
|
|
284
|
+
}
|
|
285
|
+
|
|
166
286
|
.stats-section {
|
|
167
287
|
padding: 30px;
|
|
168
288
|
background: var(--gray-50);
|
|
@@ -2078,6 +2198,39 @@
|
|
|
2078
2198
|
</div>
|
|
2079
2199
|
</header>
|
|
2080
2200
|
|
|
2201
|
+
{{#if description}}
|
|
2202
|
+
<section class='description-section'>
|
|
2203
|
+
<div class='description-card'>
|
|
2204
|
+
<h2 class='description-title'>
|
|
2205
|
+
<i class='fas fa-info-circle'></i>
|
|
2206
|
+
Description
|
|
2207
|
+
</h2>
|
|
2208
|
+
<div class='description-body markdown-body'>{{markdown description}}</div>
|
|
2209
|
+
</div>
|
|
2210
|
+
</section>
|
|
2211
|
+
{{/if}}
|
|
2212
|
+
|
|
2213
|
+
{{#if configuration}}
|
|
2214
|
+
<section class='configuration-section'>
|
|
2215
|
+
<div class='configuration-card'>
|
|
2216
|
+
<h2 class='configuration-title'>
|
|
2217
|
+
<i class='fas fa-sliders-h'></i>
|
|
2218
|
+
Configuration
|
|
2219
|
+
</h2>
|
|
2220
|
+
<table class='configuration-table'>
|
|
2221
|
+
<tbody>
|
|
2222
|
+
{{#each configuration}}
|
|
2223
|
+
<tr>
|
|
2224
|
+
<th>{{this.key}}</th>
|
|
2225
|
+
<td>{{this.value}}</td>
|
|
2226
|
+
</tr>
|
|
2227
|
+
{{/each}}
|
|
2228
|
+
</tbody>
|
|
2229
|
+
</table>
|
|
2230
|
+
</div>
|
|
2231
|
+
</section>
|
|
2232
|
+
{{/if}}
|
|
2233
|
+
|
|
2081
2234
|
<section class='stats-section'>
|
|
2082
2235
|
<div class='stats-grid'>
|
|
2083
2236
|
<div class='chart-container'>
|