@allurereport/plugin-dashboard 3.0.0-beta.12

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 ADDED
@@ -0,0 +1,103 @@
1
+ # Dashboard 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 dashboard with trend graphs for Allure reports, allowing you to track test execution statistics over time.
16
+
17
+ ## Install
18
+
19
+ Use your favorite package manager to install the package:
20
+
21
+ ```shell
22
+ npm add @allurereport/plugin-dashboard
23
+ yarn add @allurereport/plugin-dashboard
24
+ pnpm add @allurereport/plugin-dashboard
25
+ ```
26
+
27
+ Then, add the plugin to the Allure configuration file:
28
+
29
+ ```typescript
30
+ import { defineConfig } from "allure";
31
+
32
+ export default defineConfig({
33
+ plugins: {
34
+ dashboard: {
35
+ options: {
36
+ singleFile: false,
37
+ reportName: "My Dashboard",
38
+ reportLanguage: "en",
39
+ layout: [
40
+ {
41
+ type: "trend",
42
+ dataType: "status",
43
+ mode: "percent"
44
+ },
45
+ {
46
+ type: "pie",
47
+ title: "Test Results"
48
+ }
49
+ ]
50
+ }
51
+ }
52
+ }
53
+ });
54
+ ```
55
+
56
+ ## Features
57
+
58
+ ### Available Widgets
59
+
60
+ #### Trend Charts
61
+
62
+ Trend charts allow you to track metrics over time. Available configurations:
63
+
64
+ ```typescript
65
+ {
66
+ type: "trend",
67
+ dataType: "status",
68
+ mode: "percent", // optional, default: "raw"
69
+ limit: 10, // optional: limit number of builds
70
+ title: "Custom Status Trend", // optional
71
+ metadata: { // optional
72
+ executionIdAccessor: (executionOrder) => `build-${executionOrder}`,
73
+ executionNameAccessor: (executionOrder) => `build #${executionOrder}`
74
+ }
75
+ }
76
+ ```
77
+
78
+ #### Pie Charts
79
+
80
+ Pie charts show distribution of test results:
81
+
82
+ ```typescript
83
+ {
84
+ type: "pie",
85
+ title: "Custom Pie" // optional
86
+ }
87
+ ```
88
+
89
+ ## Options
90
+
91
+ The plugin accepts the following options:
92
+
93
+ | Option | Description | Type | Default |
94
+ |------------------|-------------------------------------------------|--------------------------------------------------------------|-----------------|
95
+ | `reportName` | Name of the report | `string` | `Allure Report` |
96
+ | `singleFile` | Writes the report as a single `index.html` file | `boolean` | `false` |
97
+ | `logo` | Path to the logo image | `string` | `null` |
98
+ | `theme` | Default color theme of the report | `light \| dark` | OS theme |
99
+ | `reportLanguage` | Default language of the report | `string` | OS language |
100
+
101
+ ## License
102
+
103
+ Apache-2.0
@@ -0,0 +1,3 @@
1
+ import type { HistoryDataPoint, TestResult } from "@allurereport/core-api";
2
+ import type { SeverityTrendChartData, TrendChartOptions } from "../model.js";
3
+ export declare const getSeverityTrendData: (testResults: TestResult[], reportName: string, historyPoints: HistoryDataPoint[], chartOptions: TrendChartOptions) => SeverityTrendChartData;
@@ -0,0 +1,46 @@
1
+ import { severityLabelName, severityLevels } from "@allurereport/core-api";
2
+ import { createEmptySeries, createEmptyStats, getTrendDataGeneric, mergeTrendDataGeneric, normalizeStatistic, } from "../utils/trend.js";
3
+ export const getSeverityTrendData = (testResults, reportName, historyPoints, chartOptions) => {
4
+ const { limit } = chartOptions;
5
+ const historyLimit = limit && limit > 0 ? Math.max(0, limit - 1) : undefined;
6
+ const limitedHistoryPoints = historyLimit !== undefined ? historyPoints.slice(-historyLimit) : historyPoints;
7
+ const firstOriginalIndex = historyLimit !== undefined ? Math.max(0, historyPoints.length - historyLimit) : 0;
8
+ const convertedHistoryPoints = limitedHistoryPoints.map((point, index) => {
9
+ const originalIndex = firstOriginalIndex + index;
10
+ return {
11
+ name: point.name,
12
+ originalIndex,
13
+ statistic: Object.values(point.testResults).reduce((stat, test) => {
14
+ const severityLabel = test.labels?.find((label) => label.name === severityLabelName);
15
+ const severity = severityLabel?.value?.toLowerCase();
16
+ if (severity) {
17
+ stat[severity] = (stat[severity] ?? 0) + 1;
18
+ }
19
+ return stat;
20
+ }, createEmptyStats(severityLevels)),
21
+ };
22
+ });
23
+ const currentSeverityStats = testResults.reduce((acc, test) => {
24
+ const severityLabel = test.labels.find((label) => label.name === severityLabelName);
25
+ const severity = severityLabel?.value?.toLowerCase();
26
+ if (severity) {
27
+ acc[severity] = (acc[severity] ?? 0) + 1;
28
+ }
29
+ return acc;
30
+ }, createEmptyStats(severityLevels));
31
+ const currentTrendData = getTrendDataGeneric(normalizeStatistic(currentSeverityStats, severityLevels), reportName, historyPoints.length + 1, severityLevels, chartOptions);
32
+ const historicalTrendData = convertedHistoryPoints.reduce((acc, historyPoint) => {
33
+ const trendDataPart = getTrendDataGeneric(normalizeStatistic(historyPoint.statistic, severityLevels), historyPoint.name, historyPoint.originalIndex + 1, severityLevels, chartOptions);
34
+ return mergeTrendDataGeneric(acc, trendDataPart, severityLevels);
35
+ }, {
36
+ type: chartOptions.type,
37
+ dataType: chartOptions.dataType,
38
+ title: chartOptions.title,
39
+ points: {},
40
+ slices: {},
41
+ series: createEmptySeries(severityLevels),
42
+ min: Infinity,
43
+ max: -Infinity,
44
+ });
45
+ return mergeTrendDataGeneric(historicalTrendData, currentTrendData, severityLevels);
46
+ };
@@ -0,0 +1,9 @@
1
+ import type { Statistic } from "@allurereport/core-api";
2
+ import type { PieArcDatum } from "d3-shape";
3
+ import { type PieChartData, type PieChartOptions, type PieSlice } from "../model.js";
4
+ type BasePieSlice = Pick<PieSlice, "status" | "count">;
5
+ export declare const d3Arc: import("d3-shape").Arc<any, PieArcDatum<BasePieSlice>>;
6
+ export declare const d3Pie: import("d3-shape").Pie<any, BasePieSlice>;
7
+ export declare const getPercentage: (value: number, total: number) => number;
8
+ export declare const getPieChartData: (stats: Statistic, chartOptions: PieChartOptions) => PieChartData;
9
+ export {};
@@ -0,0 +1,28 @@
1
+ import { statusesList } from "@allurereport/core-api";
2
+ import { arc, pie } from "d3-shape";
3
+ export const d3Arc = arc().innerRadius(40).outerRadius(50).cornerRadius(2).padAngle(0.03);
4
+ export const d3Pie = pie()
5
+ .value((d) => d.count)
6
+ .padAngle(0.03)
7
+ .sortValues((a, b) => a - b);
8
+ export const getPercentage = (value, total) => Math.floor((value / total) * 10000) / 100;
9
+ export const getPieChartData = (stats, chartOptions) => {
10
+ const convertedStatuses = statusesList
11
+ .filter((status) => !!stats?.[status])
12
+ .map((status) => ({
13
+ status,
14
+ count: stats[status],
15
+ }));
16
+ const arcsData = d3Pie(convertedStatuses);
17
+ const slices = arcsData.map((arcData) => ({
18
+ d: d3Arc(arcData),
19
+ ...arcData.data,
20
+ }));
21
+ const percentage = getPercentage(stats.passed ?? 0, stats.total);
22
+ return {
23
+ type: chartOptions.type,
24
+ title: chartOptions?.title,
25
+ slices,
26
+ percentage,
27
+ };
28
+ };
@@ -0,0 +1,3 @@
1
+ import type { HistoryDataPoint, Statistic } from "@allurereport/core-api";
2
+ import type { StatusTrendChartData, TrendChartOptions } from "../model.js";
3
+ export declare const getStatusTrendData: (currentStatistic: Statistic, reportName: string, historyPoints: HistoryDataPoint[], chartOptions: TrendChartOptions) => StatusTrendChartData;
@@ -0,0 +1,37 @@
1
+ import { statusesList } from "@allurereport/core-api";
2
+ import { createEmptySeries, createEmptyStats, getTrendDataGeneric, mergeTrendDataGeneric, normalizeStatistic, } from "../utils/trend.js";
3
+ export const getStatusTrendData = (currentStatistic, reportName, historyPoints, chartOptions) => {
4
+ const { limit } = chartOptions;
5
+ const historyLimit = limit && limit > 0 ? Math.max(0, limit - 1) : undefined;
6
+ const limitedHistoryPoints = historyLimit !== undefined ? historyPoints.slice(-historyLimit) : historyPoints;
7
+ const firstOriginalIndex = historyLimit !== undefined ? Math.max(0, historyPoints.length - historyLimit) : 0;
8
+ const convertedHistoryPoints = limitedHistoryPoints.map((point, index) => {
9
+ const originalIndex = firstOriginalIndex + index;
10
+ return {
11
+ name: point.name,
12
+ originalIndex,
13
+ statistic: Object.values(point.testResults).reduce((stat, test) => {
14
+ if (test.status) {
15
+ stat[test.status] = (stat[test.status] ?? 0) + 1;
16
+ stat.total = (stat.total ?? 0) + 1;
17
+ }
18
+ return stat;
19
+ }, { total: 0, ...createEmptyStats(statusesList) }),
20
+ };
21
+ });
22
+ const currentTrendData = getTrendDataGeneric(normalizeStatistic(currentStatistic, statusesList), reportName, historyPoints.length + 1, statusesList, chartOptions);
23
+ const historicalTrendData = convertedHistoryPoints.reduce((acc, historyPoint) => {
24
+ const trendDataPart = getTrendDataGeneric(normalizeStatistic(historyPoint.statistic, statusesList), historyPoint.name, historyPoint.originalIndex + 1, statusesList, chartOptions);
25
+ return mergeTrendDataGeneric(acc, trendDataPart, statusesList);
26
+ }, {
27
+ type: chartOptions.type,
28
+ dataType: chartOptions.dataType,
29
+ title: chartOptions.title,
30
+ points: {},
31
+ slices: {},
32
+ series: createEmptySeries(statusesList),
33
+ min: Infinity,
34
+ max: -Infinity,
35
+ });
36
+ return mergeTrendDataGeneric(historicalTrendData, currentTrendData, statusesList);
37
+ };
@@ -0,0 +1,13 @@
1
+ import type { AllureStore, PluginContext, ReportFiles } from "@allurereport/plugin-api";
2
+ import type { DashboardOptions, DashboardPluginOptions, GeneratedChartsData, TemplateManifest } from "./model.js";
3
+ import type { DashboardDataWriter, ReportFile } from "./writer.js";
4
+ export declare const readTemplateManifest: (singleFileMode?: boolean) => Promise<TemplateManifest>;
5
+ export declare const generateCharts: (options: DashboardPluginOptions, store: AllureStore, context: PluginContext) => Promise<GeneratedChartsData | undefined>;
6
+ export declare const generateAllCharts: (writer: DashboardDataWriter, store: AllureStore, options: DashboardPluginOptions, context: PluginContext) => Promise<void>;
7
+ export declare const generateStaticFiles: (payload: DashboardOptions & {
8
+ allureVersion: string;
9
+ reportFiles: ReportFiles;
10
+ reportDataFiles: ReportFile[];
11
+ reportUuid: string;
12
+ reportName: string;
13
+ }) => Promise<void>;
@@ -0,0 +1,153 @@
1
+ import { createBaseUrlScript, createFontLinkTag, createReportDataScript, createScriptTag, createStylesLinkTag, } from "@allurereport/web-commons";
2
+ import { randomUUID } from "crypto";
3
+ import Handlebars from "handlebars";
4
+ import { readFile } from "node:fs/promises";
5
+ import { createRequire } from "node:module";
6
+ import { basename, join } from "node:path";
7
+ import { getSeverityTrendData } from "./charts/severityTrend.js";
8
+ import { getPieChartData } from "./charts/statusPie.js";
9
+ import { getStatusTrendData } from "./charts/statusTrend.js";
10
+ import { ChartData, ChartType } from "./model.js";
11
+ const require = createRequire(import.meta.url);
12
+ const template = `<!DOCTYPE html>
13
+ <html dir="ltr" lang="en">
14
+ <head>
15
+ <meta charset="utf-8">
16
+ <title> {{ reportName }} </title>
17
+ <link rel="icon" href="favicon.ico">
18
+ {{{ headTags }}}
19
+ </head>
20
+ <body>
21
+ <div id="app"></div>
22
+ ${createBaseUrlScript()}
23
+ <script>
24
+ window.allure = window.allure || {};
25
+ </script>
26
+ {{{ bodyTags }}}
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
+ "report": "dashboard",
35
+ "allureVersion": "{{ allureVersion }}",
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-dashboard/dist/${singleFileMode ? "single" : "multi"}/manifest.json`);
50
+ const templateManifest = await readFile(templateManifestSource, { encoding: "utf-8" });
51
+ return JSON.parse(templateManifest);
52
+ };
53
+ const generateTrendChart = (options, stores, context) => {
54
+ const { dataType } = options;
55
+ const { statistic, historyDataPoints, testResults } = stores;
56
+ if (dataType === ChartData.Status) {
57
+ return getStatusTrendData(statistic, context.reportName, historyDataPoints, options);
58
+ }
59
+ else if (dataType === ChartData.Severity) {
60
+ return getSeverityTrendData(testResults, context.reportName, historyDataPoints, options);
61
+ }
62
+ };
63
+ const generatePieChart = (options, stores) => {
64
+ const { statistic } = stores;
65
+ return getPieChartData(statistic, options);
66
+ };
67
+ export const generateCharts = async (options, store, context) => {
68
+ const { layout } = options;
69
+ if (!layout) {
70
+ return undefined;
71
+ }
72
+ const historyDataPoints = await store.allHistoryDataPoints();
73
+ const statistic = await store.testsStatistic();
74
+ const testResults = await store.allTestResults();
75
+ return layout.reduce((acc, chartOptions) => {
76
+ const chartId = randomUUID();
77
+ let chart;
78
+ if (chartOptions.type === ChartType.Trend) {
79
+ chart = generateTrendChart(chartOptions, {
80
+ historyDataPoints,
81
+ statistic,
82
+ testResults,
83
+ }, context);
84
+ }
85
+ else if (chartOptions.type === ChartType.Pie) {
86
+ chart = generatePieChart(chartOptions, { statistic });
87
+ }
88
+ if (chart) {
89
+ acc[chartId] = chart;
90
+ }
91
+ return acc;
92
+ }, {});
93
+ };
94
+ export const generateAllCharts = async (writer, store, options, context) => {
95
+ const charts = await generateCharts(options, store, context);
96
+ if (charts && Object.keys(charts).length > 0) {
97
+ await writer.writeWidget("charts.json", charts);
98
+ }
99
+ };
100
+ export const generateStaticFiles = async (payload) => {
101
+ const { reportName = "Allure Report", reportLanguage = "en", singleFile, logo = "", theme = "light", reportFiles, reportDataFiles, reportUuid, allureVersion, } = payload;
102
+ const compile = Handlebars.compile(template);
103
+ const manifest = await readTemplateManifest(payload.singleFile);
104
+ const headTags = [];
105
+ const bodyTags = [];
106
+ if (!payload.singleFile) {
107
+ for (const key in manifest) {
108
+ const fileName = manifest[key];
109
+ const filePath = require.resolve(join("@allurereport/web-dashboard/dist", singleFile ? "single" : "multi", fileName));
110
+ if (key.includes(".woff")) {
111
+ headTags.push(createFontLinkTag(fileName));
112
+ }
113
+ if (key === "main.css") {
114
+ headTags.push(createStylesLinkTag(fileName));
115
+ }
116
+ if (key === "main.js") {
117
+ bodyTags.push(createScriptTag(fileName));
118
+ }
119
+ if (singleFile) {
120
+ continue;
121
+ }
122
+ const fileContent = await readFile(filePath);
123
+ await reportFiles.addFile(basename(filePath), fileContent);
124
+ }
125
+ }
126
+ else {
127
+ const mainJs = manifest["main.js"];
128
+ const mainJsSource = require.resolve(`@allurereport/web-dashboard/dist/single/${mainJs}`);
129
+ const mainJsContentBuffer = await readFile(mainJsSource);
130
+ bodyTags.push(createScriptTag(`data:text/javascript;base64,${mainJsContentBuffer.toString("base64")}`));
131
+ }
132
+ const reportOptions = {
133
+ reportName,
134
+ logo,
135
+ theme,
136
+ reportLanguage,
137
+ createdAt: Date.now(),
138
+ reportUuid,
139
+ allureVersion,
140
+ };
141
+ const html = compile({
142
+ headTags: headTags.join("\n"),
143
+ bodyTags: bodyTags.join("\n"),
144
+ reportFilesScript: createReportDataScript(reportDataFiles),
145
+ reportOptions: JSON.stringify(reportOptions),
146
+ analyticsEnable: true,
147
+ allureVersion,
148
+ reportUuid,
149
+ reportName,
150
+ singleFile: payload.singleFile,
151
+ });
152
+ await reportFiles.addFile("index.html", Buffer.from(html, "utf8"));
153
+ };
@@ -0,0 +1 @@
1
+ export { DashboardPlugin as default } from "./plugin.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { DashboardPlugin as default } from "./plugin.js";
@@ -0,0 +1,92 @@
1
+ import type { SeverityLevel, TestResult, TestStatus } from "@allurereport/core-api";
2
+ export declare enum ChartType {
3
+ Trend = "trend",
4
+ Pie = "pie"
5
+ }
6
+ export declare enum ChartData {
7
+ Status = "status",
8
+ Severity = "severity"
9
+ }
10
+ export type ChartMode = "raw" | "percent";
11
+ export type ChartId = string;
12
+ export type ExecutionIdFn = (executionOrder: number) => string;
13
+ export type ExecutionNameFn = (executionOrder: number) => string;
14
+ export type TrendMetadataFnOverrides = {
15
+ executionIdAccessor?: ExecutionIdFn;
16
+ executionNameAccessor?: ExecutionNameFn;
17
+ };
18
+ export type TrendChartOptions = {
19
+ type: ChartType.Trend;
20
+ dataType: ChartData;
21
+ mode?: ChartMode;
22
+ title?: string;
23
+ limit?: number;
24
+ metadata?: TrendMetadataFnOverrides;
25
+ };
26
+ export type TrendPointId = string;
27
+ export type TrendSliceId = string;
28
+ export type BaseMetadata = Record<string, unknown>;
29
+ export interface BaseTrendSliceMetadata extends Record<string, unknown> {
30
+ executionId: string;
31
+ executionName: string;
32
+ }
33
+ export type TrendSliceMetadata<Metadata extends BaseMetadata> = BaseTrendSliceMetadata & Metadata;
34
+ export type TrendPoint = {
35
+ x: string;
36
+ y: number;
37
+ };
38
+ export type TrendSlice<Metadata extends BaseMetadata> = {
39
+ min: number;
40
+ max: number;
41
+ metadata: TrendSliceMetadata<Metadata>;
42
+ };
43
+ export type GenericTrendChartData<Metadata extends BaseMetadata, SeriesType extends string> = {
44
+ type: ChartType.Trend;
45
+ dataType: ChartData;
46
+ title?: string;
47
+ points: Record<TrendPointId, TrendPoint>;
48
+ slices: Record<TrendSliceId, TrendSlice<Metadata>>;
49
+ series: Record<SeriesType, TrendPointId[]>;
50
+ min: number;
51
+ max: number;
52
+ };
53
+ export interface StatusMetadata extends BaseTrendSliceMetadata {
54
+ }
55
+ export type StatusTrendSliceMetadata = TrendSliceMetadata<StatusMetadata>;
56
+ export type StatusTrendSlice = TrendSlice<StatusTrendSliceMetadata>;
57
+ export type StatusTrendChartData = GenericTrendChartData<StatusTrendSliceMetadata, TestStatus>;
58
+ export interface SeverityMetadata extends BaseTrendSliceMetadata {
59
+ }
60
+ export type SeverityTrendSliceMetadata = TrendSliceMetadata<SeverityMetadata>;
61
+ export type SeverityTrendSlice = TrendSlice<SeverityTrendSliceMetadata>;
62
+ export type SeverityTrendChartData = GenericTrendChartData<SeverityTrendSliceMetadata, SeverityLevel>;
63
+ export type TrendChartData = StatusTrendChartData | SeverityTrendChartData;
64
+ export type PieChartOptions = {
65
+ type: ChartType.Pie;
66
+ title?: string;
67
+ };
68
+ export type PieSlice = {
69
+ status: TestStatus;
70
+ count: number;
71
+ d: string | null;
72
+ };
73
+ export type PieChartData = {
74
+ type: ChartType.Pie;
75
+ title?: string;
76
+ slices: PieSlice[];
77
+ percentage: number;
78
+ };
79
+ export type GeneratedChartData = TrendChartData | PieChartData;
80
+ export type GeneratedChartsData = Record<ChartId, GeneratedChartData>;
81
+ export type ChartOptions = TrendChartOptions | PieChartOptions;
82
+ export type DashboardOptions = {
83
+ reportName?: string;
84
+ singleFile?: boolean;
85
+ logo?: string;
86
+ theme?: "light" | "dark";
87
+ reportLanguage?: "en" | "ru";
88
+ layout?: ChartOptions[];
89
+ filter?: (testResult: TestResult) => boolean;
90
+ };
91
+ export type DashboardPluginOptions = DashboardOptions;
92
+ export type TemplateManifest = Record<string, string>;
package/dist/model.js ADDED
@@ -0,0 +1,10 @@
1
+ export var ChartType;
2
+ (function (ChartType) {
3
+ ChartType["Trend"] = "trend";
4
+ ChartType["Pie"] = "pie";
5
+ })(ChartType || (ChartType = {}));
6
+ export var ChartData;
7
+ (function (ChartData) {
8
+ ChartData["Status"] = "status";
9
+ ChartData["Severity"] = "severity";
10
+ })(ChartData || (ChartData = {}));
@@ -0,0 +1,11 @@
1
+ import type { AllureStore, Plugin, PluginContext, PluginSummary } from "@allurereport/plugin-api";
2
+ import type { DashboardPluginOptions } from "./model.js";
3
+ export declare class DashboardPlugin implements Plugin {
4
+ #private;
5
+ readonly options: DashboardPluginOptions;
6
+ constructor(options?: DashboardPluginOptions);
7
+ start: (context: PluginContext) => Promise<void>;
8
+ update: (context: PluginContext, store: AllureStore) => Promise<void>;
9
+ done: (context: PluginContext, store: AllureStore) => Promise<void>;
10
+ info(context: PluginContext, store: AllureStore): Promise<PluginSummary>;
11
+ }
package/dist/plugin.js ADDED
@@ -0,0 +1,65 @@
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 __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
7
+ if (kind === "m") throw new TypeError("Private method is not writable");
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
10
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
11
+ };
12
+ var _DashboardPlugin_writer, _DashboardPlugin_generate;
13
+ import { getWorstStatus } from "@allurereport/core-api";
14
+ import { generateAllCharts, generateStaticFiles } from "./generators.js";
15
+ import { InMemoryDashboardDataWriter, ReportFileDashboardDataWriter } from "./writer.js";
16
+ export class DashboardPlugin {
17
+ constructor(options = {}) {
18
+ this.options = options;
19
+ _DashboardPlugin_writer.set(this, void 0);
20
+ _DashboardPlugin_generate.set(this, async (context, store) => {
21
+ await generateAllCharts(__classPrivateFieldGet(this, _DashboardPlugin_writer, "f"), store, this.options, context);
22
+ const reportDataFiles = this.options.singleFile ? __classPrivateFieldGet(this, _DashboardPlugin_writer, "f").reportFiles() : [];
23
+ await generateStaticFiles({
24
+ ...this.options,
25
+ allureVersion: context.allureVersion,
26
+ reportFiles: context.reportFiles,
27
+ reportDataFiles,
28
+ reportUuid: context.reportUuid,
29
+ reportName: context.reportName,
30
+ });
31
+ });
32
+ this.start = async (context) => {
33
+ if (this.options.singleFile) {
34
+ __classPrivateFieldSet(this, _DashboardPlugin_writer, new InMemoryDashboardDataWriter(), "f");
35
+ }
36
+ else {
37
+ __classPrivateFieldSet(this, _DashboardPlugin_writer, new ReportFileDashboardDataWriter(context.reportFiles), "f");
38
+ }
39
+ };
40
+ this.update = async (context, store) => {
41
+ if (!__classPrivateFieldGet(this, _DashboardPlugin_writer, "f")) {
42
+ throw new Error("call start first");
43
+ }
44
+ await __classPrivateFieldGet(this, _DashboardPlugin_generate, "f").call(this, context, store);
45
+ };
46
+ this.done = async (context, store) => {
47
+ if (!__classPrivateFieldGet(this, _DashboardPlugin_writer, "f")) {
48
+ throw new Error("call start first");
49
+ }
50
+ await __classPrivateFieldGet(this, _DashboardPlugin_generate, "f").call(this, context, store);
51
+ };
52
+ }
53
+ async info(context, store) {
54
+ const allTrs = (await store.allTestResults()).filter(this.options.filter ? this.options.filter : () => true);
55
+ const duration = allTrs.reduce((acc, { duration: trDuration = 0 }) => acc + trDuration, 0);
56
+ const worstStatus = getWorstStatus(allTrs.map(({ status }) => status));
57
+ return {
58
+ name: this.options.reportName || context.reportName,
59
+ stats: await store.testsStatistic(this.options.filter),
60
+ status: worstStatus ?? "passed",
61
+ duration,
62
+ };
63
+ }
64
+ }
65
+ _DashboardPlugin_writer = new WeakMap(), _DashboardPlugin_generate = new WeakMap();
@@ -0,0 +1,9 @@
1
+ import type { SeverityLevel, TestStatus } from "@allurereport/core-api";
2
+ import type { BaseTrendSliceMetadata, GenericTrendChartData, TrendChartOptions } from "../model.js";
3
+ type TrendDataType = TestStatus | SeverityLevel;
4
+ export declare const createEmptyStats: <T extends TrendDataType>(items: readonly T[]) => Record<T, number>;
5
+ export declare const createEmptySeries: <T extends TrendDataType>(items: readonly T[]) => Record<T, string[]>;
6
+ export declare const normalizeStatistic: <T extends TrendDataType>(statistic: Partial<Record<T, number>>, itemType: readonly T[]) => Record<T, number>;
7
+ export declare const mergeTrendDataGeneric: <M extends BaseTrendSliceMetadata, T extends TrendDataType>(trendData: GenericTrendChartData<M, T>, trendDataPart: GenericTrendChartData<M, T>, itemType: readonly T[]) => GenericTrendChartData<M, T>;
8
+ export declare const getTrendDataGeneric: <M extends BaseTrendSliceMetadata, T extends TrendDataType>(stats: Record<T, number>, reportName: string, executionOrder: number, itemType: readonly T[], chartOptions: TrendChartOptions) => GenericTrendChartData<M, T>;
9
+ export {};
@@ -0,0 +1,100 @@
1
+ export const createEmptyStats = (items) => items.reduce((acc, item) => ({ ...acc, [item]: 0 }), {});
2
+ export const createEmptySeries = (items) => items.reduce((acc, item) => ({ ...acc, [item]: [] }), {});
3
+ export const normalizeStatistic = (statistic, itemType) => {
4
+ return itemType.reduce((acc, item) => {
5
+ acc[item] = statistic[item] ?? 0;
6
+ return acc;
7
+ }, {});
8
+ };
9
+ const calculateRawValues = (stats, executionId, itemType) => {
10
+ const points = {};
11
+ const series = createEmptySeries(itemType);
12
+ itemType.forEach((item) => {
13
+ const pointId = `${executionId}-${item}`;
14
+ const value = stats[item] ?? 0;
15
+ points[pointId] = {
16
+ x: executionId,
17
+ y: value,
18
+ };
19
+ series[item].push(pointId);
20
+ });
21
+ return { points, series };
22
+ };
23
+ const calculatePercentValues = (stats, executionId, itemType) => {
24
+ const points = {};
25
+ const series = createEmptySeries(itemType);
26
+ const values = Object.values(stats);
27
+ const total = values.reduce((sum, value) => sum + value, 0);
28
+ if (total === 0) {
29
+ return { points, series };
30
+ }
31
+ itemType.forEach((item) => {
32
+ const pointId = `${executionId}-${item}`;
33
+ const value = stats[item] ?? 0;
34
+ points[pointId] = {
35
+ x: executionId,
36
+ y: (value / total) * 100,
37
+ };
38
+ series[item].push(pointId);
39
+ });
40
+ return { points, series };
41
+ };
42
+ export const mergeTrendDataGeneric = (trendData, trendDataPart, itemType) => {
43
+ return {
44
+ ...trendData,
45
+ points: {
46
+ ...trendData.points,
47
+ ...trendDataPart.points,
48
+ },
49
+ slices: {
50
+ ...trendData.slices,
51
+ ...trendDataPart.slices,
52
+ },
53
+ series: Object.entries(trendDataPart.series).reduce((series, [group, pointIds]) => {
54
+ if (Array.isArray(pointIds)) {
55
+ return {
56
+ ...series,
57
+ [group]: [...(trendData.series?.[group] || []), ...pointIds],
58
+ };
59
+ }
60
+ return series;
61
+ }, trendData.series || createEmptySeries(itemType)),
62
+ min: Math.min(trendData.min ?? Infinity, trendDataPart.min),
63
+ max: Math.max(trendData.max ?? -Infinity, trendDataPart.max),
64
+ };
65
+ };
66
+ export const getTrendDataGeneric = (stats, reportName, executionOrder, itemType, chartOptions) => {
67
+ const { type, dataType, title, mode = "raw", metadata = {} } = chartOptions;
68
+ const { executionIdAccessor, executionNameAccessor } = metadata;
69
+ const executionId = executionIdAccessor ? executionIdAccessor(executionOrder) : `execution-${executionOrder}`;
70
+ const { points, series } = mode === "percent"
71
+ ? calculatePercentValues(stats, executionId, itemType)
72
+ : calculateRawValues(stats, executionId, itemType);
73
+ const slices = {};
74
+ const pointsAsArray = Object.values(points);
75
+ const pointsCount = pointsAsArray.length;
76
+ const values = pointsAsArray.map((point) => point.y);
77
+ const min = pointsCount ? Math.min(...values) : 0;
78
+ const max = pointsCount ? Math.max(...values) : 0;
79
+ if (pointsCount > 0) {
80
+ const executionName = executionNameAccessor ? executionNameAccessor(executionOrder) : reportName;
81
+ slices[executionId] = {
82
+ min,
83
+ max,
84
+ metadata: {
85
+ executionId,
86
+ executionName,
87
+ },
88
+ };
89
+ }
90
+ return {
91
+ type,
92
+ dataType,
93
+ title,
94
+ points,
95
+ slices,
96
+ series,
97
+ min,
98
+ max,
99
+ };
100
+ };
@@ -0,0 +1,23 @@
1
+ import type { ReportFiles } from "@allurereport/plugin-api";
2
+ export type ReportFile = {
3
+ name: string;
4
+ value: string;
5
+ };
6
+ export interface DashboardDataWriter {
7
+ writeWidget<T>(fileName: string, data: T): Promise<void>;
8
+ }
9
+ export declare class FileSystemReportDataWriter implements DashboardDataWriter {
10
+ private readonly output;
11
+ constructor(output: string);
12
+ writeWidget<T>(fileName: string, data: T): Promise<void>;
13
+ }
14
+ export declare class InMemoryDashboardDataWriter implements DashboardDataWriter {
15
+ #private;
16
+ writeWidget<T>(fileName: string, data: T): Promise<void>;
17
+ reportFiles(): ReportFile[];
18
+ }
19
+ export declare class ReportFileDashboardDataWriter implements DashboardDataWriter {
20
+ readonly reportFiles: ReportFiles;
21
+ constructor(reportFiles: ReportFiles);
22
+ writeWidget<T>(fileName: string, data: T): Promise<void>;
23
+ }
package/dist/writer.js ADDED
@@ -0,0 +1,43 @@
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 _InMemoryDashboardDataWriter_data;
7
+ import { mkdir, writeFile } from "node:fs/promises";
8
+ import { resolve } from "node:path";
9
+ import { join as joinPosix } from "node:path/posix";
10
+ export class FileSystemReportDataWriter {
11
+ constructor(output) {
12
+ this.output = output;
13
+ }
14
+ async writeWidget(fileName, data) {
15
+ const distFolder = resolve(this.output, "widgets");
16
+ await mkdir(distFolder, { recursive: true });
17
+ await writeFile(resolve(distFolder, fileName), JSON.stringify(data), { encoding: "utf-8" });
18
+ }
19
+ }
20
+ export class InMemoryDashboardDataWriter {
21
+ constructor() {
22
+ _InMemoryDashboardDataWriter_data.set(this, {});
23
+ }
24
+ async writeWidget(fileName, data) {
25
+ const dist = joinPosix("widgets", fileName);
26
+ __classPrivateFieldGet(this, _InMemoryDashboardDataWriter_data, "f")[dist] = Buffer.from(JSON.stringify(data), "utf-8");
27
+ }
28
+ reportFiles() {
29
+ return Object.keys(__classPrivateFieldGet(this, _InMemoryDashboardDataWriter_data, "f")).map((key) => ({
30
+ name: key,
31
+ value: __classPrivateFieldGet(this, _InMemoryDashboardDataWriter_data, "f")[key].toString("base64"),
32
+ }));
33
+ }
34
+ }
35
+ _InMemoryDashboardDataWriter_data = new WeakMap();
36
+ export class ReportFileDashboardDataWriter {
37
+ constructor(reportFiles) {
38
+ this.reportFiles = reportFiles;
39
+ }
40
+ async writeWidget(fileName, data) {
41
+ await this.reportFiles.addFile(joinPosix("widgets", fileName), Buffer.from(JSON.stringify(data), "utf-8"));
42
+ }
43
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@allurereport/plugin-dashboard",
3
+ "version": "3.0.0-beta.12",
4
+ "description": "Allure Dashboard Plugin – plugin for generating dashboard with a mix of charts",
5
+ "keywords": [
6
+ "allure",
7
+ "testing",
8
+ "report",
9
+ "plugin",
10
+ "dashboard",
11
+ "charts",
12
+ "trends"
13
+ ],
14
+ "repository": "https://github.com/allure-framework/allure3",
15
+ "license": "Apache-2.0",
16
+ "author": "Qameta Software",
17
+ "type": "module",
18
+ "exports": {
19
+ ".": "./dist/index.js"
20
+ },
21
+ "main": "./dist/index.js",
22
+ "module": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "files": [
25
+ "./dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "run clean && tsc --project ./tsconfig.json",
29
+ "clean": "rimraf ./dist",
30
+ "eslint": "eslint ./src/**/*.{js,jsx,ts,tsx}",
31
+ "eslint:format": "eslint --fix ./src/**/*.{js,jsx,ts,tsx}",
32
+ "test": "rimraf ./out && vitest run"
33
+ },
34
+ "dependencies": {
35
+ "@allurereport/core-api": "3.0.0-beta.12",
36
+ "@allurereport/plugin-api": "3.0.0-beta.12",
37
+ "@allurereport/web-commons": "3.0.0-beta.12",
38
+ "@allurereport/web-dashboard": "3.0.0-beta.12",
39
+ "d3-shape": "^3.2.0",
40
+ "handlebars": "^4.7.8"
41
+ },
42
+ "devDependencies": {
43
+ "@stylistic/eslint-plugin": "^2.6.1",
44
+ "@types/d3-shape": "^3.1.6",
45
+ "@types/eslint": "^8.56.11",
46
+ "@types/node": "^20.17.9",
47
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
48
+ "@typescript-eslint/parser": "^8.0.0",
49
+ "@vitest/runner": "^2.1.8",
50
+ "allure-vitest": "^3.0.9",
51
+ "eslint": "^8.57.0",
52
+ "eslint-config-prettier": "^9.1.0",
53
+ "eslint-plugin-import": "^2.29.1",
54
+ "eslint-plugin-jsdoc": "^50.0.0",
55
+ "eslint-plugin-n": "^17.10.1",
56
+ "eslint-plugin-no-null": "^1.0.2",
57
+ "eslint-plugin-prefer-arrow": "^1.2.3",
58
+ "rimraf": "^6.0.1",
59
+ "typescript": "^5.6.3",
60
+ "vitest": "^2.1.8"
61
+ }
62
+ }