@testomatio/reporter 2.7.9-beta.2-markdown → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +16 -30
- 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 +44 -2
- 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 +13 -26
- package/src/pipe/html.js +35 -2
- package/src/pipe/markdown.js +41 -2
- package/src/pipe/testomatio.js +10 -1
- package/src/template/testomatio.hbs +153 -0
package/lib/client.js
CHANGED
|
@@ -214,9 +214,10 @@ class Client {
|
|
|
214
214
|
const { time = 0, example = null, filesBuffers = [], code = null, file, suite_id, test_id, timestamp, links, overwrite, tags, } = testData;
|
|
215
215
|
let { files = [], manuallyAttachedArtifacts, message = '', meta = {} } = testData;
|
|
216
216
|
if (stepArtifactPaths.size) {
|
|
217
|
-
|
|
217
|
+
const isStepArtifact = item => stepArtifactPaths.has(typeof item === 'object' ? item?.path : item);
|
|
218
|
+
files = files.filter(f => !isStepArtifact(f));
|
|
218
219
|
if (Array.isArray(manuallyAttachedArtifacts)) {
|
|
219
|
-
manuallyAttachedArtifacts = manuallyAttachedArtifacts.filter(a => !isStepArtifact(a
|
|
220
|
+
manuallyAttachedArtifacts = manuallyAttachedArtifacts.filter(a => !isStepArtifact(a));
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
meta = Object.entries(meta)
|
|
@@ -385,42 +386,27 @@ exports.Client = Client;
|
|
|
385
386
|
* referenced by `step.artifacts` at any depth.
|
|
386
387
|
*
|
|
387
388
|
* @param {any} steps
|
|
389
|
+
* @param {Set<string>} [paths]
|
|
388
390
|
* @returns {Set<string>}
|
|
389
391
|
*/
|
|
390
|
-
function collectStepArtifactPaths(steps) {
|
|
391
|
-
const paths = new Set();
|
|
392
|
+
function collectStepArtifactPaths(steps, paths = new Set()) {
|
|
392
393
|
if (!Array.isArray(steps))
|
|
393
394
|
return paths;
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
paths.add(a.path);
|
|
404
|
-
}
|
|
395
|
+
for (const step of steps) {
|
|
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);
|
|
405
404
|
}
|
|
406
|
-
if (Array.isArray(step.steps))
|
|
407
|
-
walk(step.steps);
|
|
408
405
|
}
|
|
409
|
-
|
|
410
|
-
|
|
406
|
+
collectStepArtifactPaths(step.steps, paths);
|
|
407
|
+
}
|
|
411
408
|
return paths;
|
|
412
409
|
}
|
|
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
|
-
}
|
|
424
410
|
/**
|
|
425
411
|
*
|
|
426
412
|
* @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,6 +112,8 @@ 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
119
|
};
|
|
@@ -135,9 +140,46 @@ function renderDocument(data) {
|
|
|
135
140
|
const sections = [];
|
|
136
141
|
sections.push(renderHeader(data));
|
|
137
142
|
sections.push(renderRunMetadata(data));
|
|
143
|
+
sections.push(renderDescription(data.description));
|
|
144
|
+
sections.push(renderConfiguration(data.configuration));
|
|
138
145
|
sections.push(renderTests(data.tests));
|
|
139
146
|
return sections.filter(Boolean).join('\n\n') + '\n';
|
|
140
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
|
+
}
|
|
141
183
|
function renderHeader(data) {
|
|
142
184
|
const overall = String(data.status || 'unknown').toLowerCase();
|
|
143
185
|
const headline = `# ${data.title} — ${overall.toUpperCase()}`;
|
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.
|
|
3
|
+
"version": "2.8.0",
|
|
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
|
@@ -245,9 +245,10 @@ class Client {
|
|
|
245
245
|
let { files = [], manuallyAttachedArtifacts, message = '', meta = {} } = testData;
|
|
246
246
|
|
|
247
247
|
if (stepArtifactPaths.size) {
|
|
248
|
-
|
|
248
|
+
const isStepArtifact = item => stepArtifactPaths.has(typeof item === 'object' ? item?.path : item);
|
|
249
|
+
files = files.filter(f => !isStepArtifact(f));
|
|
249
250
|
if (Array.isArray(manuallyAttachedArtifacts)) {
|
|
250
|
-
manuallyAttachedArtifacts = manuallyAttachedArtifacts.filter(a => !isStepArtifact(a
|
|
251
|
+
manuallyAttachedArtifacts = manuallyAttachedArtifacts.filter(a => !isStepArtifact(a));
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
254
|
|
|
@@ -464,38 +465,24 @@ class Client {
|
|
|
464
465
|
* referenced by `step.artifacts` at any depth.
|
|
465
466
|
*
|
|
466
467
|
* @param {any} steps
|
|
468
|
+
* @param {Set<string>} [paths]
|
|
467
469
|
* @returns {Set<string>}
|
|
468
470
|
*/
|
|
469
|
-
function collectStepArtifactPaths(steps) {
|
|
470
|
-
const paths = new Set();
|
|
471
|
+
function collectStepArtifactPaths(steps, paths = new Set()) {
|
|
471
472
|
if (!Array.isArray(steps)) return paths;
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
else if (a && typeof a === 'object' && typeof a.path === 'string') paths.add(a.path);
|
|
479
|
-
}
|
|
473
|
+
for (const step of steps) {
|
|
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);
|
|
480
479
|
}
|
|
481
|
-
if (Array.isArray(step.steps)) walk(step.steps);
|
|
482
480
|
}
|
|
483
|
-
|
|
484
|
-
|
|
481
|
+
collectStepArtifactPaths(step.steps, paths);
|
|
482
|
+
}
|
|
485
483
|
return paths;
|
|
486
484
|
}
|
|
487
485
|
|
|
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
|
-
|
|
499
486
|
/**
|
|
500
487
|
*
|
|
501
488
|
* @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,6 +134,8 @@ 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
141
|
};
|
|
@@ -162,10 +167,44 @@ function renderDocument(data) {
|
|
|
162
167
|
const sections = [];
|
|
163
168
|
sections.push(renderHeader(data));
|
|
164
169
|
sections.push(renderRunMetadata(data));
|
|
170
|
+
sections.push(renderDescription(data.description));
|
|
171
|
+
sections.push(renderConfiguration(data.configuration));
|
|
165
172
|
sections.push(renderTests(data.tests));
|
|
166
173
|
return sections.filter(Boolean).join('\n\n') + '\n';
|
|
167
174
|
}
|
|
168
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
|
+
|
|
169
208
|
function renderHeader(data) {
|
|
170
209
|
const overall = String(data.status || 'unknown').toLowerCase();
|
|
171
210
|
const headline = `# ${data.title} — ${overall.toUpperCase()}`;
|
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'>
|