testomatio-reporter-cli 2.14.0-beta → 2.14.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/allureReader.js +70 -24
- package/src/bin/cli.js +2 -2
- package/src/pipe/html.js +13 -10
- package/src/xmlReader.js +4 -2
- package/types/types.d.ts +2 -2
package/package.json
CHANGED
package/src/allureReader.js
CHANGED
|
@@ -183,7 +183,7 @@ class AllureReader {
|
|
|
183
183
|
suite_title: this.extractSuiteTitle(result),
|
|
184
184
|
file: this.extractFile(result),
|
|
185
185
|
run_time: this.calculateRunTime(result),
|
|
186
|
-
steps: this.convertSteps(result.steps || []),
|
|
186
|
+
steps: this.convertSteps(result.steps || [], resultsDir),
|
|
187
187
|
message: result.statusDetails?.message || '',
|
|
188
188
|
stack: result.statusDetails?.trace || '',
|
|
189
189
|
meta: this.extractMeta(result),
|
|
@@ -209,21 +209,9 @@ class AllureReader {
|
|
|
209
209
|
test.example = this.convertParameters(result.parameters);
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const fullPath = path.join(resultsDir, att.source);
|
|
216
|
-
if (fs.existsSync(fullPath)) {
|
|
217
|
-
return fullPath;
|
|
218
|
-
}
|
|
219
|
-
debug('Attachment file not found:', fullPath);
|
|
220
|
-
return null;
|
|
221
|
-
})
|
|
222
|
-
.filter(Boolean);
|
|
223
|
-
|
|
224
|
-
if (attachments.length > 0) {
|
|
225
|
-
test.files = attachments;
|
|
226
|
-
}
|
|
212
|
+
const attachments = this.resolveAttachments(result.attachments, resultsDir);
|
|
213
|
+
if (attachments.length > 0) {
|
|
214
|
+
test.files = attachments;
|
|
227
215
|
}
|
|
228
216
|
|
|
229
217
|
return test;
|
|
@@ -531,7 +519,7 @@ class AllureReader {
|
|
|
531
519
|
return this.extractTmsIdsFromSource(contents, test)[0] || null;
|
|
532
520
|
}
|
|
533
521
|
|
|
534
|
-
convertSteps(steps, depth = 0) {
|
|
522
|
+
convertSteps(steps, resultsDir = '', depth = 0) {
|
|
535
523
|
if (depth >= 10) return null;
|
|
536
524
|
|
|
537
525
|
return steps
|
|
@@ -541,9 +529,15 @@ class AllureReader {
|
|
|
541
529
|
title: step.name || step.title || 'Unknown step',
|
|
542
530
|
status: this.mapStepStatus(step.status),
|
|
543
531
|
duration: this.calculateRunTime(step),
|
|
544
|
-
steps: this.convertSteps(step.steps || [], depth + 1),
|
|
532
|
+
steps: this.convertSteps(step.steps || [], resultsDir, depth + 1),
|
|
545
533
|
};
|
|
546
534
|
|
|
535
|
+
// step attachments stay on the step; uploadArtifacts() swaps the paths for links
|
|
536
|
+
const attachments = this.resolveAttachments(step.attachments, resultsDir);
|
|
537
|
+
if (attachments.length > 0) {
|
|
538
|
+
convertedStep.artifacts = attachments;
|
|
539
|
+
}
|
|
540
|
+
|
|
547
541
|
// Attach the failure description (error message + trace with the failing
|
|
548
542
|
// code line) straight onto the failed step. Testomat.io renders a step's
|
|
549
543
|
// `error` inline in the step tree, so the failure shows up on the exact
|
|
@@ -876,16 +870,68 @@ class AllureReader {
|
|
|
876
870
|
return paths;
|
|
877
871
|
}
|
|
878
872
|
|
|
873
|
+
/**
|
|
874
|
+
* @param {Array<{source?: string}>|undefined} attachments
|
|
875
|
+
* @param {string} resultsDir
|
|
876
|
+
* @returns {string[]} paths of attachments that exist on disk
|
|
877
|
+
*/
|
|
878
|
+
resolveAttachments(attachments, resultsDir) {
|
|
879
|
+
if (!attachments || !attachments.length) return [];
|
|
880
|
+
|
|
881
|
+
return attachments
|
|
882
|
+
.map(att => {
|
|
883
|
+
if (!att?.source) return null;
|
|
884
|
+
const fullPath = path.join(resultsDir || '', att.source);
|
|
885
|
+
if (fs.existsSync(fullPath)) return fullPath;
|
|
886
|
+
debug('Attachment file not found:', fullPath);
|
|
887
|
+
return null;
|
|
888
|
+
})
|
|
889
|
+
.filter(Boolean);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Replaces step artifact paths with S3 links, dropping failed uploads (a local path
|
|
894
|
+
* would be a dead link in the UI).
|
|
895
|
+
*
|
|
896
|
+
* @param {Array<object>|undefined} steps
|
|
897
|
+
* @param {string} runId
|
|
898
|
+
* @param {string} rid
|
|
899
|
+
* @returns {Promise<number>} number of uploaded step artifacts
|
|
900
|
+
*/
|
|
901
|
+
async uploadStepArtifacts(steps, runId, rid) {
|
|
902
|
+
if (!steps || !steps.length) return 0;
|
|
903
|
+
|
|
904
|
+
let uploaded = 0;
|
|
905
|
+
for (const step of steps) {
|
|
906
|
+
if (step.artifacts?.length) {
|
|
907
|
+
const links = await Promise.all(
|
|
908
|
+
step.artifacts.map(f => this.uploader.uploadFileByPath(f, [runId, rid, 'steps', path.basename(f)])),
|
|
909
|
+
);
|
|
910
|
+
step.artifacts = links.filter(link => !!link);
|
|
911
|
+
uploaded += step.artifacts.length;
|
|
912
|
+
if (!step.artifacts.length) delete step.artifacts;
|
|
913
|
+
}
|
|
914
|
+
uploaded += await this.uploadStepArtifacts(step.steps, runId, rid);
|
|
915
|
+
}
|
|
916
|
+
return uploaded;
|
|
917
|
+
}
|
|
918
|
+
|
|
879
919
|
async uploadArtifacts() {
|
|
880
|
-
for (const test of this._tests
|
|
920
|
+
for (const test of this._tests) {
|
|
881
921
|
const runId = this.runId || this.store.runId || Date.now().toString();
|
|
882
|
-
|
|
883
|
-
|
|
922
|
+
|
|
923
|
+
// uploadFileByPath resolves to a link string, or undefined if skipped or failed
|
|
924
|
+
const links = await Promise.all(
|
|
925
|
+
(test.files || []).map(f => this.uploader.uploadFileByPath(f, [runId, test.rid, path.basename(f)])),
|
|
884
926
|
);
|
|
885
|
-
test.artifacts =
|
|
927
|
+
test.artifacts = links.filter(link => !!link);
|
|
886
928
|
delete test.files;
|
|
887
|
-
|
|
888
|
-
|
|
929
|
+
|
|
930
|
+
const stepArtifacts = await this.uploadStepArtifacts(test.steps, runId, test.rid);
|
|
931
|
+
|
|
932
|
+
const total = test.artifacts.length + stepArtifacts;
|
|
933
|
+
if (total > 0) {
|
|
934
|
+
console.log(APP_PREFIX, `🗄️ Uploaded ${pc.bold(`${total} artifacts`)} for test ${test.title}`);
|
|
889
935
|
}
|
|
890
936
|
}
|
|
891
937
|
}
|
package/src/bin/cli.js
CHANGED
|
@@ -51,7 +51,7 @@ program
|
|
|
51
51
|
program
|
|
52
52
|
.command('start')
|
|
53
53
|
.description('Start a new run and return its ID')
|
|
54
|
-
.option('--kind <type>', 'Specify run type: automated, manual, or
|
|
54
|
+
.option('--kind <type>', 'Specify run type: automated, manual, mixed, or detect')
|
|
55
55
|
.option('--filter <filter>', 'Scope the prepared run to tests matching the filter (no execution)')
|
|
56
56
|
.option('--format <format>', 'Machine-readable output: print only the run id to stdout (e.g. --format id)')
|
|
57
57
|
.option('--warn', 'Exit 0 instead of 1 when the filter matches no tests (warn only)')
|
|
@@ -130,7 +130,7 @@ program
|
|
|
130
130
|
.option('--filter <filter>', 'Additional execution filter')
|
|
131
131
|
.option('--filter-list <filter>', 'Get a list of all tests by filter before running')
|
|
132
132
|
.option('--format <format>', 'Machine-readable output format for --filter-list (grep, json, newline, ids)')
|
|
133
|
-
.option('--kind <type>', 'Specify run type: automated, manual, or
|
|
133
|
+
.option('--kind <type>', 'Specify run type: automated, manual, mixed, or detect')
|
|
134
134
|
.option('--remote <profile>', 'Trigger run on the named Testomat.io CI profile instead of executing locally')
|
|
135
135
|
.option(
|
|
136
136
|
'--remote-param <kv>',
|
package/src/pipe/html.js
CHANGED
|
@@ -32,6 +32,7 @@ class HtmlPipe {
|
|
|
32
32
|
this.filenameMsg = '';
|
|
33
33
|
this.tests = [];
|
|
34
34
|
this.configuration = null;
|
|
35
|
+
this.startedAt = null;
|
|
35
36
|
|
|
36
37
|
if (this.isHtml) {
|
|
37
38
|
this.isEnabled = true;
|
|
@@ -70,6 +71,8 @@ class HtmlPipe {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
async createRun(params = {}) {
|
|
74
|
+
this.startedAt ??= new Date();
|
|
75
|
+
|
|
73
76
|
if (params?.configuration && typeof params.configuration === 'object') {
|
|
74
77
|
this.configuration = { ...(this.configuration || {}), ...params.configuration };
|
|
75
78
|
}
|
|
@@ -266,7 +269,7 @@ class HtmlPipe {
|
|
|
266
269
|
parallel: runParams.isParallel || 'No parallel info',
|
|
267
270
|
runUrl: this.store.runUrl || '',
|
|
268
271
|
executionTime: testExecutionSumTime(aggregatedTests),
|
|
269
|
-
executionDate:
|
|
272
|
+
executionDate: getDateTimeFormatted(this.startedAt || new Date()),
|
|
270
273
|
description:
|
|
271
274
|
[this.description, runParams.description || this.store.coverageDescription || this.store.description]
|
|
272
275
|
.filter(Boolean)
|
|
@@ -722,17 +725,17 @@ function formatDuration(duration) {
|
|
|
722
725
|
}
|
|
723
726
|
|
|
724
727
|
/**
|
|
725
|
-
*
|
|
728
|
+
* Formats a date and time for display in the report.
|
|
729
|
+
* @param {Date} date - Date and time to format.
|
|
726
730
|
* @returns {string} - The formatted date and time string (e.g., "(01/01/2023 12:00:00)").
|
|
727
731
|
*/
|
|
728
|
-
function
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
const
|
|
733
|
-
const
|
|
734
|
-
const
|
|
735
|
-
const seconds = currentDate.getSeconds().toString().padStart(2, '0');
|
|
732
|
+
function getDateTimeFormatted(date) {
|
|
733
|
+
const day = date.getDate().toString().padStart(2, '0');
|
|
734
|
+
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
735
|
+
const year = date.getFullYear();
|
|
736
|
+
const hours = date.getHours().toString().padStart(2, '0');
|
|
737
|
+
const minutes = date.getMinutes().toString().padStart(2, '0');
|
|
738
|
+
const seconds = date.getSeconds().toString().padStart(2, '0');
|
|
736
739
|
|
|
737
740
|
return `(${day}/${month}/${year} ${hours}:${minutes}:${seconds})`;
|
|
738
741
|
}
|
package/src/xmlReader.js
CHANGED
|
@@ -533,8 +533,10 @@ class XmlReader {
|
|
|
533
533
|
if (!files.length) continue;
|
|
534
534
|
|
|
535
535
|
const runId = this.runId || this.store.runId || Date.now().toString();
|
|
536
|
-
|
|
537
|
-
|
|
536
|
+
// undefined for skipped/failed uploads; keeping those serializes as `null` links
|
|
537
|
+
const links = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path.basename(f)])));
|
|
538
|
+
test.artifacts = links.filter(link => !!link);
|
|
539
|
+
log.info(`🗄️ Uploaded ${pc.bold(`${test.artifacts.length} artifacts`)} for test ${test.title}`);
|
|
538
540
|
}
|
|
539
541
|
}
|
|
540
542
|
|
package/types/types.d.ts
CHANGED
|
@@ -337,8 +337,8 @@ export interface PipeResult {
|
|
|
337
337
|
* `TESTOMATIO_CI_PROFILE` and `TESTOMATIO_CI_OVERRIDE`.
|
|
338
338
|
*/
|
|
339
339
|
export interface CreateRunParams {
|
|
340
|
-
/** Run kind. Defaults to `automated` server-side. */
|
|
341
|
-
kind?: 'automated' | 'manual' | 'mixed';
|
|
340
|
+
/** Run kind. Defaults to `automated` server-side. `detect` resolves to one of the other three from the scoped tests. */
|
|
341
|
+
kind?: 'automated' | 'manual' | 'mixed' | 'detect';
|
|
342
342
|
|
|
343
343
|
/** Run title. */
|
|
344
344
|
title?: string;
|