@allurereport/plugin-allure2 3.0.0-beta.9
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 +54 -0
- package/dist/categories.d.ts +7 -0
- package/dist/categories.js +41 -0
- package/dist/converters.d.ts +11 -0
- package/dist/converters.js +182 -0
- package/dist/generators.d.ts +37 -0
- package/dist/generators.js +276 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/model.d.ts +146 -0
- package/dist/model.js +2 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +76 -0
- package/dist/tree.d.ts +44 -0
- package/dist/tree.js +112 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +43 -0
- package/dist/writer.d.ts +36 -0
- package/dist/writer.js +85 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Classic Plugin
|
|
2
|
+
|
|
3
|
+
[<img src="https://allurereport.org/public/img/allure-report.svg" height="85px" alt="Allure Report logo" align="right" />](https://allurereport.org "Allure Report")
|
|
4
|
+
|
|
5
|
+
- Learn more about Allure Report at https://allurereport.org
|
|
6
|
+
- 📚 [Documentation](https://allurereport.org/docs/) – discover official documentation for Allure Report
|
|
7
|
+
- ❓ [Questions and Support](https://github.com/orgs/allure-framework/discussions/categories/questions-support) – get help from the team and community
|
|
8
|
+
- 📢 [Official announcements](https://github.com/orgs/allure-framework/discussions/categories/announcements) – be in touch with the latest updates
|
|
9
|
+
- 💬 [General Discussion ](https://github.com/orgs/allure-framework/discussions/categories/general-discussion) – engage in casual conversations, share insights and ideas with the community
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
The plugin generates a classic Allure Report.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Use your favorite package manager to install the package:
|
|
20
|
+
|
|
21
|
+
```shell
|
|
22
|
+
npm add @allurereport/plugin-classic
|
|
23
|
+
yarn add @allurereport/plugin-classic
|
|
24
|
+
pnpm add @allurereport/plugin-classic
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then, add the plugin to the Allure configuration file:
|
|
28
|
+
|
|
29
|
+
```diff
|
|
30
|
+
import { defineConfig } from "allure";
|
|
31
|
+
|
|
32
|
+
export default defineConfig({
|
|
33
|
+
name: "Allure Report",
|
|
34
|
+
output: "./allure-report",
|
|
35
|
+
historyPath: "./history.jsonl",
|
|
36
|
+
plugins: {
|
|
37
|
+
+ classic: {
|
|
38
|
+
+ options: {
|
|
39
|
+
+ reportName: "HelloWorld",
|
|
40
|
+
+ },
|
|
41
|
+
+ },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Options
|
|
47
|
+
|
|
48
|
+
The plugin accepts the following options:
|
|
49
|
+
|
|
50
|
+
| Option | Description | Type | Default |
|
|
51
|
+
|------------------|-------------------------------------------------|--------------------------------------------------------------|-----------------|
|
|
52
|
+
| `reportName` | Name of the report | `string` | `Allure Report` |
|
|
53
|
+
| `singleFile` | Writes the report as a single `index.html` file | `boolean` | `false` |
|
|
54
|
+
| `reportLanguage` | Default language of the report | `string` | OS language |
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const productDefects = {
|
|
2
|
+
name: "Product defects",
|
|
3
|
+
matchedStatuses: ["failed"],
|
|
4
|
+
};
|
|
5
|
+
const testDefects = {
|
|
6
|
+
name: "Test defects",
|
|
7
|
+
matchedStatuses: ["broken"],
|
|
8
|
+
};
|
|
9
|
+
export const matchCategories = (categories, result) => {
|
|
10
|
+
const matched = categories.filter((category) => categoryMatch(category, result));
|
|
11
|
+
if (matched.length === 0 && categoryMatch(productDefects, result)) {
|
|
12
|
+
matched.push(productDefects);
|
|
13
|
+
}
|
|
14
|
+
if (matched.length === 0 && categoryMatch(testDefects, result)) {
|
|
15
|
+
matched.push(testDefects);
|
|
16
|
+
}
|
|
17
|
+
return matched;
|
|
18
|
+
};
|
|
19
|
+
const categoryMatch = (category, result) => {
|
|
20
|
+
const { status, statusMessage, statusTrace, flaky } = result;
|
|
21
|
+
const matchesStatus = !category.matchedStatuses || category.matchedStatuses.length === 0 || category.matchedStatuses.includes(status);
|
|
22
|
+
const matchesMessage = match(category.messageRegex, statusMessage);
|
|
23
|
+
const matchesTrace = match(category.traceRegex, statusTrace);
|
|
24
|
+
const matchesFlaky = (category.flaky ?? flaky) === flaky;
|
|
25
|
+
return matchesStatus && matchesMessage && matchesTrace && matchesFlaky;
|
|
26
|
+
};
|
|
27
|
+
const match = (regex, value) => {
|
|
28
|
+
if (!regex) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (!value) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const b = new RegExp(regex, "s").test(value);
|
|
36
|
+
return b;
|
|
37
|
+
}
|
|
38
|
+
catch (ignored) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HistoryTestResult, TestFixtureResult, TestResult } from "@allurereport/core-api";
|
|
2
|
+
import type { Allure2Category, Allure2TestResult } from "./model.js";
|
|
3
|
+
type ConvertContext = {
|
|
4
|
+
attachmentMap: Map<string, string>;
|
|
5
|
+
fixtures: TestFixtureResult[];
|
|
6
|
+
categories: Allure2Category[];
|
|
7
|
+
retries: TestResult[];
|
|
8
|
+
history: HistoryTestResult[];
|
|
9
|
+
};
|
|
10
|
+
export declare const convertTestResult: (context: ConvertContext, test: TestResult) => Allure2TestResult;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { filterIncludedInSuccessRate, isStep } from "@allurereport/core-api";
|
|
2
|
+
import { matchCategories } from "./categories.js";
|
|
3
|
+
import { severityValues } from "./model.js";
|
|
4
|
+
import { updateStatistic } from "./utils.js";
|
|
5
|
+
const sortByTime = (a, b) => a.time.start !== undefined && b.time.start !== undefined ? a.time.start - b.time.start : 0;
|
|
6
|
+
const convertStatus = (status) => status;
|
|
7
|
+
const convertStageResult = (context, result) => {
|
|
8
|
+
const { name, ...testStage } = convertStep(context, {
|
|
9
|
+
name: "test",
|
|
10
|
+
steps: result.steps,
|
|
11
|
+
start: result.start,
|
|
12
|
+
stop: result.stop,
|
|
13
|
+
duration: result.duration,
|
|
14
|
+
status: result.status,
|
|
15
|
+
parameters: [],
|
|
16
|
+
type: "step",
|
|
17
|
+
});
|
|
18
|
+
return testStage;
|
|
19
|
+
};
|
|
20
|
+
const convertStep = (context, step) => {
|
|
21
|
+
if (isStep(step)) {
|
|
22
|
+
const name = step.name;
|
|
23
|
+
const steps = step.steps.map((child) => convertStep(context, child));
|
|
24
|
+
const stepsCount = steps.length;
|
|
25
|
+
const parameters = step.parameters;
|
|
26
|
+
const parametersCount = parameters.length;
|
|
27
|
+
const statusMessage = step.error?.message;
|
|
28
|
+
const shouldDisplayMessage = !!statusMessage || steps.findIndex((s) => s.statusMessage === statusMessage) > 0;
|
|
29
|
+
return {
|
|
30
|
+
name,
|
|
31
|
+
time: {
|
|
32
|
+
start: step.start,
|
|
33
|
+
stop: step.stop,
|
|
34
|
+
duration: step.duration,
|
|
35
|
+
},
|
|
36
|
+
status: step.status,
|
|
37
|
+
statusMessage,
|
|
38
|
+
statusTrace: step.error?.trace,
|
|
39
|
+
steps,
|
|
40
|
+
attachments: [],
|
|
41
|
+
parameters,
|
|
42
|
+
stepsCount,
|
|
43
|
+
attachmentsCount: 0,
|
|
44
|
+
hasContent: stepsCount + parametersCount > 0 || shouldDisplayMessage,
|
|
45
|
+
shouldDisplayMessage,
|
|
46
|
+
attachmentStep: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const stepName = step.link.name;
|
|
50
|
+
return {
|
|
51
|
+
name: stepName,
|
|
52
|
+
time: {},
|
|
53
|
+
status: "unknown",
|
|
54
|
+
steps: [],
|
|
55
|
+
attachments: [
|
|
56
|
+
{
|
|
57
|
+
...convertAttachment(context, step),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
parameters: [],
|
|
61
|
+
stepsCount: 0,
|
|
62
|
+
attachmentsCount: 1,
|
|
63
|
+
hasContent: true,
|
|
64
|
+
shouldDisplayMessage: false,
|
|
65
|
+
attachmentStep: true,
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const convertAttachment = (context, { link }) => ({
|
|
69
|
+
uid: link.id,
|
|
70
|
+
name: link.name,
|
|
71
|
+
source: context.attachmentMap.get(link.id) ?? link.originalFileName,
|
|
72
|
+
type: link.contentType,
|
|
73
|
+
size: !link.missed ? link.contentLength : undefined,
|
|
74
|
+
});
|
|
75
|
+
const findAllLabels = (test, labelName) => {
|
|
76
|
+
return test.labels
|
|
77
|
+
.filter((label) => label.name === labelName)
|
|
78
|
+
.map((label) => label.value)
|
|
79
|
+
.filter((value) => value)
|
|
80
|
+
.map((value) => value);
|
|
81
|
+
};
|
|
82
|
+
const findLabelValue = (test, labelName) => {
|
|
83
|
+
return test.labels.find((label) => label.name === labelName)?.value;
|
|
84
|
+
};
|
|
85
|
+
const extractSeverity = (test) => {
|
|
86
|
+
const maybeSeverity = findLabelValue(test, "severity")?.toLowerCase();
|
|
87
|
+
return (maybeSeverity ? severityValues.find((value) => value === maybeSeverity) : undefined) ?? "normal";
|
|
88
|
+
};
|
|
89
|
+
const importantStatuses = ["failed", "broken", "passed"];
|
|
90
|
+
export const convertTestResult = (context, test) => {
|
|
91
|
+
const testStage = convertStageResult(context, test);
|
|
92
|
+
const beforeStages = context.fixtures
|
|
93
|
+
.filter((value) => value.type === "before")
|
|
94
|
+
.map((fixture) => convertStageResult(context, fixture))
|
|
95
|
+
.sort(sortByTime);
|
|
96
|
+
const afterStages = context.fixtures
|
|
97
|
+
.filter((value) => value.type === "after")
|
|
98
|
+
.map((fixture) => convertStageResult(context, fixture))
|
|
99
|
+
.sort(sortByTime);
|
|
100
|
+
const owner = findLabelValue(test, "owner");
|
|
101
|
+
const severity = extractSeverity(test);
|
|
102
|
+
const tags = findAllLabels(test, "tag");
|
|
103
|
+
const status = convertStatus(test.status);
|
|
104
|
+
const statusMessage = test.error?.message;
|
|
105
|
+
const statusTrace = test.error?.trace;
|
|
106
|
+
const flaky = false;
|
|
107
|
+
const categories = matchCategories(context.categories, { statusMessage, statusTrace, status, flaky });
|
|
108
|
+
const retries = context.retries.map((retry) => ({
|
|
109
|
+
uid: retry.id,
|
|
110
|
+
status: convertStatus(retry.status),
|
|
111
|
+
statusDetails: retry?.error?.message,
|
|
112
|
+
time: {
|
|
113
|
+
start: retry.start,
|
|
114
|
+
stop: retry.stop,
|
|
115
|
+
duration: retry.duration,
|
|
116
|
+
},
|
|
117
|
+
}));
|
|
118
|
+
const retriesStatusChange = status in importantStatuses &&
|
|
119
|
+
retries.find((tr) => tr.status in importantStatuses && tr.status !== status) !== undefined;
|
|
120
|
+
const lastHistoryStatus = context.history.find(filterIncludedInSuccessRate)?.status;
|
|
121
|
+
const newFailed = lastHistoryStatus === "passed" && test.status === "failed";
|
|
122
|
+
const newBroken = lastHistoryStatus === "passed" && test.status === "broken";
|
|
123
|
+
const newPassed = lastHistoryStatus !== undefined && lastHistoryStatus !== "passed" && test.status === "passed";
|
|
124
|
+
const historyItems = context.history.map((htr) => ({
|
|
125
|
+
uid: htr.id,
|
|
126
|
+
status: convertStatus(htr.status),
|
|
127
|
+
reportUrl: "unsupported",
|
|
128
|
+
statusDetails: htr.error?.message,
|
|
129
|
+
time: {
|
|
130
|
+
start: htr.start,
|
|
131
|
+
stop: htr.stop,
|
|
132
|
+
duration: htr.duration,
|
|
133
|
+
},
|
|
134
|
+
}));
|
|
135
|
+
const statistic = { total: 0 };
|
|
136
|
+
historyItems.forEach((historyItem) => updateStatistic(statistic, historyItem));
|
|
137
|
+
const history = {
|
|
138
|
+
statistic,
|
|
139
|
+
items: historyItems,
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
uid: test.id,
|
|
143
|
+
name: test.name,
|
|
144
|
+
fullName: test.fullName,
|
|
145
|
+
historyId: test.historyId,
|
|
146
|
+
testId: test.testCase?.id,
|
|
147
|
+
time: {
|
|
148
|
+
start: test.start,
|
|
149
|
+
stop: test.stop,
|
|
150
|
+
duration: test.duration,
|
|
151
|
+
},
|
|
152
|
+
status,
|
|
153
|
+
description: test.description,
|
|
154
|
+
descriptionHtml: test.descriptionHtml,
|
|
155
|
+
statusMessage,
|
|
156
|
+
statusTrace,
|
|
157
|
+
labels: test.labels,
|
|
158
|
+
links: test.links,
|
|
159
|
+
parameters: test.parameters,
|
|
160
|
+
afterStages,
|
|
161
|
+
beforeStages,
|
|
162
|
+
testStage: testStage,
|
|
163
|
+
flaky,
|
|
164
|
+
hidden: test.hidden,
|
|
165
|
+
newFailed,
|
|
166
|
+
newBroken,
|
|
167
|
+
newPassed,
|
|
168
|
+
retry: test.hidden,
|
|
169
|
+
retriesStatusChange,
|
|
170
|
+
retriesCount: retries.length,
|
|
171
|
+
hostId: test.hostId,
|
|
172
|
+
threadId: test.threadId,
|
|
173
|
+
extra: {
|
|
174
|
+
owner,
|
|
175
|
+
severity,
|
|
176
|
+
tags,
|
|
177
|
+
categories,
|
|
178
|
+
retries,
|
|
179
|
+
history,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AttachmentLink, HistoryDataPoint } from "@allurereport/core-api";
|
|
2
|
+
import type { ReportFiles, ResultFile } from "@allurereport/plugin-api";
|
|
3
|
+
import type { Allure2ExecutorInfo, Allure2TestResult } from "./model.js";
|
|
4
|
+
import type { Allure2DataWriter, ReportFile } from "./writer.js";
|
|
5
|
+
export type TemplateManifest = Record<string, string>;
|
|
6
|
+
export declare const readTemplateManifest: (singleFileMode?: boolean) => Promise<TemplateManifest>;
|
|
7
|
+
export declare const readManifestEntry: (options: {
|
|
8
|
+
fileName: string;
|
|
9
|
+
singleFile?: boolean;
|
|
10
|
+
mimeType: string;
|
|
11
|
+
reportFiles: ReportFiles;
|
|
12
|
+
inserter: (content: string) => string;
|
|
13
|
+
}) => Promise<string>;
|
|
14
|
+
export declare const generateStaticFiles: (payload: {
|
|
15
|
+
allureVersion: string;
|
|
16
|
+
reportName: string;
|
|
17
|
+
reportLanguage: string;
|
|
18
|
+
singleFile: boolean;
|
|
19
|
+
reportFiles: ReportFiles;
|
|
20
|
+
reportDataFiles: ReportFile[];
|
|
21
|
+
reportUuid: string;
|
|
22
|
+
}) => Promise<void>;
|
|
23
|
+
export declare const generateTree: (writer: Allure2DataWriter, name: string, labelNames: string[], tests: Allure2TestResult[]) => Promise<void>;
|
|
24
|
+
export declare const generatePackagesData: (writer: Allure2DataWriter, tests: Allure2TestResult[]) => Promise<void>;
|
|
25
|
+
export declare const generateCategoriesData: (writer: Allure2DataWriter, tests: Allure2TestResult[]) => Promise<void>;
|
|
26
|
+
export declare const generateTimelineData: (writer: Allure2DataWriter, tests: Allure2TestResult[]) => Promise<void>;
|
|
27
|
+
export declare const generateTestResults: (writer: Allure2DataWriter, tests: Allure2TestResult[]) => Promise<void>;
|
|
28
|
+
export declare const generateSummaryJson: (writer: Allure2DataWriter, reportName: string, tests: Allure2TestResult[]) => Promise<void>;
|
|
29
|
+
export declare const generateEnvironmentJson: (writer: Allure2DataWriter, env: {
|
|
30
|
+
name: string;
|
|
31
|
+
values: string[];
|
|
32
|
+
}[]) => Promise<void>;
|
|
33
|
+
export declare const generateExecutorJson: (writer: Allure2DataWriter, executor?: Partial<Allure2ExecutorInfo>) => Promise<void>;
|
|
34
|
+
export declare const generateDefaultWidgetData: (writer: Allure2DataWriter, tests: Allure2TestResult[], ...fileNames: string[]) => Promise<void>;
|
|
35
|
+
export declare const generateEmptyTrendData: (writer: Allure2DataWriter, ...fileNames: string[]) => Promise<void>;
|
|
36
|
+
export declare const generateTrendData: (writer: Allure2DataWriter, reportName: string, tests: Allure2TestResult[], historyDataPoints: HistoryDataPoint[]) => Promise<void>;
|
|
37
|
+
export declare const generateAttachmentsData: (writer: Allure2DataWriter, attachmentLinks: AttachmentLink[], contentFunction: (id: string) => Promise<ResultFile | undefined>) => Promise<Map<string, string>>;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { createBaseUrlScript, createFaviconLinkTag, createReportDataScript, createScriptTag, createStylesLinkTag, } from "@allurereport/web-commons";
|
|
2
|
+
import Handlebars from "handlebars";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { basename, join } from "node:path";
|
|
6
|
+
import { byLabels, collapseTree, createTree, createWidget } from "./tree.js";
|
|
7
|
+
import { updateStatistic, updateTime } from "./utils.js";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const template = `<!DOCTYPE html>
|
|
10
|
+
<html dir="ltr" lang="{{reportLanguage}}">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="utf-8">
|
|
13
|
+
<title>{{reportName}}</title>
|
|
14
|
+
{{{ headTags }}}
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<svg id="__SVG_SPRITE_NODE__" aria-hidden="true" style="position: absolute; width: 0; height: 0"></svg>
|
|
18
|
+
<div id="alert"></div>
|
|
19
|
+
<div id="content">
|
|
20
|
+
<span class="spinner">
|
|
21
|
+
<span class="spinner__circle"></span>
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div id="popup"></div>
|
|
25
|
+
{{{ bodyTags }}}
|
|
26
|
+
${createBaseUrlScript()}
|
|
27
|
+
{{#if analyticsEnable}}
|
|
28
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-LNDJ3J7WT0"></script>
|
|
29
|
+
<script>
|
|
30
|
+
window.dataLayer = window.dataLayer || [];
|
|
31
|
+
function gtag(){dataLayer.push(arguments);}
|
|
32
|
+
gtag('js', new Date());
|
|
33
|
+
gtag('config', 'G-LNDJ3J7WT0', {
|
|
34
|
+
'allureVersion': '{{allureVersion}}',
|
|
35
|
+
'report':'classic',
|
|
36
|
+
'reportUuid': '{{reportUuid}}',
|
|
37
|
+
'single_file': '{{singleFile}}'
|
|
38
|
+
});
|
|
39
|
+
</script>
|
|
40
|
+
{{/if}}
|
|
41
|
+
<script>
|
|
42
|
+
window.allureReportOptions = {{{ reportOptions }}};
|
|
43
|
+
</script>
|
|
44
|
+
{{{ reportFilesScript }}}
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|
|
47
|
+
`;
|
|
48
|
+
export const readTemplateManifest = async (singleFileMode) => {
|
|
49
|
+
const templateManifestSource = require.resolve(`@allurereport/web-allure2/dist/${singleFileMode ? "single" : "multi"}/manifest.json`);
|
|
50
|
+
const templateManifest = await readFile(templateManifestSource, { encoding: "utf-8" });
|
|
51
|
+
return JSON.parse(templateManifest);
|
|
52
|
+
};
|
|
53
|
+
export const readManifestEntry = async (options) => {
|
|
54
|
+
const { fileName, singleFile, mimeType, inserter, reportFiles } = options;
|
|
55
|
+
const filePath = require.resolve(join("@allurereport/web-allure2/dist", singleFile ? "single" : "multi", fileName));
|
|
56
|
+
const scriptContentBuffer = await readFile(filePath);
|
|
57
|
+
if (singleFile) {
|
|
58
|
+
return inserter(`data:${mimeType};base64,${scriptContentBuffer.toString("base64")}`);
|
|
59
|
+
}
|
|
60
|
+
await reportFiles.addFile(fileName, scriptContentBuffer);
|
|
61
|
+
return inserter(fileName);
|
|
62
|
+
};
|
|
63
|
+
export const generateStaticFiles = async (payload) => {
|
|
64
|
+
const { reportName, reportLanguage, singleFile, reportFiles, reportDataFiles, reportUuid, allureVersion } = payload;
|
|
65
|
+
const compile = Handlebars.compile(template);
|
|
66
|
+
const manifest = await readTemplateManifest(singleFile);
|
|
67
|
+
const headTags = [];
|
|
68
|
+
const bodyTags = [];
|
|
69
|
+
for (const key in manifest) {
|
|
70
|
+
const fileName = manifest[key];
|
|
71
|
+
const filePath = require.resolve(join("@allurereport/web-allure2/dist", singleFile ? "single" : "multi", fileName));
|
|
72
|
+
if (key === "favicon.ico") {
|
|
73
|
+
const tag = await readManifestEntry({
|
|
74
|
+
fileName,
|
|
75
|
+
singleFile,
|
|
76
|
+
reportFiles,
|
|
77
|
+
inserter: createFaviconLinkTag,
|
|
78
|
+
mimeType: "image/x-icon",
|
|
79
|
+
});
|
|
80
|
+
headTags.push(tag);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (key === "main.css") {
|
|
84
|
+
const tag = await readManifestEntry({
|
|
85
|
+
fileName,
|
|
86
|
+
singleFile,
|
|
87
|
+
reportFiles,
|
|
88
|
+
inserter: createStylesLinkTag,
|
|
89
|
+
mimeType: "text/css",
|
|
90
|
+
});
|
|
91
|
+
headTags.push(tag);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (key === "main.js") {
|
|
95
|
+
const tag = await readManifestEntry({
|
|
96
|
+
fileName,
|
|
97
|
+
singleFile,
|
|
98
|
+
reportFiles,
|
|
99
|
+
inserter: createScriptTag,
|
|
100
|
+
mimeType: "text/javascript",
|
|
101
|
+
});
|
|
102
|
+
bodyTags.push(tag);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (singleFile) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const fileContent = await readFile(filePath);
|
|
109
|
+
await reportFiles.addFile(basename(filePath), fileContent);
|
|
110
|
+
}
|
|
111
|
+
const reportOptions = {
|
|
112
|
+
reportName: reportName ?? "Allure Report",
|
|
113
|
+
reportLanguage: reportLanguage ?? "en",
|
|
114
|
+
createdAt: Date.now(),
|
|
115
|
+
};
|
|
116
|
+
const html = compile({
|
|
117
|
+
headTags: headTags.join("\n"),
|
|
118
|
+
bodyTags: bodyTags.join("\n"),
|
|
119
|
+
reportFilesScript: createReportDataScript(reportDataFiles),
|
|
120
|
+
reportOptions: JSON.stringify(reportOptions),
|
|
121
|
+
analyticsEnable: true,
|
|
122
|
+
allureVersion,
|
|
123
|
+
reportLanguage,
|
|
124
|
+
reportUuid,
|
|
125
|
+
reportName,
|
|
126
|
+
singleFile,
|
|
127
|
+
});
|
|
128
|
+
await reportFiles.addFile("index.html", Buffer.from(html, "utf8"));
|
|
129
|
+
};
|
|
130
|
+
export const generateTree = async (writer, name, labelNames, tests) => {
|
|
131
|
+
const fileName = `${name}.json`;
|
|
132
|
+
const data = createTree(tests, byLabels(labelNames));
|
|
133
|
+
await writer.writeData(fileName, data);
|
|
134
|
+
const widgetData = createWidget(data);
|
|
135
|
+
await writer.writeWidget(fileName, widgetData);
|
|
136
|
+
};
|
|
137
|
+
export const generatePackagesData = async (writer, tests) => {
|
|
138
|
+
const classifier = (test) => {
|
|
139
|
+
return (test.labels
|
|
140
|
+
.find((label) => label.name === "package")
|
|
141
|
+
?.value?.split(".")
|
|
142
|
+
?.map((group) => ({
|
|
143
|
+
groups: [group],
|
|
144
|
+
})) ?? []);
|
|
145
|
+
};
|
|
146
|
+
const data = createTree(tests, classifier);
|
|
147
|
+
const packagesData = collapseTree(data);
|
|
148
|
+
await writer.writeData("packages.json", packagesData);
|
|
149
|
+
};
|
|
150
|
+
export const generateCategoriesData = async (writer, tests) => {
|
|
151
|
+
const classifier = (test) => {
|
|
152
|
+
const byMessage = { groups: [test.statusMessage ?? "No message"] };
|
|
153
|
+
const categories = test.extra.categories;
|
|
154
|
+
if (!categories || categories.length === 0) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
const groups = categories.map((c) => c.name);
|
|
158
|
+
return [{ groups }, byMessage];
|
|
159
|
+
};
|
|
160
|
+
const data = createTree(tests, classifier);
|
|
161
|
+
const fileName = "categories.json";
|
|
162
|
+
await writer.writeData(fileName, data);
|
|
163
|
+
const widgetData = createWidget(data);
|
|
164
|
+
await writer.writeWidget(fileName, widgetData);
|
|
165
|
+
};
|
|
166
|
+
export const generateTimelineData = async (writer, tests) => {
|
|
167
|
+
const classifier = (test) => {
|
|
168
|
+
return [{ groups: [test.hostId ?? "Default"] }, { groups: [test.threadId ?? "Default"] }];
|
|
169
|
+
};
|
|
170
|
+
const data = createTree(tests, classifier);
|
|
171
|
+
await writer.writeData("timeline.json", data);
|
|
172
|
+
};
|
|
173
|
+
export const generateTestResults = async (writer, tests) => {
|
|
174
|
+
for (const test of tests) {
|
|
175
|
+
await writer.writeTestCase(test);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
export const generateSummaryJson = async (writer, reportName, tests) => {
|
|
179
|
+
const statistic = { total: 0 };
|
|
180
|
+
const time = {};
|
|
181
|
+
tests
|
|
182
|
+
.filter((test) => !test.hidden)
|
|
183
|
+
.forEach((test) => {
|
|
184
|
+
updateStatistic(statistic, test);
|
|
185
|
+
updateTime(time, test);
|
|
186
|
+
});
|
|
187
|
+
const data = {
|
|
188
|
+
reportName,
|
|
189
|
+
statistic,
|
|
190
|
+
time,
|
|
191
|
+
};
|
|
192
|
+
await writer.writeWidget("summary.json", data);
|
|
193
|
+
};
|
|
194
|
+
export const generateEnvironmentJson = async (writer, env) => {
|
|
195
|
+
await writer.writeWidget("environment.json", env);
|
|
196
|
+
};
|
|
197
|
+
export const generateExecutorJson = async (writer, executor) => {
|
|
198
|
+
await writer.writeWidget("executors.json", executor ? [executor] : []);
|
|
199
|
+
};
|
|
200
|
+
export const generateDefaultWidgetData = async (writer, tests, ...fileNames) => {
|
|
201
|
+
const statusChartData = tests
|
|
202
|
+
.filter((test) => !test.hidden)
|
|
203
|
+
.map(({ uid, name, status, time, extra: { severity = "normal" } }) => {
|
|
204
|
+
return {
|
|
205
|
+
uid,
|
|
206
|
+
name,
|
|
207
|
+
status,
|
|
208
|
+
time,
|
|
209
|
+
severity,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
for (const fileName of fileNames) {
|
|
213
|
+
await writer.writeWidget(fileName, statusChartData);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
export const generateEmptyTrendData = async (writer, ...fileNames) => {
|
|
217
|
+
for (const fileName of fileNames) {
|
|
218
|
+
await writer.writeWidget(fileName, [
|
|
219
|
+
{
|
|
220
|
+
uid: "invalid",
|
|
221
|
+
name: "invalid",
|
|
222
|
+
statistic: { total: 0 },
|
|
223
|
+
},
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
export const generateTrendData = async (writer, reportName, tests, historyDataPoints) => {
|
|
228
|
+
const statistic = { total: 0 };
|
|
229
|
+
tests
|
|
230
|
+
.filter((test) => !test.hidden)
|
|
231
|
+
.forEach((test) => {
|
|
232
|
+
updateStatistic(statistic, test);
|
|
233
|
+
});
|
|
234
|
+
const history = historyDataPoints.map((point) => {
|
|
235
|
+
const stat = { total: 0 };
|
|
236
|
+
Object.values(point.testResults).forEach((testResult) => {
|
|
237
|
+
updateStatistic(stat, testResult);
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
data: stat,
|
|
241
|
+
timestamp: point.timestamp,
|
|
242
|
+
reportName: point.name,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
history
|
|
246
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
247
|
+
.forEach((element, index) => {
|
|
248
|
+
element.buildOrder = history.length - index;
|
|
249
|
+
});
|
|
250
|
+
const data = [
|
|
251
|
+
{
|
|
252
|
+
data: statistic,
|
|
253
|
+
timestamp: new Date().getTime(),
|
|
254
|
+
buildOrder: history.length + 1,
|
|
255
|
+
reportName: reportName,
|
|
256
|
+
},
|
|
257
|
+
...history,
|
|
258
|
+
];
|
|
259
|
+
await writer.writeWidget("history-trend.json", data);
|
|
260
|
+
};
|
|
261
|
+
export const generateAttachmentsData = async (writer, attachmentLinks, contentFunction) => {
|
|
262
|
+
const result = new Map();
|
|
263
|
+
for (const { id, ext, ...link } of attachmentLinks) {
|
|
264
|
+
if (link.missed) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const content = await contentFunction(id);
|
|
268
|
+
if (!content) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const src = `${id}${ext}`;
|
|
272
|
+
await writer.writeAttachment(src, content);
|
|
273
|
+
result.set(id, src);
|
|
274
|
+
}
|
|
275
|
+
return result;
|
|
276
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { Statistic } from "@allurereport/core-api";
|
|
2
|
+
export type Allure2Status = "failed" | "broken" | "passed" | "skipped" | "unknown";
|
|
3
|
+
export interface Allure2Time {
|
|
4
|
+
start?: number;
|
|
5
|
+
stop?: number;
|
|
6
|
+
duration?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface Allure2Label {
|
|
9
|
+
name?: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Allure2Link {
|
|
13
|
+
name?: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
type?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface Allure2Parameter {
|
|
18
|
+
name?: string;
|
|
19
|
+
value?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface Allure2Attachment {
|
|
22
|
+
uid: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
source?: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
size?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface Allure2Step {
|
|
29
|
+
name: string;
|
|
30
|
+
time: Allure2Time;
|
|
31
|
+
status: Allure2Status;
|
|
32
|
+
statusMessage?: string;
|
|
33
|
+
statusTrace?: string;
|
|
34
|
+
steps: Allure2Step[];
|
|
35
|
+
attachments: Allure2Attachment[];
|
|
36
|
+
parameters: Allure2Parameter[];
|
|
37
|
+
stepsCount: number;
|
|
38
|
+
attachmentsCount: number;
|
|
39
|
+
shouldDisplayMessage: boolean;
|
|
40
|
+
hasContent: boolean;
|
|
41
|
+
attachmentStep: boolean;
|
|
42
|
+
}
|
|
43
|
+
export type Allure2StageResult = Allure2Step | Omit<Allure2Step, "name">;
|
|
44
|
+
export interface Allure2TestResult {
|
|
45
|
+
uid: string;
|
|
46
|
+
name: string;
|
|
47
|
+
fullName?: string;
|
|
48
|
+
historyId?: string;
|
|
49
|
+
testId?: string;
|
|
50
|
+
time: Allure2Time;
|
|
51
|
+
description?: string;
|
|
52
|
+
descriptionHtml?: string;
|
|
53
|
+
status: Allure2Status;
|
|
54
|
+
statusMessage?: string;
|
|
55
|
+
statusTrace?: string;
|
|
56
|
+
flaky: boolean;
|
|
57
|
+
newFailed: boolean;
|
|
58
|
+
newBroken: boolean;
|
|
59
|
+
newPassed: boolean;
|
|
60
|
+
retriesCount: number;
|
|
61
|
+
retriesStatusChange: boolean;
|
|
62
|
+
beforeStages: Allure2StageResult[];
|
|
63
|
+
testStage: Allure2StageResult;
|
|
64
|
+
afterStages: Allure2StageResult[];
|
|
65
|
+
labels: Allure2Label[];
|
|
66
|
+
parameters: Allure2Parameter[];
|
|
67
|
+
links: Allure2Link[];
|
|
68
|
+
hostId?: string;
|
|
69
|
+
threadId?: string;
|
|
70
|
+
hidden: boolean;
|
|
71
|
+
retry: boolean;
|
|
72
|
+
extra: {
|
|
73
|
+
[key: string]: any;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export declare const statisticKeys: (keyof Statistic)[];
|
|
77
|
+
export interface GroupTime {
|
|
78
|
+
start?: number;
|
|
79
|
+
stop?: number;
|
|
80
|
+
duration?: number;
|
|
81
|
+
minDuration?: number;
|
|
82
|
+
maxDuration?: number;
|
|
83
|
+
sumDuration?: number;
|
|
84
|
+
}
|
|
85
|
+
export interface SummaryData {
|
|
86
|
+
reportName: string;
|
|
87
|
+
statistic: Statistic;
|
|
88
|
+
time: GroupTime;
|
|
89
|
+
}
|
|
90
|
+
export type Allure2SeverityLevel = "blocker" | "critical" | "normal" | "minor" | "trivial";
|
|
91
|
+
export declare const severityValues: Allure2SeverityLevel[];
|
|
92
|
+
export interface StatusChartData {
|
|
93
|
+
uid: string;
|
|
94
|
+
name: string;
|
|
95
|
+
time: Allure2Time;
|
|
96
|
+
status: Allure2Status;
|
|
97
|
+
severity: Allure2SeverityLevel;
|
|
98
|
+
}
|
|
99
|
+
export interface Allure2Category {
|
|
100
|
+
name: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
descriptionHtml?: string;
|
|
103
|
+
messageRegex?: string;
|
|
104
|
+
traceRegex?: string;
|
|
105
|
+
matchedStatuses?: Allure2Status[];
|
|
106
|
+
flaky?: boolean;
|
|
107
|
+
}
|
|
108
|
+
export interface Allure2RetryItem {
|
|
109
|
+
uid: string;
|
|
110
|
+
status: Allure2Status;
|
|
111
|
+
statusDetails?: string;
|
|
112
|
+
time: Allure2Time;
|
|
113
|
+
}
|
|
114
|
+
export interface Allure2HistoryItem {
|
|
115
|
+
uid: string;
|
|
116
|
+
reportUrl: string;
|
|
117
|
+
status: Allure2Status;
|
|
118
|
+
statusDetails?: string;
|
|
119
|
+
time: Allure2Time;
|
|
120
|
+
}
|
|
121
|
+
export interface Allure2HistoryData {
|
|
122
|
+
statistic: Statistic;
|
|
123
|
+
items: Allure2HistoryItem[];
|
|
124
|
+
}
|
|
125
|
+
export interface Allure2HistoryTrendItem {
|
|
126
|
+
data: Statistic;
|
|
127
|
+
buildOrder?: number;
|
|
128
|
+
reportUrl?: string;
|
|
129
|
+
reportName?: string;
|
|
130
|
+
}
|
|
131
|
+
export type Allure2Options = {
|
|
132
|
+
reportName?: string;
|
|
133
|
+
singleFile?: boolean;
|
|
134
|
+
reportLanguage?: string;
|
|
135
|
+
};
|
|
136
|
+
export type Allure2PluginOptions = Allure2Options;
|
|
137
|
+
export interface Allure2ExecutorInfo {
|
|
138
|
+
name: string;
|
|
139
|
+
type: string;
|
|
140
|
+
url: string;
|
|
141
|
+
buildOrder: number;
|
|
142
|
+
buildName: string;
|
|
143
|
+
buildUrl: string;
|
|
144
|
+
reportName: string;
|
|
145
|
+
reportUrl: string;
|
|
146
|
+
}
|
package/dist/model.js
ADDED
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AllureStore, type Plugin, type PluginContext } from "@allurereport/plugin-api";
|
|
2
|
+
import type { Allure2PluginOptions } from "./model.js";
|
|
3
|
+
export declare class Allure2Plugin implements Plugin {
|
|
4
|
+
#private;
|
|
5
|
+
readonly options: Allure2PluginOptions;
|
|
6
|
+
constructor(options?: Allure2PluginOptions);
|
|
7
|
+
update: (context: PluginContext, store: AllureStore) => Promise<void>;
|
|
8
|
+
done: (context: PluginContext, store: AllureStore) => Promise<void>;
|
|
9
|
+
}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var _Allure2Plugin_generate;
|
|
7
|
+
import { preciseTreeLabels } from "@allurereport/plugin-api";
|
|
8
|
+
import { convertTestResult } from "./converters.js";
|
|
9
|
+
import { generateAttachmentsData, generateCategoriesData, generateDefaultWidgetData, generateEmptyTrendData, generateEnvironmentJson, generateExecutorJson, generatePackagesData, generateStaticFiles, generateSummaryJson, generateTestResults, generateTimelineData, generateTree, generateTrendData, } from "./generators.js";
|
|
10
|
+
import { InMemoryReportDataWriter, ReportFileDataWriter } from "./writer.js";
|
|
11
|
+
export class Allure2Plugin {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
_Allure2Plugin_generate.set(this, async (context, store) => {
|
|
15
|
+
const { reportName = "Allure Report", singleFile = false, reportLanguage = "en" } = this.options ?? {};
|
|
16
|
+
const writer = singleFile ? new InMemoryReportDataWriter() : new ReportFileDataWriter(context.reportFiles);
|
|
17
|
+
const attachmentLinks = await store.allAttachments();
|
|
18
|
+
const attachmentMap = await generateAttachmentsData(writer, attachmentLinks, (id) => store.attachmentContentById(id));
|
|
19
|
+
const categories = (await store.metadataByKey("allure2_categories")) ?? [];
|
|
20
|
+
const environmentItems = (await store.metadataByKey("allure_environment")) ?? [];
|
|
21
|
+
const tests = await store.allTestResults({ includeHidden: true });
|
|
22
|
+
const allTr = [];
|
|
23
|
+
for (const value of tests) {
|
|
24
|
+
const fixtures = await store.fixturesByTrId(value.id);
|
|
25
|
+
const retries = await store.retriesByTrId(value.id);
|
|
26
|
+
const history = await store.historyByTrId(value.id);
|
|
27
|
+
const allure2TestResult = convertTestResult({
|
|
28
|
+
attachmentMap,
|
|
29
|
+
fixtures,
|
|
30
|
+
categories,
|
|
31
|
+
retries,
|
|
32
|
+
history,
|
|
33
|
+
}, value);
|
|
34
|
+
allTr.push(allure2TestResult);
|
|
35
|
+
}
|
|
36
|
+
await generateTestResults(writer, allTr);
|
|
37
|
+
const displayedTr = allTr.filter((atr) => !atr.hidden);
|
|
38
|
+
const treeLabelNamesFactory = (labelNames) => preciseTreeLabels(labelNames, displayedTr, (tr) => {
|
|
39
|
+
if (tr.labels) {
|
|
40
|
+
return tr.labels.map(({ name }) => name);
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
});
|
|
44
|
+
await generateTree(writer, "suites", treeLabelNamesFactory(["parentSuite", "suite", "subSuite"]), displayedTr);
|
|
45
|
+
await generateTree(writer, "behaviors", treeLabelNamesFactory(["epic", "feature", "story"]), displayedTr);
|
|
46
|
+
await generatePackagesData(writer, displayedTr);
|
|
47
|
+
await generateCategoriesData(writer, displayedTr);
|
|
48
|
+
await generateTimelineData(writer, allTr);
|
|
49
|
+
await generateSummaryJson(writer, reportName, displayedTr);
|
|
50
|
+
await generateEnvironmentJson(writer, environmentItems);
|
|
51
|
+
const executor = await store.metadataByKey("allure2_executor");
|
|
52
|
+
const historyDataPoints = await store.allHistoryDataPoints();
|
|
53
|
+
await generateExecutorJson(writer, executor);
|
|
54
|
+
await generateDefaultWidgetData(writer, displayedTr, "duration.json", "status-chart.json", "severity.json");
|
|
55
|
+
await generateTrendData(writer, reportName, displayedTr, historyDataPoints);
|
|
56
|
+
await generateEmptyTrendData(writer, "duration-trend.json", "categories-trend.json", "retry-trend.json");
|
|
57
|
+
const reportDataFiles = singleFile ? writer.reportFiles() : [];
|
|
58
|
+
await generateStaticFiles({
|
|
59
|
+
allureVersion: context.allureVersion,
|
|
60
|
+
reportName,
|
|
61
|
+
reportLanguage,
|
|
62
|
+
singleFile,
|
|
63
|
+
reportFiles: context.reportFiles,
|
|
64
|
+
reportDataFiles,
|
|
65
|
+
reportUuid: context.reportUuid,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
this.update = async (context, store) => {
|
|
69
|
+
await __classPrivateFieldGet(this, _Allure2Plugin_generate, "f").call(this, context, store);
|
|
70
|
+
};
|
|
71
|
+
this.done = async (context, store) => {
|
|
72
|
+
await this.update(context, store);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
_Allure2Plugin_generate = new WeakMap();
|
package/dist/tree.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Statistic } from "@allurereport/core-api";
|
|
2
|
+
import type { Allure2Status, Allure2TestResult, Allure2Time } from "./model.js";
|
|
3
|
+
export interface TreeNode {
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TreeGroup extends TreeNode {
|
|
7
|
+
uid: string;
|
|
8
|
+
name: string;
|
|
9
|
+
children: TreeNode[];
|
|
10
|
+
}
|
|
11
|
+
export interface TreeLeaf extends TreeNode {
|
|
12
|
+
uid: string;
|
|
13
|
+
parentUid: string;
|
|
14
|
+
status: Allure2Status;
|
|
15
|
+
time: Allure2Time;
|
|
16
|
+
flaky: boolean;
|
|
17
|
+
newFailed: boolean;
|
|
18
|
+
newBroken: boolean;
|
|
19
|
+
newPassed: boolean;
|
|
20
|
+
retriesCount: number;
|
|
21
|
+
retriesStatusChange: boolean;
|
|
22
|
+
tags: string[];
|
|
23
|
+
parameters: string[];
|
|
24
|
+
}
|
|
25
|
+
export interface TreeLayer {
|
|
26
|
+
groups: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface WidgetItem {
|
|
29
|
+
uid: string;
|
|
30
|
+
name: string;
|
|
31
|
+
statistic: Statistic;
|
|
32
|
+
}
|
|
33
|
+
export interface WidgetData {
|
|
34
|
+
items: WidgetItem[];
|
|
35
|
+
total: number;
|
|
36
|
+
}
|
|
37
|
+
declare const createLeaf: (endNode: TreeGroup, test: Allure2TestResult) => TreeLeaf;
|
|
38
|
+
export type LeafFactory = typeof createLeaf;
|
|
39
|
+
export type Classifier = (test: Allure2TestResult) => TreeLayer[] | undefined;
|
|
40
|
+
export declare const byLabels: (labelNames: string[]) => Classifier;
|
|
41
|
+
export declare const createTree: (tests: Allure2TestResult[], classifier: Classifier, leafFactory?: LeafFactory) => TreeGroup;
|
|
42
|
+
export declare const collapseTree: (treeGroup: TreeGroup, separator?: string) => TreeGroup;
|
|
43
|
+
export declare const createWidget: (root: TreeGroup) => WidgetData;
|
|
44
|
+
export {};
|
package/dist/tree.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { byStatistic, compareBy } from "@allurereport/core-api";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { calculateStatisticByLeafs } from "./utils.js";
|
|
4
|
+
const rootNodeUid = "__ROOT__";
|
|
5
|
+
const createLeaf = (endNode, test) => {
|
|
6
|
+
const leaf = {
|
|
7
|
+
parentUid: endNode.uid,
|
|
8
|
+
uid: test.uid,
|
|
9
|
+
name: test.name,
|
|
10
|
+
status: test.status,
|
|
11
|
+
time: { ...test.time },
|
|
12
|
+
flaky: test.flaky,
|
|
13
|
+
newFailed: test.newFailed,
|
|
14
|
+
newBroken: test.newBroken,
|
|
15
|
+
newPassed: test.newPassed,
|
|
16
|
+
retriesCount: test.retriesCount,
|
|
17
|
+
retriesStatusChange: test.retriesStatusChange,
|
|
18
|
+
parameters: test.parameters.filter((p) => p.value).map((p) => p.value),
|
|
19
|
+
tags: test.extra.tags ?? [],
|
|
20
|
+
};
|
|
21
|
+
return leaf;
|
|
22
|
+
};
|
|
23
|
+
export const byLabels = (labelNames) => {
|
|
24
|
+
return (test) => groupByLabels(test, labelNames);
|
|
25
|
+
};
|
|
26
|
+
export const createTree = (tests, classifier, leafFactory = createLeaf) => {
|
|
27
|
+
const groups = new Map();
|
|
28
|
+
const root = { uid: rootNodeUid, name: rootNodeUid, children: [] };
|
|
29
|
+
groups.set(root.uid, root);
|
|
30
|
+
for (const test of tests) {
|
|
31
|
+
const treeLayers = classifier(test);
|
|
32
|
+
if (!treeLayers) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
getEndNodes(test, root, treeLayers, 0, groups).forEach((endNode) => {
|
|
36
|
+
const leaf = leafFactory(endNode, test);
|
|
37
|
+
endNode.children.push(leaf);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return root;
|
|
41
|
+
};
|
|
42
|
+
const getEndNodes = (item, node, classifiers, index, groups) => {
|
|
43
|
+
if (index >= classifiers.length) {
|
|
44
|
+
return [node];
|
|
45
|
+
}
|
|
46
|
+
const layer = classifiers[index];
|
|
47
|
+
return layer.groups.flatMap((name) => {
|
|
48
|
+
const uid = groupUid(node.uid, name);
|
|
49
|
+
if (!groups.has(uid)) {
|
|
50
|
+
const value = { uid, name, children: [] };
|
|
51
|
+
groups.set(uid, value);
|
|
52
|
+
node.children.push(value);
|
|
53
|
+
}
|
|
54
|
+
const treeGroup = groups.get(uid);
|
|
55
|
+
return getEndNodes(item, treeGroup, classifiers, index + 1, groups);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const groupByLabels = (test, labelNames) => {
|
|
59
|
+
const result = [];
|
|
60
|
+
for (const name of labelNames) {
|
|
61
|
+
const groups = test.labels
|
|
62
|
+
.filter((label) => label.name === name)
|
|
63
|
+
.filter((label) => !!label.value)
|
|
64
|
+
.map((label) => label.value);
|
|
65
|
+
if (groups.length !== 0) {
|
|
66
|
+
result.push({ groups: Array.from(new Set(groups)) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
const groupUid = (parentUid, groupName) => {
|
|
72
|
+
return md5(parentUid ? `${parentUid}.${groupName}` : groupName);
|
|
73
|
+
};
|
|
74
|
+
const md5 = (data) => createHash("md5").update(data).digest("hex");
|
|
75
|
+
export const collapseTree = (treeGroup, separator = ".") => {
|
|
76
|
+
const newChildren = treeGroup.children.map((c) => {
|
|
77
|
+
if (!("children" in c)) {
|
|
78
|
+
return c;
|
|
79
|
+
}
|
|
80
|
+
let res = c;
|
|
81
|
+
while (res.children.length === 1 && "children" in res.children[0]) {
|
|
82
|
+
const child = res.children[0];
|
|
83
|
+
res = {
|
|
84
|
+
uid: groupUid(res.uid, child.uid),
|
|
85
|
+
name: res.name + separator + child.name,
|
|
86
|
+
children: child.children,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return res;
|
|
90
|
+
});
|
|
91
|
+
treeGroup.children = newChildren;
|
|
92
|
+
return treeGroup;
|
|
93
|
+
};
|
|
94
|
+
export const createWidget = (root) => {
|
|
95
|
+
const items = root.children
|
|
96
|
+
.map((c) => {
|
|
97
|
+
if (!("children" in c)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const res = c;
|
|
101
|
+
const statistic = calculateStatisticByLeafs(res);
|
|
102
|
+
return { uid: res.uid, name: res.name, statistic };
|
|
103
|
+
})
|
|
104
|
+
.filter((value) => value)
|
|
105
|
+
.map((value) => value)
|
|
106
|
+
.sort(compareBy("statistic", byStatistic()))
|
|
107
|
+
.slice(0, 10);
|
|
108
|
+
return {
|
|
109
|
+
items,
|
|
110
|
+
total: root.children.length,
|
|
111
|
+
};
|
|
112
|
+
};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Statistic } from "@allurereport/core-api";
|
|
2
|
+
import type { Allure2Status, Allure2TestResult, GroupTime } from "./model.js";
|
|
3
|
+
import type { TreeGroup } from "./tree.js";
|
|
4
|
+
export declare const updateStatistic: (statistic: Statistic, test: {
|
|
5
|
+
status: Allure2Status;
|
|
6
|
+
}) => undefined;
|
|
7
|
+
export declare const updateTime: (time: GroupTime, test: Allure2TestResult) => undefined;
|
|
8
|
+
export declare const calculateStatisticByLeafs: (treeGroup: TreeGroup) => Statistic;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { statisticKeys } from "./model.js";
|
|
2
|
+
export const updateStatistic = (statistic, test) => {
|
|
3
|
+
statistic[test.status] = (statistic[test.status] ?? 0) + 1;
|
|
4
|
+
statistic.total = (statistic.total ?? 0) + 1;
|
|
5
|
+
};
|
|
6
|
+
export const updateTime = (time, test) => {
|
|
7
|
+
const { start, stop, duration } = test.time;
|
|
8
|
+
if (duration === undefined) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
time.maxDuration = Math.max(time.maxDuration ?? 0, duration);
|
|
12
|
+
time.minDuration = Math.min(time.minDuration ?? Number.MAX_VALUE, duration);
|
|
13
|
+
time.sumDuration = (time.sumDuration ?? 0) + duration;
|
|
14
|
+
if (start !== undefined) {
|
|
15
|
+
time.start = Math.min(time.start ?? Number.MAX_VALUE, start);
|
|
16
|
+
}
|
|
17
|
+
if (stop !== undefined) {
|
|
18
|
+
time.stop = Math.max(time.stop ?? Number.MIN_VALUE, stop);
|
|
19
|
+
}
|
|
20
|
+
if (time.start !== undefined && time.stop !== undefined) {
|
|
21
|
+
time.duration = Math.max(0, time.stop - time.start);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export const calculateStatisticByLeafs = (treeGroup) => {
|
|
25
|
+
const current = { total: 0 };
|
|
26
|
+
treeGroup.children.forEach((child) => {
|
|
27
|
+
if ("children" in child) {
|
|
28
|
+
const childGroup = child;
|
|
29
|
+
const statistic = calculateStatisticByLeafs(childGroup);
|
|
30
|
+
statisticKeys.forEach((key) => {
|
|
31
|
+
const statisticElement = statistic[key];
|
|
32
|
+
if (statisticElement !== undefined) {
|
|
33
|
+
current[key] = (current[key] ?? 0) + statisticElement;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const childLeaf = child;
|
|
39
|
+
updateStatistic(current, childLeaf);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return current;
|
|
43
|
+
};
|
package/dist/writer.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReportFiles, ResultFile } from "@allurereport/plugin-api";
|
|
2
|
+
import type { Allure2TestResult } from "./model.js";
|
|
3
|
+
export interface ReportFile {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Allure2DataWriter {
|
|
8
|
+
writeData(fileName: string, data: any): Promise<void>;
|
|
9
|
+
writeWidget(fileName: string, data: any): Promise<void>;
|
|
10
|
+
writeTestCase(test: Allure2TestResult): Promise<void>;
|
|
11
|
+
writeAttachment(source: string, file: ResultFile): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare class FileSystemReportDataWriter implements Allure2DataWriter {
|
|
14
|
+
private readonly output;
|
|
15
|
+
constructor(output: string);
|
|
16
|
+
writeData(fileName: string, data: any): Promise<void>;
|
|
17
|
+
writeWidget(fileName: string, data: any): Promise<void>;
|
|
18
|
+
writeTestCase(test: Allure2TestResult): Promise<void>;
|
|
19
|
+
writeAttachment(source: string, file: ResultFile): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare class InMemoryReportDataWriter implements Allure2DataWriter {
|
|
22
|
+
#private;
|
|
23
|
+
writeData(fileName: string, data: any): Promise<void>;
|
|
24
|
+
writeWidget(fileName: string, data: any): Promise<void>;
|
|
25
|
+
writeTestCase(test: Allure2TestResult): Promise<void>;
|
|
26
|
+
writeAttachment(fileName: string, file: ResultFile): Promise<void>;
|
|
27
|
+
reportFiles(): ReportFile[];
|
|
28
|
+
}
|
|
29
|
+
export declare class ReportFileDataWriter implements Allure2DataWriter {
|
|
30
|
+
readonly reportFiles: ReportFiles;
|
|
31
|
+
constructor(reportFiles: ReportFiles);
|
|
32
|
+
writeData(fileName: string, data: any): Promise<void>;
|
|
33
|
+
writeWidget(fileName: string, data: any): Promise<void>;
|
|
34
|
+
writeAttachment(source: string, file: ResultFile): Promise<void>;
|
|
35
|
+
writeTestCase(test: Allure2TestResult): Promise<void>;
|
|
36
|
+
}
|
package/dist/writer.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
+
};
|
|
6
|
+
var _InMemoryReportDataWriter_data;
|
|
7
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
export class FileSystemReportDataWriter {
|
|
10
|
+
constructor(output) {
|
|
11
|
+
this.output = output;
|
|
12
|
+
}
|
|
13
|
+
async writeData(fileName, data) {
|
|
14
|
+
const distFolder = resolve(this.output, "data");
|
|
15
|
+
await mkdir(distFolder, { recursive: true });
|
|
16
|
+
await writeFile(resolve(distFolder, fileName), JSON.stringify(data), { encoding: "utf-8" });
|
|
17
|
+
}
|
|
18
|
+
async writeWidget(fileName, data) {
|
|
19
|
+
const distFolder = resolve(this.output, "widgets");
|
|
20
|
+
await mkdir(distFolder, { recursive: true });
|
|
21
|
+
await writeFile(resolve(distFolder, fileName), JSON.stringify(data), { encoding: "utf-8" });
|
|
22
|
+
}
|
|
23
|
+
async writeTestCase(test) {
|
|
24
|
+
const distFolder = resolve(this.output, "data", "test-cases");
|
|
25
|
+
await mkdir(distFolder, { recursive: true });
|
|
26
|
+
await writeFile(resolve(distFolder, `${test.uid}.json`), JSON.stringify(test), { encoding: "utf-8" });
|
|
27
|
+
}
|
|
28
|
+
async writeAttachment(source, file) {
|
|
29
|
+
const distFolder = resolve(this.output, "data", "attachments");
|
|
30
|
+
await mkdir(distFolder, { recursive: true });
|
|
31
|
+
await file.writeTo(resolve(distFolder, source));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class InMemoryReportDataWriter {
|
|
35
|
+
constructor() {
|
|
36
|
+
_InMemoryReportDataWriter_data.set(this, {});
|
|
37
|
+
}
|
|
38
|
+
async writeData(fileName, data) {
|
|
39
|
+
const dist = join("data", fileName);
|
|
40
|
+
__classPrivateFieldGet(this, _InMemoryReportDataWriter_data, "f")[dist] = Buffer.from(JSON.stringify(data), "utf-8");
|
|
41
|
+
}
|
|
42
|
+
async writeWidget(fileName, data) {
|
|
43
|
+
const dist = join("widgets", fileName);
|
|
44
|
+
__classPrivateFieldGet(this, _InMemoryReportDataWriter_data, "f")[dist] = Buffer.from(JSON.stringify(data), "utf-8");
|
|
45
|
+
}
|
|
46
|
+
async writeTestCase(test) {
|
|
47
|
+
const dist = join("data", "test-cases", `${test.uid}.json`);
|
|
48
|
+
__classPrivateFieldGet(this, _InMemoryReportDataWriter_data, "f")[dist] = Buffer.from(JSON.stringify(test), "utf-8");
|
|
49
|
+
}
|
|
50
|
+
async writeAttachment(fileName, file) {
|
|
51
|
+
const dist = join("data", "attachments", fileName);
|
|
52
|
+
const content = await file.asBuffer();
|
|
53
|
+
if (content) {
|
|
54
|
+
__classPrivateFieldGet(this, _InMemoryReportDataWriter_data, "f")[dist] = content;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
reportFiles() {
|
|
58
|
+
return Object.keys(__classPrivateFieldGet(this, _InMemoryReportDataWriter_data, "f")).map((key) => ({
|
|
59
|
+
name: key,
|
|
60
|
+
value: __classPrivateFieldGet(this, _InMemoryReportDataWriter_data, "f")[key].toString("base64"),
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
_InMemoryReportDataWriter_data = new WeakMap();
|
|
65
|
+
export class ReportFileDataWriter {
|
|
66
|
+
constructor(reportFiles) {
|
|
67
|
+
this.reportFiles = reportFiles;
|
|
68
|
+
}
|
|
69
|
+
async writeData(fileName, data) {
|
|
70
|
+
await this.reportFiles.addFile(join("data", fileName), Buffer.from(JSON.stringify(data), "utf-8"));
|
|
71
|
+
}
|
|
72
|
+
async writeWidget(fileName, data) {
|
|
73
|
+
await this.reportFiles.addFile(join("widgets", fileName), Buffer.from(JSON.stringify(data), "utf-8"));
|
|
74
|
+
}
|
|
75
|
+
async writeAttachment(source, file) {
|
|
76
|
+
const contentBuffer = await file.asBuffer();
|
|
77
|
+
if (!contentBuffer) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await this.reportFiles.addFile(join("data", "attachments", source), contentBuffer);
|
|
81
|
+
}
|
|
82
|
+
async writeTestCase(test) {
|
|
83
|
+
await this.reportFiles.addFile(join("data", "test-cases", `${test.uid}.json`), Buffer.from(JSON.stringify(test), "utf8"));
|
|
84
|
+
}
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@allurereport/plugin-allure2",
|
|
3
|
+
"version": "3.0.0-beta.9",
|
|
4
|
+
"description": "The classic version of Allure HTML report",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"allure",
|
|
7
|
+
"testing",
|
|
8
|
+
"report",
|
|
9
|
+
"plugin",
|
|
10
|
+
"html"
|
|
11
|
+
],
|
|
12
|
+
"repository": "https://github.com/allure-framework/allure3",
|
|
13
|
+
"license": "Apache-2.0",
|
|
14
|
+
"author": "Qameta Software",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"./dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "run clean && tsc --project ./tsconfig.json",
|
|
27
|
+
"clean": "rimraf ./dist",
|
|
28
|
+
"eslint": "eslint ./src/**/*.{js,jsx,ts,tsx}",
|
|
29
|
+
"eslint:format": "eslint --fix ./src/**/*.{js,jsx,ts,tsx}",
|
|
30
|
+
"test": "rimraf ./out && vitest run"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@allurereport/core-api": "3.0.0-beta.9",
|
|
34
|
+
"@allurereport/plugin-api": "3.0.0-beta.9",
|
|
35
|
+
"@allurereport/web-allure2": "3.0.0-beta.9",
|
|
36
|
+
"@allurereport/web-commons": "3.0.0-beta.9",
|
|
37
|
+
"handlebars": "^4.7.8"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@stylistic/eslint-plugin": "^2.6.1",
|
|
41
|
+
"@types/eslint": "^8.56.11",
|
|
42
|
+
"@types/node": "^20.17.9",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
44
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
45
|
+
"@vitest/runner": "^2.1.8",
|
|
46
|
+
"allure-vitest": "^3.0.9",
|
|
47
|
+
"eslint": "^8.57.0",
|
|
48
|
+
"eslint-config-prettier": "^9.1.0",
|
|
49
|
+
"eslint-plugin-import": "^2.29.1",
|
|
50
|
+
"eslint-plugin-jsdoc": "^50.0.0",
|
|
51
|
+
"eslint-plugin-n": "^17.10.1",
|
|
52
|
+
"eslint-plugin-no-null": "^1.0.2",
|
|
53
|
+
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
54
|
+
"rimraf": "^6.0.1",
|
|
55
|
+
"typescript": "^5.6.3",
|
|
56
|
+
"vitest": "^2.1.8"
|
|
57
|
+
}
|
|
58
|
+
}
|