@allurereport/web-commons 3.0.0-beta.9 → 3.0.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/dist/attachments.d.ts +21 -0
- package/dist/attachments.js +162 -0
- package/dist/charts/accessors/coverageDiffTreeMapAccessor.d.ts +16 -0
- package/dist/charts/accessors/coverageDiffTreeMapAccessor.js +183 -0
- package/dist/charts/accessors/problemsDistributionHeatMap.d.ts +2 -0
- package/dist/charts/accessors/problemsDistributionHeatMap.js +65 -0
- package/dist/charts/accessors/severityTrendAccessor.d.ts +3 -0
- package/dist/charts/accessors/severityTrendAccessor.js +21 -0
- package/dist/charts/accessors/statusTrendAccessor.d.ts +3 -0
- package/dist/charts/accessors/statusTrendAccessor.js +19 -0
- package/dist/charts/accessors/successRateDistributionTreeMapAccessor.d.ts +14 -0
- package/dist/charts/accessors/successRateDistributionTreeMapAccessor.js +110 -0
- package/dist/charts/accessors/utils/behavior.d.ts +5 -0
- package/dist/charts/accessors/utils/behavior.js +4 -0
- package/dist/charts/chart-utils.d.ts +8 -0
- package/dist/charts/chart-utils.js +22 -0
- package/dist/charts/colors.d.ts +3 -0
- package/dist/charts/colors.js +18 -0
- package/dist/charts/d3pie.d.ts +3 -0
- package/dist/charts/d3pie.js +52 -0
- package/dist/charts/generateCurrentStatusChart.d.ts +3 -0
- package/dist/charts/generateCurrentStatusChart.js +10 -0
- package/dist/charts/generateDurationDynamicsChart.d.ts +5 -0
- package/dist/charts/generateDurationDynamicsChart.js +83 -0
- package/dist/charts/generateDurationsChart.d.ts +5 -0
- package/dist/charts/generateDurationsChart.js +81 -0
- package/dist/charts/generateFBSUAgePyramid.d.ts +5 -0
- package/dist/charts/generateFBSUAgePyramid.js +77 -0
- package/dist/charts/generateStabilityDistributionChart.d.ts +5 -0
- package/dist/charts/generateStabilityDistributionChart.js +64 -0
- package/dist/charts/generateStatusDynamicsChart.d.ts +6 -0
- package/dist/charts/generateStatusDynamicsChart.js +44 -0
- package/dist/charts/generateStatusTransitionsChart.d.ts +5 -0
- package/dist/charts/generateStatusTransitionsChart.js +91 -0
- package/dist/charts/generateTestBaseGrowthDynamicsChart.d.ts +5 -0
- package/dist/charts/generateTestBaseGrowthDynamicsChart.js +79 -0
- package/dist/charts/generateTestingPyramidChart.d.ts +2 -0
- package/dist/charts/generateTestingPyramidChart.js +52 -0
- package/dist/charts/generateTrSeveritiesChart.d.ts +5 -0
- package/dist/charts/generateTrSeveritiesChart.js +53 -0
- package/dist/charts/generators.d.ts +10 -0
- package/dist/charts/generators.js +97 -0
- package/dist/charts/heatMap.d.ts +3 -0
- package/dist/charts/heatMap.js +9 -0
- package/dist/charts/index.d.ts +9 -0
- package/dist/charts/index.js +8 -0
- package/dist/charts/line.d.ts +8 -0
- package/dist/charts/line.js +140 -0
- package/dist/charts/treeMap.d.ts +6 -0
- package/dist/charts/treeMap.js +87 -0
- package/dist/charts/types.d.ts +76 -0
- package/dist/charts/types.js +1 -0
- package/dist/charts/utils.d.ts +21 -0
- package/dist/charts/utils.js +188 -0
- package/dist/data.d.ts +7 -7
- package/dist/data.js +9 -35
- package/dist/i18n.d.ts +8 -0
- package/dist/i18n.js +119 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/sanitize.d.ts +1 -0
- package/dist/sanitize.js +2 -0
- package/dist/strings.d.ts +12 -0
- package/dist/strings.js +7 -0
- package/package.json +17 -6
- package/dist/static.d.ts +0 -8
- package/dist/static.js +0 -25
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface Attachments {
|
|
2
|
+
id?: string;
|
|
3
|
+
ext?: string;
|
|
4
|
+
contentType?: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
src?: string;
|
|
7
|
+
img?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const fetchFromUrl: ({ id, ext, contentType }: Attachments) => Promise<Response>;
|
|
10
|
+
export declare const fetchAttachment: (id: string, ext: string, contentType?: string) => Promise<Attachments | null>;
|
|
11
|
+
export declare const blobAttachment: (id: string, ext: string, contentType: string) => Promise<Blob>;
|
|
12
|
+
export declare const downloadAttachment: (id: string, ext: string, contentType: string) => Promise<void>;
|
|
13
|
+
export declare const openAttachmentInNewTab: (id: string, ext: string, contentType: string) => Promise<void>;
|
|
14
|
+
export declare const attachmentType: (type?: string) => {
|
|
15
|
+
type: string;
|
|
16
|
+
icon: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: null;
|
|
19
|
+
icon: string;
|
|
20
|
+
};
|
|
21
|
+
export declare const restrictedContentTypes: string[];
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { fetchReportAttachment } from "./data.js";
|
|
2
|
+
export const fetchFromUrl = async ({ id, ext, contentType }) => {
|
|
3
|
+
const fileName = `${id || "-"}${ext || ""}`;
|
|
4
|
+
return fetchReportAttachment(`data/attachments/${fileName}?attachment`, contentType);
|
|
5
|
+
};
|
|
6
|
+
export const fetchAttachment = async (id, ext, contentType) => {
|
|
7
|
+
if (!id && !ext) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const response = await fetchFromUrl({ id, ext, contentType });
|
|
11
|
+
const fileType = attachmentType(contentType);
|
|
12
|
+
switch (fileType.type) {
|
|
13
|
+
case "svg":
|
|
14
|
+
case "image": {
|
|
15
|
+
const blob = await response.blob();
|
|
16
|
+
const img = URL.createObjectURL(blob);
|
|
17
|
+
return { img, id };
|
|
18
|
+
}
|
|
19
|
+
case "uri":
|
|
20
|
+
case "code":
|
|
21
|
+
case "html":
|
|
22
|
+
case "table":
|
|
23
|
+
case "text": {
|
|
24
|
+
const text = await response.text();
|
|
25
|
+
return { text };
|
|
26
|
+
}
|
|
27
|
+
case "video": {
|
|
28
|
+
const blob = await response.blob();
|
|
29
|
+
const src = URL.createObjectURL(blob);
|
|
30
|
+
return { src, id, contentType };
|
|
31
|
+
}
|
|
32
|
+
default:
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export const blobAttachment = async (id, ext, contentType) => {
|
|
37
|
+
const response = await fetchFromUrl({ id, ext, contentType });
|
|
38
|
+
return await response.blob();
|
|
39
|
+
};
|
|
40
|
+
export const downloadAttachment = async (id, ext, contentType) => {
|
|
41
|
+
if (!id && !ext) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const fileName = `${id}${ext}`;
|
|
45
|
+
const blob = await blobAttachment(id, ext, contentType);
|
|
46
|
+
const linkUrl = URL.createObjectURL(blob);
|
|
47
|
+
const link = document.createElement("a");
|
|
48
|
+
link.href = linkUrl;
|
|
49
|
+
link.download = fileName;
|
|
50
|
+
link.click();
|
|
51
|
+
URL.revokeObjectURL(linkUrl);
|
|
52
|
+
};
|
|
53
|
+
export const openAttachmentInNewTab = async (id, ext, contentType) => {
|
|
54
|
+
if (!id && !ext) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const blob = await blobAttachment(id, ext, contentType);
|
|
58
|
+
const linkUrl = URL.createObjectURL(blob);
|
|
59
|
+
globalThis.open(linkUrl, "_blank");
|
|
60
|
+
};
|
|
61
|
+
export const attachmentType = (type) => {
|
|
62
|
+
switch (type) {
|
|
63
|
+
case "image/bmp":
|
|
64
|
+
case "image/gif":
|
|
65
|
+
case "image/tiff":
|
|
66
|
+
case "image/jpeg":
|
|
67
|
+
case "image/jpg":
|
|
68
|
+
case "image/png":
|
|
69
|
+
case "image/*":
|
|
70
|
+
return {
|
|
71
|
+
type: "image",
|
|
72
|
+
icon: "file",
|
|
73
|
+
};
|
|
74
|
+
case "text/xml":
|
|
75
|
+
case "text/json":
|
|
76
|
+
case "text/yaml":
|
|
77
|
+
case "text/javascript":
|
|
78
|
+
case "text/typescript":
|
|
79
|
+
case "text/ruby":
|
|
80
|
+
case "text/python":
|
|
81
|
+
case "text/php":
|
|
82
|
+
case "text/java":
|
|
83
|
+
case "text/csharp":
|
|
84
|
+
case "text/cpp":
|
|
85
|
+
case "text/c":
|
|
86
|
+
case "text/go":
|
|
87
|
+
case "text/rust":
|
|
88
|
+
case "text/swift":
|
|
89
|
+
case "text/kotlin":
|
|
90
|
+
case "text/scala":
|
|
91
|
+
case "text/perl":
|
|
92
|
+
case "text/r":
|
|
93
|
+
case "text/dart":
|
|
94
|
+
case "text/lua":
|
|
95
|
+
case "text/haskell":
|
|
96
|
+
case "text/sql":
|
|
97
|
+
case "text/x-yaml":
|
|
98
|
+
case "text/css":
|
|
99
|
+
case "application/yaml":
|
|
100
|
+
case "application/x-yaml":
|
|
101
|
+
case "application/xml":
|
|
102
|
+
case "application/json":
|
|
103
|
+
return {
|
|
104
|
+
type: "code",
|
|
105
|
+
icon: "file",
|
|
106
|
+
};
|
|
107
|
+
case "text/plain":
|
|
108
|
+
case "text/markdown":
|
|
109
|
+
case "text/*":
|
|
110
|
+
return {
|
|
111
|
+
type: "text",
|
|
112
|
+
icon: "txt",
|
|
113
|
+
};
|
|
114
|
+
case "text/html":
|
|
115
|
+
return {
|
|
116
|
+
type: "html",
|
|
117
|
+
icon: "file",
|
|
118
|
+
};
|
|
119
|
+
case "text/csv":
|
|
120
|
+
return {
|
|
121
|
+
type: "table",
|
|
122
|
+
icon: "csv",
|
|
123
|
+
};
|
|
124
|
+
case "text/tab-separated-values":
|
|
125
|
+
return {
|
|
126
|
+
type: "table",
|
|
127
|
+
icon: "table",
|
|
128
|
+
};
|
|
129
|
+
case "image/svg+xml":
|
|
130
|
+
return {
|
|
131
|
+
type: "svg",
|
|
132
|
+
icon: "file",
|
|
133
|
+
};
|
|
134
|
+
case "video/mp4":
|
|
135
|
+
case "video/ogg":
|
|
136
|
+
case "video/webm":
|
|
137
|
+
return {
|
|
138
|
+
type: "video",
|
|
139
|
+
icon: "file",
|
|
140
|
+
};
|
|
141
|
+
case "text/uri-list":
|
|
142
|
+
return {
|
|
143
|
+
type: "uri",
|
|
144
|
+
icon: "list",
|
|
145
|
+
};
|
|
146
|
+
case "application/x-tar":
|
|
147
|
+
case "application/x-gtar":
|
|
148
|
+
case "application/x-bzip2":
|
|
149
|
+
case "application/gzip":
|
|
150
|
+
case "application/zip":
|
|
151
|
+
return {
|
|
152
|
+
type: "archive",
|
|
153
|
+
icon: "file",
|
|
154
|
+
};
|
|
155
|
+
default:
|
|
156
|
+
return {
|
|
157
|
+
type: null,
|
|
158
|
+
icon: "file",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
export const restrictedContentTypes = ["application/gzip"];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TreeMapDataAccessor, TreeMapNode } from "@allurereport/charts-api";
|
|
2
|
+
type ChangeType = "new" | "deleted" | "enabled" | "disabled" | "unchanged";
|
|
3
|
+
type SubtreeMetrics = {
|
|
4
|
+
totalTests: number;
|
|
5
|
+
newCount: number;
|
|
6
|
+
deletedCount: number;
|
|
7
|
+
disabledCount: number;
|
|
8
|
+
enabledCount: number;
|
|
9
|
+
};
|
|
10
|
+
type LeafMetrics = {
|
|
11
|
+
changeType: ChangeType;
|
|
12
|
+
};
|
|
13
|
+
type GroupMetrics = Omit<SubtreeMetrics, "totalTests">;
|
|
14
|
+
type ExtendedTreeMapNode = TreeMapNode<GroupMetrics & Partial<LeafMetrics>>;
|
|
15
|
+
export declare const coverageDiffTreeMapAccessor: TreeMapDataAccessor<ExtendedTreeMapNode>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { createTreeByLabels, md5 } from "@allurereport/plugin-api";
|
|
2
|
+
import { isChildrenLeavesOnly } from "../chart-utils.js";
|
|
3
|
+
import { convertTreeDataToTreeMapNode, transformTreeMapNode } from "../treeMap.js";
|
|
4
|
+
import { behaviorLabels, filterTestsWithBehaviorLabels } from "./utils/behavior.js";
|
|
5
|
+
const groupFactoryFn = (parentId, groupClassifier) => ({
|
|
6
|
+
nodeId: md5((parentId ? `${parentId}.` : "") + groupClassifier),
|
|
7
|
+
name: groupClassifier,
|
|
8
|
+
value: 0,
|
|
9
|
+
newCount: 0,
|
|
10
|
+
deletedCount: 0,
|
|
11
|
+
disabledCount: 0,
|
|
12
|
+
enabledCount: 0,
|
|
13
|
+
});
|
|
14
|
+
const addLeafToGroupFn = (group, leaf) => {
|
|
15
|
+
group.value += leaf.value;
|
|
16
|
+
switch (leaf.changeType) {
|
|
17
|
+
case "new":
|
|
18
|
+
group.newCount++;
|
|
19
|
+
break;
|
|
20
|
+
case "deleted":
|
|
21
|
+
group.deletedCount++;
|
|
22
|
+
break;
|
|
23
|
+
case "enabled":
|
|
24
|
+
group.enabledCount++;
|
|
25
|
+
break;
|
|
26
|
+
case "disabled":
|
|
27
|
+
group.disabledCount++;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const calculateColorValue = (metrics) => {
|
|
32
|
+
const netChange = metrics.newCount + metrics.enabledCount - (metrics.deletedCount + metrics.disabledCount);
|
|
33
|
+
const normalizedChange = netChange / metrics.totalTests;
|
|
34
|
+
return Math.max(0, Math.min(1, (normalizedChange + 1) / 2));
|
|
35
|
+
};
|
|
36
|
+
const isSkipped = (tr) => tr.status === "skipped";
|
|
37
|
+
const getNewTestResults = (trs, closestHtrs) => {
|
|
38
|
+
return trs.filter((tr) => !closestHtrs[tr.historyId]);
|
|
39
|
+
};
|
|
40
|
+
const getRemovedTestResults = (trs, closestHtrs) => {
|
|
41
|
+
const historyPointTestResultsAsArray = Object.values(closestHtrs);
|
|
42
|
+
const testResultsAsDictionary = Object.fromEntries(trs.map((tr) => [tr.historyId, tr]));
|
|
43
|
+
return historyPointTestResultsAsArray.filter((htr) => !testResultsAsDictionary[htr.historyId]);
|
|
44
|
+
};
|
|
45
|
+
const getEnabledTestResults = (trs, closestHtrs) => {
|
|
46
|
+
return trs.filter((tr) => {
|
|
47
|
+
const historyPointTestResult = closestHtrs[tr.historyId];
|
|
48
|
+
return historyPointTestResult && isSkipped(historyPointTestResult) && !isSkipped(tr);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const getDisabledTestResults = (trs, closestHtrs) => {
|
|
52
|
+
return trs.filter((tr) => {
|
|
53
|
+
const historyPointTestResult = closestHtrs[tr.historyId];
|
|
54
|
+
return historyPointTestResult && !isSkipped(historyPointTestResult) && isSkipped(tr);
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const calculateSubtreeMetrics = (node) => {
|
|
58
|
+
if (!node.children || node.children.length === 0) {
|
|
59
|
+
const changeType = node?.changeType;
|
|
60
|
+
return {
|
|
61
|
+
totalTests: 1,
|
|
62
|
+
newCount: changeType === "new" ? 1 : 0,
|
|
63
|
+
deletedCount: changeType === "deleted" ? 1 : 0,
|
|
64
|
+
disabledCount: changeType === "disabled" ? 1 : 0,
|
|
65
|
+
enabledCount: changeType === "enabled" ? 1 : 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
let totalTests = 0;
|
|
69
|
+
let newCount = 0;
|
|
70
|
+
let deletedCount = 0;
|
|
71
|
+
let disabledCount = 0;
|
|
72
|
+
let enabledCount = 0;
|
|
73
|
+
for (const child of node.children) {
|
|
74
|
+
const childMetrics = calculateSubtreeMetrics(child);
|
|
75
|
+
totalTests += childMetrics.totalTests;
|
|
76
|
+
newCount += childMetrics.newCount;
|
|
77
|
+
deletedCount += childMetrics.deletedCount;
|
|
78
|
+
disabledCount += childMetrics.disabledCount;
|
|
79
|
+
enabledCount += childMetrics.enabledCount;
|
|
80
|
+
}
|
|
81
|
+
return { totalTests, newCount, deletedCount, disabledCount, enabledCount };
|
|
82
|
+
};
|
|
83
|
+
const createCoverageDiffTreeMap = (trs, closestHtrs) => {
|
|
84
|
+
const newTrs = getNewTestResults(trs, closestHtrs);
|
|
85
|
+
const removedHtrs = getRemovedTestResults(trs, closestHtrs);
|
|
86
|
+
const enabledTrs = getEnabledTestResults(trs, closestHtrs);
|
|
87
|
+
const disabledTrs = getDisabledTestResults(trs, closestHtrs);
|
|
88
|
+
const newTestsById = new Map(newTrs.map((tr) => [tr.historyId, tr]));
|
|
89
|
+
const deletedTestsById = new Map(removedHtrs.map((htr) => [htr.historyId, htr]));
|
|
90
|
+
const enabledTestsById = new Map(enabledTrs.map((tr) => [tr.historyId, tr]));
|
|
91
|
+
const disabledTestsById = new Map(disabledTrs.map((tr) => [tr.historyId, tr]));
|
|
92
|
+
const allTests = [...trs, ...removedHtrs];
|
|
93
|
+
const getChangeType = (historyId) => {
|
|
94
|
+
if (newTestsById.has(historyId)) {
|
|
95
|
+
return "new";
|
|
96
|
+
}
|
|
97
|
+
if (deletedTestsById.has(historyId)) {
|
|
98
|
+
return "deleted";
|
|
99
|
+
}
|
|
100
|
+
if (enabledTestsById.has(historyId)) {
|
|
101
|
+
return "enabled";
|
|
102
|
+
}
|
|
103
|
+
if (disabledTestsById.has(historyId)) {
|
|
104
|
+
return "disabled";
|
|
105
|
+
}
|
|
106
|
+
return "unchanged";
|
|
107
|
+
};
|
|
108
|
+
const leafFactoryFnWithMaps = (test) => {
|
|
109
|
+
const changeType = getChangeType(test.historyId);
|
|
110
|
+
return {
|
|
111
|
+
nodeId: test.id,
|
|
112
|
+
name: test.name,
|
|
113
|
+
value: 1,
|
|
114
|
+
changeType,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
const treeByLabels = createTreeByLabels(allTests, behaviorLabels, leafFactoryFnWithMaps, groupFactoryFn, addLeafToGroupFn);
|
|
118
|
+
const convertedTree = convertTreeDataToTreeMapNode(treeByLabels, (node, isGroup) => {
|
|
119
|
+
const baseNode = {
|
|
120
|
+
id: node.name,
|
|
121
|
+
value: isGroup ? undefined : node.value,
|
|
122
|
+
};
|
|
123
|
+
if (isGroup) {
|
|
124
|
+
const group = node;
|
|
125
|
+
return {
|
|
126
|
+
...baseNode,
|
|
127
|
+
newCount: group.newCount,
|
|
128
|
+
deletedCount: group.deletedCount,
|
|
129
|
+
disabledCount: group.disabledCount,
|
|
130
|
+
enabledCount: group.enabledCount,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const leaf = node;
|
|
135
|
+
return {
|
|
136
|
+
...baseNode,
|
|
137
|
+
changeType: leaf.changeType,
|
|
138
|
+
newCount: leaf.changeType === "new" ? 1 : 0,
|
|
139
|
+
deletedCount: leaf.changeType === "deleted" ? 1 : 0,
|
|
140
|
+
disabledCount: leaf.changeType === "disabled" ? 1 : 0,
|
|
141
|
+
enabledCount: leaf.changeType === "enabled" ? 1 : 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}, () => ({
|
|
145
|
+
id: "root",
|
|
146
|
+
newCount: 0,
|
|
147
|
+
deletedCount: 0,
|
|
148
|
+
disabledCount: 0,
|
|
149
|
+
enabledCount: 0,
|
|
150
|
+
}));
|
|
151
|
+
return transformTreeMapNode(convertedTree, (node) => {
|
|
152
|
+
const subtreeMetrics = calculateSubtreeMetrics(node);
|
|
153
|
+
const colorValue = calculateColorValue(subtreeMetrics);
|
|
154
|
+
const { totalTests, ...restSubtreeMetrics } = subtreeMetrics;
|
|
155
|
+
if (isChildrenLeavesOnly(node)) {
|
|
156
|
+
return {
|
|
157
|
+
...node,
|
|
158
|
+
value: totalTests,
|
|
159
|
+
children: undefined,
|
|
160
|
+
colorValue,
|
|
161
|
+
...restSubtreeMetrics,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
...node,
|
|
166
|
+
colorValue,
|
|
167
|
+
...restSubtreeMetrics,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
export const coverageDiffTreeMapAccessor = {
|
|
172
|
+
getTreeMap: ({ testResults, historyDataPoints }) => {
|
|
173
|
+
const testsWithBehaviorLabels = filterTestsWithBehaviorLabels(testResults);
|
|
174
|
+
if (!historyDataPoints || historyDataPoints.length === 0) {
|
|
175
|
+
return createCoverageDiffTreeMap(testsWithBehaviorLabels, {});
|
|
176
|
+
}
|
|
177
|
+
const closestHdp = historyDataPoints[0];
|
|
178
|
+
const closestHtrs = closestHdp.testResults;
|
|
179
|
+
const closestHtrsWithBehaviorLabels = filterTestsWithBehaviorLabels(Object.values(closestHtrs));
|
|
180
|
+
const closestHtrsWithBehaviorLabelsById = Object.fromEntries(closestHtrsWithBehaviorLabels.map((htr) => [htr.historyId, htr]));
|
|
181
|
+
return createCoverageDiffTreeMap(testsWithBehaviorLabels, closestHtrsWithBehaviorLabelsById);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { filterIncludedInSuccessRate } from "@allurereport/core-api";
|
|
2
|
+
const groupTestsByEnvironment = (testResults) => {
|
|
3
|
+
return testResults.reduce((acc, testResult) => {
|
|
4
|
+
const key = testResult.environment;
|
|
5
|
+
if (key) {
|
|
6
|
+
const bucket = acc[key] || (acc[key] = []);
|
|
7
|
+
bucket.push(testResult);
|
|
8
|
+
}
|
|
9
|
+
return acc;
|
|
10
|
+
}, {});
|
|
11
|
+
};
|
|
12
|
+
const groupByExactLabel = (testResults, labelNames) => {
|
|
13
|
+
return testResults.reduce((acc, testResult) => {
|
|
14
|
+
const labels = testResult.labels;
|
|
15
|
+
if (!labels) {
|
|
16
|
+
return acc;
|
|
17
|
+
}
|
|
18
|
+
for (const label of labels) {
|
|
19
|
+
const key = label.value;
|
|
20
|
+
if (labelNames.includes(label.name) && key) {
|
|
21
|
+
const bucket = acc[key] || (acc[key] = []);
|
|
22
|
+
bucket.push(testResult);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return acc;
|
|
26
|
+
}, {});
|
|
27
|
+
};
|
|
28
|
+
const makeHeatMapSerie = (env, testResults) => {
|
|
29
|
+
const testResultsByExactLabel = groupByExactLabel(testResults, ["feature"]);
|
|
30
|
+
const data = [];
|
|
31
|
+
for (const [labelValue, testsByLabelValue] of Object.entries(testResultsByExactLabel)) {
|
|
32
|
+
const testsTotal = testsByLabelValue.length;
|
|
33
|
+
const totalNegative = testsByLabelValue.reduce((acc, test) => acc + (test.status !== "passed" ? 1 : 0), 0);
|
|
34
|
+
data.push({
|
|
35
|
+
x: labelValue,
|
|
36
|
+
y: totalNegative / testsTotal,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
id: env,
|
|
41
|
+
data: data.sort((a, b) => (a.y || 0) - (b.y || 0)),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const makeHeatMapData = (testsByEnvironment) => {
|
|
45
|
+
return Object.entries(testsByEnvironment).map(([env, tests]) => makeHeatMapSerie(env, tests));
|
|
46
|
+
};
|
|
47
|
+
const filterTestResultsBySignificantStatus = (testResults) => {
|
|
48
|
+
return testResults.filter(filterIncludedInSuccessRate);
|
|
49
|
+
};
|
|
50
|
+
const filterTestResultsByLabelNames = (testResults, labelNames) => {
|
|
51
|
+
return testResults.filter((test) => test.labels?.some((l) => labelNames.includes(l.name)));
|
|
52
|
+
};
|
|
53
|
+
export const problemsDistributionHeatMapAccessor = {
|
|
54
|
+
getHeatMap: ({ testResults }) => {
|
|
55
|
+
const filteredTestResults = filterTestResultsBySignificantStatus(filterTestResultsByLabelNames(testResults, ["feature"]));
|
|
56
|
+
const testsResultsByEnvironment = groupTestsByEnvironment(filteredTestResults);
|
|
57
|
+
const data = makeHeatMapData(testsResultsByEnvironment);
|
|
58
|
+
const totals = new Map();
|
|
59
|
+
for (const serie of data) {
|
|
60
|
+
const total = serie.data.reduce((acc, sd) => acc + (sd.y || 0), 0);
|
|
61
|
+
totals.set(serie.id, total);
|
|
62
|
+
}
|
|
63
|
+
return data.sort((a, b) => totals.get(a.id) - totals.get(b.id));
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { severityLabelName, severityLevels } from "@allurereport/core-api";
|
|
2
|
+
import { createEmptyStats } from "../chart-utils.js";
|
|
3
|
+
const processTestResults = (testResults) => {
|
|
4
|
+
return testResults.reduce((acc, test) => {
|
|
5
|
+
const severityLabel = test.labels?.find((label) => label.name === severityLabelName);
|
|
6
|
+
const severity = severityLabel?.value?.toLowerCase();
|
|
7
|
+
if (severity) {
|
|
8
|
+
acc[severity] = (acc[severity] ?? 0) + 1;
|
|
9
|
+
}
|
|
10
|
+
return acc;
|
|
11
|
+
}, createEmptyStats(severityLevels));
|
|
12
|
+
};
|
|
13
|
+
export const severityTrendDataAccessor = {
|
|
14
|
+
getCurrentData: ({ testResults }) => {
|
|
15
|
+
return processTestResults(testResults);
|
|
16
|
+
},
|
|
17
|
+
getHistoricalData: (historyPoint) => {
|
|
18
|
+
return processTestResults(Object.values(historyPoint.testResults));
|
|
19
|
+
},
|
|
20
|
+
getAllValues: () => severityLevels,
|
|
21
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { statusesList } from "@allurereport/core-api";
|
|
2
|
+
import { createEmptyStats } from "../chart-utils.js";
|
|
3
|
+
export const statusTrendDataAccessor = {
|
|
4
|
+
getCurrentData: ({ statistic }) => {
|
|
5
|
+
return {
|
|
6
|
+
...createEmptyStats(statusesList),
|
|
7
|
+
...statistic,
|
|
8
|
+
};
|
|
9
|
+
},
|
|
10
|
+
getHistoricalData: (historyPoint) => {
|
|
11
|
+
return Object.values(historyPoint.testResults).reduce((stat, test) => {
|
|
12
|
+
if (test.status) {
|
|
13
|
+
stat[test.status] = (stat[test.status] ?? 0) + 1;
|
|
14
|
+
}
|
|
15
|
+
return stat;
|
|
16
|
+
}, createEmptyStats(statusesList));
|
|
17
|
+
},
|
|
18
|
+
getAllValues: () => statusesList,
|
|
19
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TreeMapDataAccessor, TreeMapNode } from "@allurereport/charts-api";
|
|
2
|
+
import type { TestResult } from "@allurereport/core-api";
|
|
3
|
+
type SubtreeMetrics = {
|
|
4
|
+
totalTests: number;
|
|
5
|
+
passedTests: number;
|
|
6
|
+
failedTests: number;
|
|
7
|
+
otherTests: number;
|
|
8
|
+
};
|
|
9
|
+
type LeafMetrics = Pick<TestResult, "status">;
|
|
10
|
+
type GroupMetrics = Omit<SubtreeMetrics, "totalTests">;
|
|
11
|
+
type ExtendedTreeMapNode = TreeMapNode<GroupMetrics & Partial<LeafMetrics>>;
|
|
12
|
+
export declare const createSuccessRateDistributionTreeMap: (testResults: TestResult[]) => ExtendedTreeMapNode;
|
|
13
|
+
export declare const successRateDistributionTreeMapAccessor: TreeMapDataAccessor<ExtendedTreeMapNode>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createTreeByLabels, md5 } from "@allurereport/plugin-api";
|
|
2
|
+
import { isChildrenLeavesOnly } from "../chart-utils.js";
|
|
3
|
+
import { convertTreeDataToTreeMapNode, transformTreeMapNode } from "../treeMap.js";
|
|
4
|
+
import { behaviorLabels, filterTestsWithBehaviorLabels } from "./utils/behavior.js";
|
|
5
|
+
const leafFactoryFn = ({ id, name, status }) => ({
|
|
6
|
+
nodeId: id,
|
|
7
|
+
name,
|
|
8
|
+
status,
|
|
9
|
+
value: 1,
|
|
10
|
+
});
|
|
11
|
+
const groupFactoryFn = (parentId, groupClassifier) => ({
|
|
12
|
+
nodeId: md5((parentId ? `${parentId}.` : "") + groupClassifier),
|
|
13
|
+
name: groupClassifier,
|
|
14
|
+
value: 0,
|
|
15
|
+
passedTests: 0,
|
|
16
|
+
failedTests: 0,
|
|
17
|
+
otherTests: 0,
|
|
18
|
+
});
|
|
19
|
+
const addLeafToGroupFn = (group, leaf) => {
|
|
20
|
+
group.value += leaf.value;
|
|
21
|
+
group.passedTests += leaf.status === "passed" ? 1 : 0;
|
|
22
|
+
group.failedTests += leaf.status === "failed" ? 1 : 0;
|
|
23
|
+
group.otherTests += leaf?.status && !["passed", "failed"].includes(leaf.status) ? 1 : 0;
|
|
24
|
+
};
|
|
25
|
+
const calculateColorValue = ({ totalTests, passedTests }) => {
|
|
26
|
+
return totalTests > 0 ? passedTests / totalTests : 0;
|
|
27
|
+
};
|
|
28
|
+
const calculateSubtreeMetrics = (node) => {
|
|
29
|
+
if (!node.children || node.children.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
totalTests: 1,
|
|
32
|
+
passedTests: node?.status === "passed" ? 1 : 0,
|
|
33
|
+
failedTests: node?.status === "failed" ? 1 : 0,
|
|
34
|
+
otherTests: node?.status && !["passed", "failed"].includes(node.status) ? 1 : 0,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
let totalTests = 0;
|
|
38
|
+
let passedTests = 0;
|
|
39
|
+
let failedTests = 0;
|
|
40
|
+
let otherTests = 0;
|
|
41
|
+
for (const child of node.children) {
|
|
42
|
+
const childMetrics = calculateSubtreeMetrics(child);
|
|
43
|
+
totalTests += childMetrics.totalTests;
|
|
44
|
+
passedTests += childMetrics.passedTests;
|
|
45
|
+
failedTests += childMetrics.failedTests;
|
|
46
|
+
otherTests += childMetrics.otherTests;
|
|
47
|
+
}
|
|
48
|
+
return { totalTests, passedTests, failedTests, otherTests };
|
|
49
|
+
};
|
|
50
|
+
export const createSuccessRateDistributionTreeMap = (testResults) => {
|
|
51
|
+
const treeByLabels = createTreeByLabels(testResults, behaviorLabels, leafFactoryFn, groupFactoryFn, addLeafToGroupFn);
|
|
52
|
+
const convertedTree = convertTreeDataToTreeMapNode(treeByLabels, (node, isGroup) => {
|
|
53
|
+
const baseNode = {
|
|
54
|
+
id: node.name,
|
|
55
|
+
value: isGroup ? undefined : node.value,
|
|
56
|
+
};
|
|
57
|
+
if (isGroup) {
|
|
58
|
+
const group = node;
|
|
59
|
+
return {
|
|
60
|
+
...baseNode,
|
|
61
|
+
passedTests: group.passedTests,
|
|
62
|
+
failedTests: group.failedTests,
|
|
63
|
+
otherTests: group.otherTests,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const leaf = node;
|
|
68
|
+
return {
|
|
69
|
+
...baseNode,
|
|
70
|
+
status: leaf.status,
|
|
71
|
+
passedTests: leaf?.status === "passed" ? 1 : 0,
|
|
72
|
+
failedTests: leaf?.status === "failed" ? 1 : 0,
|
|
73
|
+
otherTests: leaf?.status && !["passed", "failed"].includes(leaf.status) ? 1 : 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}, () => ({
|
|
77
|
+
id: "root",
|
|
78
|
+
passedTests: 0,
|
|
79
|
+
failedTests: 0,
|
|
80
|
+
otherTests: 0,
|
|
81
|
+
}));
|
|
82
|
+
return transformTreeMapNode(convertedTree, (node) => {
|
|
83
|
+
const subtreeMetrics = calculateSubtreeMetrics(node);
|
|
84
|
+
const colorValue = calculateColorValue(subtreeMetrics);
|
|
85
|
+
const { totalTests, ...restSubtreeMetrics } = subtreeMetrics;
|
|
86
|
+
if (isChildrenLeavesOnly(node)) {
|
|
87
|
+
const value = node.children?.reduce((acc, child) => {
|
|
88
|
+
return acc + (child.value ?? 0);
|
|
89
|
+
}, 0);
|
|
90
|
+
return {
|
|
91
|
+
...node,
|
|
92
|
+
value,
|
|
93
|
+
children: undefined,
|
|
94
|
+
colorValue,
|
|
95
|
+
...restSubtreeMetrics,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
...node,
|
|
100
|
+
colorValue,
|
|
101
|
+
...restSubtreeMetrics,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
export const successRateDistributionTreeMapAccessor = {
|
|
106
|
+
getTreeMap: ({ testResults }) => {
|
|
107
|
+
const testsWithBehaviorLabels = filterTestsWithBehaviorLabels(testResults);
|
|
108
|
+
return createSuccessRateDistributionTreeMap(testsWithBehaviorLabels);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HistoryTestResult, TestResult } from "@allurereport/core-api";
|
|
2
|
+
export type BehaviorLabel = "epic" | "feature" | "story";
|
|
3
|
+
export declare const behaviorLabels: BehaviorLabel[];
|
|
4
|
+
export declare const hasBehaviorLabels: <T extends TestResult | HistoryTestResult>(test: T) => boolean;
|
|
5
|
+
export declare const filterTestsWithBehaviorLabels: <T extends TestResult | HistoryTestResult>(tests: T[]) => T[];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { hasLabels } from "../../chart-utils.js";
|
|
2
|
+
export const behaviorLabels = ["epic", "feature", "story"];
|
|
3
|
+
export const hasBehaviorLabels = (test) => hasLabels(test, behaviorLabels);
|
|
4
|
+
export const filterTestsWithBehaviorLabels = (tests) => tests.filter(hasBehaviorLabels);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TreeMapNode } from "@allurereport/charts-api";
|
|
2
|
+
import type { HistoryDataPoint, HistoryTestResult, TestResult } from "@allurereport/core-api";
|
|
3
|
+
export declare const limitHistoryDataPoints: (historyDataPoints: HistoryDataPoint[], limit: number) => HistoryDataPoint[];
|
|
4
|
+
export declare const createEmptySeries: <T extends string>(items: readonly T[]) => Record<T, string[]>;
|
|
5
|
+
export declare const createEmptyStats: <T extends string>(items: readonly T[]) => Record<T, number>;
|
|
6
|
+
export declare const normalizeStatistic: <T extends string>(statistic: Partial<Record<T, number>>, itemType: readonly T[]) => Record<T, number>;
|
|
7
|
+
export declare const hasLabels: <T extends string, TR extends TestResult | HistoryTestResult>(test: TR, labelHierarchy: T[]) => boolean;
|
|
8
|
+
export declare const isChildrenLeavesOnly: <T extends TreeMapNode>(node: T) => boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const limitHistoryDataPoints = (historyDataPoints, limit) => {
|
|
2
|
+
if (limit <= 0 || historyDataPoints.length === 0) {
|
|
3
|
+
return [];
|
|
4
|
+
}
|
|
5
|
+
const clampedLimit = Math.max(0, Math.floor(limit));
|
|
6
|
+
return historyDataPoints.slice(0, clampedLimit);
|
|
7
|
+
};
|
|
8
|
+
export const createEmptySeries = (items) => items.reduce((acc, item) => ({ ...acc, [item]: [] }), {});
|
|
9
|
+
export const createEmptyStats = (items) => items.reduce((acc, item) => ({ ...acc, [item]: 0 }), {});
|
|
10
|
+
export const normalizeStatistic = (statistic, itemType) => {
|
|
11
|
+
return itemType.reduce((acc, item) => {
|
|
12
|
+
acc[item] = statistic[item] ?? 0;
|
|
13
|
+
return acc;
|
|
14
|
+
}, {});
|
|
15
|
+
};
|
|
16
|
+
export const hasLabels = (test, labelHierarchy) => test.labels?.some((label) => {
|
|
17
|
+
const { name } = label;
|
|
18
|
+
return name && labelHierarchy.includes(name);
|
|
19
|
+
}) ?? false;
|
|
20
|
+
export const isChildrenLeavesOnly = (node) => {
|
|
21
|
+
return node.children ? node.children.every((child) => child.children === undefined) : false;
|
|
22
|
+
};
|