@allurereport/plugin-allure2 3.15.0 → 3.16.1

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 CHANGED
@@ -1,4 +1,4 @@
1
- # Classic Plugin
1
+ # Allure 2 Plugin
2
2
 
3
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
4
 
@@ -12,16 +12,17 @@
12
12
 
13
13
  ## Overview
14
14
 
15
- The plugin generates a classic Allure Report.
15
+ The plugin generates an Allure 2-compatible report using the Allure 2.46.0 UI built from
16
+ the checked-in `@allurereport/web-allure2` sources and embedded in the plugin package.
16
17
 
17
18
  ## Install
18
19
 
19
20
  Use your favorite package manager to install the package:
20
21
 
21
22
  ```shell
22
- npm add @allurereport/plugin-classic
23
- yarn add @allurereport/plugin-classic
24
- pnpm add @allurereport/plugin-classic
23
+ npm add @allurereport/plugin-allure2
24
+ yarn add @allurereport/plugin-allure2
25
+ pnpm add @allurereport/plugin-allure2
25
26
  ```
26
27
 
27
28
  Then, add the plugin to the Allure configuration file:
@@ -34,7 +35,7 @@ export default defineConfig({
34
35
  output: "./allure-report",
35
36
  historyPath: "./history.jsonl",
36
37
  plugins: {
37
- + classic: {
38
+ + allure2: {
38
39
  + options: {
39
40
  + reportName: "HelloWorld",
40
41
  + },
@@ -47,8 +48,8 @@ export default defineConfig({
47
48
 
48
49
  The plugin accepts the following options:
49
50
 
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 |
51
+ | Option | Description | Type | Default |
52
+ | ---------------- | ----------------------------------------------- | --------- | --------------- |
53
+ | `reportName` | Name of the report | `string` | `Allure Report` |
54
+ | `singleFile` | Writes the report as a single `index.html` file | `boolean` | `false` |
55
+ | `reportLanguage` | Default language of the report | `string` | OS language |
@@ -7,14 +7,17 @@ const testDefects = {
7
7
  matchedStatuses: ["broken"],
8
8
  };
9
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);
10
+ const matched = categories.find((category) => categoryMatch(category, result));
11
+ if (matched) {
12
+ return [matched];
13
13
  }
14
- if (matched.length === 0 && categoryMatch(testDefects, result)) {
15
- matched.push(testDefects);
14
+ if (categoryMatch(productDefects, result)) {
15
+ return [productDefects];
16
16
  }
17
- return matched;
17
+ if (categoryMatch(testDefects, result)) {
18
+ return [testDefects];
19
+ }
20
+ return [];
18
21
  };
19
22
  const categoryMatch = (category, result) => {
20
23
  const { status, statusMessage, statusTrace, flaky } = result;
@@ -1,11 +1,12 @@
1
1
  import type { HistoryTestResult, TestFixtureResult, TestResult } from "@allurereport/core-api";
2
- import type { Allure2Category, Allure2TestResult } from "./model.js";
2
+ import type { Allure2Category, Allure2HistoryData, Allure2TestResult } from "./model.js";
3
3
  type ConvertContext = {
4
4
  attachmentMap: Map<string, string>;
5
5
  fixtures: TestFixtureResult[];
6
6
  categories: Allure2Category[];
7
7
  retries: TestResult[];
8
8
  history: HistoryTestResult[];
9
+ legacyHistory?: Allure2HistoryData;
9
10
  };
10
11
  export declare const convertTestResult: (context: ConvertContext, test: TestResult) => Allure2TestResult;
11
12
  export {};
@@ -1,31 +1,70 @@
1
- import { filterIncludedInSuccessRate, isStep, redactParameters } from "@allurereport/core-api";
1
+ import { isStep, redactParameters } from "@allurereport/core-api";
2
2
  import { matchCategories } from "./categories.js";
3
3
  import { severityValues } from "./model.js";
4
- import { updateStatistic } from "./utils.js";
4
+ import { createStatistic, updateStatistic } from "./utils.js";
5
5
  const sortByTime = (a, b) => a.time.start !== undefined && b.time.start !== undefined ? a.time.start - b.time.start : 0;
6
6
  const convertStatus = (status) => status;
7
+ const getTopLevelAttachmentCount = (result) => {
8
+ if (result.sourceMetadata.readerId !== "allure2") {
9
+ return 0;
10
+ }
11
+ const value = result.sourceMetadata.metadata.allure2_top_level_attachment_count;
12
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : 0;
13
+ };
14
+ const hasMessage = (step, message) => step.statusMessage === message || step.steps.some((child) => hasMessage(child, message));
15
+ const summarize = (name, statusMessage, steps, attachments, parametersCount) => {
16
+ const stepsCount = steps.reduce((count, step) => count + 1 + step.stepsCount, 0);
17
+ const attachmentsCount = attachments.length + steps.reduce((count, step) => count + step.attachmentsCount, 0);
18
+ const shouldDisplayMessage = !!statusMessage && !steps.some((step) => hasMessage(step, statusMessage));
19
+ return {
20
+ stepsCount,
21
+ attachmentsCount,
22
+ shouldDisplayMessage,
23
+ hasContent: steps.length + attachments.length + parametersCount > 0 || shouldDisplayMessage,
24
+ attachmentStep: stepsCount === 0 && attachmentsCount === 1 && name !== undefined && name === attachments[0]?.name,
25
+ };
26
+ };
7
27
  const convertStageResult = (context, result) => {
8
- const { name: _name, ...testStage } = convertStep(context, {
9
- name: "test",
10
- steps: result.steps,
11
- start: result.start,
12
- stop: result.stop,
13
- duration: result.duration,
28
+ const topLevelAttachmentCount = Math.min(getTopLevelAttachmentCount(result), result.steps.length);
29
+ const attachmentStart = result.steps.length - topLevelAttachmentCount;
30
+ const attachmentCandidates = result.steps.slice(attachmentStart);
31
+ const stageSteps = [
32
+ ...result.steps.slice(0, attachmentStart),
33
+ ...attachmentCandidates.filter((step) => isStep(step)),
34
+ ];
35
+ const attachments = attachmentCandidates
36
+ .filter((step) => !isStep(step))
37
+ .map((step) => convertAttachment(context, step));
38
+ const steps = stageSteps.map((step) => convertStep(context, step));
39
+ const name = "type" in result ? result.name : undefined;
40
+ const statusMessage = result.error?.message;
41
+ const summary = summarize(name, statusMessage, steps, attachments, 0);
42
+ return {
43
+ ...(name === undefined ? {} : { name }),
44
+ time: {
45
+ start: result.start,
46
+ stop: result.stop,
47
+ duration: result.duration,
48
+ },
14
49
  status: result.status,
50
+ statusMessage,
51
+ statusTrace: result.error?.trace,
52
+ steps,
53
+ attachments,
15
54
  parameters: [],
16
- type: "step",
17
- });
18
- return testStage;
55
+ ...summary,
56
+ };
19
57
  };
20
58
  const convertStep = (context, step) => {
21
59
  if (isStep(step)) {
22
60
  const name = step.name;
23
- const steps = step.steps.map((child) => convertStep(context, child));
24
- const stepsCount = steps.length;
61
+ const steps = step.steps.filter((child) => isStep(child)).map((child) => convertStep(context, child));
62
+ const attachments = step.steps
63
+ .filter((child) => !isStep(child))
64
+ .map((child) => convertAttachment(context, child));
25
65
  const parameters = redactParameters(step.parameters);
26
- const parametersCount = parameters.length;
27
- const statusMessage = step.error?.message;
28
- const shouldDisplayMessage = !!statusMessage || steps.findIndex((s) => s.statusMessage === statusMessage) > 0;
66
+ const statusMessage = step.error?.message ?? step.message;
67
+ const summary = summarize(name, statusMessage, steps, attachments, parameters.length);
29
68
  return {
30
69
  name,
31
70
  time: {
@@ -35,15 +74,11 @@ const convertStep = (context, step) => {
35
74
  },
36
75
  status: step.status,
37
76
  statusMessage,
38
- statusTrace: step.error?.trace,
77
+ statusTrace: step.error?.trace ?? step.trace,
39
78
  steps,
40
- attachments: [],
79
+ attachments,
41
80
  parameters,
42
- stepsCount,
43
- attachmentsCount: 0,
44
- hasContent: stepsCount + parametersCount > 0 || shouldDisplayMessage,
45
- shouldDisplayMessage,
46
- attachmentStep: false,
81
+ ...summary,
47
82
  };
48
83
  }
49
84
  const stepName = step.link.name;
@@ -87,8 +122,30 @@ const extractSeverity = (test) => {
87
122
  return (maybeSeverity ? severityValues.find((value) => value === maybeSeverity) : undefined) ?? "normal";
88
123
  };
89
124
  const importantStatuses = ["failed", "broken", "passed"];
125
+ const isImportantStatus = (status) => importantStatuses.includes(status);
126
+ const getAllure2Links = (test) => {
127
+ if (test.sourceMetadata.readerId !== "allure2") {
128
+ return undefined;
129
+ }
130
+ const links = test.sourceMetadata.metadata.allure2_links;
131
+ if (!Array.isArray(links)) {
132
+ return undefined;
133
+ }
134
+ return links.map((link) => ({
135
+ name: typeof link?.name === "string" ? link.name : undefined,
136
+ url: typeof link?.url === "string" ? link.url : undefined,
137
+ type: typeof link?.type === "string" ? link.type : undefined,
138
+ }));
139
+ };
140
+ const isFlakyFromHistory = (currentStatus, historyItems) => {
141
+ if (currentStatus !== "failed" && currentStatus !== "broken") {
142
+ return false;
143
+ }
144
+ const statuses = historyItems.slice(0, 5).map(({ status }) => status);
145
+ return statuses.includes("passed") && statuses.indexOf("passed") < statuses.lastIndexOf("failed");
146
+ };
90
147
  export const convertTestResult = (context, test) => {
91
- const testStage = convertStageResult(context, test);
148
+ const testStage = test.steps.length > 0 ? convertStageResult(context, test) : null;
92
149
  const beforeStages = context.fixtures
93
150
  .filter((value) => value.type === "before")
94
151
  .map((fixture) => convertStageResult(context, fixture))
@@ -103,8 +160,6 @@ export const convertTestResult = (context, test) => {
103
160
  const status = convertStatus(test.status);
104
161
  const statusMessage = test.error?.message;
105
162
  const statusTrace = test.error?.trace;
106
- const flaky = false;
107
- const categories = matchCategories(context.categories, { statusMessage, statusTrace, status, flaky });
108
163
  const retries = context.retries.map((retry) => ({
109
164
  uid: retry.id,
110
165
  status: convertStatus(retry.status),
@@ -115,25 +170,38 @@ export const convertTestResult = (context, test) => {
115
170
  duration: retry.duration,
116
171
  },
117
172
  }));
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;
173
+ const retriesStatusChange = isImportantStatus(status) &&
174
+ retries.find((retry) => isImportantStatus(retry.status) && retry.status !== status) !== undefined;
175
+ const historyItems = context.legacyHistory
176
+ ? context.legacyHistory.items.map((item) => ({
177
+ uid: item.uid,
178
+ status: item.status,
179
+ reportUrl: item.reportUrl,
180
+ statusDetails: item.statusDetails,
181
+ time: { ...item.time },
182
+ }))
183
+ : context.history.map((historyTestResult) => ({
184
+ uid: historyTestResult.id,
185
+ status: convertStatus(historyTestResult.status),
186
+ reportUrl: historyTestResult.url,
187
+ statusDetails: historyTestResult.error?.message,
188
+ time: {
189
+ start: historyTestResult.start,
190
+ stop: historyTestResult.stop,
191
+ duration: historyTestResult.duration,
192
+ },
193
+ }));
194
+ const lastHistoryStatus = historyItems.find(({ status: historyStatus }) => isImportantStatus(historyStatus))?.status;
121
195
  const newFailed = lastHistoryStatus === "passed" && test.status === "failed";
122
196
  const newBroken = lastHistoryStatus === "passed" && test.status === "broken";
123
197
  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));
198
+ const flaky = test.flaky || isFlakyFromHistory(status, historyItems);
199
+ const categories = matchCategories(context.categories, { statusMessage, statusTrace, status, flaky });
200
+ const statistic = createStatistic(context.legacyHistory?.statistic);
201
+ if (!context.legacyHistory) {
202
+ historyItems.forEach((historyItem) => updateStatistic(statistic, historyItem));
203
+ }
204
+ updateStatistic(statistic, test);
137
205
  const history = {
138
206
  statistic,
139
207
  items: historyItems,
@@ -155,7 +223,7 @@ export const convertTestResult = (context, test) => {
155
223
  statusMessage,
156
224
  statusTrace,
157
225
  labels: test.labels,
158
- links: test.links,
226
+ links: getAllure2Links(test) ?? test.links,
159
227
  parameters: redactParameters(test.parameters),
160
228
  afterStages,
161
229
  beforeStages,
@@ -1,18 +1,10 @@
1
- import type { AttachmentLink, HistoryDataPoint } from "@allurereport/core-api";
1
+ import type { AttachmentLink, GlobalAttachmentLink, HistoryDataPoint, TestError } from "@allurereport/core-api";
2
2
  import type { ReportFiles, ResultFile } from "@allurereport/plugin-api";
3
- import type { Allure2ExecutorInfo, Allure2TestResult } from "./model.js";
3
+ import type { ReportStaticManifest } from "@allurereport/plugin-api/static-assets";
4
+ import type { Allure2CategoriesTrendItem, Allure2DurationTrendItem, Allure2ExecutorInfo, Allure2LegacyHistoryTrendItem, Allure2RetryTrendItem, Allure2TestResult } from "./model.js";
4
5
  import type { Allure2DataWriter, ReportFile } from "./writer.js";
5
- export type TemplateManifest = Record<string, string>;
6
- export declare const getPackageRoot: () => Promise<string>;
7
- export declare const readTemplateManifest: (packageRoot: string, singleFileMode?: boolean) => Promise<TemplateManifest>;
8
- export declare const readManifestEntry: (options: {
9
- fileName: string;
10
- singleFile?: boolean;
11
- mimeType: string;
12
- reportFiles: ReportFiles;
13
- inserter: (content: string) => string;
14
- packageRoot: string;
15
- }) => Promise<string>;
6
+ export type TemplateManifest = ReportStaticManifest;
7
+ export declare const readTemplateManifest: () => Promise<TemplateManifest>;
16
8
  export declare const generateStaticFiles: (payload: {
17
9
  allureVersion: string;
18
10
  reportName: string;
@@ -34,6 +26,9 @@ export declare const generateEnvironmentJson: (writer: Allure2DataWriter, env: {
34
26
  }[]) => Promise<void>;
35
27
  export declare const generateExecutorJson: (writer: Allure2DataWriter, executor?: Partial<Allure2ExecutorInfo>) => Promise<void>;
36
28
  export declare const generateDefaultWidgetData: (writer: Allure2DataWriter, tests: Allure2TestResult[], ...fileNames: string[]) => Promise<void>;
37
- export declare const generateEmptyTrendData: (writer: Allure2DataWriter, ...fileNames: string[]) => Promise<void>;
38
- export declare const generateTrendData: (writer: Allure2DataWriter, reportName: string, tests: Allure2TestResult[], historyDataPoints: HistoryDataPoint[]) => Promise<void>;
29
+ export declare const generateHistoryTrendData: (writer: Allure2DataWriter, reportName: string, tests: Allure2TestResult[], historyDataPoints: HistoryDataPoint[], legacyHistory?: Allure2LegacyHistoryTrendItem[], executor?: Partial<Allure2ExecutorInfo>) => Promise<void>;
30
+ export declare const generateDurationTrendData: (writer: Allure2DataWriter, tests: Allure2TestResult[], history?: Allure2DurationTrendItem[], executor?: Partial<Allure2ExecutorInfo>) => Promise<void>;
31
+ export declare const generateRetryTrendData: (writer: Allure2DataWriter, tests: Allure2TestResult[], history?: Allure2RetryTrendItem[], executor?: Partial<Allure2ExecutorInfo>) => Promise<void>;
32
+ export declare const generateCategoriesTrendData: (writer: Allure2DataWriter, tests: Allure2TestResult[], history?: Allure2CategoriesTrendItem[], executor?: Partial<Allure2ExecutorInfo>) => Promise<void>;
39
33
  export declare const generateAttachmentsData: (writer: Allure2DataWriter, attachmentLinks: AttachmentLink[], contentFunction: (id: string) => Promise<ResultFile | undefined>) => Promise<Map<string, string>>;
34
+ export declare const generateGlobalsData: (writer: Allure2DataWriter, globalErrors: TestError[], globalAttachments: GlobalAttachmentLink[], attachmentMap: Map<string, string>, attachmentTimestamps?: Record<string, number>, errorTimestamps?: Array<number | undefined>) => Promise<void>;
@@ -1,11 +1,9 @@
1
- import { readFile } from "node:fs/promises";
2
- import { basename, dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { createBaseUrlScript, createFaviconLinkTag, createReportDataScript, stringifyForInlineScript, createScriptTag, createStylesLinkTag, } from "@allurereport/core-api";
5
- import { findUp } from "find-up";
1
+ import { createBaseUrlScript, createScriptTag, createStylesLinkTag } from "@allurereport/core-api";
2
+ import { copyReportStaticAssets, getReportStaticAsset, readReportStaticAssets, } from "@allurereport/plugin-api/static-assets";
6
3
  import Handlebars from "handlebars";
7
4
  import { byLabels, collapseTree, createTree, createWidget } from "./tree.js";
8
- import { updateStatistic, updateTime } from "./utils.js";
5
+ import { createStatistic, updateStatistic, updateTime } from "./utils.js";
6
+ const reportStaticArchive = new URL("../dist/static/report.tar", import.meta.url);
9
7
  const writeConcurrently = async (items, write, concurrency = 64) => {
10
8
  for (let i = 0; i < items.length; i += concurrency) {
11
9
  await Promise.all(items.slice(i, i + concurrency).map(write));
@@ -15,14 +13,11 @@ const template = `<!DOCTYPE html>
15
13
  <html dir="ltr" lang="{{reportLanguage}}">
16
14
  <head>
17
15
  <meta charset="utf-8">
16
+ <meta name="allure-report-uuid" content="{{reportUuid}}">
18
17
  <title>{{reportName}}</title>
19
18
  {{{ headTags }}}
20
- <script>
21
- window.allureReportOptions = {{{ reportOptions }}};
22
- </script>
23
19
  </head>
24
20
  <body>
25
- <svg id="__SVG_SPRITE_NODE__" aria-hidden="true" style="position: absolute; width: 0; height: 0"></svg>
26
21
  <div id="alert"></div>
27
22
  <div id="content">
28
23
  <span class="spinner">
@@ -30,19 +25,29 @@ const template = `<!DOCTYPE html>
30
25
  </span>
31
26
  </div>
32
27
  <div id="popup"></div>
28
+ <script>
29
+ window.__allureCoreLoaded = new Promise(function (resolve, reject) {
30
+ window.__allureResolveCoreLoaded = resolve;
31
+ window.__allureRejectCoreLoaded = reject;
32
+ });
33
+ </script>
33
34
  {{{ bodyTags }}}
35
+ <script>
36
+ if (typeof window.__allureResolveCoreLoaded === "function") {
37
+ window.__allureResolveCoreLoaded([]);
38
+ }
39
+ </script>
34
40
  ${createBaseUrlScript()}
35
41
  {{#if analyticsEnable}}
36
- <script async src="https://www.googletagmanager.com/gtag/js?id=G-LNDJ3J7WT0"></script>
42
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-FVWC4GKEYS"></script>
37
43
  <script>
38
44
  window.dataLayer = window.dataLayer || [];
39
45
  function gtag(){dataLayer.push(arguments);}
40
46
  gtag('js', new Date());
41
- gtag('config', 'G-LNDJ3J7WT0', {
47
+ gtag('config', 'G-FVWC4GKEYS', {
42
48
  'allureVersion': '{{allureVersion}}',
43
- 'report':'classic',
44
49
  'reportUuid': '{{reportUuid}}',
45
- 'single_file': '{{singleFile}}'
50
+ 'single_file': {{singleFile}}
46
51
  });
47
52
  </script>
48
53
  {{/if}}
@@ -51,90 +56,62 @@ const template = `<!DOCTYPE html>
51
56
  </html>
52
57
  `;
53
58
  const compiledTemplate = Handlebars.compile(template);
54
- export const getPackageRoot = async () => {
55
- const packageJsonPath = await findUp("package.json", {
56
- cwd: dirname(fileURLToPath(import.meta.url)),
57
- });
58
- return dirname(packageJsonPath);
59
- };
60
- export const readTemplateManifest = async (packageRoot, singleFileMode) => {
61
- const templateManifestPath = join(packageRoot, "static", singleFileMode ? "single" : "multi", "manifest.json");
62
- const templateManifest = await readFile(templateManifestPath, { encoding: "utf-8" });
63
- return JSON.parse(templateManifest);
59
+ const createEmbeddedReportDataScript = (reportFiles) => {
60
+ const reportFilesDeclaration = reportFiles
61
+ .map(({ name, value }) => `d(${JSON.stringify(name)},${JSON.stringify(value)})`)
62
+ .join(",");
63
+ return `
64
+ <script>
65
+ window.reportDataReady = false;
66
+ window.reportData = window.reportData || {};
67
+
68
+ function d(name, value) {
69
+ return new Promise(function (resolve) {
70
+ window.reportData[name] = value;
71
+ resolve(true);
72
+ });
73
+ }
74
+ </script>
75
+ <script>
76
+ Promise.allSettled([${reportFilesDeclaration}]).then(function () {
77
+ window.reportDataReady = true;
78
+ });
79
+ </script>
80
+ `;
64
81
  };
65
- export const readManifestEntry = async (options) => {
66
- const { fileName, singleFile, mimeType, inserter, reportFiles, packageRoot } = options;
67
- const filePath = join(packageRoot, "static", singleFile ? "single" : "multi", fileName);
68
- const scriptContentBuffer = await readFile(filePath);
69
- if (singleFile) {
70
- return inserter(`data:${mimeType};base64,${scriptContentBuffer.toString("base64")}`);
71
- }
72
- await reportFiles.addFile(fileName, scriptContentBuffer);
73
- return inserter(fileName);
82
+ export const readTemplateManifest = async () => {
83
+ const { manifest } = await readReportStaticAssets(reportStaticArchive);
84
+ return manifest;
74
85
  };
75
86
  export const generateStaticFiles = async (payload) => {
76
- const packageRoot = await getPackageRoot();
77
87
  const { reportName, reportLanguage, singleFile, reportFiles, reportDataFiles, reportUuid, allureVersion } = payload;
78
- const manifest = await readTemplateManifest(packageRoot, singleFile);
88
+ const staticAssets = await readReportStaticAssets(reportStaticArchive);
89
+ const { manifest } = staticAssets;
90
+ const mainJs = manifest["main.js"];
91
+ const mainCss = manifest["main.css"];
79
92
  const headTags = [];
80
93
  const bodyTags = [];
81
- for (const key in manifest) {
82
- const fileName = manifest[key];
83
- if (key === "favicon.ico") {
84
- const tag = await readManifestEntry({
85
- fileName,
86
- singleFile,
87
- reportFiles,
88
- inserter: createFaviconLinkTag,
89
- mimeType: "image/x-icon",
90
- packageRoot,
91
- });
92
- headTags.push(tag);
93
- continue;
94
+ if (!singleFile) {
95
+ if (mainCss) {
96
+ headTags.push(createStylesLinkTag(mainCss));
94
97
  }
95
- if (key === "main.css") {
96
- const tag = await readManifestEntry({
97
- fileName,
98
- singleFile,
99
- reportFiles,
100
- inserter: createStylesLinkTag,
101
- mimeType: "text/css",
102
- packageRoot,
103
- });
104
- headTags.push(tag);
105
- continue;
106
- }
107
- if (key === "main.js") {
108
- const tag = await readManifestEntry({
109
- fileName,
110
- singleFile,
111
- reportFiles,
112
- inserter: createScriptTag,
113
- mimeType: "text/javascript",
114
- packageRoot,
115
- });
116
- bodyTags.push(tag);
117
- continue;
118
- }
119
- if (singleFile) {
120
- continue;
98
+ bodyTags.push(createScriptTag(mainJs));
99
+ await copyReportStaticAssets(staticAssets, reportFiles);
100
+ }
101
+ else {
102
+ if (mainCss) {
103
+ const mainCssContent = getReportStaticAsset(staticAssets, mainCss);
104
+ headTags.push(createStylesLinkTag(`data:text/css;base64,${mainCssContent.toString("base64")}`));
121
105
  }
122
- const filePath = join(packageRoot, "static", singleFile ? "single" : "multi", fileName);
123
- const fileContent = await readFile(filePath);
124
- await reportFiles.addFile(basename(filePath), fileContent);
106
+ const mainJsContent = getReportStaticAsset(staticAssets, mainJs);
107
+ bodyTags.push(createScriptTag(`data:text/javascript;base64,${mainJsContent.toString("base64")}`));
125
108
  }
126
- const reportOptions = {
127
- reportName: reportName ?? "Allure Report",
128
- reportLanguage: reportLanguage ?? "en",
129
- createdAt: Date.now(),
130
- };
131
109
  try {
132
110
  const html = compiledTemplate({
133
111
  headTags: headTags.join("\n"),
134
112
  bodyTags: bodyTags.join("\n"),
135
- reportFilesScript: createReportDataScript(reportDataFiles),
136
- reportOptions: stringifyForInlineScript(reportOptions),
137
- analyticsEnable: true,
113
+ reportFilesScript: singleFile ? createEmbeddedReportDataScript(reportDataFiles) : "",
114
+ analyticsEnable: process.env.ALLURE_NO_ANALYTICS?.toLowerCase() !== "true",
138
115
  allureVersion,
139
116
  reportLanguage,
140
117
  reportUuid,
@@ -198,7 +175,7 @@ export const generateTestResults = async (writer, tests) => {
198
175
  await writeConcurrently(tests, (test) => writer.writeTestCase(test));
199
176
  };
200
177
  export const generateSummaryJson = async (writer, reportName, tests) => {
201
- const statistic = { total: 0 };
178
+ const statistic = createStatistic();
202
179
  const time = {};
203
180
  tests
204
181
  .filter((test) => !test.isRetry)
@@ -235,50 +212,85 @@ export const generateDefaultWidgetData = async (writer, tests, ...fileNames) =>
235
212
  await writer.writeWidget(fileName, statusChartData);
236
213
  }
237
214
  };
238
- export const generateEmptyTrendData = async (writer, ...fileNames) => {
239
- for (const fileName of fileNames) {
240
- await writer.writeWidget(fileName, [
241
- {
242
- uid: "invalid",
243
- name: "invalid",
244
- statistic: { total: 0 },
245
- },
246
- ]);
247
- }
248
- };
249
- export const generateTrendData = async (writer, reportName, tests, historyDataPoints) => {
250
- const statistic = { total: 0 };
215
+ const trendIdentity = (executor) => ({
216
+ buildOrder: executor?.buildOrder,
217
+ reportName: executor?.reportName,
218
+ reportUrl: executor?.reportUrl,
219
+ });
220
+ const prependTrendItem = (current, previous) => [current, ...previous].slice(0, 20);
221
+ export const generateHistoryTrendData = async (writer, reportName, tests, historyDataPoints, legacyHistory = [], executor) => {
222
+ const statistic = createStatistic();
251
223
  tests
252
224
  .filter((test) => !test.isRetry)
253
225
  .forEach((test) => {
254
226
  updateStatistic(statistic, test);
255
227
  });
256
- const history = historyDataPoints.map((point) => {
257
- const stat = { total: 0 };
258
- Object.values(point.testResults).forEach((testResult) => {
259
- updateStatistic(stat, testResult);
228
+ const history = legacyHistory.length
229
+ ? legacyHistory.map(({ data, statistic: legacyStatistic, ...item }) => ({
230
+ ...item,
231
+ data: createStatistic(data ?? legacyStatistic),
232
+ }))
233
+ : historyDataPoints
234
+ .map((point) => {
235
+ const stat = createStatistic();
236
+ Object.values(point.testResults).forEach((testResult) => {
237
+ updateStatistic(stat, testResult);
238
+ });
239
+ return {
240
+ data: stat,
241
+ reportName: point.name,
242
+ reportUrl: point.url,
243
+ timestamp: point.timestamp,
244
+ };
245
+ })
246
+ .sort((left, right) => right.timestamp - left.timestamp)
247
+ .map(({ timestamp: _timestamp, ...item }, index, values) => ({
248
+ ...item,
249
+ buildOrder: values.length - index,
250
+ }));
251
+ const current = {
252
+ data: statistic,
253
+ ...(executor
254
+ ? trendIdentity(executor)
255
+ : {
256
+ buildOrder: history.length + 1,
257
+ reportName,
258
+ }),
259
+ };
260
+ await writer.writeWidget("history-trend.json", prependTrendItem(current, history));
261
+ };
262
+ export const generateDurationTrendData = async (writer, tests, history = [], executor) => {
263
+ const time = {};
264
+ tests.forEach((test) => updateTime(time, test));
265
+ const current = {
266
+ data: { duration: time.duration ?? 0 },
267
+ ...trendIdentity(executor),
268
+ };
269
+ await writer.writeWidget("duration-trend.json", prependTrendItem(current, history));
270
+ };
271
+ export const generateRetryTrendData = async (writer, tests, history = [], executor) => {
272
+ const current = {
273
+ data: {
274
+ run: tests.filter((test) => !test.isRetry).length,
275
+ retry: tests.filter((test) => test.isRetry).length,
276
+ },
277
+ ...trendIdentity(executor),
278
+ };
279
+ await writer.writeWidget("retry-trend.json", prependTrendItem(current, history));
280
+ };
281
+ export const generateCategoriesTrendData = async (writer, tests, history = [], executor) => {
282
+ const categories = {};
283
+ tests.forEach((test) => {
284
+ const testCategories = test.extra.categories ?? [];
285
+ testCategories.forEach(({ name }) => {
286
+ categories[name] = (categories[name] ?? 0) + 1;
260
287
  });
261
- return {
262
- data: stat,
263
- timestamp: point.timestamp,
264
- reportName: point.name,
265
- };
266
- });
267
- history
268
- .sort((a, b) => b.timestamp - a.timestamp)
269
- .forEach((element, index) => {
270
- element.buildOrder = history.length - index;
271
288
  });
272
- const data = [
273
- {
274
- data: statistic,
275
- timestamp: new Date().getTime(),
276
- buildOrder: history.length + 1,
277
- reportName: reportName,
278
- },
279
- ...history,
280
- ];
281
- await writer.writeWidget("history-trend.json", data);
289
+ const current = {
290
+ data: categories,
291
+ ...trendIdentity(executor),
292
+ };
293
+ await writer.writeWidget("categories-trend.json", prependTrendItem(current, history));
282
294
  };
283
295
  export const generateAttachmentsData = async (writer, attachmentLinks, contentFunction) => {
284
296
  const result = new Map();
@@ -296,3 +308,31 @@ export const generateAttachmentsData = async (writer, attachmentLinks, contentFu
296
308
  }
297
309
  return result;
298
310
  };
311
+ export const generateGlobalsData = async (writer, globalErrors, globalAttachments, attachmentMap, attachmentTimestamps = {}, errorTimestamps = []) => {
312
+ const data = {
313
+ errors: globalErrors.map(({ message, trace, actual, expected }, index) => ({
314
+ message,
315
+ trace,
316
+ actual,
317
+ expected,
318
+ timestamp: errorTimestamps[index],
319
+ })),
320
+ attachments: globalAttachments.flatMap((attachment) => {
321
+ const source = attachmentMap.get(attachment.id);
322
+ if (!source) {
323
+ return [];
324
+ }
325
+ return [
326
+ {
327
+ uid: attachment.id,
328
+ name: attachment.name,
329
+ source,
330
+ type: attachment.contentType ?? "application/octet-stream",
331
+ size: !attachment.missed ? attachment.contentLength : undefined,
332
+ timestamp: attachment.originalFileName ? attachmentTimestamps[attachment.originalFileName] : undefined,
333
+ },
334
+ ];
335
+ }),
336
+ };
337
+ await writer.writeWidget("globals.json", data);
338
+ };
package/dist/model.d.ts CHANGED
@@ -25,6 +25,22 @@ export interface Allure2Attachment {
25
25
  type?: string;
26
26
  size?: number;
27
27
  }
28
+ export interface Allure2GlobalError {
29
+ timestamp?: number;
30
+ message?: string;
31
+ trace?: string;
32
+ actual?: string;
33
+ expected?: string;
34
+ }
35
+ export interface Allure2GlobalAttachment extends Allure2Attachment {
36
+ source: string;
37
+ type: string;
38
+ timestamp?: number;
39
+ }
40
+ export interface Allure2GlobalsData {
41
+ errors: Allure2GlobalError[];
42
+ attachments: Allure2GlobalAttachment[];
43
+ }
28
44
  export interface Allure2Step {
29
45
  name: string;
30
46
  time: Allure2Time;
@@ -60,7 +76,7 @@ export interface Allure2TestResult {
60
76
  retriesCount: number;
61
77
  retriesStatusChange: boolean;
62
78
  beforeStages: Allure2StageResult[];
63
- testStage: Allure2StageResult;
79
+ testStage: Allure2StageResult | null;
64
80
  afterStages: Allure2StageResult[];
65
81
  labels: Allure2Label[];
66
82
  parameters: Allure2Parameter[];
@@ -113,7 +129,7 @@ export interface Allure2RetryItem {
113
129
  }
114
130
  export interface Allure2HistoryItem {
115
131
  uid: string;
116
- reportUrl: string;
132
+ reportUrl?: string;
117
133
  status: Allure2Status;
118
134
  statusDetails?: string;
119
135
  time: Allure2Time;
@@ -128,6 +144,25 @@ export interface Allure2HistoryTrendItem {
128
144
  reportUrl?: string;
129
145
  reportName?: string;
130
146
  }
147
+ export interface Allure2LegacyHistoryTrendItem extends Omit<Allure2HistoryTrendItem, "data"> {
148
+ data?: Statistic;
149
+ statistic?: Statistic;
150
+ }
151
+ export interface Allure2TrendItem<T extends Record<string, number> = Record<string, number>> {
152
+ data: T;
153
+ buildOrder?: number;
154
+ reportUrl?: string;
155
+ reportName?: string;
156
+ }
157
+ export type Allure2DurationTrendItem = Allure2TrendItem<{
158
+ duration: number;
159
+ }>;
160
+ export type Allure2RetryTrendItem = Allure2TrendItem<{
161
+ run: number;
162
+ retry: number;
163
+ }>;
164
+ export type Allure2CategoriesTrendItem = Allure2TrendItem<Record<string, number>>;
165
+ export type Allure2LegacyHistory = Record<string, Allure2HistoryData>;
131
166
  export type Allure2Options = {
132
167
  reportName?: string;
133
168
  singleFile?: boolean;
package/dist/model.js CHANGED
@@ -1,2 +1,2 @@
1
- export const statisticKeys = ["failed", "broken", "passed", "skipped", "unknown", "total"];
1
+ export const statisticKeys = ["failed", "broken", "skipped", "passed", "unknown", "total"];
2
2
  export const severityValues = ["blocker", "critical", "normal", "minor", "trivial"];
package/dist/plugin.js CHANGED
@@ -6,7 +6,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
6
6
  var _Allure2Plugin_generate;
7
7
  import { createPluginSummary, preciseTreeLabels, } from "@allurereport/plugin-api";
8
8
  import { convertTestResult } from "./converters.js";
9
- import { generateAttachmentsData, generateCategoriesData, generateDefaultWidgetData, generateEmptyTrendData, generateEnvironmentJson, generateExecutorJson, generatePackagesData, generateStaticFiles, generateSummaryJson, generateTestResults, generateTimelineData, generateTree, generateTrendData, } from "./generators.js";
9
+ import { generateAttachmentsData, generateCategoriesData, generateCategoriesTrendData, generateDefaultWidgetData, generateDurationTrendData, generateEnvironmentJson, generateExecutorJson, generateGlobalsData, generateHistoryTrendData, generatePackagesData, generateRetryTrendData, generateStaticFiles, generateSummaryJson, generateTestResults, generateTimelineData, generateTree, } from "./generators.js";
10
10
  import { InMemoryReportDataWriter, ReportFileDataWriter } from "./writer.js";
11
11
  export class Allure2Plugin {
12
12
  constructor(options = {}) {
@@ -16,7 +16,15 @@ export class Allure2Plugin {
16
16
  const writer = singleFile ? new InMemoryReportDataWriter() : new ReportFileDataWriter(context.reportFiles);
17
17
  const attachmentLinks = await store.allAttachments();
18
18
  const attachmentMap = await generateAttachmentsData(writer, attachmentLinks, (id) => store.attachmentContentById(id));
19
+ const [globalErrors, globalAttachments] = await Promise.all([
20
+ store.allGlobalErrors(),
21
+ store.allGlobalAttachments(),
22
+ ]);
23
+ const globalAttachmentTimestamps = (await store.metadataByKey("allure2_global_attachment_timestamps")) ?? {};
24
+ const globalErrorTimestamps = (await store.metadataByKey("allure2_global_error_timestamps")) ?? [];
25
+ await generateGlobalsData(writer, globalErrors, globalAttachments, attachmentMap, globalAttachmentTimestamps, globalErrorTimestamps);
19
26
  const categories = (await store.metadataByKey("allure2_categories")) ?? [];
27
+ const legacyHistory = (await store.metadataByKey("allure2_history")) ?? {};
20
28
  const environmentItems = (await store.metadataByKey("allure_environment")) ?? [];
21
29
  const tests = await store.allTestResults({ includeRetries: true });
22
30
  const related = await store.relatedByTestResultIds(tests.map(({ id }) => id));
@@ -31,6 +39,7 @@ export class Allure2Plugin {
31
39
  categories,
32
40
  retries,
33
41
  history,
42
+ legacyHistory: value.historyId ? legacyHistory[value.historyId] : undefined,
34
43
  }, value);
35
44
  allTr.push(allure2TestResult);
36
45
  }
@@ -51,10 +60,16 @@ export class Allure2Plugin {
51
60
  await generateEnvironmentJson(writer, environmentItems);
52
61
  const executor = await store.metadataByKey("allure2_executor");
53
62
  const historyDataPoints = await store.allHistoryDataPoints();
63
+ const historyTrend = (await store.metadataByKey("allure2_history_trend")) ?? [];
64
+ const durationTrend = (await store.metadataByKey("allure2_duration_trend")) ?? [];
65
+ const retryTrend = (await store.metadataByKey("allure2_retry_trend")) ?? [];
66
+ const categoriesTrend = (await store.metadataByKey("allure2_categories_trend")) ?? [];
54
67
  await generateExecutorJson(writer, executor);
55
68
  await generateDefaultWidgetData(writer, displayedTr, "duration.json", "status-chart.json", "severity.json");
56
- await generateTrendData(writer, reportName, displayedTr, historyDataPoints);
57
- await generateEmptyTrendData(writer, "duration-trend.json", "categories-trend.json", "retry-trend.json");
69
+ await generateHistoryTrendData(writer, reportName, displayedTr, historyDataPoints, historyTrend, executor);
70
+ await generateDurationTrendData(writer, displayedTr, durationTrend, executor);
71
+ await generateRetryTrendData(writer, allTr, retryTrend, executor);
72
+ await generateCategoriesTrendData(writer, allTr, categoriesTrend, executor);
58
73
  const reportDataFiles = singleFile ? writer.reportFiles() : [];
59
74
  await generateStaticFiles({
60
75
  allureVersion: context.allureVersion,
Binary file
package/dist/tree.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface TreeLeaf extends TreeNode {
19
19
  newPassed: boolean;
20
20
  retriesCount: number;
21
21
  retriesStatusChange: boolean;
22
+ retry: boolean;
22
23
  tags: string[];
23
24
  parameters: string[];
24
25
  }
package/dist/tree.js CHANGED
@@ -15,6 +15,7 @@ const createLeaf = (endNode, test) => {
15
15
  newPassed: test.newPassed,
16
16
  retriesCount: test.retriesCount,
17
17
  retriesStatusChange: test.retriesStatusChange,
18
+ retry: test.retry,
18
19
  parameters: test.parameters.filter((p) => p.value).map((p) => p.value),
19
20
  tags: test.extra.tags ?? [],
20
21
  };
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Statistic } from "@allurereport/core-api";
2
2
  import type { Allure2Status, Allure2TestResult, GroupTime } from "./model.js";
3
3
  import type { TreeGroup } from "./tree.js";
4
+ export declare const createStatistic: (source?: Partial<Statistic>) => Statistic;
4
5
  export declare const updateStatistic: (statistic: Statistic, test: {
5
6
  status: Allure2Status;
6
7
  }) => undefined;
package/dist/utils.js CHANGED
@@ -1,4 +1,14 @@
1
1
  import { statisticKeys } from "./model.js";
2
+ export const createStatistic = (source = {}) => ({
3
+ failed: 0,
4
+ broken: 0,
5
+ skipped: 0,
6
+ passed: 0,
7
+ unknown: 0,
8
+ ...source,
9
+ total: source.total ??
10
+ (source.failed ?? 0) + (source.broken ?? 0) + (source.skipped ?? 0) + (source.passed ?? 0) + (source.unknown ?? 0),
11
+ });
2
12
  export const updateStatistic = (statistic, test) => {
3
13
  statistic[test.status] = (statistic[test.status] ?? 0) + 1;
4
14
  statistic.total = (statistic.total ?? 0) + 1;
@@ -22,7 +32,7 @@ export const updateTime = (time, test) => {
22
32
  }
23
33
  };
24
34
  export const calculateStatisticByLeafs = (treeGroup) => {
25
- const current = { total: 0 };
35
+ const current = createStatistic();
26
36
  treeGroup.children.forEach((child) => {
27
37
  if ("children" in child) {
28
38
  const childGroup = child;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@allurereport/plugin-allure2",
3
- "version": "3.15.0",
4
- "description": "The classic version of Allure HTML report",
3
+ "version": "3.16.1",
4
+ "description": "The Allure 2-compatible HTML report",
5
5
  "keywords": [
6
6
  "allure",
7
7
  "html",
@@ -23,19 +23,19 @@
23
23
  ".": "./dist/index.js"
24
24
  },
25
25
  "scripts": {
26
- "build": "run clean && tsc --project ./tsconfig.json",
26
+ "build": "run clean && tsc --project ./tsconfig.json && node ../../scripts/pack-report-static.mjs ../web-allure2/dist/multi ./dist/static/report.tar",
27
27
  "clean": "rimraf ./dist",
28
28
  "test": "rimraf ./out && vitest run",
29
29
  "lint": "oxlint --import-plugin src test features stories",
30
30
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
31
31
  },
32
32
  "dependencies": {
33
- "@allurereport/core-api": "3.15.0",
34
- "@allurereport/plugin-api": "3.15.0",
35
- "find-up": "^8.0.0",
33
+ "@allurereport/core-api": "3.16.1",
34
+ "@allurereport/plugin-api": "3.16.1",
36
35
  "handlebars": "^4.7.9"
37
36
  },
38
37
  "devDependencies": {
38
+ "@allurereport/web-allure2": "3.16.1",
39
39
  "@types/node": "^20",
40
40
  "@vitest/runner": "^2",
41
41
  "allure-js-commons": "^3",