@testomatio/reporter 2.12.0 → 2.14.0-beta
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 +2 -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/testomatio.js +4 -0
- package/lib/utils/pipe_utils.d.ts +44 -0
- package/lib/utils/pipe_utils.js +85 -0
- package/package.json +2 -2
- package/src/adapter/playwright.js +7 -6
- package/src/bin/cli.js +2 -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/testomatio.js +3 -0
- package/src/utils/pipe_utils.js +79 -0
- package/types/types.d.ts +3 -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) {
|
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/testomatio.js
CHANGED
|
@@ -290,6 +290,7 @@ class TestomatioPipe {
|
|
|
290
290
|
shared_run: this.sharedRun,
|
|
291
291
|
shared_run_timeout: this.sharedRunTimeout,
|
|
292
292
|
kind: params.kind,
|
|
293
|
+
status: params.status,
|
|
293
294
|
configuration,
|
|
294
295
|
description,
|
|
295
296
|
ci,
|
|
@@ -336,6 +337,9 @@ class TestomatioPipe {
|
|
|
336
337
|
this.store.runUrl = this.runUrl;
|
|
337
338
|
this.store.runPublicUrl = this.runPublicUrl;
|
|
338
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;
|
|
339
343
|
log_js_1.log.info('📊 Report created. Report ID:', this.runId);
|
|
340
344
|
process.env.runId = this.runId;
|
|
341
345
|
debug('Run created', this.runId);
|
|
@@ -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.
|
|
@@ -225,6 +233,75 @@ function formatFilterListIds(ids, format) {
|
|
|
225
233
|
default: return ids.join(',');
|
|
226
234
|
}
|
|
227
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
|
+
}
|
|
228
305
|
|
|
229
306
|
module.exports.updateFilterType = updateFilterType;
|
|
230
307
|
|
|
@@ -238,6 +315,14 @@ module.exports.statusEmoji = statusEmoji;
|
|
|
238
315
|
|
|
239
316
|
module.exports.fullName = fullName;
|
|
240
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
|
+
|
|
241
326
|
module.exports.parsePipeOptions = parsePipeOptions;
|
|
242
327
|
|
|
243
328
|
module.exports.formatFilterListIds = formatFilterListIds;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0-beta",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"@types/cross-spawn": "^6.0.6",
|
|
92
92
|
"@types/cucumber": "^7.0.0",
|
|
93
93
|
"@types/mocha": "^10.0.7",
|
|
94
|
-
"@wdio/reporter": "^
|
|
94
|
+
"@wdio/reporter": "^8.43.0",
|
|
95
95
|
"chai": "^4.3.6",
|
|
96
96
|
"codeceptjs": "^3.6.5",
|
|
97
97
|
"cucumber": "^6.0.7",
|
|
@@ -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) {
|
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/testomatio.js
CHANGED
|
@@ -323,6 +323,7 @@ class TestomatioPipe {
|
|
|
323
323
|
shared_run: this.sharedRun,
|
|
324
324
|
shared_run_timeout: this.sharedRunTimeout,
|
|
325
325
|
kind: params.kind,
|
|
326
|
+
status: params.status,
|
|
326
327
|
configuration,
|
|
327
328
|
description,
|
|
328
329
|
ci,
|
|
@@ -373,6 +374,8 @@ class TestomatioPipe {
|
|
|
373
374
|
this.store.runUrl = this.runUrl;
|
|
374
375
|
this.store.runPublicUrl = this.runPublicUrl;
|
|
375
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;
|
|
376
379
|
log.info('📊 Report created. Report ID:', this.runId);
|
|
377
380
|
process.env.runId = this.runId;
|
|
378
381
|
debug('Run created', this.runId);
|
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
|
/**
|
|
@@ -229,6 +230,80 @@ function formatFilterListIds(ids, format) {
|
|
|
229
230
|
}
|
|
230
231
|
}
|
|
231
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
|
+
|
|
232
307
|
export {
|
|
233
308
|
updateFilterType,
|
|
234
309
|
parseFilterParams,
|
|
@@ -236,6 +311,10 @@ export {
|
|
|
236
311
|
setS3Credentials,
|
|
237
312
|
statusEmoji,
|
|
238
313
|
fullName,
|
|
314
|
+
markdownTable,
|
|
315
|
+
runSummary,
|
|
316
|
+
totalDuration,
|
|
317
|
+
plannedTestsLabel,
|
|
239
318
|
parsePipeOptions,
|
|
240
319
|
formatFilterListIds,
|
|
241
320
|
getObjectSize,
|
package/types/types.d.ts
CHANGED
|
@@ -346,6 +346,9 @@ export interface CreateRunParams {
|
|
|
346
346
|
/** Run configuration merged into the server-side run configuration. */
|
|
347
347
|
configuration?: Record<string, any>;
|
|
348
348
|
|
|
349
|
+
/** Initial run status. `scheduled` marks a prepared run; the server flips it to running. */
|
|
350
|
+
status?: 'scheduled';
|
|
351
|
+
|
|
349
352
|
/** Override batch upload mode. */
|
|
350
353
|
batchMode?: BatchMode;
|
|
351
354
|
}
|