@testomatio/reporter 2.9.3-beta.4-allure-links → 2.9.4
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/README.md +2 -4
- package/lib/adapter/codecept.js +33 -14
- package/lib/adapter/playwright.js +19 -8
- package/lib/bin/cli.js +68 -34
- package/lib/client.js +4 -3
- package/lib/junit-adapter/index.js +0 -4
- package/lib/pipe/debug.d.ts +6 -0
- package/lib/pipe/debug.js +11 -0
- package/lib/pipe/html.d.ts +1 -0
- package/lib/pipe/html.js +152 -45
- package/lib/replay.js +21 -0
- package/lib/template/testomatio.hbs +77 -11
- package/lib/utils/pipe_utils.d.ts +0 -17
- package/lib/utils/pipe_utils.js +0 -45
- package/lib/utils/utils.js +0 -9
- package/lib/xmlReader.d.ts +1 -0
- package/lib/xmlReader.js +38 -2
- package/package.json +1 -1
- package/src/adapter/codecept.js +35 -12
- package/src/adapter/playwright.js +22 -9
- package/src/bin/cli.js +74 -43
- package/src/client.js +4 -3
- package/src/junit-adapter/index.js +0 -4
- package/src/pipe/debug.js +11 -0
- package/src/pipe/html.js +171 -52
- package/src/replay.js +21 -0
- package/src/template/testomatio.hbs +77 -11
- package/src/utils/pipe_utils.js +0 -49
- package/src/utils/utils.js +0 -5
- package/src/xmlReader.js +47 -2
- package/lib/allureReader.d.ts +0 -217
- package/lib/allureReader.js +0 -860
- package/lib/junit-adapter/kotlin.d.ts +0 -5
- package/lib/junit-adapter/kotlin.js +0 -46
- package/src/allureReader.js +0 -954
- package/src/junit-adapter/kotlin.js +0 -48
package/src/pipe/html.js
CHANGED
|
@@ -6,11 +6,12 @@ import pc from 'picocolors';
|
|
|
6
6
|
import handlebars from 'handlebars';
|
|
7
7
|
import { marked } from 'marked';
|
|
8
8
|
import fileUrl from 'file-url';
|
|
9
|
-
import { fileSystem, isSameTest, ansiRegExp, formatStep } from '../utils/utils.js';
|
|
9
|
+
import { fileSystem, isSameTest, ansiRegExp, formatStep, transformEnvVarToBoolean } from '../utils/utils.js';
|
|
10
10
|
import { HTML_REPORT } from '../constants.js';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
|
|
13
13
|
const debug = createDebugMessages('@testomatio/reporter:pipe:html');
|
|
14
|
+
const HTML_ARTIFACTS_DIR = 'artifacts';
|
|
14
15
|
|
|
15
16
|
// @ts-ignore – this line will be removed in compiled code (already defined in the global scope of commonjs)
|
|
16
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -22,6 +23,7 @@ class HtmlPipe {
|
|
|
22
23
|
this.description = params.description || process.env.TESTOMATIO_DESCRIPTION;
|
|
23
24
|
this.apiKey = params.apiKey || process.env.TESTOMATIO;
|
|
24
25
|
this.isHtml = params.html ?? process.env.TESTOMATIO_HTML_REPORT_SAVE;
|
|
26
|
+
this.htmlCopyArtifacts = params.htmlCopyArtifacts;
|
|
25
27
|
|
|
26
28
|
debug('HTML Pipe: ', this.apiKey ? 'API KEY' : '*no api key provided*');
|
|
27
29
|
|
|
@@ -151,6 +153,8 @@ class HtmlPipe {
|
|
|
151
153
|
|
|
152
154
|
const aggregatedTests = aggregateTestRetries(tests);
|
|
153
155
|
|
|
156
|
+
const copyLocalArtifacts = resolveHtmlCopyArtifacts(this.htmlCopyArtifacts);
|
|
157
|
+
const portableArtifacts = new Map();
|
|
154
158
|
aggregatedTests.forEach(test => {
|
|
155
159
|
const logsRaw =
|
|
156
160
|
test.logs || test.meta?.logs || test.meta?.console || test.meta?.stdout || test.meta?.stderr || '';
|
|
@@ -228,7 +232,12 @@ class HtmlPipe {
|
|
|
228
232
|
test.title = 'Unknown test title';
|
|
229
233
|
}
|
|
230
234
|
|
|
235
|
+
test.title = buildExampleTitle(test.title, test.example);
|
|
236
|
+
|
|
231
237
|
test.artifacts = normalizeArtifacts(test);
|
|
238
|
+
if (copyLocalArtifacts) {
|
|
239
|
+
makeLocalArtifactsPortable(test, outputPath, portableArtifacts);
|
|
240
|
+
}
|
|
232
241
|
|
|
233
242
|
const allPossibleArtifacts = [
|
|
234
243
|
...(test.artifacts || []),
|
|
@@ -460,6 +469,21 @@ function parseRetryInfo(test) {
|
|
|
460
469
|
}
|
|
461
470
|
}
|
|
462
471
|
|
|
472
|
+
function buildExampleTitle(title, example) {
|
|
473
|
+
if (example == null) return title;
|
|
474
|
+
|
|
475
|
+
let exampleText;
|
|
476
|
+
try {
|
|
477
|
+
exampleText = JSON.stringify(example);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
return title;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!exampleText || title.includes(exampleText)) return title;
|
|
483
|
+
|
|
484
|
+
return `${title} | ${exampleText}`;
|
|
485
|
+
}
|
|
486
|
+
|
|
463
487
|
function escapeHtml(str = '') {
|
|
464
488
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
465
489
|
}
|
|
@@ -1013,57 +1037,7 @@ function normalizeArtifacts(test) {
|
|
|
1013
1037
|
];
|
|
1014
1038
|
|
|
1015
1039
|
return allArtifacts
|
|
1016
|
-
.map(
|
|
1017
|
-
if (typeof artifact === 'string') {
|
|
1018
|
-
if (/^https?:\/\//i.test(artifact)) {
|
|
1019
|
-
const base = path.basename(new URL(artifact).pathname) || artifact;
|
|
1020
|
-
|
|
1021
|
-
return {
|
|
1022
|
-
name: base,
|
|
1023
|
-
title: base,
|
|
1024
|
-
path: artifact,
|
|
1025
|
-
fsPath: null,
|
|
1026
|
-
relativePath: artifact,
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const abs = path.isAbsolute(artifact) ? artifact : path.resolve(process.cwd(), artifact);
|
|
1031
|
-
const href = artifact.startsWith('file://') ? artifact : fileUrl(abs, { resolve: true });
|
|
1032
|
-
const base = path.basename(abs);
|
|
1033
|
-
|
|
1034
|
-
return {
|
|
1035
|
-
name: base,
|
|
1036
|
-
title: base,
|
|
1037
|
-
path: href,
|
|
1038
|
-
fsPath: abs,
|
|
1039
|
-
relativePath: artifact,
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
if (artifact?.path) {
|
|
1044
|
-
const raw = String(artifact.path);
|
|
1045
|
-
const isFileUrl = raw.startsWith('file://');
|
|
1046
|
-
const isHttpUrl = /^https?:\/\//i.test(raw);
|
|
1047
|
-
const abs = isFileUrl || isHttpUrl ? null : path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
|
|
1048
|
-
const href = isFileUrl || isHttpUrl ? raw : fileUrl(abs, { resolve: true });
|
|
1049
|
-
const base = abs
|
|
1050
|
-
? path.basename(abs)
|
|
1051
|
-
: isHttpUrl
|
|
1052
|
-
? path.basename(new URL(raw).pathname) || artifact.name || artifact.title || 'attachment'
|
|
1053
|
-
: artifact.name || artifact.title || 'attachment';
|
|
1054
|
-
|
|
1055
|
-
return {
|
|
1056
|
-
...artifact,
|
|
1057
|
-
name: artifact.name || artifact.title || base,
|
|
1058
|
-
title: artifact.title || artifact.name || base,
|
|
1059
|
-
path: href,
|
|
1060
|
-
fsPath: abs || artifact.fsPath || null,
|
|
1061
|
-
relativePath: artifact.relativePath || raw,
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
return artifact;
|
|
1066
|
-
})
|
|
1040
|
+
.map(normalizeArtifact)
|
|
1067
1041
|
.filter(Boolean)
|
|
1068
1042
|
.filter(artifact => {
|
|
1069
1043
|
const isTrace = (artifact.title === 'trace' || artifact.name === 'trace') &&
|
|
@@ -1074,6 +1048,151 @@ function normalizeArtifacts(test) {
|
|
|
1074
1048
|
});
|
|
1075
1049
|
}
|
|
1076
1050
|
|
|
1051
|
+
function resolveHtmlCopyArtifacts(value) {
|
|
1052
|
+
if (value !== undefined) return transformEnvVarToBoolean(value);
|
|
1053
|
+
if (process.env.TESTOMATIO_HTML_REPORT_COPY_ARTIFACTS !== undefined) {
|
|
1054
|
+
return transformEnvVarToBoolean(process.env.TESTOMATIO_HTML_REPORT_COPY_ARTIFACTS);
|
|
1055
|
+
}
|
|
1056
|
+
return !isS3ArtifactsUploadEnabled();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function isS3ArtifactsUploadEnabled() {
|
|
1060
|
+
return Boolean(process.env.S3_BUCKET && !transformEnvVarToBoolean(process.env.TESTOMATIO_DISABLE_ARTIFACTS));
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function normalizeArtifact(artifact) {
|
|
1064
|
+
if (typeof artifact === 'string') {
|
|
1065
|
+
return normalizeArtifactPath({ raw: artifact });
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (artifact?.path) {
|
|
1069
|
+
return normalizeArtifactPath({
|
|
1070
|
+
raw: String(artifact.path),
|
|
1071
|
+
artifact,
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return artifact;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* @param {{
|
|
1080
|
+
* raw: string,
|
|
1081
|
+
* artifact?: { name?: string, title?: string, relativePath?: string, [key: string]: any }
|
|
1082
|
+
* }} params
|
|
1083
|
+
*/
|
|
1084
|
+
function normalizeArtifactPath({ raw, artifact = {} }) {
|
|
1085
|
+
const url = parseUrl(raw);
|
|
1086
|
+
const fsPath = getLocalArtifactPath(raw, url);
|
|
1087
|
+
const base = getArtifactBaseName({ raw, url, fsPath, artifact });
|
|
1088
|
+
const href = fsPath && url?.protocol !== 'file:' ? fileUrl(fsPath, { resolve: true }) : raw;
|
|
1089
|
+
|
|
1090
|
+
return {
|
|
1091
|
+
...artifact,
|
|
1092
|
+
name: artifact.name || artifact.title || base,
|
|
1093
|
+
title: artifact.title || artifact.name || base,
|
|
1094
|
+
path: href,
|
|
1095
|
+
fsPath,
|
|
1096
|
+
relativePath: artifact.relativePath || raw,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function makeLocalArtifactsPortable(test, outputPath, portableArtifacts) {
|
|
1101
|
+
const reportDir = path.dirname(path.resolve(outputPath));
|
|
1102
|
+
|
|
1103
|
+
test.artifacts = makeArtifactsPortable(test.artifacts, reportDir, portableArtifacts);
|
|
1104
|
+
|
|
1105
|
+
if (Array.isArray(test.stepsArray)) {
|
|
1106
|
+
makeStepArtifactsPortable(test.stepsArray, reportDir, portableArtifacts);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function makeStepArtifactsPortable(steps, reportDir, portableArtifacts) {
|
|
1111
|
+
steps.forEach(step => {
|
|
1112
|
+
if (Array.isArray(step.artifacts)) {
|
|
1113
|
+
const normalizedArtifacts = step.artifacts.map(normalizeArtifact).filter(Boolean);
|
|
1114
|
+
step.artifacts = makeArtifactsPortable(normalizedArtifacts, reportDir, portableArtifacts);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (Array.isArray(step.steps)) {
|
|
1118
|
+
makeStepArtifactsPortable(step.steps, reportDir, portableArtifacts);
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function makeArtifactsPortable(artifacts, reportDir, portableArtifacts) {
|
|
1124
|
+
if (!Array.isArray(artifacts)) return artifacts;
|
|
1125
|
+
|
|
1126
|
+
return artifacts.map(artifact => {
|
|
1127
|
+
const fsPath = artifact?.fsPath;
|
|
1128
|
+
if (!fsPath || !fs.existsSync(fsPath)) return artifact;
|
|
1129
|
+
|
|
1130
|
+
const relativePath = copyArtifactToReport(fsPath, reportDir, portableArtifacts);
|
|
1131
|
+
return { ...artifact, path: relativePath, relativePath };
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function copyArtifactToReport(fsPath, reportDir, portableArtifacts) {
|
|
1136
|
+
const absoluteSource = path.resolve(fsPath);
|
|
1137
|
+
if (portableArtifacts.has(absoluteSource)) return portableArtifacts.get(absoluteSource);
|
|
1138
|
+
|
|
1139
|
+
const artifactsDir = path.join(reportDir, HTML_ARTIFACTS_DIR);
|
|
1140
|
+
const ext = path.extname(absoluteSource);
|
|
1141
|
+
const baseName = path.basename(absoluteSource, ext) || 'attachment';
|
|
1142
|
+
let destinationPath = path.join(artifactsDir, `${baseName}${ext}`);
|
|
1143
|
+
let index = 1;
|
|
1144
|
+
|
|
1145
|
+
while (Array.from(portableArtifacts.values()).includes(getRelativeArtifactPath(reportDir, destinationPath))) {
|
|
1146
|
+
destinationPath = path.join(artifactsDir, `${baseName}-${index}${ext}`);
|
|
1147
|
+
index += 1;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
1151
|
+
fs.copyFileSync(absoluteSource, destinationPath);
|
|
1152
|
+
|
|
1153
|
+
const relativePath = getRelativeArtifactPath(reportDir, destinationPath);
|
|
1154
|
+
portableArtifacts.set(absoluteSource, relativePath);
|
|
1155
|
+
return relativePath;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function getLocalArtifactPath(raw, url = parseUrl(raw)) {
|
|
1159
|
+
if (path.isAbsolute(raw)) return raw;
|
|
1160
|
+
if (isRemoteUrl(url)) return null;
|
|
1161
|
+
|
|
1162
|
+
if (url?.protocol === 'file:') {
|
|
1163
|
+
try {
|
|
1164
|
+
return fileURLToPath(url);
|
|
1165
|
+
} catch (_) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (url) return null;
|
|
1171
|
+
return path.resolve(process.cwd(), raw);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function getArtifactBaseName({ raw, url, fsPath, artifact }) {
|
|
1175
|
+
if (fsPath) return path.basename(fsPath);
|
|
1176
|
+
if (url?.pathname) return path.basename(url.pathname) || artifact.name || artifact.title || 'attachment';
|
|
1177
|
+
return artifact.name || artifact.title || path.basename(raw) || 'attachment';
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function parseUrl(value) {
|
|
1181
|
+
try {
|
|
1182
|
+
return new URL(String(value));
|
|
1183
|
+
} catch (_) {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function isRemoteUrl(url) {
|
|
1189
|
+
return url?.protocol === 'http:' || url?.protocol === 'https:';
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function getRelativeArtifactPath(reportDir, artifactPath) {
|
|
1193
|
+
return `./${String(path.relative(reportDir, artifactPath)).split(path.sep).join('/')}`;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1077
1196
|
/**
|
|
1078
1197
|
* Loads trace files from test.files and converts them to base64 data URLs
|
|
1079
1198
|
* @param {object} test - Test object with files array
|
package/src/replay.js
CHANGED
|
@@ -49,6 +49,7 @@ export class Replay {
|
|
|
49
49
|
const testsWithoutRid = []; // For tests without rid (backward compatibility)
|
|
50
50
|
const envVars = {};
|
|
51
51
|
let runId = null;
|
|
52
|
+
const artifactsToAdd = []; // Store artifacts to add after all tests are loaded
|
|
52
53
|
|
|
53
54
|
// Parse debug file line by line
|
|
54
55
|
for (const [lineIndex, line] of lines.entries()) {
|
|
@@ -119,6 +120,11 @@ export class Replay {
|
|
|
119
120
|
// Handle tests without rid (no deduplication)
|
|
120
121
|
testsWithoutRid.push({ ...test });
|
|
121
122
|
}
|
|
123
|
+
} else if (logEntry.action === 'addArtifacts' && logEntry.artifacts) {
|
|
124
|
+
if (logEntry.runId && !runId) {
|
|
125
|
+
runId = logEntry.runId;
|
|
126
|
+
}
|
|
127
|
+
artifactsToAdd.push(...logEntry.artifacts);
|
|
122
128
|
} else if (logEntry.action === 'finishRun') {
|
|
123
129
|
finishParams = logEntry.params || {};
|
|
124
130
|
if (logEntry.runId && !runId) {
|
|
@@ -138,6 +144,21 @@ export class Replay {
|
|
|
138
144
|
this.onError(`${parseErrors - 3} more parse errors occurred`);
|
|
139
145
|
}
|
|
140
146
|
|
|
147
|
+
for (const artifact of artifactsToAdd) {
|
|
148
|
+
if (artifact.rid) {
|
|
149
|
+
const ridToFind = artifact.rid;
|
|
150
|
+
const fullRid = runId ? `${runId}-${ridToFind}` : ridToFind;
|
|
151
|
+
|
|
152
|
+
const test = testsMap.get(fullRid) || testsMap.get(ridToFind);
|
|
153
|
+
if (test && artifact.path) {
|
|
154
|
+
if (!test.files) test.files = [];
|
|
155
|
+
if (!test.files.includes(artifact.path)) {
|
|
156
|
+
test.files.push(artifact.path);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
141
162
|
// Combine tests with rid and tests without rid
|
|
142
163
|
const allTests = [...Array.from(testsMap.values()), ...testsWithoutRid];
|
|
143
164
|
|
|
@@ -681,8 +681,9 @@
|
|
|
681
681
|
|
|
682
682
|
.test-group-header {
|
|
683
683
|
padding: 20px;
|
|
684
|
-
background:
|
|
685
|
-
color:
|
|
684
|
+
background: var(--gray-50);
|
|
685
|
+
color: var(--gray-800);
|
|
686
|
+
border-left: 4px solid var(--primary-color);
|
|
686
687
|
cursor: pointer;
|
|
687
688
|
display: flex;
|
|
688
689
|
justify-content: space-between;
|
|
@@ -691,7 +692,37 @@
|
|
|
691
692
|
}
|
|
692
693
|
|
|
693
694
|
.test-group-header:hover {
|
|
694
|
-
|
|
695
|
+
background: var(--gray-100);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.test-group-header.has-failures {
|
|
699
|
+
background: #fef2f2;
|
|
700
|
+
color: var(--gray-900);
|
|
701
|
+
border-left-color: var(--danger-color);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.test-group-header.has-failures:hover {
|
|
705
|
+
background: #fee2e2;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.test-group-header.has-skips {
|
|
709
|
+
background: #fffbeb;
|
|
710
|
+
color: var(--gray-900);
|
|
711
|
+
border-left-color: var(--warning-color);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.test-group-header.has-skips:hover {
|
|
715
|
+
background: #fef3c7;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.test-group-header.all-passed {
|
|
719
|
+
background: #ecfdf5;
|
|
720
|
+
color: var(--gray-900);
|
|
721
|
+
border-left-color: var(--success-color);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.test-group-header.all-passed:hover {
|
|
725
|
+
background: #d1fae5;
|
|
695
726
|
}
|
|
696
727
|
|
|
697
728
|
.test-group-title {
|
|
@@ -707,28 +738,56 @@
|
|
|
707
738
|
}
|
|
708
739
|
|
|
709
740
|
.test-group-count {
|
|
710
|
-
background:
|
|
741
|
+
background: white;
|
|
742
|
+
color: var(--gray-600);
|
|
711
743
|
padding: 6px 16px;
|
|
712
744
|
border-radius: 20px;
|
|
713
745
|
font-size: 14px;
|
|
714
746
|
font-weight: 600;
|
|
747
|
+
border: 1px solid var(--gray-200);
|
|
715
748
|
}
|
|
716
749
|
|
|
717
750
|
.test-group-stats {
|
|
718
751
|
display: flex;
|
|
719
|
-
gap:
|
|
752
|
+
gap: 8px;
|
|
720
753
|
font-size: 14px;
|
|
721
754
|
margin-right: 10px;
|
|
755
|
+
flex-wrap: wrap;
|
|
756
|
+
justify-content: flex-end;
|
|
722
757
|
}
|
|
723
758
|
|
|
724
759
|
.test-group-stat {
|
|
725
760
|
display: flex;
|
|
726
761
|
align-items: center;
|
|
727
762
|
gap: 6px;
|
|
763
|
+
padding: 6px 10px;
|
|
764
|
+
border-radius: 999px;
|
|
765
|
+
font-weight: 700;
|
|
766
|
+
background: white;
|
|
767
|
+
border: 1px solid currentColor;
|
|
728
768
|
}
|
|
729
769
|
|
|
730
|
-
.test-group-
|
|
770
|
+
.test-group-stat.passed {
|
|
771
|
+
color: var(--success-color);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.test-group-stat.failed {
|
|
731
775
|
color: white;
|
|
776
|
+
background: var(--danger-color);
|
|
777
|
+
border-color: var(--danger-color);
|
|
778
|
+
box-shadow: 0 1px 3px rgba(239, 68, 68, 0.35);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.test-group-stat.skipped {
|
|
782
|
+
color: var(--warning-color);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.test-group-stat.todo {
|
|
786
|
+
color: #8b5cf6;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.test-group-expand {
|
|
790
|
+
color: var(--gray-500);
|
|
732
791
|
transition: var(--transition);
|
|
733
792
|
font-size: 18px;
|
|
734
793
|
}
|
|
@@ -3366,11 +3425,18 @@
|
|
|
3366
3425
|
const failedCount = group.tests.filter(t => t.status.toLowerCase() === 'failed').length;
|
|
3367
3426
|
const skippedCount = group.tests.filter(t => t.status.toLowerCase() === 'skipped').length;
|
|
3368
3427
|
const todoCount = group.tests.filter(t => t.status.toLowerCase() === 'todo').length;
|
|
3428
|
+
const groupStateClass = failedCount > 0
|
|
3429
|
+
? 'has-failures'
|
|
3430
|
+
: skippedCount > 0 || todoCount > 0
|
|
3431
|
+
? 'has-skips'
|
|
3432
|
+
: passedCount > 0
|
|
3433
|
+
? 'all-passed'
|
|
3434
|
+
: '';
|
|
3369
3435
|
|
|
3370
3436
|
const groupIcon = group.type === 'suite' ? 'layer-group' : 'file-code';
|
|
3371
3437
|
|
|
3372
3438
|
const header = document.createElement('div');
|
|
3373
|
-
header.className = 'test-group-header';
|
|
3439
|
+
header.className = ['test-group-header', groupStateClass].filter(Boolean).join(' ');
|
|
3374
3440
|
header.innerHTML = `
|
|
3375
3441
|
<div class='test-group-title'>
|
|
3376
3442
|
<i class='fas fa-${groupIcon}'></i>
|
|
@@ -3378,10 +3444,10 @@
|
|
|
3378
3444
|
<div class='test-group-count'>${group.tests.length} tests</div>
|
|
3379
3445
|
</div>
|
|
3380
3446
|
<div class='test-group-stats'>
|
|
3381
|
-
${passedCount > 0 ? `<div class='test-group-stat
|
|
3382
|
-
${failedCount > 0 ? `<div class='test-group-stat
|
|
3383
|
-
${skippedCount > 0 ? `<div class='test-group-stat
|
|
3384
|
-
${todoCount > 0 ? `<div class='test-group-stat
|
|
3447
|
+
${passedCount > 0 ? `<div class='test-group-stat passed'><i class='fas fa-check-circle'></i> ${passedCount}</div>` : ''}
|
|
3448
|
+
${failedCount > 0 ? `<div class='test-group-stat failed'><i class='fas fa-times-circle'></i> ${failedCount}</div>` : ''}
|
|
3449
|
+
${skippedCount > 0 ? `<div class='test-group-stat skipped'><i class='fas fa-forward'></i> ${skippedCount}</div>` : ''}
|
|
3450
|
+
${todoCount > 0 ? `<div class='test-group-stat todo'><i class='fas fa-clock'></i> ${todoCount}</div>` : ''}
|
|
3385
3451
|
</div>
|
|
3386
3452
|
<i class='fas fa-chevron-down test-group-expand'></i>
|
|
3387
3453
|
`;
|
package/src/utils/pipe_utils.js
CHANGED
|
@@ -161,53 +161,6 @@ function parsePipeOptions(optionsStr) {
|
|
|
161
161
|
return options;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/**
|
|
165
|
-
* Calculate the approximate size of data in bytes (JSON stringified, UTF-8 encoded length).
|
|
166
|
-
* @param {Object} data - Data to measure
|
|
167
|
-
* @returns {number} Size in bytes
|
|
168
|
-
*/
|
|
169
|
-
function getObjectSize(data) {
|
|
170
|
-
const body = JSON.stringify(data);
|
|
171
|
-
return new TextEncoder().encode(body).length;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Split a tests array into chunks bounded by serialized size.
|
|
176
|
-
* Used by the XML and Allure readers so both upload tests with identical batching:
|
|
177
|
-
* each chunk becomes a single batch request (manual batch mode), which keeps requests
|
|
178
|
-
* under the size limit and guarantees every test is sent exactly once.
|
|
179
|
-
*
|
|
180
|
-
* @param {Array} tests - Array of tests to split
|
|
181
|
-
* @param {number} [maxSizeBytes=1048576] - Maximum serialized size per chunk (default 1MB)
|
|
182
|
-
* @returns {Array<Array>} Array of test chunks
|
|
183
|
-
*/
|
|
184
|
-
function splitTestsIntoChunks(tests, maxSizeBytes = 1 * 1024 * 1024) {
|
|
185
|
-
const chunks = [];
|
|
186
|
-
let currentChunk = [];
|
|
187
|
-
let currentChunkSize = 0;
|
|
188
|
-
|
|
189
|
-
for (const test of tests) {
|
|
190
|
-
const testSize = getObjectSize(test);
|
|
191
|
-
|
|
192
|
-
const wouldExceedSize = currentChunkSize + testSize > maxSizeBytes;
|
|
193
|
-
|
|
194
|
-
if (wouldExceedSize && currentChunk.length > 0) {
|
|
195
|
-
chunks.push(currentChunk);
|
|
196
|
-
currentChunk = [];
|
|
197
|
-
currentChunkSize = 0;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
currentChunk.push(test);
|
|
201
|
-
currentChunkSize += testSize;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (currentChunk.length > 0) {
|
|
205
|
-
chunks.push(currentChunk);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return chunks;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
164
|
/**
|
|
212
165
|
* Format a list of test IDs for `--filter-list` machine-readable output.
|
|
213
166
|
* Used when the CLI `--format` option is passed,
|
|
@@ -237,6 +190,4 @@ export {
|
|
|
237
190
|
fullName,
|
|
238
191
|
parsePipeOptions,
|
|
239
192
|
formatFilterListIds,
|
|
240
|
-
getObjectSize,
|
|
241
|
-
splitTestsIntoChunks,
|
|
242
193
|
};
|
package/src/utils/utils.js
CHANGED
|
@@ -289,11 +289,6 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
289
289
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
|
|
290
290
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
291
291
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
292
|
-
} else if (opts.lang === 'kotlin') {
|
|
293
|
-
lineIndex = lines.findIndex(l => l.includes(`fun test${title}`));
|
|
294
|
-
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
|
|
295
|
-
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`fun ${title}`));
|
|
296
|
-
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
297
292
|
} else if (opts.lang === 'csharp') {
|
|
298
293
|
// Find the method declaration line
|
|
299
294
|
let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
|
package/src/xmlReader.js
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
transformEnvVarToBoolean,
|
|
19
19
|
} from './utils/utils.js';
|
|
20
20
|
import { pipesFactory } from './pipe/index.js';
|
|
21
|
-
import { splitTestsIntoChunks } from './utils/pipe_utils.js';
|
|
22
21
|
import adapterFactory from './junit-adapter/index.js';
|
|
23
22
|
import { config } from './config.js';
|
|
24
23
|
import { S3Uploader } from './uploader.js';
|
|
@@ -554,6 +553,52 @@ class XmlReader {
|
|
|
554
553
|
return run;
|
|
555
554
|
}
|
|
556
555
|
|
|
556
|
+
/**
|
|
557
|
+
* Calculate the approximate size of data in bytes (JSON stringified length)
|
|
558
|
+
* @param {Object} data - Data to measure
|
|
559
|
+
* @returns {number} Size in bytes
|
|
560
|
+
*/
|
|
561
|
+
#getObjectSize(data) {
|
|
562
|
+
const body = JSON.stringify(data);
|
|
563
|
+
return new TextEncoder().encode(body).length;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Split tests array into chunks based on data size
|
|
568
|
+
* @param {Array} tests - Array of tests to split
|
|
569
|
+
* @returns {Array<Array>} Array of test chunks
|
|
570
|
+
*/
|
|
571
|
+
#splitTestsIntoChunks(tests) {
|
|
572
|
+
const maxSizeBytes = 1 * 1024 * 1024;
|
|
573
|
+
|
|
574
|
+
const chunks = [];
|
|
575
|
+
let currentChunk = [];
|
|
576
|
+
let currentChunkSize = 0;
|
|
577
|
+
|
|
578
|
+
for (const test of tests) {
|
|
579
|
+
const testSize = this.#getObjectSize(test);
|
|
580
|
+
|
|
581
|
+
const wouldExceedSize = currentChunkSize + testSize > maxSizeBytes;
|
|
582
|
+
|
|
583
|
+
if (wouldExceedSize) {
|
|
584
|
+
if (currentChunk.length > 0) {
|
|
585
|
+
chunks.push(currentChunk);
|
|
586
|
+
}
|
|
587
|
+
currentChunk = [];
|
|
588
|
+
currentChunkSize = 0;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
currentChunk.push(test);
|
|
592
|
+
currentChunkSize += testSize;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (currentChunk.length > 0) {
|
|
596
|
+
chunks.push(currentChunk);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return chunks;
|
|
600
|
+
}
|
|
601
|
+
|
|
557
602
|
async uploadData() {
|
|
558
603
|
await this.uploadArtifacts();
|
|
559
604
|
this.calculateStats();
|
|
@@ -578,7 +623,7 @@ class XmlReader {
|
|
|
578
623
|
return Promise.all(this.pipes.map(p => p.finishRun(finishData)));
|
|
579
624
|
}
|
|
580
625
|
|
|
581
|
-
const testChunks = splitTestsIntoChunks(this.tests);
|
|
626
|
+
const testChunks = this.#splitTestsIntoChunks(this.tests);
|
|
582
627
|
|
|
583
628
|
const totalChunks = testChunks.length;
|
|
584
629
|
const totalTests = this.tests.length;
|