@testomatio/reporter 2.11.0 → 2.13.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/adapter/playwright.js +7 -4
- package/lib/bin/cli.js +6 -1
- package/lib/client.d.ts +2 -2
- package/lib/client.js +1 -1
- package/lib/pipe/bitbucket.js +22 -17
- package/lib/pipe/github.js +23 -16
- package/lib/pipe/gitlab.js +20 -16
- package/lib/pipe/html.js +1 -1
- package/lib/pipe/markdown.js +1 -1
- package/lib/pipe/testomatio.js +10 -2
- package/lib/utils/pipe_utils.d.ts +44 -0
- package/lib/utils/pipe_utils.js +87 -0
- package/package.json +1 -1
- package/src/adapter/playwright.js +7 -6
- package/src/bin/cli.js +7 -1
- package/src/client.js +1 -1
- package/src/pipe/bitbucket.js +31 -29
- package/src/pipe/github.js +32 -30
- package/src/pipe/gitlab.js +29 -28
- package/src/pipe/html.js +1 -1
- package/src/pipe/markdown.js +1 -1
- package/src/pipe/testomatio.js +9 -2
- package/src/utils/pipe_utils.js +80 -0
- package/types/types.d.ts +5 -0
|
@@ -234,7 +234,8 @@ function appendStep(step, shift = 0) {
|
|
|
234
234
|
newCategory = 'hook';
|
|
235
235
|
break;
|
|
236
236
|
case 'attach':
|
|
237
|
-
|
|
237
|
+
case 'test.attach':
|
|
238
|
+
return null; // Attachments are reported as artifacts, not standalone steps
|
|
238
239
|
default:
|
|
239
240
|
newCategory = 'framework';
|
|
240
241
|
}
|
|
@@ -261,9 +262,11 @@ function appendStep(step, shift = 0) {
|
|
|
261
262
|
if (step.log) {
|
|
262
263
|
resultStep.log = (0, utils_js_1.truncate)(String(step.log), 250);
|
|
263
264
|
}
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
// Playwright also associates automatic failure screenshots with the active
|
|
266
|
+
// hook. Only attachments created for an explicit test.step belong to a step;
|
|
267
|
+
// hook and fixture attachments remain test-level result artifacts.
|
|
268
|
+
if (step.category === 'test.step' && step.attachments?.length && constants_js_1.SCREENSHOTS_ON_STEPS) {
|
|
269
|
+
const screenshotAttachment = step.attachments.find(isScreenshotArtifact);
|
|
267
270
|
if (screenshotAttachment && screenshotAttachment.path) {
|
|
268
271
|
const artifacts = { screenshot: screenshotAttachment.path };
|
|
269
272
|
(0, step_formatter_js_1.addArtifactsToStep)(resultStep, artifacts);
|
package/lib/bin/cli.js
CHANGED
|
@@ -62,7 +62,8 @@ program
|
|
|
62
62
|
log_js_1.log.info('Starting a new Run on Testomat.io...');
|
|
63
63
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
|
|
64
64
|
const client = new client_js_1.default({ apiKey });
|
|
65
|
-
|
|
65
|
+
// nothing is executed yet; the server flips it to running on the first reported test
|
|
66
|
+
const createRunParams = { status: 'scheduled' };
|
|
66
67
|
if (opts.kind)
|
|
67
68
|
createRunParams.kind = opts.kind;
|
|
68
69
|
if (opts.filter) {
|
|
@@ -84,6 +85,10 @@ program
|
|
|
84
85
|
log_js_1.log.error(picocolors_1.default.red('Failed to create run on Testomat.io.'));
|
|
85
86
|
process.exit(1);
|
|
86
87
|
}
|
|
88
|
+
// tests are executed later, so report the run as pending with the tests it was scoped to:
|
|
89
|
+
// pipes add their report now and replace it when the run is finished
|
|
90
|
+
const plannedTests = (client.pipeStore.preparedTestIds || []).map(id => ({ test_id: id, title: id }));
|
|
91
|
+
await client.updateRunStatus('pending', { tests: plannedTests });
|
|
87
92
|
// stdout carries ONLY the run id so it can be captured: RUN_ID=$(reporter start)
|
|
88
93
|
console.log(runId);
|
|
89
94
|
process.exit(0);
|
package/lib/client.d.ts
CHANGED
|
@@ -64,11 +64,11 @@ export class Client {
|
|
|
64
64
|
/**
|
|
65
65
|
*
|
|
66
66
|
* Updates the status of the current test run and finishes the run.
|
|
67
|
-
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
67
|
+
* @param {'passed' | 'failed' | 'skipped' | 'finished' | 'pending'} status - The status of the current test run.
|
|
68
68
|
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
69
69
|
* Must be one of "passed", "failed", or "finished"
|
|
70
70
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
71
71
|
*/
|
|
72
|
-
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished", params?: Partial<import("../types/types.js").RunData>): Promise<any>;
|
|
72
|
+
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished" | "pending", params?: Partial<import("../types/types.js").RunData>): Promise<any>;
|
|
73
73
|
}
|
|
74
74
|
import { S3Uploader } from './uploader.js';
|
package/lib/client.js
CHANGED
|
@@ -319,7 +319,7 @@ class Client {
|
|
|
319
319
|
/**
|
|
320
320
|
*
|
|
321
321
|
* Updates the status of the current test run and finishes the run.
|
|
322
|
-
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
322
|
+
* @param {'passed' | 'failed' | 'skipped' | 'finished' | 'pending'} status - The status of the current test run.
|
|
323
323
|
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
324
324
|
* Must be one of "passed", "failed", or "finished"
|
|
325
325
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
package/lib/pipe/bitbucket.js
CHANGED
|
@@ -118,24 +118,29 @@ class BitbucketPipe {
|
|
|
118
118
|
this.tests[i].stack = await this.cleanLog(this.tests[i].stack || '');
|
|
119
119
|
}
|
|
120
120
|
// Create a comment on Bitbucket
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
`;
|
|
121
|
+
// a scheduled run has no results yet: no counters, no duration
|
|
122
|
+
const isPendingRun = runParams.status === 'pending';
|
|
123
|
+
/** @type {Object<string, string>} */
|
|
124
|
+
const rows = {};
|
|
125
|
+
if (isPendingRun) {
|
|
126
|
+
if (this.tests.length)
|
|
127
|
+
rows.Tests = `⚪ ${(0, pipe_utils_js_1.plannedTestsLabel)(this.tests, this.store.runTestsCount)}`;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
rows.Tests = `✔️ **${this.tests.length}** tests run`;
|
|
131
|
+
rows.Summary = (0, pipe_utils_js_1.runSummary)(this.tests);
|
|
132
|
+
rows.Duration = `🕐 **${(0, pipe_utils_js_1.totalDuration)(this.tests)}**`;
|
|
133
|
+
}
|
|
135
134
|
if (this.ENV.BITBUCKET_BRANCH && this.ENV.BITBUCKET_COMMIT) {
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
const buildNumber = this.ENV.BITBUCKET_BUILD_NUMBER;
|
|
136
|
+
const buildUrl = `https://bitbucket.org/${this.ENV.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${buildNumber}`;
|
|
137
|
+
rows.Job = `👷 [#${buildNumber}](${buildUrl}) by commit: **${this.ENV.BITBUCKET_COMMIT}**`;
|
|
138
138
|
}
|
|
139
|
+
const header = [
|
|
140
|
+
``,
|
|
141
|
+
`${(0, pipe_utils_js_1.statusEmoji)(runParams.status)} ${runParams.status.toUpperCase()} ${(0, pipe_utils_js_1.statusEmoji)(runParams.status)}`,
|
|
142
|
+
];
|
|
143
|
+
const summary = `${this.hiddenCommentData}\n\n${(0, pipe_utils_js_1.markdownTable)(header, rows, { boldLabels: true })}`;
|
|
139
144
|
const failures = this.tests
|
|
140
145
|
.filter(t => t.status === 'failed')
|
|
141
146
|
.slice(0, 20)
|
|
@@ -178,7 +183,7 @@ class BitbucketPipe {
|
|
|
178
183
|
body += `\n> Notice: Only the first 10 failures are shown.`;
|
|
179
184
|
}
|
|
180
185
|
}
|
|
181
|
-
if (this.tests.length > 0) {
|
|
186
|
+
if (this.tests.length > 0 && !isPendingRun) {
|
|
182
187
|
body += `\n\n**🐢 Slowest Tests**\n\n`;
|
|
183
188
|
body += this.tests
|
|
184
189
|
.sort((a, b) => b.run_time - a.run_time)
|
package/lib/pipe/github.js
CHANGED
|
@@ -104,27 +104,34 @@ class GitHubPipe {
|
|
|
104
104
|
if (!(owner || repo))
|
|
105
105
|
return;
|
|
106
106
|
// ... create a comment on GitHub
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
107
|
+
// a scheduled run has no results yet: no counters, no duration
|
|
108
|
+
const isPendingRun = runParams.status === 'pending';
|
|
109
|
+
/** @type {Object<string, string>} */
|
|
110
|
+
const rows = {};
|
|
111
|
+
if (isPendingRun) {
|
|
112
|
+
if (this.tests.length)
|
|
113
|
+
rows.Tests = `⚪ ${(0, pipe_utils_js_1.plannedTestsLabel)(this.tests, this.store.runTestsCount)}`;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
rows.Tests = `✔️ **${this.tests.length}** tests run`;
|
|
117
|
+
rows.Summary = (0, pipe_utils_js_1.runSummary)(this.tests);
|
|
118
|
+
rows.Duration = `🕐 **${(0, pipe_utils_js_1.totalDuration)(this.tests)}**`;
|
|
119
|
+
}
|
|
119
120
|
if (this.store.runUrl) {
|
|
120
|
-
|
|
121
|
+
rows['Testomat.io Report'] = `📊 [Run #${this.store.runId}](${this.store.runUrl})`;
|
|
121
122
|
}
|
|
122
123
|
if (process.env.GITHUB_WORKFLOW) {
|
|
123
|
-
|
|
124
|
+
const server = process.env.GITHUB_SERVER_URL || 'https://github.com';
|
|
125
|
+
rows.Job = `🗂️ [${this.jobKey}](${server}/${this.repo}/actions/runs/${process.env.GITHUB_RUN_ID})`;
|
|
124
126
|
}
|
|
125
127
|
if (process.env.RUNNER_OS) {
|
|
126
|
-
|
|
128
|
+
rows['Operating System'] = `🖥️ \`${process.env.RUNNER_OS}\` ${process.env.RUNNER_ARCH || ''}`;
|
|
127
129
|
}
|
|
130
|
+
const header = [
|
|
131
|
+
`[](https://testomat.io)`,
|
|
132
|
+
`${(0, pipe_utils_js_1.statusEmoji)(runParams.status)} ${`${process.env.GITHUB_JOB} ${runParams.status}`.toUpperCase()}`,
|
|
133
|
+
];
|
|
134
|
+
const summary = `${this.hiddenCommentData}\n\n${(0, pipe_utils_js_1.markdownTable)(header, rows)}`;
|
|
128
135
|
const failures = this.tests
|
|
129
136
|
.filter(t => t.status === 'failed')
|
|
130
137
|
.slice(0, 20)
|
|
@@ -180,7 +187,7 @@ class GitHubPipe {
|
|
|
180
187
|
}
|
|
181
188
|
body += '\n\n</details>';
|
|
182
189
|
}
|
|
183
|
-
if (this.tests.length > 0) {
|
|
190
|
+
if (this.tests.length > 0 && !isPendingRun) {
|
|
184
191
|
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
185
192
|
body += this.tests
|
|
186
193
|
.sort((a, b) => b?.run_time - a?.run_time)
|
package/lib/pipe/gitlab.js
CHANGED
|
@@ -69,24 +69,28 @@ class GitLabPipe {
|
|
|
69
69
|
if (runParams.tests)
|
|
70
70
|
runParams.tests.forEach(t => this.addTest(t));
|
|
71
71
|
// ... create a comment on GitLab
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
`;
|
|
72
|
+
// a scheduled run has no results yet: no counters, no duration
|
|
73
|
+
const isPendingRun = runParams.status === 'pending';
|
|
74
|
+
/** @type {Object<string, string>} */
|
|
75
|
+
const rows = {};
|
|
76
|
+
if (isPendingRun) {
|
|
77
|
+
if (this.tests.length)
|
|
78
|
+
rows.Tests = `⚪ ${(0, pipe_utils_js_1.plannedTestsLabel)(this.tests, this.store.runTestsCount)}`;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
rows.Tests = `✔️ **${this.tests.length}** tests run`;
|
|
82
|
+
rows.Summary = (0, pipe_utils_js_1.runSummary)(this.tests);
|
|
83
|
+
rows.Duration = `🕐 **${(0, pipe_utils_js_1.totalDuration)(this.tests)}**`;
|
|
84
|
+
}
|
|
86
85
|
if (this.ENV.CI_JOB_NAME && this.ENV.CI_JOB_ID) {
|
|
87
86
|
// eslint-disable-next-line max-len
|
|
88
|
-
|
|
87
|
+
rows.Job = `👷 [${this.ENV.CI_JOB_ID}](${this.ENV.CI_JOB_URL})<br>Name: **${this.ENV.CI_JOB_NAME}**<br>Stage: **${this.ENV.CI_JOB_STAGE}**`;
|
|
89
88
|
}
|
|
89
|
+
const header = [
|
|
90
|
+
`[](https://testomat.io)`,
|
|
91
|
+
`${(0, pipe_utils_js_1.statusEmoji)(runParams.status)} ${runParams.status.toUpperCase()} ${(0, pipe_utils_js_1.statusEmoji)(runParams.status)}`,
|
|
92
|
+
];
|
|
93
|
+
const summary = `${this.hiddenCommentData}\n\n${(0, pipe_utils_js_1.markdownTable)(header, rows)}`;
|
|
90
94
|
const failures = this.tests
|
|
91
95
|
.filter(t => t.status === 'failed')
|
|
92
96
|
.slice(0, 20)
|
|
@@ -127,7 +131,7 @@ class GitLabPipe {
|
|
|
127
131
|
}
|
|
128
132
|
body += '\n\n</details>';
|
|
129
133
|
}
|
|
130
|
-
if (this.tests.length > 0) {
|
|
134
|
+
if (this.tests.length > 0 && !isPendingRun) {
|
|
131
135
|
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
132
136
|
body += this.tests
|
|
133
137
|
.sort((a, b) => b?.run_time - a?.run_time)
|
package/lib/pipe/html.js
CHANGED
|
@@ -216,7 +216,7 @@ class HtmlPipe {
|
|
|
216
216
|
runUrl: this.store.runUrl || '',
|
|
217
217
|
executionTime: testExecutionSumTime(aggregatedTests),
|
|
218
218
|
executionDate: getCurrentDateTimeFormatted(),
|
|
219
|
-
description: [runParams.description || this.store.coverageDescription || this.store.description
|
|
219
|
+
description: [this.description, runParams.description || this.store.coverageDescription || this.store.description]
|
|
220
220
|
.filter(Boolean)
|
|
221
221
|
.join('\n\n') || '',
|
|
222
222
|
configuration: buildDisplayConfiguration(this.configuration || this.store.configuration || runParams.configuration || null),
|
package/lib/pipe/markdown.js
CHANGED
|
@@ -117,7 +117,7 @@ class MarkdownPipe {
|
|
|
117
117
|
isParallel: runParams?.isParallel,
|
|
118
118
|
executionTime: testExecutionSumTime(aggregated),
|
|
119
119
|
executionDate: getCurrentDateTimeFormatted(),
|
|
120
|
-
description: [runParams?.description || this.store.coverageDescription || this.store.description
|
|
120
|
+
description: [this.description, runParams?.description || this.store.coverageDescription || this.store.description]
|
|
121
121
|
.filter(Boolean)
|
|
122
122
|
.join('\n\n') || '',
|
|
123
123
|
configuration: this.configuration || this.store.configuration || runParams?.configuration || null,
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -247,8 +247,9 @@ class TestomatioPipe {
|
|
|
247
247
|
suites: coverageConfiguration.suites?.map(id => id.replace(/^S/, '')) || [],
|
|
248
248
|
};
|
|
249
249
|
}
|
|
250
|
-
// Run description:
|
|
251
|
-
|
|
250
|
+
// Run description: the user-provided TESTOMATIO_DESCRIPTION with the coverage-derived block
|
|
251
|
+
// (if any) added after it. Neither overrides the other.
|
|
252
|
+
const description = [this.description, coverageDescription].filter(Boolean).join('\n\n') || null;
|
|
252
253
|
// Merge caller-supplied configuration (e.g. { exploratory: true }) into runParams.configuration.
|
|
253
254
|
// Caller values win on key conflict; coverage-derived tests/suites lists are preserved when not overridden.
|
|
254
255
|
if (params.configuration && typeof params.configuration === 'object') {
|
|
@@ -289,6 +290,7 @@ class TestomatioPipe {
|
|
|
289
290
|
shared_run: this.sharedRun,
|
|
290
291
|
shared_run_timeout: this.sharedRunTimeout,
|
|
291
292
|
kind: params.kind,
|
|
293
|
+
status: params.status,
|
|
292
294
|
configuration,
|
|
293
295
|
description,
|
|
294
296
|
ci,
|
|
@@ -335,6 +337,9 @@ class TestomatioPipe {
|
|
|
335
337
|
this.store.runUrl = this.runUrl;
|
|
336
338
|
this.store.runPublicUrl = this.runPublicUrl;
|
|
337
339
|
this.store.runId = this.runId;
|
|
340
|
+
// only the server knows how many tests a configuration expands to; automated runs report 0
|
|
341
|
+
if (resp.data.tests_count > 0)
|
|
342
|
+
this.store.runTestsCount = resp.data.tests_count;
|
|
338
343
|
log_js_1.log.info('📊 Report created. Report ID:', this.runId);
|
|
339
344
|
process.env.runId = this.runId;
|
|
340
345
|
debug('Run created', this.runId);
|
|
@@ -511,6 +516,9 @@ class TestomatioPipe {
|
|
|
511
516
|
console.warn(`${constants_js_1.APP_PREFIX} ${errorMessage}`);
|
|
512
517
|
}
|
|
513
518
|
const { status } = params;
|
|
519
|
+
// a pending run was just created: nothing to update here, only other pipes report it
|
|
520
|
+
if (status === 'pending')
|
|
521
|
+
return;
|
|
514
522
|
let status_event;
|
|
515
523
|
if (status === constants_js_1.STATUS.FINISHED)
|
|
516
524
|
status_event = 'finish';
|
|
@@ -39,6 +39,50 @@ export function statusEmoji(status: string): string;
|
|
|
39
39
|
* @returns {string} - A formatted full name string for the test object.
|
|
40
40
|
*/
|
|
41
41
|
export function fullName(t: object): string;
|
|
42
|
+
/**
|
|
43
|
+
* Render a two-column markdown table. Rows with an empty value are skipped, so optional rows
|
|
44
|
+
* need no surrounding `if`.
|
|
45
|
+
*
|
|
46
|
+
* @param {string[]} header - The two header cells.
|
|
47
|
+
* @param {Object<string, string>} rows - Label to value, rendered in insertion order.
|
|
48
|
+
* @param {{ boldLabels?: boolean }} [opts] - Set `boldLabels` to wrap every label in `**`.
|
|
49
|
+
* @returns {string} The table, with no trailing newline.
|
|
50
|
+
*/
|
|
51
|
+
export function markdownTable(header: string[], rows: {
|
|
52
|
+
[x: string]: string;
|
|
53
|
+
}, opts?: {
|
|
54
|
+
boldLabels?: boolean;
|
|
55
|
+
}): string;
|
|
56
|
+
/**
|
|
57
|
+
* Summarize a finished run, e.g. `🔴 **1** failed; 🟢 **8** passed; 🟡 **1** skipped`.
|
|
58
|
+
* The failed part is omitted when nothing failed.
|
|
59
|
+
*
|
|
60
|
+
* @param {Array<{status?: string}>} tests
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function runSummary(tests: Array<{
|
|
64
|
+
status?: string;
|
|
65
|
+
}>): string;
|
|
66
|
+
/**
|
|
67
|
+
* Total run time of the given tests, humanized — e.g. `2 seconds`.
|
|
68
|
+
*
|
|
69
|
+
* @param {Array<{run_time?: number}>} tests
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
export function totalDuration(tests: Array<{
|
|
73
|
+
run_time?: number;
|
|
74
|
+
}>): string;
|
|
75
|
+
/**
|
|
76
|
+
* Describe the scope of a run that was prepared but not executed yet. Prefers the server's count;
|
|
77
|
+
* without it, falls back to the scoped ids, which mix tests (`T…`) and suites (`S…`).
|
|
78
|
+
*
|
|
79
|
+
* @param {Array<{test_id?: string}>} tests - Prepared tests the run was scoped to.
|
|
80
|
+
* @param {number} [testsCount] - Real number of tests, as reported by Testomat.io.
|
|
81
|
+
* @returns {string} Markdown label, e.g. `**159** tests planned` or `**6** suites planned`.
|
|
82
|
+
*/
|
|
83
|
+
export function plannedTestsLabel(tests: Array<{
|
|
84
|
+
test_id?: string;
|
|
85
|
+
}>, testsCount?: number): string;
|
|
42
86
|
/**
|
|
43
87
|
* Parses a comma-separated list of key-value pairs into an options object.
|
|
44
88
|
*
|
package/lib/utils/pipe_utils.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.updateFilterType = updateFilterType;
|
|
4
7
|
exports.parseFilterParams = parseFilterParams;
|
|
@@ -6,10 +9,15 @@ exports.generateFilterRequestParams = generateFilterRequestParams;
|
|
|
6
9
|
exports.setS3Credentials = setS3Credentials;
|
|
7
10
|
exports.statusEmoji = statusEmoji;
|
|
8
11
|
exports.fullName = fullName;
|
|
12
|
+
exports.markdownTable = markdownTable;
|
|
13
|
+
exports.runSummary = runSummary;
|
|
14
|
+
exports.totalDuration = totalDuration;
|
|
15
|
+
exports.plannedTestsLabel = plannedTestsLabel;
|
|
9
16
|
exports.parsePipeOptions = parsePipeOptions;
|
|
10
17
|
exports.formatFilterListIds = formatFilterListIds;
|
|
11
18
|
exports.getObjectSize = getObjectSize;
|
|
12
19
|
exports.splitTestsIntoChunks = splitTestsIntoChunks;
|
|
20
|
+
const humanize_duration_1 = __importDefault(require("humanize-duration"));
|
|
13
21
|
const log_js_1 = require("./log.js");
|
|
14
22
|
/**
|
|
15
23
|
* Set S3 credentials from the provided artifacts object.
|
|
@@ -121,6 +129,8 @@ function statusEmoji(status) {
|
|
|
121
129
|
return '🔴';
|
|
122
130
|
if (status === 'skipped')
|
|
123
131
|
return '🟡';
|
|
132
|
+
if (status === 'pending')
|
|
133
|
+
return '🕐';
|
|
124
134
|
return '';
|
|
125
135
|
}
|
|
126
136
|
/**
|
|
@@ -223,6 +233,75 @@ function formatFilterListIds(ids, format) {
|
|
|
223
233
|
default: return ids.join(',');
|
|
224
234
|
}
|
|
225
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Summarize a finished run, e.g. `🔴 **1** failed; 🟢 **8** passed; 🟡 **1** skipped`.
|
|
238
|
+
* The failed part is omitted when nothing failed.
|
|
239
|
+
*
|
|
240
|
+
* @param {Array<{status?: string}>} tests
|
|
241
|
+
* @returns {string}
|
|
242
|
+
*/
|
|
243
|
+
function runSummary(tests) {
|
|
244
|
+
const countOf = status => tests.filter(t => t.status === status).length;
|
|
245
|
+
const failedCount = countOf('failed');
|
|
246
|
+
const parts = [];
|
|
247
|
+
if (failedCount)
|
|
248
|
+
parts.push(`${statusEmoji('failed')} **${failedCount}** failed`);
|
|
249
|
+
parts.push(`${statusEmoji('passed')} **${countOf('passed')}** passed`);
|
|
250
|
+
parts.push(`${statusEmoji('skipped')} **${countOf('skipped')}** skipped`);
|
|
251
|
+
return parts.join('; ');
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Total run time of the given tests, humanized — e.g. `2 seconds`.
|
|
255
|
+
*
|
|
256
|
+
* @param {Array<{run_time?: number}>} tests
|
|
257
|
+
* @returns {string}
|
|
258
|
+
*/
|
|
259
|
+
function totalDuration(tests) {
|
|
260
|
+
const milliseconds = tests.reduce((total, t) => total + (t.run_time || 0), 0);
|
|
261
|
+
return (0, humanize_duration_1.default)(Math.trunc(milliseconds), { maxDecimalPoints: 0 });
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Render a two-column markdown table. Rows with an empty value are skipped, so optional rows
|
|
265
|
+
* need no surrounding `if`.
|
|
266
|
+
*
|
|
267
|
+
* @param {string[]} header - The two header cells.
|
|
268
|
+
* @param {Object<string, string>} rows - Label to value, rendered in insertion order.
|
|
269
|
+
* @param {{ boldLabels?: boolean }} [opts] - Set `boldLabels` to wrap every label in `**`.
|
|
270
|
+
* @returns {string} The table, with no trailing newline.
|
|
271
|
+
*/
|
|
272
|
+
function markdownTable(header, rows, opts = {}) {
|
|
273
|
+
const lines = [`| ${header[0]} | ${header[1]} |`, '| --- | --- |'];
|
|
274
|
+
for (const [label, value] of Object.entries(rows)) {
|
|
275
|
+
if (!value)
|
|
276
|
+
continue;
|
|
277
|
+
if (opts.boldLabels) {
|
|
278
|
+
lines.push(`| **${label}** | ${value} |`);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
lines.push(`| ${label} | ${value} |`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return lines.join('\n');
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Describe the scope of a run that was prepared but not executed yet. Prefers the server's count;
|
|
288
|
+
* without it, falls back to the scoped ids, which mix tests (`T…`) and suites (`S…`).
|
|
289
|
+
*
|
|
290
|
+
* @param {Array<{test_id?: string}>} tests - Prepared tests the run was scoped to.
|
|
291
|
+
* @param {number} [testsCount] - Real number of tests, as reported by Testomat.io.
|
|
292
|
+
* @returns {string} Markdown label, e.g. `**159** tests planned` or `**6** suites planned`.
|
|
293
|
+
*/
|
|
294
|
+
function plannedTestsLabel(tests, testsCount) {
|
|
295
|
+
if (testsCount > 0)
|
|
296
|
+
return `**${testsCount}** tests planned`;
|
|
297
|
+
const suitesCount = tests.filter(t => `${t.test_id || ''}`.startsWith('S')).length;
|
|
298
|
+
const knownTestsCount = tests.length - suitesCount;
|
|
299
|
+
if (!suitesCount)
|
|
300
|
+
return `**${knownTestsCount}** tests planned`;
|
|
301
|
+
if (!knownTestsCount)
|
|
302
|
+
return `**${suitesCount}** suites planned`;
|
|
303
|
+
return `**${knownTestsCount}** tests and **${suitesCount}** suites planned`;
|
|
304
|
+
}
|
|
226
305
|
|
|
227
306
|
module.exports.updateFilterType = updateFilterType;
|
|
228
307
|
|
|
@@ -236,6 +315,14 @@ module.exports.statusEmoji = statusEmoji;
|
|
|
236
315
|
|
|
237
316
|
module.exports.fullName = fullName;
|
|
238
317
|
|
|
318
|
+
module.exports.markdownTable = markdownTable;
|
|
319
|
+
|
|
320
|
+
module.exports.runSummary = runSummary;
|
|
321
|
+
|
|
322
|
+
module.exports.totalDuration = totalDuration;
|
|
323
|
+
|
|
324
|
+
module.exports.plannedTestsLabel = plannedTestsLabel;
|
|
325
|
+
|
|
239
326
|
module.exports.parsePipeOptions = parsePipeOptions;
|
|
240
327
|
|
|
241
328
|
module.exports.formatFilterListIds = formatFilterListIds;
|
package/package.json
CHANGED
|
@@ -255,7 +255,8 @@ function appendStep(step, shift = 0) {
|
|
|
255
255
|
newCategory = 'hook';
|
|
256
256
|
break;
|
|
257
257
|
case 'attach':
|
|
258
|
-
|
|
258
|
+
case 'test.attach':
|
|
259
|
+
return null; // Attachments are reported as artifacts, not standalone steps
|
|
259
260
|
default:
|
|
260
261
|
newCategory = 'framework';
|
|
261
262
|
}
|
|
@@ -286,11 +287,11 @@ function appendStep(step, shift = 0) {
|
|
|
286
287
|
resultStep.log = truncate(String(step.log), 250);
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
);
|
|
290
|
+
// Playwright also associates automatic failure screenshots with the active
|
|
291
|
+
// hook. Only attachments created for an explicit test.step belong to a step;
|
|
292
|
+
// hook and fixture attachments remain test-level result artifacts.
|
|
293
|
+
if (step.category === 'test.step' && step.attachments?.length && SCREENSHOTS_ON_STEPS) {
|
|
294
|
+
const screenshotAttachment = step.attachments.find(isScreenshotArtifact);
|
|
294
295
|
if (screenshotAttachment && screenshotAttachment.path) {
|
|
295
296
|
const artifacts = { screenshot: screenshotAttachment.path };
|
|
296
297
|
addArtifactsToStep(resultStep, artifacts);
|
package/src/bin/cli.js
CHANGED
|
@@ -62,7 +62,8 @@ program
|
|
|
62
62
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
63
63
|
const client = new TestomatClient({ apiKey });
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
// nothing is executed yet; the server flips it to running on the first reported test
|
|
66
|
+
const createRunParams = { status: 'scheduled' };
|
|
66
67
|
if (opts.kind) createRunParams.kind = opts.kind;
|
|
67
68
|
|
|
68
69
|
if (opts.filter) {
|
|
@@ -87,6 +88,11 @@ program
|
|
|
87
88
|
process.exit(1);
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
// tests are executed later, so report the run as pending with the tests it was scoped to:
|
|
92
|
+
// pipes add their report now and replace it when the run is finished
|
|
93
|
+
const plannedTests = (client.pipeStore.preparedTestIds || []).map(id => ({ test_id: id, title: id }));
|
|
94
|
+
await client.updateRunStatus('pending', { tests: plannedTests });
|
|
95
|
+
|
|
90
96
|
// stdout carries ONLY the run id so it can be captured: RUN_ID=$(reporter start)
|
|
91
97
|
console.log(runId);
|
|
92
98
|
process.exit(0);
|
package/src/client.js
CHANGED
|
@@ -371,7 +371,7 @@ class Client {
|
|
|
371
371
|
/**
|
|
372
372
|
*
|
|
373
373
|
* Updates the status of the current test run and finishes the run.
|
|
374
|
-
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
374
|
+
* @param {'passed' | 'failed' | 'skipped' | 'finished' | 'pending'} status - The status of the current test run.
|
|
375
375
|
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
376
376
|
* Must be one of "passed", "failed", or "finished"
|
|
377
377
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
package/src/pipe/bitbucket.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { APP_PREFIX, testomatLogoURL } from '../constants.js';
|
|
2
2
|
import { ansiRegExp, isSameTest, truncate } from '../utils/utils.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
statusEmoji,
|
|
5
|
+
fullName,
|
|
6
|
+
plannedTestsLabel,
|
|
7
|
+
markdownTable,
|
|
8
|
+
runSummary,
|
|
9
|
+
totalDuration,
|
|
10
|
+
} from '../utils/pipe_utils.js';
|
|
4
11
|
import { Gaxios } from 'gaxios';
|
|
5
12
|
import pc from 'picocolors';
|
|
6
13
|
import humanizeDuration from 'humanize-duration';
|
|
@@ -103,37 +110,32 @@ export class BitbucketPipe {
|
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
// Create a comment on Bitbucket
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
'passed',
|
|
120
|
-
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
121
|
-
| **Duration** | 🕐 **${humanizeDuration(
|
|
122
|
-
parseInt(
|
|
123
|
-
this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
|
|
124
|
-
10,
|
|
125
|
-
),
|
|
126
|
-
{
|
|
127
|
-
maxDecimalPoints: 0,
|
|
128
|
-
},
|
|
129
|
-
)}** |
|
|
130
|
-
`;
|
|
113
|
+
// a scheduled run has no results yet: no counters, no duration
|
|
114
|
+
const isPendingRun = runParams.status === 'pending';
|
|
115
|
+
|
|
116
|
+
/** @type {Object<string, string>} */
|
|
117
|
+
const rows = {};
|
|
118
|
+
|
|
119
|
+
if (isPendingRun) {
|
|
120
|
+
if (this.tests.length) rows.Tests = `⚪ ${plannedTestsLabel(this.tests, this.store.runTestsCount)}`;
|
|
121
|
+
} else {
|
|
122
|
+
rows.Tests = `✔️ **${this.tests.length}** tests run`;
|
|
123
|
+
rows.Summary = runSummary(this.tests);
|
|
124
|
+
rows.Duration = `🕐 **${totalDuration(this.tests)}**`;
|
|
125
|
+
}
|
|
131
126
|
|
|
132
127
|
if (this.ENV.BITBUCKET_BRANCH && this.ENV.BITBUCKET_COMMIT) {
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
const buildNumber = this.ENV.BITBUCKET_BUILD_NUMBER;
|
|
129
|
+
const buildUrl = `https://bitbucket.org/${this.ENV.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${buildNumber}`;
|
|
130
|
+
rows.Job = `👷 [#${buildNumber}](${buildUrl}) by commit: **${this.ENV.BITBUCKET_COMMIT}**`;
|
|
135
131
|
}
|
|
136
132
|
|
|
133
|
+
const header = [
|
|
134
|
+
``,
|
|
135
|
+
`${statusEmoji(runParams.status)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)}`,
|
|
136
|
+
];
|
|
137
|
+
const summary = `${this.hiddenCommentData}\n\n${markdownTable(header, rows, { boldLabels: true })}`;
|
|
138
|
+
|
|
137
139
|
const failures = this.tests
|
|
138
140
|
.filter(t => t.status === 'failed')
|
|
139
141
|
.slice(0, 20)
|
|
@@ -182,7 +184,7 @@ export class BitbucketPipe {
|
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
|
|
185
|
-
if (this.tests.length > 0) {
|
|
187
|
+
if (this.tests.length > 0 && !isPendingRun) {
|
|
186
188
|
body += `\n\n**🐢 Slowest Tests**\n\n`;
|
|
187
189
|
body += this.tests
|
|
188
190
|
.sort((a, b) => b.run_time - a.run_time)
|
package/src/pipe/github.js
CHANGED
|
@@ -5,7 +5,14 @@ import humanizeDuration from 'humanize-duration';
|
|
|
5
5
|
import merge from 'lodash.merge';
|
|
6
6
|
import { testomatLogoURL } from '../constants.js';
|
|
7
7
|
import { ansiRegExp, isSameTest, truncate } from '../utils/utils.js';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
statusEmoji,
|
|
10
|
+
fullName,
|
|
11
|
+
plannedTestsLabel,
|
|
12
|
+
markdownTable,
|
|
13
|
+
runSummary,
|
|
14
|
+
totalDuration,
|
|
15
|
+
} from '../utils/pipe_utils.js';
|
|
9
16
|
import { log } from '../utils/log.js';
|
|
10
17
|
|
|
11
18
|
const debug = createDebugMessages('@testomatio/reporter:pipe:github');
|
|
@@ -75,42 +82,37 @@ class GitHubPipe {
|
|
|
75
82
|
if (!(owner || repo)) return;
|
|
76
83
|
|
|
77
84
|
// ... create a comment on GitHub
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
92
|
-
| Duration | 🕐 **${humanizeDuration(
|
|
93
|
-
parseInt(
|
|
94
|
-
this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
|
|
95
|
-
10,
|
|
96
|
-
),
|
|
97
|
-
{
|
|
98
|
-
maxDecimalPoints: 0,
|
|
99
|
-
},
|
|
100
|
-
)}** |`;
|
|
85
|
+
// a scheduled run has no results yet: no counters, no duration
|
|
86
|
+
const isPendingRun = runParams.status === 'pending';
|
|
87
|
+
|
|
88
|
+
/** @type {Object<string, string>} */
|
|
89
|
+
const rows = {};
|
|
90
|
+
|
|
91
|
+
if (isPendingRun) {
|
|
92
|
+
if (this.tests.length) rows.Tests = `⚪ ${plannedTestsLabel(this.tests, this.store.runTestsCount)}`;
|
|
93
|
+
} else {
|
|
94
|
+
rows.Tests = `✔️ **${this.tests.length}** tests run`;
|
|
95
|
+
rows.Summary = runSummary(this.tests);
|
|
96
|
+
rows.Duration = `🕐 **${totalDuration(this.tests)}**`;
|
|
97
|
+
}
|
|
101
98
|
|
|
102
99
|
if (this.store.runUrl) {
|
|
103
|
-
|
|
100
|
+
rows['Testomat.io Report'] = `📊 [Run #${this.store.runId}](${this.store.runUrl})`;
|
|
104
101
|
}
|
|
105
102
|
if (process.env.GITHUB_WORKFLOW) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}/actions/runs/${process.env.GITHUB_RUN_ID}) | `;
|
|
103
|
+
const server = process.env.GITHUB_SERVER_URL || 'https://github.com';
|
|
104
|
+
rows.Job = `🗂️ [${this.jobKey}](${server}/${this.repo}/actions/runs/${process.env.GITHUB_RUN_ID})`;
|
|
109
105
|
}
|
|
110
106
|
if (process.env.RUNNER_OS) {
|
|
111
|
-
|
|
107
|
+
rows['Operating System'] = `🖥️ \`${process.env.RUNNER_OS}\` ${process.env.RUNNER_ARCH || ''}`;
|
|
112
108
|
}
|
|
113
109
|
|
|
110
|
+
const header = [
|
|
111
|
+
`[](https://testomat.io)`,
|
|
112
|
+
`${statusEmoji(runParams.status)} ${`${process.env.GITHUB_JOB} ${runParams.status}`.toUpperCase()}`,
|
|
113
|
+
];
|
|
114
|
+
const summary = `${this.hiddenCommentData}\n\n${markdownTable(header, rows)}`;
|
|
115
|
+
|
|
114
116
|
const failures = this.tests
|
|
115
117
|
.filter(t => t.status === 'failed')
|
|
116
118
|
.slice(0, 20)
|
|
@@ -170,7 +172,7 @@ class GitHubPipe {
|
|
|
170
172
|
body += '\n\n</details>';
|
|
171
173
|
}
|
|
172
174
|
|
|
173
|
-
if (this.tests.length > 0) {
|
|
175
|
+
if (this.tests.length > 0 && !isPendingRun) {
|
|
174
176
|
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
175
177
|
body += this.tests
|
|
176
178
|
.sort((a, b) => b?.run_time - a?.run_time)
|
package/src/pipe/gitlab.js
CHANGED
|
@@ -6,7 +6,14 @@ import merge from 'lodash.merge';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import { APP_PREFIX, testomatLogoURL } from '../constants.js';
|
|
8
8
|
import { ansiRegExp, isSameTest, truncate } from '../utils/utils.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
statusEmoji,
|
|
11
|
+
fullName,
|
|
12
|
+
plannedTestsLabel,
|
|
13
|
+
markdownTable,
|
|
14
|
+
runSummary,
|
|
15
|
+
totalDuration,
|
|
16
|
+
} from '../utils/pipe_utils.js';
|
|
10
17
|
import { log } from '../utils/log.js';
|
|
11
18
|
|
|
12
19
|
const debug = createDebugMessages('@testomatio/reporter:pipe:gitlab');
|
|
@@ -81,37 +88,31 @@ class GitLabPipe {
|
|
|
81
88
|
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
82
89
|
|
|
83
90
|
// ... create a comment on GitLab
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
'passed',
|
|
98
|
-
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
99
|
-
| Duration | 🕐 **${humanizeDuration(
|
|
100
|
-
parseInt(
|
|
101
|
-
this.tests.reduce((a, t) => a + (t.run_time || 0), 0),
|
|
102
|
-
10,
|
|
103
|
-
),
|
|
104
|
-
{
|
|
105
|
-
maxDecimalPoints: 0,
|
|
106
|
-
},
|
|
107
|
-
)}** |
|
|
108
|
-
`;
|
|
91
|
+
// a scheduled run has no results yet: no counters, no duration
|
|
92
|
+
const isPendingRun = runParams.status === 'pending';
|
|
93
|
+
|
|
94
|
+
/** @type {Object<string, string>} */
|
|
95
|
+
const rows = {};
|
|
96
|
+
|
|
97
|
+
if (isPendingRun) {
|
|
98
|
+
if (this.tests.length) rows.Tests = `⚪ ${plannedTestsLabel(this.tests, this.store.runTestsCount)}`;
|
|
99
|
+
} else {
|
|
100
|
+
rows.Tests = `✔️ **${this.tests.length}** tests run`;
|
|
101
|
+
rows.Summary = runSummary(this.tests);
|
|
102
|
+
rows.Duration = `🕐 **${totalDuration(this.tests)}**`;
|
|
103
|
+
}
|
|
109
104
|
|
|
110
105
|
if (this.ENV.CI_JOB_NAME && this.ENV.CI_JOB_ID) {
|
|
111
106
|
// eslint-disable-next-line max-len
|
|
112
|
-
|
|
107
|
+
rows.Job = `👷 [${this.ENV.CI_JOB_ID}](${this.ENV.CI_JOB_URL})<br>Name: **${this.ENV.CI_JOB_NAME}**<br>Stage: **${this.ENV.CI_JOB_STAGE}**`;
|
|
113
108
|
}
|
|
114
109
|
|
|
110
|
+
const header = [
|
|
111
|
+
`[](https://testomat.io)`,
|
|
112
|
+
`${statusEmoji(runParams.status)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)}`,
|
|
113
|
+
];
|
|
114
|
+
const summary = `${this.hiddenCommentData}\n\n${markdownTable(header, rows)}`;
|
|
115
|
+
|
|
115
116
|
const failures = this.tests
|
|
116
117
|
.filter(t => t.status === 'failed')
|
|
117
118
|
.slice(0, 20)
|
|
@@ -158,7 +159,7 @@ class GitLabPipe {
|
|
|
158
159
|
body += '\n\n</details>';
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
if (this.tests.length > 0) {
|
|
162
|
+
if (this.tests.length > 0 && !isPendingRun) {
|
|
162
163
|
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
163
164
|
body += this.tests
|
|
164
165
|
.sort((a, b) => b?.run_time - a?.run_time)
|
package/src/pipe/html.js
CHANGED
|
@@ -268,7 +268,7 @@ class HtmlPipe {
|
|
|
268
268
|
executionTime: testExecutionSumTime(aggregatedTests),
|
|
269
269
|
executionDate: getCurrentDateTimeFormatted(),
|
|
270
270
|
description:
|
|
271
|
-
[runParams.description || this.store.coverageDescription || this.store.description
|
|
271
|
+
[this.description, runParams.description || this.store.coverageDescription || this.store.description]
|
|
272
272
|
.filter(Boolean)
|
|
273
273
|
.join('\n\n') || '',
|
|
274
274
|
configuration: buildDisplayConfiguration(
|
package/src/pipe/markdown.js
CHANGED
|
@@ -140,7 +140,7 @@ class MarkdownPipe {
|
|
|
140
140
|
executionTime: testExecutionSumTime(aggregated),
|
|
141
141
|
executionDate: getCurrentDateTimeFormatted(),
|
|
142
142
|
description:
|
|
143
|
-
[runParams?.description || this.store.coverageDescription || this.store.description
|
|
143
|
+
[this.description, runParams?.description || this.store.coverageDescription || this.store.description]
|
|
144
144
|
.filter(Boolean)
|
|
145
145
|
.join('\n\n') || '',
|
|
146
146
|
configuration: this.configuration || this.store.configuration || runParams?.configuration || null,
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -279,8 +279,9 @@ class TestomatioPipe {
|
|
|
279
279
|
suites: coverageConfiguration.suites?.map(id => id.replace(/^S/, '')) || [],
|
|
280
280
|
};
|
|
281
281
|
}
|
|
282
|
-
// Run description:
|
|
283
|
-
|
|
282
|
+
// Run description: the user-provided TESTOMATIO_DESCRIPTION with the coverage-derived block
|
|
283
|
+
// (if any) added after it. Neither overrides the other.
|
|
284
|
+
const description = [this.description, coverageDescription].filter(Boolean).join('\n\n') || null;
|
|
284
285
|
|
|
285
286
|
// Merge caller-supplied configuration (e.g. { exploratory: true }) into runParams.configuration.
|
|
286
287
|
// Caller values win on key conflict; coverage-derived tests/suites lists are preserved when not overridden.
|
|
@@ -322,6 +323,7 @@ class TestomatioPipe {
|
|
|
322
323
|
shared_run: this.sharedRun,
|
|
323
324
|
shared_run_timeout: this.sharedRunTimeout,
|
|
324
325
|
kind: params.kind,
|
|
326
|
+
status: params.status,
|
|
325
327
|
configuration,
|
|
326
328
|
description,
|
|
327
329
|
ci,
|
|
@@ -372,6 +374,8 @@ class TestomatioPipe {
|
|
|
372
374
|
this.store.runUrl = this.runUrl;
|
|
373
375
|
this.store.runPublicUrl = this.runPublicUrl;
|
|
374
376
|
this.store.runId = this.runId;
|
|
377
|
+
// only the server knows how many tests a configuration expands to; automated runs report 0
|
|
378
|
+
if (resp.data.tests_count > 0) this.store.runTestsCount = resp.data.tests_count;
|
|
375
379
|
log.info('📊 Report created. Report ID:', this.runId);
|
|
376
380
|
process.env.runId = this.runId;
|
|
377
381
|
debug('Run created', this.runId);
|
|
@@ -558,6 +562,9 @@ class TestomatioPipe {
|
|
|
558
562
|
|
|
559
563
|
const { status } = params;
|
|
560
564
|
|
|
565
|
+
// a pending run was just created: nothing to update here, only other pipes report it
|
|
566
|
+
if (status === 'pending') return;
|
|
567
|
+
|
|
561
568
|
let status_event;
|
|
562
569
|
|
|
563
570
|
if (status === STATUS.FINISHED) status_event = 'finish';
|
package/src/utils/pipe_utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import humanizeDuration from 'humanize-duration';
|
|
1
2
|
import { log } from './log.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -117,6 +118,7 @@ function statusEmoji(status) {
|
|
|
117
118
|
if (status === 'passed') return '🟢';
|
|
118
119
|
if (status === 'failed') return '🔴';
|
|
119
120
|
if (status === 'skipped') return '🟡';
|
|
121
|
+
if (status === 'pending') return '🕐';
|
|
120
122
|
return '';
|
|
121
123
|
}
|
|
122
124
|
|
|
@@ -228,6 +230,80 @@ function formatFilterListIds(ids, format) {
|
|
|
228
230
|
}
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Summarize a finished run, e.g. `🔴 **1** failed; 🟢 **8** passed; 🟡 **1** skipped`.
|
|
235
|
+
* The failed part is omitted when nothing failed.
|
|
236
|
+
*
|
|
237
|
+
* @param {Array<{status?: string}>} tests
|
|
238
|
+
* @returns {string}
|
|
239
|
+
*/
|
|
240
|
+
function runSummary(tests) {
|
|
241
|
+
const countOf = status => tests.filter(t => t.status === status).length;
|
|
242
|
+
const failedCount = countOf('failed');
|
|
243
|
+
|
|
244
|
+
const parts = [];
|
|
245
|
+
if (failedCount) parts.push(`${statusEmoji('failed')} **${failedCount}** failed`);
|
|
246
|
+
parts.push(`${statusEmoji('passed')} **${countOf('passed')}** passed`);
|
|
247
|
+
parts.push(`${statusEmoji('skipped')} **${countOf('skipped')}** skipped`);
|
|
248
|
+
|
|
249
|
+
return parts.join('; ');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Total run time of the given tests, humanized — e.g. `2 seconds`.
|
|
254
|
+
*
|
|
255
|
+
* @param {Array<{run_time?: number}>} tests
|
|
256
|
+
* @returns {string}
|
|
257
|
+
*/
|
|
258
|
+
function totalDuration(tests) {
|
|
259
|
+
const milliseconds = tests.reduce((total, t) => total + (t.run_time || 0), 0);
|
|
260
|
+
return humanizeDuration(Math.trunc(milliseconds), { maxDecimalPoints: 0 });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Render a two-column markdown table. Rows with an empty value are skipped, so optional rows
|
|
265
|
+
* need no surrounding `if`.
|
|
266
|
+
*
|
|
267
|
+
* @param {string[]} header - The two header cells.
|
|
268
|
+
* @param {Object<string, string>} rows - Label to value, rendered in insertion order.
|
|
269
|
+
* @param {{ boldLabels?: boolean }} [opts] - Set `boldLabels` to wrap every label in `**`.
|
|
270
|
+
* @returns {string} The table, with no trailing newline.
|
|
271
|
+
*/
|
|
272
|
+
function markdownTable(header, rows, opts = {}) {
|
|
273
|
+
const lines = [`| ${header[0]} | ${header[1]} |`, '| --- | --- |'];
|
|
274
|
+
|
|
275
|
+
for (const [label, value] of Object.entries(rows)) {
|
|
276
|
+
if (!value) continue;
|
|
277
|
+
|
|
278
|
+
if (opts.boldLabels) {
|
|
279
|
+
lines.push(`| **${label}** | ${value} |`);
|
|
280
|
+
} else {
|
|
281
|
+
lines.push(`| ${label} | ${value} |`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return lines.join('\n');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Describe the scope of a run that was prepared but not executed yet. Prefers the server's count;
|
|
290
|
+
* without it, falls back to the scoped ids, which mix tests (`T…`) and suites (`S…`).
|
|
291
|
+
*
|
|
292
|
+
* @param {Array<{test_id?: string}>} tests - Prepared tests the run was scoped to.
|
|
293
|
+
* @param {number} [testsCount] - Real number of tests, as reported by Testomat.io.
|
|
294
|
+
* @returns {string} Markdown label, e.g. `**159** tests planned` or `**6** suites planned`.
|
|
295
|
+
*/
|
|
296
|
+
function plannedTestsLabel(tests, testsCount) {
|
|
297
|
+
if (testsCount > 0) return `**${testsCount}** tests planned`;
|
|
298
|
+
|
|
299
|
+
const suitesCount = tests.filter(t => `${t.test_id || ''}`.startsWith('S')).length;
|
|
300
|
+
const knownTestsCount = tests.length - suitesCount;
|
|
301
|
+
|
|
302
|
+
if (!suitesCount) return `**${knownTestsCount}** tests planned`;
|
|
303
|
+
if (!knownTestsCount) return `**${suitesCount}** suites planned`;
|
|
304
|
+
return `**${knownTestsCount}** tests and **${suitesCount}** suites planned`;
|
|
305
|
+
}
|
|
306
|
+
|
|
231
307
|
export {
|
|
232
308
|
updateFilterType,
|
|
233
309
|
parseFilterParams,
|
|
@@ -235,6 +311,10 @@ export {
|
|
|
235
311
|
setS3Credentials,
|
|
236
312
|
statusEmoji,
|
|
237
313
|
fullName,
|
|
314
|
+
markdownTable,
|
|
315
|
+
runSummary,
|
|
316
|
+
totalDuration,
|
|
317
|
+
plannedTestsLabel,
|
|
238
318
|
parsePipeOptions,
|
|
239
319
|
formatFilterListIds,
|
|
240
320
|
getObjectSize,
|
package/types/types.d.ts
CHANGED
|
@@ -289,6 +289,8 @@ export enum RunStatus {
|
|
|
289
289
|
Passed = 'passed',
|
|
290
290
|
Failed = 'failed',
|
|
291
291
|
Finished = 'finished',
|
|
292
|
+
/** run created but not executed yet, reported by `reporter start` */
|
|
293
|
+
Pending = 'pending',
|
|
292
294
|
}
|
|
293
295
|
|
|
294
296
|
/** Batch upload strategy:
|
|
@@ -344,6 +346,9 @@ export interface CreateRunParams {
|
|
|
344
346
|
/** Run configuration merged into the server-side run configuration. */
|
|
345
347
|
configuration?: Record<string, any>;
|
|
346
348
|
|
|
349
|
+
/** Initial run status. `scheduled` marks a prepared run; the server flips it to running. */
|
|
350
|
+
status?: 'scheduled';
|
|
351
|
+
|
|
347
352
|
/** Override batch upload mode. */
|
|
348
353
|
batchMode?: BatchMode;
|
|
349
354
|
}
|