@testomatio/reporter 2.19.0-beta → 2.20.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/codecept.js +1 -0
- package/lib/adapter/cucumber/current.js +1 -0
- package/lib/adapter/cypress-plugin/index.js +1 -0
- package/lib/adapter/e2e.d.ts +49 -0
- package/lib/adapter/e2e.js +250 -0
- package/lib/adapter/jasmine.js +1 -0
- package/lib/adapter/jest.js +1 -0
- package/lib/adapter/mocha.d.ts +4 -1
- package/lib/adapter/mocha.js +123 -122
- package/lib/adapter/nightwatch.js +1 -0
- package/lib/adapter/playwright.js +1 -0
- package/lib/adapter/vitest.js +1 -0
- package/lib/adapter/webdriver.js +1 -0
- package/lib/allureReader.d.ts +4 -1
- package/lib/allureReader.js +2 -1
- package/lib/bin/cli.js +32 -13
- package/lib/client.d.ts +1 -0
- package/lib/client.js +12 -3
- package/lib/data-storage.js +4 -3
- package/lib/pipe/bitbucket.js +5 -1
- package/lib/pipe/github.js +5 -1
- package/lib/pipe/gitlab.js +5 -1
- package/lib/pipe/testomatio.js +17 -1
- package/lib/replay.js +1 -0
- package/lib/uploader.d.ts +1 -0
- package/lib/uploader.js +21 -1
- package/lib/utils/pipe_utils.d.ts +32 -3
- package/lib/utils/pipe_utils.js +82 -4
- package/lib/xmlReader.js +2 -0
- package/package.json +7 -2
- package/src/adapter/codecept.js +2 -0
- package/src/adapter/cucumber/current.js +2 -0
- package/src/adapter/cypress-plugin/index.js +2 -0
- package/src/adapter/e2e.js +245 -0
- package/src/adapter/jasmine.js +2 -0
- package/src/adapter/jest.js +2 -0
- package/src/adapter/mocha.js +146 -145
- package/src/adapter/nightwatch.js +2 -0
- package/src/adapter/playwright.js +2 -0
- package/src/adapter/vitest.js +2 -0
- package/src/adapter/webdriver.js +2 -0
- package/src/allureReader.js +2 -1
- package/src/bin/cli.js +30 -11
- package/src/client.js +11 -3
- package/src/data-storage.js +4 -3
- package/src/pipe/bitbucket.js +7 -1
- package/src/pipe/github.js +7 -1
- package/src/pipe/gitlab.js +7 -1
- package/src/pipe/testomatio.js +27 -1
- package/src/replay.js +1 -0
- package/src/uploader.js +30 -1
- package/src/utils/pipe_utils.js +85 -4
- package/src/xmlReader.js +3 -0
- package/types/types.d.ts +1 -0
package/lib/allureReader.d.ts
CHANGED
package/lib/allureReader.js
CHANGED
|
@@ -163,7 +163,8 @@ class AllureReader {
|
|
|
163
163
|
steps: this.convertSteps(result.steps || [], resultsDir),
|
|
164
164
|
message: result.statusDetails?.message || '',
|
|
165
165
|
stack: result.statusDetails?.trace || '',
|
|
166
|
-
meta
|
|
166
|
+
// meta must always include reporter version and adapter used
|
|
167
|
+
meta: { ...this.extractMeta(result), reporterVersion: this.version, adapter: 'allure' },
|
|
167
168
|
links: this.extractLinks(result),
|
|
168
169
|
artifacts: [],
|
|
169
170
|
create: true,
|
package/lib/bin/cli.js
CHANGED
|
@@ -121,35 +121,54 @@ program
|
|
|
121
121
|
program
|
|
122
122
|
.command('fetch')
|
|
123
123
|
.description('Fetch runs from Testomat.io API v2')
|
|
124
|
+
.argument('[url]', 'URL of a run or of a (filtered) runs page in Testomat.io')
|
|
124
125
|
.option('--project <slug>', 'Project slug (or set TESTOMATIO_PROJECT)')
|
|
126
|
+
.option('--run <id>', 'Fetch a single run by its id')
|
|
125
127
|
.option('--title <text>', 'Filter by run title')
|
|
126
128
|
.option('--tql <query>', 'Filter using Testomat Query Language')
|
|
127
129
|
.option('--rungroup <uid>', 'Filter by rungroup id')
|
|
128
130
|
.option('--limit <number>', `Max number of runs to return (max ${FETCH_RUNS_MAX_LIMIT})`, String(FETCH_RUNS_DEFAULT_LIMIT))
|
|
129
131
|
.option('--latest', 'Only fetch the most recent run (shorthand for --limit 1)')
|
|
130
132
|
.option('--format <format>', 'Machine-readable output: run ids, one per line (--format id) or run details (--format json)')
|
|
131
|
-
.action(async (opts) => {
|
|
133
|
+
.action(async (pageUrl, opts) => {
|
|
132
134
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
|
|
133
135
|
if (!apiKey) {
|
|
134
136
|
log_js_1.log.error(picocolors_1.default.red('⚠️ TESTOMATIO API key required'));
|
|
135
137
|
process.exit(1);
|
|
136
138
|
}
|
|
137
|
-
|
|
139
|
+
let page = {};
|
|
140
|
+
if (pageUrl) {
|
|
141
|
+
page = (0, pipe_utils_js_1.parseRunsUrl)(pageUrl);
|
|
142
|
+
if (!page) {
|
|
143
|
+
log_js_1.log.error(picocolors_1.default.red(`⚠️ Not a Testomat.io run or runs page URL: ${pageUrl}`));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const project = opts.project || page.project || process.env.TESTOMATIO_PROJECT;
|
|
138
148
|
if (!project) {
|
|
139
149
|
log_js_1.log.error(picocolors_1.default.red('⚠️ --project (or TESTOMATIO_PROJECT) is required'));
|
|
140
150
|
process.exit(1);
|
|
141
151
|
}
|
|
142
|
-
const baseUrl = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
url
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
const baseUrl = page.baseUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
153
|
+
const runId = opts.run || page.runId;
|
|
154
|
+
let url;
|
|
155
|
+
if (runId) {
|
|
156
|
+
url = new URL(`/api/v2/${project}/runs/${runId}`, baseUrl);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
url = new URL(`/api/v2/${project}/runs`, baseUrl);
|
|
160
|
+
for (const [key, value] of page.params || [])
|
|
161
|
+
url.searchParams.append(key, value);
|
|
162
|
+
if (opts.title)
|
|
163
|
+
url.searchParams.set('search', opts.title);
|
|
164
|
+
if (opts.tql)
|
|
165
|
+
url.searchParams.set('tql', opts.tql);
|
|
166
|
+
if (opts.rungroup)
|
|
167
|
+
url.searchParams.set('groupId', opts.rungroup);
|
|
168
|
+
const requestedLimit = opts.latest ? 1 : parseInt(opts.limit, 10) || FETCH_RUNS_DEFAULT_LIMIT;
|
|
169
|
+
const limit = Math.min(Math.max(requestedLimit, 1), FETCH_RUNS_MAX_LIMIT);
|
|
170
|
+
url.searchParams.set('per_page', String(limit));
|
|
171
|
+
}
|
|
153
172
|
const response = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
154
173
|
const body = await response.json();
|
|
155
174
|
if (!response.ok) {
|
package/lib/client.d.ts
CHANGED
package/lib/client.js
CHANGED
|
@@ -37,6 +37,8 @@ class Client {
|
|
|
37
37
|
this.pipeStore = {};
|
|
38
38
|
this.runId = '';
|
|
39
39
|
this.queue = Promise.resolve();
|
|
40
|
+
// adapter name is set by the adapter module itself on load (global.TESTOMATIO_ADAPTER)
|
|
41
|
+
this.adapterName = params.adapter || global.TESTOMATIO_ADAPTER;
|
|
40
42
|
// @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
|
|
41
43
|
const pathToPackageJSON = path_1.default.join(__dirname, '../package.json');
|
|
42
44
|
try {
|
|
@@ -214,7 +216,9 @@ class Client {
|
|
|
214
216
|
const uploadedFiles = [];
|
|
215
217
|
const stackArtifactsEnabled = (0, utils_js_1.transformEnvVarToBoolean)(process.env.TESTOMATIO_STACK_ARTIFACTS);
|
|
216
218
|
const { time = 0, example = null, filesBuffers = [], code = null, file, suite_id, test_id, timestamp, links, overwrite, tags, } = testData;
|
|
217
|
-
let { files = [], manuallyAttachedArtifacts, message = ''
|
|
219
|
+
let { files = [], manuallyAttachedArtifacts, message = '' } = testData;
|
|
220
|
+
/** @type {Record<string, any>} */
|
|
221
|
+
let meta = testData.meta || {};
|
|
218
222
|
if (stepArtifactPaths.size) {
|
|
219
223
|
const isStepArtifact = item => stepArtifactPaths.has(typeof item === 'object' ? item?.path : item);
|
|
220
224
|
files = files.filter(f => !isStepArtifact(f));
|
|
@@ -229,6 +233,11 @@ class Client {
|
|
|
229
233
|
acc[key] = value;
|
|
230
234
|
return acc;
|
|
231
235
|
}, {});
|
|
236
|
+
// meta must always include reporter version and adapter used
|
|
237
|
+
if (this.version)
|
|
238
|
+
meta.reporterVersion = this.version;
|
|
239
|
+
if (this.adapterName)
|
|
240
|
+
meta.adapter = this.adapterName;
|
|
232
241
|
const testContext = suite_title ? `${suite_title} ${title}` : title;
|
|
233
242
|
let errorFormatted = '';
|
|
234
243
|
if (error) {
|
|
@@ -335,8 +344,8 @@ class Client {
|
|
|
335
344
|
this.queue = this.queue
|
|
336
345
|
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
337
346
|
.then(() => {
|
|
338
|
-
|
|
339
|
-
|
|
347
|
+
// no early return when uploading is disabled:
|
|
348
|
+
// skipped artifacts (e.g. no S3 credentials obtained) must still be reported to the user
|
|
340
349
|
const filesizeStrMaxLength = 7;
|
|
341
350
|
if (this.uploader.successfulUploads.length) {
|
|
342
351
|
debug('\n', constants_js_1.APP_PREFIX, `🗄️ ${this.uploader.successfulUploads.length} artifacts uploaded to S3 bucket`);
|
package/lib/data-storage.js
CHANGED
|
@@ -220,9 +220,10 @@ class DataStorage {
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
function stringToMD5Hash(str) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
223
|
+
// sha256 instead of md5 for FIPS-enabled hosts compatibility
|
|
224
|
+
const sha256 = crypto_1.default.createHash('sha256');
|
|
225
|
+
sha256.update(str);
|
|
226
|
+
const hash = sha256.digest('hex');
|
|
226
227
|
return `${process.env.runId || 'run'}_${hash}`;
|
|
227
228
|
}
|
|
228
229
|
exports.dataStorage = DataStorage.getInstance();
|
package/lib/pipe/bitbucket.js
CHANGED
|
@@ -175,7 +175,8 @@ class BitbucketPipe {
|
|
|
175
175
|
});
|
|
176
176
|
let body = summary;
|
|
177
177
|
if (this.description) {
|
|
178
|
-
|
|
178
|
+
// Bitbucket strips HTML, so no <details> here
|
|
179
|
+
body += `\n\n**📝 Description**\n\n${(0, utils_js_1.truncate)(this.description, 1024)}\n`;
|
|
179
180
|
}
|
|
180
181
|
if (failures.length) {
|
|
181
182
|
body += `\n🟥 **Failures (${failures.length})**\n\n* ${failures.join('\n* ')}\n`;
|
|
@@ -183,6 +184,9 @@ class BitbucketPipe {
|
|
|
183
184
|
body += `\n> Notice: Only the first 10 failures are shown.`;
|
|
184
185
|
}
|
|
185
186
|
}
|
|
187
|
+
for (const { title, list } of (0, pipe_utils_js_1.smallRunSections)(this.tests)) {
|
|
188
|
+
body += `\n\n**${title}**\n\n${list.join('\n')}`;
|
|
189
|
+
}
|
|
186
190
|
if (this.tests.length > 0 && !isPendingRun) {
|
|
187
191
|
body += `\n\n**🐢 Slowest Tests**\n\n`;
|
|
188
192
|
body += this.tests
|
package/lib/pipe/github.js
CHANGED
|
@@ -163,7 +163,8 @@ class GitHubPipe {
|
|
|
163
163
|
});
|
|
164
164
|
let body = summary;
|
|
165
165
|
if (this.description) {
|
|
166
|
-
body +=
|
|
166
|
+
body += '\n\n<details>\n<summary><h3>📝 Description</h3></summary>\n\n';
|
|
167
|
+
body += `${(0, utils_js_1.truncate)(this.description, 1024)}\n\n</details>`;
|
|
167
168
|
}
|
|
168
169
|
const coverageConfiguration = this.store?.coverageConfiguration;
|
|
169
170
|
const isManualRun = this.store?.runKind === 'manual';
|
|
@@ -187,6 +188,9 @@ class GitHubPipe {
|
|
|
187
188
|
}
|
|
188
189
|
body += '\n\n</details>';
|
|
189
190
|
}
|
|
191
|
+
for (const { title, list } of (0, pipe_utils_js_1.smallRunSections)(this.tests)) {
|
|
192
|
+
body += `\n<details>\n<summary><h3>${title}</h3></summary>\n\n${list.join('\n')}\n</details>`;
|
|
193
|
+
}
|
|
190
194
|
if (this.tests.length > 0 && !isPendingRun) {
|
|
191
195
|
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
192
196
|
body += this.tests
|
package/lib/pipe/gitlab.js
CHANGED
|
@@ -122,7 +122,8 @@ class GitLabPipe {
|
|
|
122
122
|
});
|
|
123
123
|
let body = summary;
|
|
124
124
|
if (this.description) {
|
|
125
|
-
body +=
|
|
125
|
+
body += '\n\n<details>\n<summary><h3>📝 Description</h3></summary>\n\n';
|
|
126
|
+
body += `${(0, utils_js_1.truncate)(this.description, 1024)}\n\n</details>`;
|
|
126
127
|
}
|
|
127
128
|
if (failures.length) {
|
|
128
129
|
body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
|
|
@@ -131,6 +132,9 @@ class GitLabPipe {
|
|
|
131
132
|
}
|
|
132
133
|
body += '\n\n</details>';
|
|
133
134
|
}
|
|
135
|
+
for (const { title, list } of (0, pipe_utils_js_1.smallRunSections)(this.tests)) {
|
|
136
|
+
body += `\n<details>\n<summary><h3>${title}</h3></summary>\n\n${list.join('\n')}\n</details>`;
|
|
137
|
+
}
|
|
134
138
|
if (this.tests.length > 0 && !isPendingRun) {
|
|
135
139
|
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
136
140
|
body += this.tests
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -317,6 +317,9 @@ class TestomatioPipe {
|
|
|
317
317
|
(0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
|
|
318
318
|
this.#startS3CredentialsRefresh(runParams);
|
|
319
319
|
}
|
|
320
|
+
else {
|
|
321
|
+
this.#warnNoS3Credentials();
|
|
322
|
+
}
|
|
320
323
|
if (resp.data.url) {
|
|
321
324
|
const respUrl = new URL(resp.data.url);
|
|
322
325
|
this.runUrl = `${this.url}${respUrl.pathname}`;
|
|
@@ -345,6 +348,9 @@ class TestomatioPipe {
|
|
|
345
348
|
(0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
|
|
346
349
|
this.#startS3CredentialsRefresh(runParams);
|
|
347
350
|
}
|
|
351
|
+
else {
|
|
352
|
+
this.#warnNoS3Credentials();
|
|
353
|
+
}
|
|
348
354
|
this.store.runUrl = this.runUrl;
|
|
349
355
|
this.store.runPublicUrl = this.runPublicUrl;
|
|
350
356
|
this.store.runId = this.runId;
|
|
@@ -403,9 +409,19 @@ class TestomatioPipe {
|
|
|
403
409
|
}
|
|
404
410
|
}
|
|
405
411
|
catch (err) {
|
|
406
|
-
|
|
412
|
+
log_js_1.log.warn('⚠️ Failed to refresh S3 credentials:', err.message || err, '\nArtifact uploads may fail once the current credentials expire.');
|
|
407
413
|
}
|
|
408
414
|
};
|
|
415
|
+
/**
|
|
416
|
+
* Warns the user when Testomat.io did not return S3 credentials for artifacts upload.
|
|
417
|
+
* Skipped when artifacts uploading was intentionally disabled or a custom S3 storage is configured via env.
|
|
418
|
+
*/
|
|
419
|
+
#warnNoS3Credentials() {
|
|
420
|
+
if (process.env.TESTOMATIO_DISABLE_ARTIFACTS || process.env.S3_BUCKET)
|
|
421
|
+
return;
|
|
422
|
+
const command = `TESTOMATIO=<your_api_key> TESTOMATIO_RUN=${this.runId || '<run_id>'} npx @testomatio/reporter upload-artifacts`;
|
|
423
|
+
log_js_1.log.warn(`⚠️ ${picocolors_1.default.yellow('Testomat.io did not return S3 credentials for artifacts upload.')}`, 'Artifacts will not be uploaded.', 'Please check the artifact sharing settings of your project.', `\nRun "${picocolors_1.default.magenta(command)}" later with valid S3 credentials to re-upload artifacts to this run.`);
|
|
424
|
+
}
|
|
409
425
|
#stopS3CredentialsRefresh() {
|
|
410
426
|
if (this.s3CredentialsRefreshTimer) {
|
|
411
427
|
clearInterval(this.s3CredentialsRefreshTimer);
|
package/lib/replay.js
CHANGED
package/lib/uploader.d.ts
CHANGED
package/lib/uploader.js
CHANGED
|
@@ -26,6 +26,7 @@ class S3Uploader {
|
|
|
26
26
|
*/
|
|
27
27
|
this.skippedUploads = [];
|
|
28
28
|
this.failedUploads = [];
|
|
29
|
+
this.warnedDisabledUpload = false;
|
|
29
30
|
/**
|
|
30
31
|
* @type {{path: string, size: number, link: string}[]}
|
|
31
32
|
*/
|
|
@@ -84,6 +85,22 @@ class S3Uploader {
|
|
|
84
85
|
disableLogStorage() {
|
|
85
86
|
this.storeEnabled = false;
|
|
86
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Warns once when artifact uploading is disabled not intentionally by the user,
|
|
90
|
+
* e.g. S3 credentials were not obtained from Testomat.io and no custom S3 storage is configured.
|
|
91
|
+
* Without this warning artifacts are skipped silently.
|
|
92
|
+
*/
|
|
93
|
+
#warnDisabledUpload(runId) {
|
|
94
|
+
if (this.warnedDisabledUpload)
|
|
95
|
+
return;
|
|
96
|
+
const { S3_BUCKET, TESTOMATIO_DISABLE_ARTIFACTS } = this.getConfig();
|
|
97
|
+
// uploading was intentionally disabled by the user or a custom S3 storage is used, nothing to warn about
|
|
98
|
+
if (TESTOMATIO_DISABLE_ARTIFACTS || S3_BUCKET)
|
|
99
|
+
return;
|
|
100
|
+
this.warnedDisabledUpload = true;
|
|
101
|
+
const command = `TESTOMATIO=<your_api_key> TESTOMATIO_RUN=${runId || '<run_id>'} npx @testomatio/reporter upload-artifacts`;
|
|
102
|
+
log_js_1.log.warn(`🗄️ ${picocolors_1.default.yellow('Artifacts uploading is disabled: S3 credentials were not obtained from Testomat.io')}`, picocolors_1.default.yellow('(and no custom S3 storage is configured).'), 'Artifacts will not be uploaded.', `\nRun "${picocolors_1.default.magenta(command)}" later with valid S3 credentials to re-upload artifacts to this run.`);
|
|
103
|
+
}
|
|
87
104
|
/**
|
|
88
105
|
*
|
|
89
106
|
* @param {*} Body
|
|
@@ -202,6 +219,7 @@ class S3Uploader {
|
|
|
202
219
|
debug(`File ${filePath} does not exist`);
|
|
203
220
|
}
|
|
204
221
|
if (!this.isEnabled) {
|
|
222
|
+
this.#warnDisabledUpload(runId);
|
|
205
223
|
this.storeUploadedFile(filePath, runId, rid, false);
|
|
206
224
|
this.skippedUploads.push({ path: filePath, size: fileSize });
|
|
207
225
|
return;
|
|
@@ -239,8 +257,10 @@ class S3Uploader {
|
|
|
239
257
|
/* WDIO: some artifacts uploading started before createRun function completion
|
|
240
258
|
probably, the reason is that run is NOT created in adapter (but via cli) */
|
|
241
259
|
this.isEnabled = this.isEnabled ?? this.checkEnabled();
|
|
242
|
-
if (!this.isEnabled)
|
|
260
|
+
if (!this.isEnabled) {
|
|
261
|
+
this.#warnDisabledUpload(pathInS3[0]);
|
|
243
262
|
return;
|
|
263
|
+
}
|
|
244
264
|
let Key = pathInS3.filter(p => !!p).join('/');
|
|
245
265
|
const ext = this.#getFileExtBase64(buffer.toString('base64'));
|
|
246
266
|
if (ext) {
|
|
@@ -72,6 +72,22 @@ export function runSummary(tests: Array<{
|
|
|
72
72
|
export function totalDuration(tests: Array<{
|
|
73
73
|
run_time?: number;
|
|
74
74
|
}>): string;
|
|
75
|
+
/**
|
|
76
|
+
* Sections listing the passed, skipped and not yet run tests of a small run; failures have their own
|
|
77
|
+
* section. Not yet run tests (no status, or `pending`) are listed without any status label.
|
|
78
|
+
*
|
|
79
|
+
* @param {Array<{status?: string, title?: string, suite_title?: string, run_time?: number}>} tests
|
|
80
|
+
* @returns {Array<{title: string, list: string[]}>} Non-empty sections; none if the run is not small.
|
|
81
|
+
*/
|
|
82
|
+
export function smallRunSections(tests: Array<{
|
|
83
|
+
status?: string;
|
|
84
|
+
title?: string;
|
|
85
|
+
suite_title?: string;
|
|
86
|
+
run_time?: number;
|
|
87
|
+
}>): Array<{
|
|
88
|
+
title: string;
|
|
89
|
+
list: string[];
|
|
90
|
+
}>;
|
|
75
91
|
/**
|
|
76
92
|
* Describe the scope of a run that was prepared but not executed yet. Prefers the server's count;
|
|
77
93
|
* without it, falls back to the scoped ids, which mix tests (`T…`) and suites (`S…`).
|
|
@@ -123,16 +139,29 @@ export function formatRunOutput(store: {
|
|
|
123
139
|
}, format?: string): string;
|
|
124
140
|
/**
|
|
125
141
|
* Format the runs fetched via the `fetch` command for machine-readable output.
|
|
126
|
-
* `json` prints the full array of run objects; `id` prints one run id per line;
|
|
142
|
+
* `json` prints the full array of run objects (a single object for one run); `id` prints one run id per line;
|
|
127
143
|
* any other format prints a human-readable multi-line summary per run.
|
|
128
144
|
*
|
|
129
|
-
* @param {{data?: Array}} body - Parsed response body from GET /api/v2/:project/runs.
|
|
145
|
+
* @param {{data?: Array|Object}} body - Parsed response body from GET /api/v2/:project/runs[/:id].
|
|
130
146
|
* @param {string} [format] - Value of the CLI `--format` option.
|
|
131
147
|
* @returns {string}
|
|
132
148
|
*/
|
|
133
149
|
export function formatFetchRunsOutput(body: {
|
|
134
|
-
data?: any[];
|
|
150
|
+
data?: any[] | any;
|
|
135
151
|
}, format?: string): string;
|
|
152
|
+
/**
|
|
153
|
+
* Parse a Testomat.io app URL of a run (/projects/:project/runs/:id)
|
|
154
|
+
* or of a runs page (/projects/:project/runs?filterParam=...) into API v2 request parts.
|
|
155
|
+
*
|
|
156
|
+
* @param {string} pageUrl
|
|
157
|
+
* @returns {{baseUrl: string, project: string, runId?: string, params: string[][]}|null}
|
|
158
|
+
*/
|
|
159
|
+
export function parseRunsUrl(pageUrl: string): {
|
|
160
|
+
baseUrl: string;
|
|
161
|
+
project: string;
|
|
162
|
+
runId?: string;
|
|
163
|
+
params: string[][];
|
|
164
|
+
} | null;
|
|
136
165
|
/**
|
|
137
166
|
* Calculate the approximate size of data in bytes (JSON stringified, UTF-8 encoded length).
|
|
138
167
|
* @param {Object} data - Data to measure
|
package/lib/utils/pipe_utils.js
CHANGED
|
@@ -12,11 +12,13 @@ exports.fullName = fullName;
|
|
|
12
12
|
exports.markdownTable = markdownTable;
|
|
13
13
|
exports.runSummary = runSummary;
|
|
14
14
|
exports.totalDuration = totalDuration;
|
|
15
|
+
exports.smallRunSections = smallRunSections;
|
|
15
16
|
exports.plannedTestsLabel = plannedTestsLabel;
|
|
16
17
|
exports.parsePipeOptions = parsePipeOptions;
|
|
17
18
|
exports.formatFilterListIds = formatFilterListIds;
|
|
18
19
|
exports.formatRunOutput = formatRunOutput;
|
|
19
20
|
exports.formatFetchRunsOutput = formatFetchRunsOutput;
|
|
21
|
+
exports.parseRunsUrl = parseRunsUrl;
|
|
20
22
|
exports.getObjectSize = getObjectSize;
|
|
21
23
|
exports.splitTestsIntoChunks = splitTestsIntoChunks;
|
|
22
24
|
const humanize_duration_1 = __importDefault(require("humanize-duration"));
|
|
@@ -135,6 +137,13 @@ function statusEmoji(status) {
|
|
|
135
137
|
return '🕐';
|
|
136
138
|
return '';
|
|
137
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} str
|
|
142
|
+
* @returns {string} The string with its first letter in upper case, e.g. `passed` → `Passed`.
|
|
143
|
+
*/
|
|
144
|
+
function capitalize(str) {
|
|
145
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
146
|
+
}
|
|
138
147
|
/**
|
|
139
148
|
* Generate a full name string based on the provided test object.
|
|
140
149
|
* @param {object} t - The test object.
|
|
@@ -262,6 +271,33 @@ function totalDuration(tests) {
|
|
|
262
271
|
const milliseconds = tests.reduce((total, t) => total + (t.run_time || 0), 0);
|
|
263
272
|
return (0, humanize_duration_1.default)(Math.trunc(milliseconds), { maxDecimalPoints: 0 });
|
|
264
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Runs with fewer tests than this list every test in the report, not only the failed ones.
|
|
276
|
+
*/
|
|
277
|
+
const SMALL_RUN_TESTS_LIMIT = 10;
|
|
278
|
+
/**
|
|
279
|
+
* Sections listing the passed, skipped and not yet run tests of a small run; failures have their own
|
|
280
|
+
* section. Not yet run tests (no status, or `pending`) are listed without any status label.
|
|
281
|
+
*
|
|
282
|
+
* @param {Array<{status?: string, title?: string, suite_title?: string, run_time?: number}>} tests
|
|
283
|
+
* @returns {Array<{title: string, list: string[]}>} Non-empty sections; none if the run is not small.
|
|
284
|
+
*/
|
|
285
|
+
function smallRunSections(tests) {
|
|
286
|
+
if (tests.length >= SMALL_RUN_TESTS_LIMIT)
|
|
287
|
+
return [];
|
|
288
|
+
const sections = ['passed', 'skipped'].map(status => {
|
|
289
|
+
const list = tests
|
|
290
|
+
.filter(t => t.status === status)
|
|
291
|
+
.map(t => {
|
|
292
|
+
const runTime = t.run_time ? ` (${(0, humanize_duration_1.default)(t.run_time)})` : '';
|
|
293
|
+
return `* ${statusEmoji(status)} ${fullName(t)}${runTime}`;
|
|
294
|
+
});
|
|
295
|
+
return { title: `${statusEmoji(status)} ${capitalize(status)} (${list.length})`, list };
|
|
296
|
+
});
|
|
297
|
+
const notRun = tests.filter(t => !t.status || t.status === 'pending').map(t => `* ${fullName(t)}`);
|
|
298
|
+
sections.push({ title: `📋 Tests (${notRun.length})`, list: notRun });
|
|
299
|
+
return sections.filter(section => section.list.length);
|
|
300
|
+
}
|
|
265
301
|
/**
|
|
266
302
|
* Render a two-column markdown table. Rows with an empty value are skipped, so optional rows
|
|
267
303
|
* need no surrounding `if`.
|
|
@@ -327,17 +363,18 @@ function formatRunOutput(store, format) {
|
|
|
327
363
|
}
|
|
328
364
|
/**
|
|
329
365
|
* Format the runs fetched via the `fetch` command for machine-readable output.
|
|
330
|
-
* `json` prints the full array of run objects; `id` prints one run id per line;
|
|
366
|
+
* `json` prints the full array of run objects (a single object for one run); `id` prints one run id per line;
|
|
331
367
|
* any other format prints a human-readable multi-line summary per run.
|
|
332
368
|
*
|
|
333
|
-
* @param {{data?: Array}} body - Parsed response body from GET /api/v2/:project/runs.
|
|
369
|
+
* @param {{data?: Array|Object}} body - Parsed response body from GET /api/v2/:project/runs[/:id].
|
|
334
370
|
* @param {string} [format] - Value of the CLI `--format` option.
|
|
335
371
|
* @returns {string}
|
|
336
372
|
*/
|
|
337
373
|
function formatFetchRunsOutput(body, format) {
|
|
338
|
-
const
|
|
374
|
+
const data = body?.data || [];
|
|
375
|
+
const runs = [data].flat();
|
|
339
376
|
if (format === 'json')
|
|
340
|
-
return JSON.stringify(
|
|
377
|
+
return JSON.stringify(data, null, 2);
|
|
341
378
|
if (format === 'id')
|
|
342
379
|
return runs.map(run => run.id).filter(Boolean).join('\n');
|
|
343
380
|
return runs
|
|
@@ -345,6 +382,8 @@ function formatFetchRunsOutput(body, format) {
|
|
|
345
382
|
const lines = [`* ID: ${run.id}`];
|
|
346
383
|
if (run.title)
|
|
347
384
|
lines.push(` title: ${run.title}`);
|
|
385
|
+
if (run.rungroup_id)
|
|
386
|
+
lines.push(` rungroup: ${run.rungroup_id}`);
|
|
348
387
|
if (run.launched_at)
|
|
349
388
|
lines.push(` started at: ${run.launched_at}`);
|
|
350
389
|
if (run.finished_at)
|
|
@@ -365,6 +404,41 @@ function formatFetchRunsOutput(body, format) {
|
|
|
365
404
|
})
|
|
366
405
|
.join('\n');
|
|
367
406
|
}
|
|
407
|
+
// filterParam keys of the runs page which are API params on their own; the rest go under filter[...]
|
|
408
|
+
const RUNS_PAGE_PARAMS = ['search', 'tql', 'groupId'];
|
|
409
|
+
/**
|
|
410
|
+
* Parse a Testomat.io app URL of a run (/projects/:project/runs/:id)
|
|
411
|
+
* or of a runs page (/projects/:project/runs?filterParam=...) into API v2 request parts.
|
|
412
|
+
*
|
|
413
|
+
* @param {string} pageUrl
|
|
414
|
+
* @returns {{baseUrl: string, project: string, runId?: string, params: string[][]}|null}
|
|
415
|
+
*/
|
|
416
|
+
function parseRunsUrl(pageUrl) {
|
|
417
|
+
let url;
|
|
418
|
+
try {
|
|
419
|
+
url = new URL(pageUrl);
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
const match = url.pathname.match(/\/projects\/([^/]+)\/runs(?:\/([^/]+))?\/?$/);
|
|
425
|
+
if (!match)
|
|
426
|
+
return null;
|
|
427
|
+
const [, project, runId] = match;
|
|
428
|
+
const params = [];
|
|
429
|
+
for (const [key, value] of new URLSearchParams(url.searchParams.get('filterParam') || '')) {
|
|
430
|
+
// "groups" only switches the page to the grouped view
|
|
431
|
+
if (!value || key === 'groups')
|
|
432
|
+
continue;
|
|
433
|
+
if (RUNS_PAGE_PARAMS.includes(key)) {
|
|
434
|
+
params.push([key, value]);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const [name, ...nested] = key.split('[');
|
|
438
|
+
params.push([`filter[${name}]${nested.map(part => `[${part}`).join('')}`, value]);
|
|
439
|
+
}
|
|
440
|
+
return { baseUrl: url.origin, project, runId, params };
|
|
441
|
+
}
|
|
368
442
|
|
|
369
443
|
module.exports.updateFilterType = updateFilterType;
|
|
370
444
|
|
|
@@ -384,6 +458,8 @@ module.exports.runSummary = runSummary;
|
|
|
384
458
|
|
|
385
459
|
module.exports.totalDuration = totalDuration;
|
|
386
460
|
|
|
461
|
+
module.exports.smallRunSections = smallRunSections;
|
|
462
|
+
|
|
387
463
|
module.exports.plannedTestsLabel = plannedTestsLabel;
|
|
388
464
|
|
|
389
465
|
module.exports.parsePipeOptions = parsePipeOptions;
|
|
@@ -394,6 +470,8 @@ module.exports.formatRunOutput = formatRunOutput;
|
|
|
394
470
|
|
|
395
471
|
module.exports.formatFetchRunsOutput = formatFetchRunsOutput;
|
|
396
472
|
|
|
473
|
+
module.exports.parseRunsUrl = parseRunsUrl;
|
|
474
|
+
|
|
397
475
|
module.exports.getObjectSize = getObjectSize;
|
|
398
476
|
|
|
399
477
|
module.exports.splitTestsIntoChunks = splitTestsIntoChunks;
|
package/lib/xmlReader.js
CHANGED
|
@@ -432,6 +432,8 @@ class XmlReader {
|
|
|
432
432
|
}
|
|
433
433
|
this.adapter.formatTest(t);
|
|
434
434
|
t.title = (0, utils_js_1.humanize)(t.title);
|
|
435
|
+
// meta must always include reporter version and adapter used
|
|
436
|
+
t.meta = { ...t.meta, reporterVersion: this.version, adapter: 'xml' };
|
|
435
437
|
});
|
|
436
438
|
}
|
|
437
439
|
formatErrors() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
"test:playwright": "mocha tests/adapter/playwright.test.js",
|
|
66
66
|
"test:codecept": "mocha tests/adapter/codecept.test.js tests/adapter/codecept_comprehensive.test.js tests/adapter/codecept_steps_sections.test.js",
|
|
67
67
|
"test:vitest": "mocha tests/adapter/vitest.test.js",
|
|
68
|
-
"test:
|
|
68
|
+
"test:e2e": "mocha tests/adapter/e2e.test.js",
|
|
69
|
+
"test:frameworks": "npm run test:playwright && npm run test:codecept && npm run test:vitest && npm run test:e2e",
|
|
69
70
|
"test:all": "npm run test && npm run test:frameworks",
|
|
70
71
|
"test:adapters": "mocha tests/adapter/*.test.js",
|
|
71
72
|
"test:codecept:bug948": "mocha tests/adapter/codecept_aftersuite_failure.test.js",
|
|
@@ -129,6 +130,10 @@
|
|
|
129
130
|
"./lib/adapter/cypress-plugin": "./lib/adapter/cypress-plugin/index.js",
|
|
130
131
|
"./cypress-plugin": "./lib/adapter/cypress-plugin/index.js",
|
|
131
132
|
"./cypress": "./lib/adapter/cypress-plugin/index.js",
|
|
133
|
+
"./e2e": {
|
|
134
|
+
"import": "./src/adapter/e2e.js",
|
|
135
|
+
"require": "./lib/adapter/e2e.js"
|
|
136
|
+
},
|
|
132
137
|
"./lib/adapter/jasmine.js": "./lib/adapter/jasmine.js",
|
|
133
138
|
"./jasmine": "./lib/adapter/jasmine.js",
|
|
134
139
|
"./lib/adapter/jest.js": "./lib/adapter/jest.js",
|
package/src/adapter/codecept.js
CHANGED
|
@@ -11,6 +11,8 @@ import codeceptjs from 'codeceptjs';
|
|
|
11
11
|
import { log } from '../utils/log.js';
|
|
12
12
|
|
|
13
13
|
const debug = createDebugMessages('@testomatio/reporter:adapter:codeceptjs');
|
|
14
|
+
|
|
15
|
+
global.TESTOMATIO_ADAPTER = 'codeceptjs';
|
|
14
16
|
// @ts-ignore
|
|
15
17
|
if (!global.codeceptjs) {
|
|
16
18
|
// @ts-ignore
|
|
@@ -8,6 +8,8 @@ import { getTestomatIdFromTestTitle, fileSystem } from '../../utils/utils.js';
|
|
|
8
8
|
import { config } from '../../config.js';
|
|
9
9
|
import { services } from '../../services/index.js';
|
|
10
10
|
|
|
11
|
+
global.TESTOMATIO_ADAPTER = 'cucumber';
|
|
12
|
+
|
|
11
13
|
const { GherkinDocumentParser, PickleParser } = formatterHelpers;
|
|
12
14
|
const { getGherkinScenarioLocationMap, getGherkinStepMap } = GherkinDocumentParser;
|
|
13
15
|
const { getPickleStepMap } = PickleParser;
|
|
@@ -3,6 +3,8 @@ import { getTestomatIdFromTestTitle, parseSuite } from '../../utils/utils.js';
|
|
|
3
3
|
import TestomatClient from '../../client.js';
|
|
4
4
|
import { config } from '../../config.js';
|
|
5
5
|
|
|
6
|
+
global.TESTOMATIO_ADAPTER = 'cypress';
|
|
7
|
+
|
|
6
8
|
const testomatioReporter = on => {
|
|
7
9
|
if (!config.TESTOMATIO) {
|
|
8
10
|
console.log('TESTOMATIO key is empty, ignoring reports');
|