@allurereport/plugin-awesome 3.0.0-beta.16 → 3.0.0-beta.18
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 +9 -8
- package/dist/charts.d.ts +11 -105
- package/dist/charts.js +10 -136
- package/dist/converters.js +1 -0
- package/dist/generators.d.ts +15 -6
- package/dist/generators.js +104 -52
- package/dist/model.d.ts +3 -7
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +45 -11
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -47,11 +47,12 @@ export default defineConfig({
|
|
|
47
47
|
|
|
48
48
|
The plugin accepts the following options:
|
|
49
49
|
|
|
50
|
-
| Option | Description | Type
|
|
51
|
-
|
|
52
|
-
| `reportName` | Name of the report | `string`
|
|
53
|
-
| `singleFile` | Writes the report as a single `index.html` file | `boolean`
|
|
54
|
-
| `logo` | Path to the logo image | `string`
|
|
55
|
-
| `theme` | Default color theme of the report | `light \| dark`
|
|
56
|
-
| `reportLanguage` | Default language of the report | `string`
|
|
57
|
-
| `ci` | CI data which will be rendered in the report | `{ type: "github" \| "jenkins", url: string, name: string }` | `undefined`
|
|
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
|
+
| `logo` | Path to the logo image | `string` | `null` |
|
|
55
|
+
| `theme` | Default color theme of the report | `light \| dark` | OS theme |
|
|
56
|
+
| `reportLanguage` | Default language of the report | `string` | OS language |
|
|
57
|
+
| `ci` | CI data which will be rendered in the report | `{ type: "github" \| "jenkins", url: string, name: string }` | `undefined` |
|
|
58
|
+
| `groupBy` | By default, tests are grouped using the `titlePath` provided by the test framework. | `string`| Grouping by `titlepath` |
|
package/dist/charts.d.ts
CHANGED
|
@@ -1,108 +1,14 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
3
|
-
import { ChartDataType, ChartMode, ChartType } from "@allurereport/web-commons";
|
|
4
|
-
import type { PieArcDatum } from "d3-shape";
|
|
1
|
+
import { type HistoryDataPoint, type Statistic, type TestResult } from "@allurereport/core-api";
|
|
2
|
+
import { type GeneratedChartsData, type PluginContext } from "@allurereport/plugin-api";
|
|
5
3
|
import type { AwesomeOptions } from "./model.js";
|
|
6
4
|
import type { AwesomeDataWriter } from "./writer.js";
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
export declare const d3Pie: import("d3-shape").Pie<any, BasePieSlice>;
|
|
10
|
-
export declare const getPercentage: (value: number, total: number) => number;
|
|
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: ChartDataType;
|
|
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: ChartDataType;
|
|
46
|
-
mode: ChartMode;
|
|
47
|
-
title?: string;
|
|
48
|
-
points: Record<TrendPointId, TrendPoint>;
|
|
49
|
-
slices: Record<TrendSliceId, TrendSlice<Metadata>>;
|
|
50
|
-
series: Record<SeriesType, TrendPointId[]>;
|
|
51
|
-
min: number;
|
|
52
|
-
max: number;
|
|
53
|
-
};
|
|
54
|
-
export interface StatusMetadata extends BaseTrendSliceMetadata {
|
|
55
|
-
}
|
|
56
|
-
export type StatusTrendSliceMetadata = TrendSliceMetadata<StatusMetadata>;
|
|
57
|
-
export type StatusTrendSlice = TrendSlice<StatusTrendSliceMetadata>;
|
|
58
|
-
export type StatusTrendChartData = GenericTrendChartData<StatusTrendSliceMetadata, TestStatus>;
|
|
59
|
-
export interface SeverityMetadata extends BaseTrendSliceMetadata {
|
|
60
|
-
}
|
|
61
|
-
export type SeverityTrendSliceMetadata = TrendSliceMetadata<SeverityMetadata>;
|
|
62
|
-
export type SeverityTrendSlice = TrendSlice<SeverityTrendSliceMetadata>;
|
|
63
|
-
export type SeverityTrendChartData = GenericTrendChartData<SeverityTrendSliceMetadata, SeverityLevel>;
|
|
64
|
-
export type TrendChartData = StatusTrendChartData | SeverityTrendChartData;
|
|
65
|
-
export type PieChartOptions = {
|
|
66
|
-
type: ChartType.Pie;
|
|
67
|
-
title?: string;
|
|
68
|
-
};
|
|
69
|
-
export type PieSlice = {
|
|
70
|
-
status: TestStatus;
|
|
71
|
-
count: number;
|
|
72
|
-
d: string | null;
|
|
73
|
-
};
|
|
74
|
-
export type PieChartData = {
|
|
75
|
-
type: ChartType.Pie;
|
|
76
|
-
title?: string;
|
|
77
|
-
slices: PieSlice[];
|
|
78
|
-
percentage: number;
|
|
79
|
-
};
|
|
80
|
-
export type GeneratedChartData = TrendChartData | PieChartData;
|
|
81
|
-
export type GeneratedChartsData = Record<ChartId, GeneratedChartData>;
|
|
82
|
-
export type ChartOptions = TrendChartOptions | PieChartOptions;
|
|
83
|
-
export type DashboardOptions = {
|
|
84
|
-
reportName?: string;
|
|
85
|
-
singleFile?: boolean;
|
|
86
|
-
logo?: string;
|
|
87
|
-
theme?: "light" | "dark";
|
|
88
|
-
reportLanguage?: "en" | "ru";
|
|
89
|
-
layout?: ChartOptions[];
|
|
90
|
-
filter?: (testResult: TestResult) => boolean;
|
|
91
|
-
};
|
|
92
|
-
export type TrendDataType = TestStatus | SeverityLevel;
|
|
93
|
-
export type TrendCalculationResult<T extends TrendDataType> = {
|
|
94
|
-
points: Record<TrendPointId, TrendPoint>;
|
|
95
|
-
series: Record<T, TrendPointId[]>;
|
|
96
|
-
};
|
|
97
|
-
export declare const createEmptyStats: <T extends TrendDataType>(items: readonly T[]) => Record<T, number>;
|
|
98
|
-
export declare const createEmptySeries: <T extends TrendDataType>(items: readonly T[]) => Record<T, string[]>;
|
|
99
|
-
export declare const normalizeStatistic: <T extends TrendDataType>(statistic: Partial<Record<T, number>>, itemType: readonly T[]) => Record<T, number>;
|
|
100
|
-
export declare const mergeTrendDataGeneric: <M extends BaseTrendSliceMetadata, T extends TrendDataType>(trendData: GenericTrendChartData<M, T>, trendDataPart: GenericTrendChartData<M, T>, itemType: readonly T[]) => GenericTrendChartData<M, T>;
|
|
101
|
-
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>;
|
|
102
|
-
export declare const generateCharts: (options: AwesomeOptions, store: AllureStore, context: PluginContext) => Promise<GeneratedChartsData | undefined>;
|
|
103
|
-
export declare const generateTrendChart: (options: TrendChartOptions, stores: {
|
|
104
|
-
historyDataPoints: HistoryDataPoint[];
|
|
5
|
+
export declare const generateCharts: (options: AwesomeOptions, context: PluginContext, stores: {
|
|
6
|
+
trs: TestResult[];
|
|
105
7
|
statistic: Statistic;
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
export declare const generateAllCharts: (writer: AwesomeDataWriter,
|
|
8
|
+
history: HistoryDataPoint[];
|
|
9
|
+
}) => Promise<GeneratedChartsData | undefined>;
|
|
10
|
+
export declare const generateAllCharts: (writer: AwesomeDataWriter, options: AwesomeOptions, context: PluginContext, stores: {
|
|
11
|
+
trs: TestResult[];
|
|
12
|
+
statistic: Statistic;
|
|
13
|
+
history: HistoryDataPoint[];
|
|
14
|
+
}) => Promise<void>;
|
package/dist/charts.js
CHANGED
|
@@ -1,137 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ChartType } from "@allurereport/core-api";
|
|
2
|
+
import { generateComingSoonChart, generatePieChart, generateTrendChart, } from "@allurereport/plugin-api";
|
|
2
3
|
import { randomUUID } from "crypto";
|
|
3
|
-
|
|
4
|
-
export const d3Arc = arc().innerRadius(40).outerRadius(50).cornerRadius(2).padAngle(0.03);
|
|
5
|
-
export const d3Pie = pie()
|
|
6
|
-
.value((d) => d.count)
|
|
7
|
-
.padAngle(0.03)
|
|
8
|
-
.sortValues((a, b) => a - b);
|
|
9
|
-
export const getPercentage = (value, total) => Math.floor((value / total) * 10000) / 100;
|
|
10
|
-
export const createEmptyStats = (items) => items.reduce((acc, item) => ({ ...acc, [item]: 0 }), {});
|
|
11
|
-
export const createEmptySeries = (items) => items.reduce((acc, item) => ({ ...acc, [item]: [] }), {});
|
|
12
|
-
export const normalizeStatistic = (statistic, itemType) => {
|
|
13
|
-
return itemType.reduce((acc, item) => {
|
|
14
|
-
acc[item] = statistic[item] ?? 0;
|
|
15
|
-
return acc;
|
|
16
|
-
}, {});
|
|
17
|
-
};
|
|
18
|
-
const calculateRawValues = (stats, executionId, itemType) => {
|
|
19
|
-
const points = {};
|
|
20
|
-
const series = createEmptySeries(itemType);
|
|
21
|
-
itemType.forEach((item) => {
|
|
22
|
-
const pointId = `${executionId}-${item}`;
|
|
23
|
-
const value = stats[item] ?? 0;
|
|
24
|
-
points[pointId] = {
|
|
25
|
-
x: executionId,
|
|
26
|
-
y: value,
|
|
27
|
-
};
|
|
28
|
-
series[item].push(pointId);
|
|
29
|
-
});
|
|
30
|
-
return { points, series };
|
|
31
|
-
};
|
|
32
|
-
const calculatePercentValues = (stats, executionId, itemType) => {
|
|
33
|
-
const points = {};
|
|
34
|
-
const series = createEmptySeries(itemType);
|
|
35
|
-
const values = Object.values(stats);
|
|
36
|
-
const total = values.reduce((sum, value) => sum + value, 0);
|
|
37
|
-
if (total === 0) {
|
|
38
|
-
return { points, series };
|
|
39
|
-
}
|
|
40
|
-
itemType.forEach((item) => {
|
|
41
|
-
const pointId = `${executionId}-${item}`;
|
|
42
|
-
const value = stats[item] ?? 0;
|
|
43
|
-
points[pointId] = {
|
|
44
|
-
x: executionId,
|
|
45
|
-
y: value / total,
|
|
46
|
-
};
|
|
47
|
-
series[item].push(pointId);
|
|
48
|
-
});
|
|
49
|
-
return { points, series };
|
|
50
|
-
};
|
|
51
|
-
export const mergeTrendDataGeneric = (trendData, trendDataPart, itemType) => {
|
|
52
|
-
return {
|
|
53
|
-
...trendData,
|
|
54
|
-
points: {
|
|
55
|
-
...trendData.points,
|
|
56
|
-
...trendDataPart.points,
|
|
57
|
-
},
|
|
58
|
-
slices: {
|
|
59
|
-
...trendData.slices,
|
|
60
|
-
...trendDataPart.slices,
|
|
61
|
-
},
|
|
62
|
-
series: Object.entries(trendDataPart.series).reduce((series, [group, pointIds]) => {
|
|
63
|
-
if (Array.isArray(pointIds)) {
|
|
64
|
-
return {
|
|
65
|
-
...series,
|
|
66
|
-
[group]: [...(trendData.series?.[group] || []), ...pointIds],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return series;
|
|
70
|
-
}, trendData.series || createEmptySeries(itemType)),
|
|
71
|
-
min: Math.min(trendData.min ?? Infinity, trendDataPart.min),
|
|
72
|
-
max: Math.max(trendData.max ?? -Infinity, trendDataPart.max),
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
export const getTrendDataGeneric = (stats, reportName, executionOrder, itemType, chartOptions) => {
|
|
76
|
-
const { type, dataType, title, mode = ChartMode.Raw, metadata = {} } = chartOptions;
|
|
77
|
-
const { executionIdAccessor, executionNameAccessor } = metadata;
|
|
78
|
-
const executionId = executionIdAccessor ? executionIdAccessor(executionOrder) : `execution-${executionOrder}`;
|
|
79
|
-
const { points, series } = mode === ChartMode.Percent
|
|
80
|
-
? calculatePercentValues(stats, executionId, itemType)
|
|
81
|
-
: calculateRawValues(stats, executionId, itemType);
|
|
82
|
-
const slices = {};
|
|
83
|
-
const pointsAsArray = Object.values(points);
|
|
84
|
-
const pointsCount = pointsAsArray.length;
|
|
85
|
-
const values = pointsAsArray.map((point) => point.y);
|
|
86
|
-
const min = pointsCount ? Math.min(...values) : 0;
|
|
87
|
-
const max = pointsCount ? Math.max(...values) : 0;
|
|
88
|
-
if (pointsCount > 0) {
|
|
89
|
-
const executionName = executionNameAccessor ? executionNameAccessor(executionOrder) : reportName;
|
|
90
|
-
slices[executionId] = {
|
|
91
|
-
min,
|
|
92
|
-
max,
|
|
93
|
-
metadata: {
|
|
94
|
-
executionId,
|
|
95
|
-
executionName,
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
type,
|
|
101
|
-
dataType,
|
|
102
|
-
mode,
|
|
103
|
-
title,
|
|
104
|
-
points,
|
|
105
|
-
slices,
|
|
106
|
-
series,
|
|
107
|
-
min,
|
|
108
|
-
max,
|
|
109
|
-
};
|
|
110
|
-
};
|
|
111
|
-
const generatePieChart = (options, stores) => {
|
|
112
|
-
const { statistic } = stores;
|
|
113
|
-
return getPieChartDataDashboard(statistic, options);
|
|
114
|
-
};
|
|
115
|
-
export const generateCharts = async (options, store, context) => {
|
|
4
|
+
export const generateCharts = async (options, context, stores) => {
|
|
116
5
|
const { charts } = options;
|
|
117
6
|
if (!charts) {
|
|
118
7
|
return undefined;
|
|
119
8
|
}
|
|
120
|
-
const historyDataPoints = await store.allHistoryDataPoints();
|
|
121
|
-
const statistic = await store.testsStatistic();
|
|
122
|
-
const testResults = await store.allTestResults();
|
|
123
9
|
return charts.reduce((acc, chartOptions) => {
|
|
124
10
|
const chartId = randomUUID();
|
|
125
11
|
let chart;
|
|
126
12
|
if (chartOptions.type === ChartType.Trend) {
|
|
127
|
-
chart = generateTrendChart(chartOptions,
|
|
128
|
-
historyDataPoints,
|
|
129
|
-
statistic,
|
|
130
|
-
testResults,
|
|
131
|
-
}, context);
|
|
13
|
+
chart = generateTrendChart(chartOptions, stores, context);
|
|
132
14
|
}
|
|
133
15
|
else if (chartOptions.type === ChartType.Pie) {
|
|
134
|
-
chart = generatePieChart(chartOptions,
|
|
16
|
+
chart = generatePieChart(chartOptions, stores);
|
|
17
|
+
}
|
|
18
|
+
else if ([ChartType.HeatMap, ChartType.Bar, ChartType.Funnel, ChartType.TreeMap].includes(chartOptions.type)) {
|
|
19
|
+
chart = generateComingSoonChart(chartOptions);
|
|
135
20
|
}
|
|
136
21
|
if (chart) {
|
|
137
22
|
acc[chartId] = chart;
|
|
@@ -139,19 +24,8 @@ export const generateCharts = async (options, store, context) => {
|
|
|
139
24
|
return acc;
|
|
140
25
|
}, {});
|
|
141
26
|
};
|
|
142
|
-
export const
|
|
143
|
-
const
|
|
144
|
-
const { dataType } = newOptions;
|
|
145
|
-
const { statistic, historyDataPoints, testResults } = stores;
|
|
146
|
-
if (dataType === ChartDataType.Status) {
|
|
147
|
-
return getStatusTrendData(statistic, context.reportName, historyDataPoints, newOptions);
|
|
148
|
-
}
|
|
149
|
-
else if (dataType === ChartDataType.Severity) {
|
|
150
|
-
return getSeverityTrendData(testResults, context.reportName, historyDataPoints, newOptions);
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
export const generateAllCharts = async (writer, store, options, context) => {
|
|
154
|
-
const charts = await generateCharts(options, store, context);
|
|
27
|
+
export const generateAllCharts = async (writer, options, context, stores) => {
|
|
28
|
+
const charts = await generateCharts(options, context, stores);
|
|
155
29
|
if (charts && Object.keys(charts).length > 0) {
|
|
156
30
|
await writer.writeWidget("charts.json", charts);
|
|
157
31
|
}
|
package/dist/converters.js
CHANGED
package/dist/generators.d.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
import { type AttachmentLink, type EnvironmentItem } from "@allurereport/core-api";
|
|
2
|
-
import { type AllureStore, type ReportFiles, type ResultFile, type TestResultFilter } from "@allurereport/plugin-api";
|
|
1
|
+
import { type AttachmentLink, type EnvironmentItem, type Statistic, type TestEnvGroup, type TestError, type TestResult } from "@allurereport/core-api";
|
|
2
|
+
import { type AllureStore, type ExitCode, type ReportFiles, type ResultFile, type TestResultFilter } from "@allurereport/plugin-api";
|
|
3
3
|
import type { AwesomeTestResult } from "@allurereport/web-awesome";
|
|
4
4
|
import type { AwesomeOptions, TemplateManifest } from "./model.js";
|
|
5
5
|
import type { AwesomeDataWriter, ReportFile } from "./writer.js";
|
|
6
6
|
export declare const readTemplateManifest: (singleFileMode?: boolean) => Promise<TemplateManifest>;
|
|
7
|
-
export declare const generateTestResults: (writer: AwesomeDataWriter, store: AllureStore, filter?: TestResultFilter) => Promise<AwesomeTestResult[]>;
|
|
7
|
+
export declare const generateTestResults: (writer: AwesomeDataWriter, store: AllureStore, trs: TestResult[], filter?: TestResultFilter) => Promise<AwesomeTestResult[]>;
|
|
8
8
|
export declare const generateTestCases: (writer: AwesomeDataWriter, trs: AwesomeTestResult[]) => Promise<void>;
|
|
9
|
-
export declare const generateTestEnvGroups: (writer: AwesomeDataWriter,
|
|
9
|
+
export declare const generateTestEnvGroups: (writer: AwesomeDataWriter, groups: TestEnvGroup[]) => Promise<void>;
|
|
10
10
|
export declare const generateNav: (writer: AwesomeDataWriter, trs: AwesomeTestResult[], filename?: string) => Promise<void>;
|
|
11
11
|
export declare const generateTree: (writer: AwesomeDataWriter, treeFilename: string, labels: string[], tests: AwesomeTestResult[]) => Promise<void>;
|
|
12
12
|
export declare const generateEnvironmentJson: (writer: AwesomeDataWriter, env: EnvironmentItem[]) => Promise<void>;
|
|
13
13
|
export declare const generateEnvirontmentsList: (writer: AwesomeDataWriter, store: AllureStore) => Promise<void>;
|
|
14
14
|
export declare const generateVariables: (writer: AwesomeDataWriter, store: AllureStore) => Promise<void>;
|
|
15
|
-
export declare const generateStatistic: (writer: AwesomeDataWriter,
|
|
16
|
-
|
|
15
|
+
export declare const generateStatistic: (writer: AwesomeDataWriter, data: {
|
|
16
|
+
stats: Statistic;
|
|
17
|
+
statsByEnv: Map<string, Statistic>;
|
|
18
|
+
envs: string[];
|
|
19
|
+
}) => Promise<void>;
|
|
17
20
|
export declare const generateAttachmentsFiles: (writer: AwesomeDataWriter, attachmentLinks: AttachmentLink[], contentFunction: (id: string) => Promise<ResultFile | undefined>) => Promise<Map<string, string> | undefined>;
|
|
18
21
|
export declare const generateHistoryDataPoints: (writer: AwesomeDataWriter, store: AllureStore) => Promise<Map<string, string>>;
|
|
22
|
+
export declare const generateGlobals: (writer: AwesomeDataWriter, payload: {
|
|
23
|
+
globalExitCode?: ExitCode;
|
|
24
|
+
globalAttachments?: AttachmentLink[];
|
|
25
|
+
globalErrors?: TestError[];
|
|
26
|
+
contentFunction: (id: string) => Promise<ResultFile | undefined>;
|
|
27
|
+
}) => Promise<void>;
|
|
19
28
|
export declare const generateStaticFiles: (payload: AwesomeOptions & {
|
|
20
29
|
id: string;
|
|
21
30
|
allureVersion: string;
|
package/dist/generators.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { compareBy, incrementStatistic, nullsLast, ordinal, } from "@allurereport/core-api";
|
|
2
|
-
import { filterTree, } from "@allurereport/plugin-api";
|
|
3
|
-
import {
|
|
4
|
-
import { createBaseUrlScript, createFontLinkTag, createReportDataScript, createScriptTag, createStylesLinkTag, getPieChartData, } from "@allurereport/web-commons";
|
|
1
|
+
import { compareBy, getPieChartValues, incrementStatistic, nullsLast, ordinal, } from "@allurereport/core-api";
|
|
2
|
+
import { createTreeByLabels, createTreeByTitlePath, filterTree, preciseTreeLabels, sortTree, transformTree, } from "@allurereport/plugin-api";
|
|
3
|
+
import { createBaseUrlScript, createFontLinkTag, createReportDataScript, createScriptTag, createStylesLinkTag, } from "@allurereport/web-commons";
|
|
5
4
|
import Handlebars from "handlebars";
|
|
6
5
|
import { readFile } from "node:fs/promises";
|
|
7
6
|
import { createRequire } from "node:module";
|
|
8
7
|
import { basename, join } from "node:path";
|
|
9
8
|
import { convertFixtureResult, convertTestResult } from "./converters.js";
|
|
10
|
-
import { filterEnv } from "./environments.js";
|
|
11
9
|
const require = createRequire(import.meta.url);
|
|
12
10
|
const template = `<!DOCTYPE html>
|
|
13
11
|
<html dir="ltr" lang="en">
|
|
@@ -73,8 +71,8 @@ const createBreadcrumbs = (convertedTr) => {
|
|
|
73
71
|
return acc;
|
|
74
72
|
}, []);
|
|
75
73
|
};
|
|
76
|
-
export const generateTestResults = async (writer, store, filter) => {
|
|
77
|
-
const allTr =
|
|
74
|
+
export const generateTestResults = async (writer, store, trs, filter) => {
|
|
75
|
+
const allTr = trs.filter((tr) => (filter ? filter(tr) : true));
|
|
78
76
|
let convertedTrs = [];
|
|
79
77
|
for (const tr of allTr) {
|
|
80
78
|
const trFixtures = await store.fixturesByTrId(tr.id);
|
|
@@ -107,8 +105,7 @@ export const generateTestCases = async (writer, trs) => {
|
|
|
107
105
|
await writer.writeTestCase(tr);
|
|
108
106
|
}
|
|
109
107
|
};
|
|
110
|
-
export const generateTestEnvGroups = async (writer,
|
|
111
|
-
const groups = await store.allTestEnvGroups();
|
|
108
|
+
export const generateTestEnvGroups = async (writer, groups) => {
|
|
112
109
|
for (const group of groups) {
|
|
113
110
|
const src = join("test-env-groups", `${group.id}.json`);
|
|
114
111
|
await writer.writeData(src, group);
|
|
@@ -119,24 +116,55 @@ export const generateNav = async (writer, trs, filename = "nav.json") => {
|
|
|
119
116
|
};
|
|
120
117
|
export const generateTree = async (writer, treeFilename, labels, tests) => {
|
|
121
118
|
const visibleTests = tests.filter((test) => !test.hidden);
|
|
122
|
-
const tree =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
retriesCount,
|
|
126
|
-
name,
|
|
127
|
-
status,
|
|
128
|
-
start,
|
|
129
|
-
duration,
|
|
130
|
-
flaky,
|
|
131
|
-
transition,
|
|
132
|
-
}), undefined, (group, leaf) => {
|
|
133
|
-
incrementStatistic(group.statistic, leaf.status);
|
|
134
|
-
});
|
|
119
|
+
const tree = labels.length
|
|
120
|
+
? buildTreeByLabels(visibleTests, labels)
|
|
121
|
+
: buildTreeByTitlePath(visibleTests);
|
|
135
122
|
filterTree(tree, (leaf) => !leaf.hidden);
|
|
136
123
|
sortTree(tree, nullsLast(compareBy("start", ordinal())));
|
|
137
124
|
transformTree(tree, (leaf, idx) => ({ ...leaf, groupOrder: idx + 1 }));
|
|
138
125
|
await writer.writeWidget(treeFilename, tree);
|
|
139
126
|
};
|
|
127
|
+
const buildTreeByLabels = (tests, labels) => {
|
|
128
|
+
return createTreeByLabels(tests, labels, leafFactory, undefined, (group, leaf) => incrementStatistic(group.statistic, leaf.status));
|
|
129
|
+
};
|
|
130
|
+
const buildTreeByTitlePath = (tests) => {
|
|
131
|
+
const testsWithTitlePath = [];
|
|
132
|
+
const testsWithoutTitlePath = [];
|
|
133
|
+
for (const test of tests) {
|
|
134
|
+
if (Array.isArray(test.titlePath) && test.titlePath.length > 0) {
|
|
135
|
+
testsWithTitlePath.push(test);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
testsWithoutTitlePath.push(test);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const treeByTitlePath = createTreeByTitlePath(testsWithTitlePath, leafFactory, undefined, (group, leaf) => incrementStatistic(group.statistic, leaf.status));
|
|
142
|
+
const defaultLabels = preciseTreeLabels(["parentSuite", "suite", "subSuite"], testsWithoutTitlePath, ({ labels }) => labels.map(({ name }) => name));
|
|
143
|
+
const treeByDefaultLabels = createTreeByLabels(testsWithoutTitlePath, defaultLabels, leafFactory, undefined, (group, leaf) => incrementStatistic(group.statistic, leaf.status));
|
|
144
|
+
const mergedLeavesById = { ...treeByTitlePath.leavesById, ...treeByDefaultLabels.leavesById };
|
|
145
|
+
const mergedGroupsById = { ...treeByTitlePath.groupsById, ...treeByDefaultLabels.groupsById };
|
|
146
|
+
const mergedRootLeaves = Array.from(new Set([...(treeByTitlePath.root.leaves ?? []), ...(treeByDefaultLabels.root.leaves ?? [])]));
|
|
147
|
+
const mergedRootGroups = Array.from(new Set([...(treeByTitlePath.root.groups ?? []), ...(treeByDefaultLabels.root.groups ?? [])]));
|
|
148
|
+
return {
|
|
149
|
+
root: {
|
|
150
|
+
leaves: mergedRootLeaves,
|
|
151
|
+
groups: mergedRootGroups,
|
|
152
|
+
},
|
|
153
|
+
leavesById: mergedLeavesById,
|
|
154
|
+
groupsById: mergedGroupsById,
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
const leafFactory = ({ id, name, status, duration, flaky, start, transition, retry, retriesCount, }) => ({
|
|
158
|
+
nodeId: id,
|
|
159
|
+
name,
|
|
160
|
+
status,
|
|
161
|
+
duration,
|
|
162
|
+
flaky,
|
|
163
|
+
start,
|
|
164
|
+
retry,
|
|
165
|
+
retriesCount,
|
|
166
|
+
transition,
|
|
167
|
+
});
|
|
140
168
|
export const generateEnvironmentJson = async (writer, env) => {
|
|
141
169
|
await writer.writeWidget("allure_environment.json", env);
|
|
142
170
|
};
|
|
@@ -153,22 +181,17 @@ export const generateVariables = async (writer, store) => {
|
|
|
153
181
|
await writer.writeWidget(join(env, "variables.json"), envVariables);
|
|
154
182
|
}
|
|
155
183
|
};
|
|
156
|
-
export const generateStatistic = async (writer,
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
await writer.writeWidget("
|
|
160
|
-
for (const env of
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const environments = await store.allEnvironments();
|
|
168
|
-
await writer.writeWidget("pie_chart.json", getPieChartData(reportStatistic));
|
|
169
|
-
for (const env of environments) {
|
|
170
|
-
const envStatistic = await store.testsStatistic(filterEnv(env, filter));
|
|
171
|
-
await writer.writeWidget(join(env, "pie_chart.json"), getPieChartData(envStatistic));
|
|
184
|
+
export const generateStatistic = async (writer, data) => {
|
|
185
|
+
const { stats, statsByEnv, envs } = data;
|
|
186
|
+
await writer.writeWidget("statistic.json", stats);
|
|
187
|
+
await writer.writeWidget("pie_chart.json", getPieChartValues(stats));
|
|
188
|
+
for (const env of envs) {
|
|
189
|
+
const envStats = statsByEnv.get(env);
|
|
190
|
+
if (!envStats) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
await writer.writeWidget(join(env, "statistic.json"), envStats);
|
|
194
|
+
await writer.writeWidget(join(env, "pie_chart.json"), envStats);
|
|
172
195
|
}
|
|
173
196
|
};
|
|
174
197
|
export const generateAttachmentsFiles = async (writer, attachmentLinks, contentFunction) => {
|
|
@@ -196,8 +219,26 @@ export const generateHistoryDataPoints = async (writer, store) => {
|
|
|
196
219
|
}
|
|
197
220
|
return result;
|
|
198
221
|
};
|
|
222
|
+
export const generateGlobals = async (writer, payload) => {
|
|
223
|
+
const { globalExitCode = { original: 0 }, globalAttachments = [], globalErrors = [], contentFunction } = payload;
|
|
224
|
+
const globals = {
|
|
225
|
+
exitCode: globalExitCode,
|
|
226
|
+
errors: globalErrors,
|
|
227
|
+
attachments: [],
|
|
228
|
+
};
|
|
229
|
+
for (const attachment of globalAttachments) {
|
|
230
|
+
const src = `${attachment.id}${attachment.ext}`;
|
|
231
|
+
const content = await contentFunction(attachment.id);
|
|
232
|
+
if (!content) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
await writer.writeAttachment(src, content);
|
|
236
|
+
globals.attachments.push(attachment);
|
|
237
|
+
}
|
|
238
|
+
await writer.writeWidget("globals.json", globals);
|
|
239
|
+
};
|
|
199
240
|
export const generateStaticFiles = async (payload) => {
|
|
200
|
-
const { id, reportName = "Allure Report", reportLanguage = "en", singleFile, logo = "", theme = "light", groupBy, reportFiles, reportDataFiles, reportUuid, allureVersion, layout = "base", charts = [], defaultSection = "", } = payload;
|
|
241
|
+
const { id, reportName = "Allure Report", reportLanguage = "en", singleFile, logo = "", theme = "light", groupBy, reportFiles, reportDataFiles, reportUuid, allureVersion, layout = "base", charts = [], defaultSection = "", ci, } = payload;
|
|
201
242
|
const compile = Handlebars.compile(template);
|
|
202
243
|
const manifest = await readTemplateManifest(payload.singleFile);
|
|
203
244
|
const headTags = [];
|
|
@@ -241,23 +282,34 @@ export const generateStaticFiles = async (payload) => {
|
|
|
241
282
|
reportLanguage,
|
|
242
283
|
createdAt: now,
|
|
243
284
|
reportUuid,
|
|
244
|
-
groupBy: groupBy?.length ? groupBy : [
|
|
285
|
+
groupBy: groupBy?.length ? groupBy : [],
|
|
245
286
|
cacheKey: now.toString(),
|
|
287
|
+
ci,
|
|
246
288
|
layout,
|
|
247
289
|
allureVersion,
|
|
248
290
|
sections,
|
|
249
291
|
defaultSection,
|
|
250
292
|
};
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
try {
|
|
294
|
+
const html = compile({
|
|
295
|
+
headTags: headTags.join("\n"),
|
|
296
|
+
bodyTags: bodyTags.join("\n"),
|
|
297
|
+
reportFilesScript: createReportDataScript(reportDataFiles),
|
|
298
|
+
reportOptions: JSON.stringify(reportOptions),
|
|
299
|
+
analyticsEnable: true,
|
|
300
|
+
allureVersion,
|
|
301
|
+
reportUuid,
|
|
302
|
+
reportName,
|
|
303
|
+
singleFile: payload.singleFile,
|
|
304
|
+
});
|
|
305
|
+
await reportFiles.addFile("index.html", Buffer.from(html, "utf8"));
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
if (err instanceof RangeError) {
|
|
309
|
+
console.error("The report is too large to be generated in the single file mode!");
|
|
310
|
+
process.exit(1);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
263
315
|
};
|
package/dist/model.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { EnvironmentsConfig, TestResult } from "@allurereport/core-api";
|
|
2
|
-
import type { ChartOptions } from "
|
|
1
|
+
import type { CiDescriptor, EnvironmentsConfig, TestResult } from "@allurereport/core-api";
|
|
2
|
+
import type { ChartOptions } from "@allurereport/plugin-api";
|
|
3
3
|
export type AwesomeOptions = {
|
|
4
4
|
reportName?: string;
|
|
5
5
|
singleFile?: boolean;
|
|
@@ -9,11 +9,7 @@ export type AwesomeOptions = {
|
|
|
9
9
|
groupBy?: string[];
|
|
10
10
|
layout?: "base" | "split";
|
|
11
11
|
environments?: Record<string, EnvironmentsConfig>;
|
|
12
|
-
ci?:
|
|
13
|
-
type: "github" | "jenkins";
|
|
14
|
-
url: string;
|
|
15
|
-
name: string;
|
|
16
|
-
};
|
|
12
|
+
ci?: CiDescriptor;
|
|
17
13
|
filter?: (testResult: TestResult) => boolean;
|
|
18
14
|
charts?: ChartOptions[];
|
|
19
15
|
sections?: string[];
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AllureStore, type Plugin, type PluginContext, type PluginSummary } from "@allurereport/plugin-api";
|
|
2
2
|
import type { AwesomePluginOptions } from "./model.js";
|
|
3
3
|
export declare class AwesomePlugin implements Plugin {
|
|
4
4
|
#private;
|
package/dist/plugin.js
CHANGED
|
@@ -11,34 +11,56 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
|
|
|
11
11
|
};
|
|
12
12
|
var _AwesomePlugin_writer, _AwesomePlugin_generate;
|
|
13
13
|
import { getWorstStatus } from "@allurereport/core-api";
|
|
14
|
+
import { convertToSummaryTestResult, } from "@allurereport/plugin-api";
|
|
14
15
|
import { preciseTreeLabels } from "@allurereport/plugin-api";
|
|
15
16
|
import { join } from "node:path";
|
|
16
17
|
import { generateAllCharts } from "./charts.js";
|
|
17
|
-
import {
|
|
18
|
+
import { filterEnv } from "./environments.js";
|
|
19
|
+
import { generateAttachmentsFiles, generateEnvironmentJson, generateEnvirontmentsList, generateGlobals, generateHistoryDataPoints, generateNav, generateStaticFiles, generateStatistic, generateTestCases, generateTestEnvGroups, generateTestResults, generateTree, generateVariables, } from "./generators.js";
|
|
18
20
|
import { InMemoryReportDataWriter, ReportFileDataWriter } from "./writer.js";
|
|
19
21
|
export class AwesomePlugin {
|
|
20
22
|
constructor(options = {}) {
|
|
21
23
|
this.options = options;
|
|
22
24
|
_AwesomePlugin_writer.set(this, void 0);
|
|
23
25
|
_AwesomePlugin_generate.set(this, async (context, store) => {
|
|
24
|
-
const { singleFile, groupBy = [] } = this.options ?? {};
|
|
26
|
+
const { singleFile, groupBy = [], filter } = this.options ?? {};
|
|
25
27
|
const environmentItems = await store.metadataByKey("allure_environment");
|
|
26
28
|
const reportEnvironments = await store.allEnvironments();
|
|
27
29
|
const attachments = await store.allAttachments();
|
|
28
|
-
await
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
30
|
+
const allTrs = await store.allTestResults({ includeHidden: true });
|
|
31
|
+
const statistics = await store.testsStatistic(filter);
|
|
32
|
+
const environments = await store.allEnvironments();
|
|
33
|
+
const envStatistics = new Map();
|
|
34
|
+
const allTestEnvGroups = await store.allTestEnvGroups();
|
|
35
|
+
const allHistoryDataPoints = await store.allHistoryDataPoints();
|
|
36
|
+
const globalAttachments = await store.allGlobalAttachments();
|
|
37
|
+
const globalExitCode = await store.globalExitCode();
|
|
38
|
+
const globalErrors = await store.allGlobalErrors();
|
|
39
|
+
for (const env of environments) {
|
|
40
|
+
envStatistics.set(env, await store.testsStatistic(filterEnv(env, filter)));
|
|
41
|
+
}
|
|
42
|
+
await generateStatistic(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), {
|
|
43
|
+
stats: statistics,
|
|
44
|
+
statsByEnv: envStatistics,
|
|
45
|
+
envs: environments,
|
|
46
|
+
});
|
|
47
|
+
await generateAllCharts(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), this.options, context, {
|
|
48
|
+
trs: allTrs,
|
|
49
|
+
statistic: statistics,
|
|
50
|
+
history: allHistoryDataPoints,
|
|
51
|
+
});
|
|
52
|
+
const convertedTrs = await generateTestResults(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), store, allTrs, this.options.filter);
|
|
53
|
+
const hasGroupBy = groupBy.length > 0;
|
|
54
|
+
const treeLabels = hasGroupBy
|
|
55
|
+
? preciseTreeLabels(groupBy, convertedTrs, ({ labels }) => labels.map(({ name }) => name))
|
|
56
|
+
: [];
|
|
33
57
|
await generateHistoryDataPoints(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), store);
|
|
34
58
|
await generateTestCases(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), convertedTrs);
|
|
35
59
|
await generateTree(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), "tree.json", treeLabels, convertedTrs);
|
|
36
60
|
await generateNav(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), convertedTrs, "nav.json");
|
|
37
|
-
await generateTestEnvGroups(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"),
|
|
61
|
+
await generateTestEnvGroups(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), allTestEnvGroups);
|
|
38
62
|
for (const reportEnvironment of reportEnvironments) {
|
|
39
|
-
const
|
|
40
|
-
const envTrsIds = envTrs.map(({ id }) => id);
|
|
41
|
-
const envConvertedTrs = convertedTrs.filter(({ id }) => envTrsIds.includes(id));
|
|
63
|
+
const envConvertedTrs = convertedTrs.filter(({ environment }) => environment === reportEnvironment);
|
|
42
64
|
await generateTree(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), join(reportEnvironment, "tree.json"), treeLabels, envConvertedTrs);
|
|
43
65
|
await generateNav(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), envConvertedTrs, join(reportEnvironment, "nav.json"));
|
|
44
66
|
}
|
|
@@ -51,6 +73,12 @@ export class AwesomePlugin {
|
|
|
51
73
|
await generateAttachmentsFiles(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), attachments, (id) => store.attachmentContentById(id));
|
|
52
74
|
}
|
|
53
75
|
const reportDataFiles = singleFile ? __classPrivateFieldGet(this, _AwesomePlugin_writer, "f").reportFiles() : [];
|
|
76
|
+
await generateGlobals(__classPrivateFieldGet(this, _AwesomePlugin_writer, "f"), {
|
|
77
|
+
globalAttachments,
|
|
78
|
+
globalErrors,
|
|
79
|
+
globalExitCode,
|
|
80
|
+
contentFunction: (id) => store.attachmentContentById(id),
|
|
81
|
+
});
|
|
54
82
|
await generateStaticFiles({
|
|
55
83
|
...this.options,
|
|
56
84
|
id: context.id,
|
|
@@ -85,6 +113,9 @@ export class AwesomePlugin {
|
|
|
85
113
|
}
|
|
86
114
|
async info(context, store) {
|
|
87
115
|
const allTrs = (await store.allTestResults()).filter((tr) => this.options.filter ? this.options.filter(tr) : true);
|
|
116
|
+
const newTrs = await store.allNewTestResults();
|
|
117
|
+
const retryTrs = allTrs.filter((tr) => !!tr?.retries?.length);
|
|
118
|
+
const flakyTrs = allTrs.filter((tr) => !!tr?.flaky);
|
|
88
119
|
const duration = allTrs.reduce((acc, { duration: trDuration = 0 }) => acc + trDuration, 0);
|
|
89
120
|
const worstStatus = getWorstStatus(allTrs.map(({ status }) => status));
|
|
90
121
|
const createdAt = allTrs.reduce((acc, { stop }) => Math.max(acc, stop || 0), 0);
|
|
@@ -95,6 +126,9 @@ export class AwesomePlugin {
|
|
|
95
126
|
duration,
|
|
96
127
|
createdAt,
|
|
97
128
|
plugin: "Awesome",
|
|
129
|
+
newTests: newTrs.map(convertToSummaryTestResult),
|
|
130
|
+
flakyTests: flakyTrs.map(convertToSummaryTestResult),
|
|
131
|
+
retryTests: retryTrs.map(convertToSummaryTestResult),
|
|
98
132
|
};
|
|
99
133
|
}
|
|
100
134
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allurereport/plugin-awesome",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.18",
|
|
4
4
|
"description": "Allure Awesome Plugin – brand new HTML report with modern design and new features",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"allure",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"test": "rimraf ./out && vitest run"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@allurereport/core-api": "3.0.0-beta.
|
|
34
|
-
"@allurereport/plugin-api": "3.0.0-beta.
|
|
35
|
-
"@allurereport/web-awesome": "3.0.0-beta.
|
|
36
|
-
"@allurereport/web-commons": "3.0.0-beta.
|
|
33
|
+
"@allurereport/core-api": "3.0.0-beta.18",
|
|
34
|
+
"@allurereport/plugin-api": "3.0.0-beta.18",
|
|
35
|
+
"@allurereport/web-awesome": "3.0.0-beta.18",
|
|
36
|
+
"@allurereport/web-commons": "3.0.0-beta.18",
|
|
37
37
|
"d3-shape": "^3.2.0",
|
|
38
38
|
"handlebars": "^4.7.8"
|
|
39
39
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
46
46
|
"@typescript-eslint/parser": "^8.0.0",
|
|
47
47
|
"@vitest/runner": "^2.1.9",
|
|
48
|
-
"allure-vitest": "^3.
|
|
48
|
+
"allure-vitest": "^3.3.3",
|
|
49
49
|
"eslint": "^8.57.0",
|
|
50
50
|
"eslint-config-prettier": "^9.1.0",
|
|
51
51
|
"eslint-plugin-import": "^2.29.1",
|