testomatio-reporter-cli 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/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
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
|
}
|