@allurereport/plugin-api
Advanced tools
| import type { BaseTrendSliceMetadata, ChartId, ChartType, HistoryDataPoint, PieSlice, SeverityLevel, Statistic, TestResult, TestStatus, TrendPoint, TrendPointId, TrendSlice, TrendSliceId } from "@allurereport/core-api"; | ||
| import { ChartDataType, ChartMode } from "@allurereport/core-api"; | ||
| import type { PluginContext } from "./plugin.js"; | ||
| export type ExecutionIdFn = (executionOrder: number) => string; | ||
| export type ExecutionNameFn = (executionOrder: number) => string; | ||
| export type TrendMetadataFnOverrides = { | ||
| executionIdAccessor?: ExecutionIdFn; | ||
| executionNameAccessor?: ExecutionNameFn; | ||
| }; | ||
| export type TrendDataType = TestStatus | SeverityLevel; | ||
| export type TrendCalculationResult<T extends TrendDataType> = { | ||
| points: Record<TrendPointId, TrendPoint>; | ||
| series: Record<T, TrendPointId[]>; | ||
| }; | ||
| export interface GenericTrendChartData<SeriesType extends string, Metadata extends BaseTrendSliceMetadata = BaseTrendSliceMetadata> { | ||
| type: ChartType.Trend; | ||
| dataType: ChartDataType; | ||
| mode: ChartMode; | ||
| title?: string; | ||
| points: Record<TrendPointId, TrendPoint>; | ||
| slices: Record<TrendSliceId, TrendSlice<Metadata>>; | ||
| series: Record<SeriesType, TrendPointId[]>; | ||
| min: number; | ||
| max: number; | ||
| } | ||
| export type StatusTrendChartData = GenericTrendChartData<TestStatus>; | ||
| export type SeverityTrendChartData = GenericTrendChartData<SeverityLevel>; | ||
| export type TrendChartData = StatusTrendChartData | SeverityTrendChartData; | ||
| export type GeneratedChartData = TrendChartData | PieChartData | ComingSoonChartData; | ||
| export type GeneratedChartsData = Record<ChartId, GeneratedChartData>; | ||
| export type TrendStats<T extends TrendDataType> = Record<T, number>; | ||
| export type TrendChartOptions = { | ||
| type: ChartType.Trend; | ||
| dataType: ChartDataType; | ||
| mode?: ChartMode; | ||
| title?: string; | ||
| limit?: number; | ||
| metadata?: TrendMetadataFnOverrides; | ||
| }; | ||
| export type PieChartOptions = { | ||
| type: ChartType.Pie; | ||
| title?: string; | ||
| }; | ||
| export type ComingSoonChartOptions = { | ||
| type: ChartType.HeatMap | ChartType.Bar | ChartType.Funnel | ChartType.TreeMap; | ||
| title?: string; | ||
| }; | ||
| export type ChartOptions = TrendChartOptions | PieChartOptions | ComingSoonChartOptions; | ||
| export interface PieChartData { | ||
| type: ChartType.Pie; | ||
| title?: string; | ||
| slices: PieSlice[]; | ||
| percentage: number; | ||
| } | ||
| export interface ComingSoonChartData { | ||
| type: ChartType.HeatMap | ChartType.Bar | ChartType.Funnel | ChartType.TreeMap; | ||
| title?: string; | ||
| } | ||
| export declare const createEmptySeries: <T extends TrendDataType>(items: readonly T[]) => Record<T, string[]>; | ||
| export declare const calculatePercentValues: <T extends TrendDataType>(stats: Record<T, number>, executionId: string, itemType: readonly T[]) => TrendCalculationResult<T>; | ||
| export declare const getTrendDataGeneric: <T extends TrendDataType, M extends BaseTrendSliceMetadata>(stats: Record<T, number>, reportName: string, executionOrder: number, itemType: readonly T[], chartOptions: TrendChartOptions) => GenericTrendChartData<T, M>; | ||
| export declare const createEmptyStats: <T extends TrendDataType>(items: readonly T[]) => TrendStats<T>; | ||
| export declare const normalizeStatistic: <T extends TrendDataType>(statistic: Partial<TrendStats<T>>, itemType: readonly T[]) => TrendStats<T>; | ||
| export declare const mergeTrendDataGeneric: <T extends TrendDataType, M extends BaseTrendSliceMetadata>(trendData: GenericTrendChartData<T, M>, trendDataPart: GenericTrendChartData<T, M>, itemType: readonly T[]) => GenericTrendChartData<T, M>; | ||
| export declare const DEFAULT_CHART_HISTORY_LIMIT = 10; | ||
| export declare const getPieChartData: (stats: Statistic, chartOptions: PieChartOptions) => PieChartData; | ||
| export declare const generatePieChart: (options: PieChartOptions, stores: { | ||
| statistic: Statistic; | ||
| }) => PieChartData; | ||
| export declare const generateComingSoonChart: (options: ComingSoonChartOptions) => ComingSoonChartData; | ||
| export interface TrendDataAccessor<T extends TrendDataType> { | ||
| getCurrentData: (trs: TestResult[], stats: Statistic) => TrendStats<T>; | ||
| getHistoricalData: (historyPoint: HistoryDataPoint) => TrendStats<T>; | ||
| getAllValues: () => readonly T[]; | ||
| } | ||
| export declare const generateTrendChartGeneric: <T extends TrendDataType>(options: TrendChartOptions, stores: { | ||
| trs: TestResult[]; | ||
| statistic: Statistic; | ||
| history: HistoryDataPoint[]; | ||
| }, context: PluginContext, dataAccessor: TrendDataAccessor<T>) => GenericTrendChartData<T> | undefined; | ||
| export declare const generateTrendChart: (options: TrendChartOptions, stores: { | ||
| trs: TestResult[]; | ||
| statistic: Statistic; | ||
| history: HistoryDataPoint[]; | ||
| }, context: PluginContext) => TrendChartData | undefined; |
+163
| import { ChartDataType, ChartMode, getPieChartValues } from "@allurereport/core-api"; | ||
| import { severityTrendDataAccessor } from "./severityTrendAccessor.js"; | ||
| import { statusTrendDataAccessor } from "./statusTrendAccessor.js"; | ||
| export const createEmptySeries = (items) => items.reduce((acc, item) => ({ ...acc, [item]: [] }), {}); | ||
| export const calculatePercentValues = (stats, executionId, itemType) => { | ||
| const points = {}; | ||
| const series = createEmptySeries(itemType); | ||
| const values = Object.values(stats); | ||
| const total = values.reduce((sum, value) => sum + value, 0); | ||
| if (total === 0) { | ||
| return { points, series }; | ||
| } | ||
| itemType.forEach((item) => { | ||
| const pointId = `${executionId}-${item}`; | ||
| const value = stats[item] ?? 0; | ||
| points[pointId] = { | ||
| x: executionId, | ||
| y: value / total, | ||
| }; | ||
| series[item].push(pointId); | ||
| }); | ||
| return { points, series }; | ||
| }; | ||
| const calculateRawValues = (stats, executionId, itemType) => { | ||
| const points = {}; | ||
| const series = createEmptySeries(itemType); | ||
| itemType.forEach((item) => { | ||
| const pointId = `${executionId}-${item}`; | ||
| const value = stats[item] ?? 0; | ||
| points[pointId] = { | ||
| x: executionId, | ||
| y: value, | ||
| }; | ||
| series[item].push(pointId); | ||
| }); | ||
| return { points, series }; | ||
| }; | ||
| export const getTrendDataGeneric = (stats, reportName, executionOrder, itemType, chartOptions) => { | ||
| const { type, dataType, title, mode = ChartMode.Raw, metadata = {} } = chartOptions; | ||
| const { executionIdAccessor, executionNameAccessor } = metadata; | ||
| const executionId = executionIdAccessor ? executionIdAccessor(executionOrder) : `execution-${executionOrder}`; | ||
| const { points, series } = mode === ChartMode.Percent | ||
| ? calculatePercentValues(stats, executionId, itemType) | ||
| : calculateRawValues(stats, executionId, itemType); | ||
| const slices = {}; | ||
| const pointsAsArray = Object.values(points); | ||
| const pointsCount = pointsAsArray.length; | ||
| const values = pointsAsArray.map((point) => point.y); | ||
| const min = pointsCount ? Math.min(...values) : 0; | ||
| const max = pointsCount ? Math.max(...values) : 0; | ||
| if (pointsCount > 0) { | ||
| const executionName = executionNameAccessor ? executionNameAccessor(executionOrder) : reportName; | ||
| slices[executionId] = { | ||
| min, | ||
| max, | ||
| metadata: { | ||
| executionId, | ||
| executionName, | ||
| }, | ||
| }; | ||
| } | ||
| return { | ||
| type, | ||
| dataType, | ||
| mode, | ||
| title, | ||
| points, | ||
| slices, | ||
| series, | ||
| min, | ||
| max, | ||
| }; | ||
| }; | ||
| export const createEmptyStats = (items) => items.reduce((acc, item) => ({ ...acc, [item]: 0 }), {}); | ||
| export const normalizeStatistic = (statistic, itemType) => { | ||
| return itemType.reduce((acc, item) => { | ||
| acc[item] = statistic[item] ?? 0; | ||
| return acc; | ||
| }, {}); | ||
| }; | ||
| export const mergeTrendDataGeneric = (trendData, trendDataPart, itemType) => { | ||
| return { | ||
| ...trendData, | ||
| points: { | ||
| ...trendData.points, | ||
| ...trendDataPart.points, | ||
| }, | ||
| slices: { | ||
| ...trendData.slices, | ||
| ...trendDataPart.slices, | ||
| }, | ||
| series: Object.entries(trendDataPart.series).reduce((series, [group, pointIds]) => { | ||
| if (Array.isArray(pointIds)) { | ||
| return { | ||
| ...series, | ||
| [group]: [...(trendData.series?.[group] || []), ...pointIds], | ||
| }; | ||
| } | ||
| return series; | ||
| }, trendData.series || createEmptySeries(itemType)), | ||
| min: Math.min(trendData.min ?? Infinity, trendDataPart.min), | ||
| max: Math.max(trendData.max ?? -Infinity, trendDataPart.max), | ||
| }; | ||
| }; | ||
| export const DEFAULT_CHART_HISTORY_LIMIT = 10; | ||
| export const getPieChartData = (stats, chartOptions) => ({ | ||
| type: chartOptions.type, | ||
| title: chartOptions?.title, | ||
| ...getPieChartValues(stats), | ||
| }); | ||
| export const generatePieChart = (options, stores) => { | ||
| const { statistic } = stores; | ||
| return getPieChartData(statistic, options); | ||
| }; | ||
| export const generateComingSoonChart = (options) => { | ||
| return { | ||
| type: options.type, | ||
| title: options.title, | ||
| }; | ||
| }; | ||
| export const generateTrendChartGeneric = (options, stores, context, dataAccessor) => { | ||
| const { trs = [], statistic, history } = stores; | ||
| const { limit } = options; | ||
| const historyLimit = limit && limit > 0 ? Math.max(0, limit - 1) : undefined; | ||
| const currentData = dataAccessor.getCurrentData(trs, statistic); | ||
| const limitedHistoryPoints = historyLimit !== undefined ? history.slice(-historyLimit) : history; | ||
| const firstOriginalIndex = historyLimit !== undefined ? Math.max(0, history.length - historyLimit) : 0; | ||
| const convertedHistoryPoints = limitedHistoryPoints.map((point, index) => { | ||
| const originalIndex = firstOriginalIndex + index; | ||
| return { | ||
| name: point.name, | ||
| originalIndex, | ||
| statistic: dataAccessor.getHistoricalData(point), | ||
| }; | ||
| }); | ||
| const allValues = dataAccessor.getAllValues(); | ||
| const currentTrendData = getTrendDataGeneric(normalizeStatistic(currentData, allValues), context.reportName, history.length + 1, allValues, options); | ||
| const historicalTrendData = convertedHistoryPoints.reduce((acc, historyPoint) => { | ||
| const trendDataPart = getTrendDataGeneric(normalizeStatistic(historyPoint.statistic, allValues), historyPoint.name, historyPoint.originalIndex + 1, allValues, options); | ||
| return mergeTrendDataGeneric(acc, trendDataPart, allValues); | ||
| }, { | ||
| type: options.type, | ||
| dataType: options.dataType, | ||
| mode: options.mode, | ||
| title: options.title, | ||
| points: {}, | ||
| slices: {}, | ||
| series: createEmptySeries(allValues), | ||
| min: Infinity, | ||
| max: -Infinity, | ||
| }); | ||
| return mergeTrendDataGeneric(historicalTrendData, currentTrendData, allValues); | ||
| }; | ||
| export const generateTrendChart = (options, stores, context) => { | ||
| const newOptions = { limit: DEFAULT_CHART_HISTORY_LIMIT, ...options }; | ||
| const { dataType } = newOptions; | ||
| if (dataType === ChartDataType.Status) { | ||
| return generateTrendChartGeneric(newOptions, stores, context, statusTrendDataAccessor); | ||
| } | ||
| else if (dataType === ChartDataType.Severity) { | ||
| return generateTrendChartGeneric(newOptions, stores, context, severityTrendDataAccessor); | ||
| } | ||
| }; |
| import type { SeverityLevel } from "@allurereport/core-api"; | ||
| import type { TrendDataAccessor } from "./charts.js"; | ||
| export declare const severityTrendDataAccessor: TrendDataAccessor<SeverityLevel>; |
| import { severityLabelName, severityLevels } from "@allurereport/core-api"; | ||
| import { createEmptyStats } from "./charts.js"; | ||
| const processTestResults = (testResults) => { | ||
| return testResults.reduce((acc, test) => { | ||
| const severityLabel = test.labels?.find((label) => label.name === severityLabelName); | ||
| const severity = severityLabel?.value?.toLowerCase(); | ||
| if (severity) { | ||
| acc[severity] = (acc[severity] ?? 0) + 1; | ||
| } | ||
| return acc; | ||
| }, createEmptyStats(severityLevels)); | ||
| }; | ||
| export const severityTrendDataAccessor = { | ||
| getCurrentData: (trs) => { | ||
| return processTestResults(trs); | ||
| }, | ||
| getHistoricalData: (historyPoint) => { | ||
| return processTestResults(Object.values(historyPoint.testResults)); | ||
| }, | ||
| getAllValues: () => severityLevels, | ||
| }; |
| import type { TestStatus } from "@allurereport/core-api"; | ||
| import type { TrendDataAccessor } from "./charts.js"; | ||
| export declare const statusTrendDataAccessor: TrendDataAccessor<TestStatus>; |
| import { statusesList } from "@allurereport/core-api"; | ||
| import { createEmptyStats } from "./charts.js"; | ||
| export const statusTrendDataAccessor = { | ||
| getCurrentData: (trs, stats) => { | ||
| return { | ||
| ...createEmptyStats(statusesList), | ||
| ...stats, | ||
| }; | ||
| }, | ||
| getHistoricalData: (historyPoint) => { | ||
| return Object.values(historyPoint.testResults).reduce((stat, test) => { | ||
| if (test.status) { | ||
| stat[test.status] = (stat[test.status] ?? 0) + 1; | ||
| } | ||
| return stat; | ||
| }, createEmptyStats(statusesList)); | ||
| }, | ||
| getAllValues: () => statusesList, | ||
| }; |
+1
-1
@@ -13,4 +13,4 @@ import type { DefaultLabelsConfig, EnvironmentsConfig, ReportVariables } from "@allurereport/core-api"; | ||
| plugins?: Record<string, PluginDescriptor>; | ||
| appendHistory?: boolean; | ||
| qualityGate?: QualityGateConfig; | ||
| appendHistory?: boolean; | ||
| allureService?: { | ||
@@ -17,0 +17,0 @@ url?: string; |
+4
-1
| export * from "./config.js"; | ||
| export type * from "./plugin.js"; | ||
| export type * from "./qualityGate.js"; | ||
| export type * from "./store.js"; | ||
| export type * from "./resultFile.js"; | ||
| export type * from "./qualityGate.js"; | ||
| export * from "./utils/misc.js"; | ||
| export * from "./utils/tree.js"; | ||
| export * from "./utils/summary.js"; | ||
| export * from "./charts.js"; | ||
| export * from "./severityTrendAccessor.js"; | ||
| export * from "./statusTrendAccessor.js"; |
+3
-0
@@ -5,1 +5,4 @@ export * from "./config.js"; | ||
| export * from "./utils/summary.js"; | ||
| export * from "./charts.js"; | ||
| export * from "./severityTrendAccessor.js"; | ||
| export * from "./statusTrendAccessor.js"; |
+30
-6
@@ -1,2 +0,4 @@ | ||
| import type { CiDescriptor, Statistic, TestResult, TestStatus } from "@allurereport/core-api"; | ||
| import type { AttachmentLink, CiDescriptor, Statistic, TestError, TestResult, TestStatus } from "@allurereport/core-api"; | ||
| import type { QualityGateValidationResult } from "./qualityGate.js"; | ||
| import type { ResultFile } from "./resultFile.js"; | ||
| import type { AllureStore } from "./store.js"; | ||
@@ -42,12 +44,34 @@ export interface PluginDescriptor { | ||
| } | ||
| export interface ExitCode { | ||
| actual?: number; | ||
| original: number; | ||
| } | ||
| export interface PluginGlobals { | ||
| exitCode: ExitCode; | ||
| errors: TestError[]; | ||
| attachments: AttachmentLink[]; | ||
| } | ||
| export interface BatchOptions { | ||
| maxTimeout?: number; | ||
| } | ||
| export interface Realtime { | ||
| onTestResults(listener: (trIds: string[]) => Promise<void>, options?: BatchOptions): void; | ||
| onTestFixtureResults(listener: (tfrIds: string[]) => Promise<void>, options?: BatchOptions): void; | ||
| onAttachmentFiles(listener: (afIds: string[]) => Promise<void>, options?: BatchOptions): void; | ||
| export interface RealtimeSubscriber { | ||
| onGlobalAttachment(listener: (attachment: ResultFile) => Promise<void>): () => void; | ||
| onGlobalExitCode(listener: (payload: ExitCode) => Promise<void>): () => void; | ||
| onGlobalError(listener: (error: TestError) => Promise<void>): () => void; | ||
| onQualityGateResults(listener: (payload: QualityGateValidationResult[]) => Promise<void>): () => void; | ||
| onTestResults(listener: (trIds: string[]) => Promise<void>, options?: BatchOptions): () => void; | ||
| onTestFixtureResults(listener: (tfrIds: string[]) => Promise<void>, options?: BatchOptions): () => void; | ||
| onAttachmentFiles(listener: (afIds: string[]) => Promise<void>, options?: BatchOptions): () => void; | ||
| } | ||
| export interface RealtimeEventsDispatcher { | ||
| sendGlobalAttachment(attachment: ResultFile): void; | ||
| sendGlobalExitCode(payload: ExitCode): void; | ||
| sendGlobalError(error: TestError): void; | ||
| sendQualityGateResults(payload: QualityGateValidationResult[]): void; | ||
| sendTestResult(trId: string): void; | ||
| sendTestFixtureResult(tfrId: string): void; | ||
| sendAttachmentFile(afId: string): void; | ||
| } | ||
| export interface Plugin { | ||
| start?(context: PluginContext, store: AllureStore, realtime: Realtime): Promise<void>; | ||
| start?(context: PluginContext, store: AllureStore, realtime: RealtimeSubscriber): Promise<void>; | ||
| update?(context: PluginContext, store: AllureStore): Promise<void>; | ||
@@ -54,0 +78,0 @@ done?(context: PluginContext, store: AllureStore): Promise<void>; |
+28
-37
@@ -1,44 +0,35 @@ | ||
| import type { AllureStore } from "./store.js"; | ||
| export type QualityGateRules = Record<string, any>; | ||
| export type QualityGateRulesBaseMeta<T> = { | ||
| type: T; | ||
| import type { KnownTestFailure, TestResult } from "@allurereport/core-api"; | ||
| export type QualityGateValidationResult = { | ||
| success: boolean; | ||
| expected: any; | ||
| actual: any; | ||
| rule: string; | ||
| message: string; | ||
| }; | ||
| export type QualityGateLabelsRulesMeta = QualityGateRulesBaseMeta<"label"> & { | ||
| name: string; | ||
| value: string; | ||
| export type QualityGateRules = Record<string, any> & { | ||
| id?: string; | ||
| fastFail?: boolean; | ||
| filter?: (tr: TestResult) => boolean; | ||
| }; | ||
| export type QualityGateParametersRulesMeta = QualityGateRulesBaseMeta<"parameter"> & { | ||
| name: string; | ||
| value: string; | ||
| export type QualityGateRuleResult = { | ||
| success: boolean; | ||
| expected: any; | ||
| actual: any; | ||
| }; | ||
| export type QualityGateLabelsEnforceConfig = { | ||
| type: "label"; | ||
| name: string; | ||
| value: string; | ||
| rules: QualityGateRules; | ||
| }; | ||
| export type QualityGateParametersEnforceConfig = { | ||
| type: "parameter"; | ||
| name: string; | ||
| value: string; | ||
| rules: QualityGateRules; | ||
| }; | ||
| export type QualityGateRulesMeta = Omit<QualityGateLabelsRulesMeta, "rules"> | Omit<QualityGateParametersRulesMeta, "rules">; | ||
| export type QualityGateEnforceConfig = QualityGateLabelsEnforceConfig | QualityGateParametersEnforceConfig; | ||
| export type QualityGateValidationResult = { | ||
| success: boolean; | ||
| export type QualityGateRule<T = any> = { | ||
| rule: string; | ||
| meta?: QualityGateRulesMeta; | ||
| expected?: number; | ||
| actual?: number; | ||
| message?: string; | ||
| message: (payload: { | ||
| expected: T; | ||
| actual: T; | ||
| }) => string; | ||
| validate: (payload: { | ||
| expected: T; | ||
| trs: TestResult[]; | ||
| knownIssues: KnownTestFailure[]; | ||
| state?: T; | ||
| }) => Promise<QualityGateRuleResult>; | ||
| }; | ||
| export interface QualityGateValidator { | ||
| validate(store: AllureStore): Promise<QualityGateValidationResult>; | ||
| } | ||
| export type QualityGateValidatorConstructor = new (limit: number, meta?: QualityGateRulesMeta) => QualityGateValidator; | ||
| export type QualityGateConfig = { | ||
| rules?: QualityGateRules; | ||
| enforce?: QualityGateEnforceConfig[]; | ||
| validators?: Record<string, QualityGateValidatorConstructor>; | ||
| rules?: QualityGateRules[]; | ||
| use?: QualityGateRule[]; | ||
| }; |
+7
-1
@@ -1,2 +0,4 @@ | ||
| import type { AttachmentLink, HistoryDataPoint, HistoryTestResult, KnownTestFailure, Statistic, TestCase, TestEnvGroup, TestFixtureResult, TestResult } from "@allurereport/core-api"; | ||
| import type { AttachmentLink, HistoryDataPoint, HistoryTestResult, KnownTestFailure, Statistic, TestCase, TestEnvGroup, TestError, TestFixtureResult, TestResult } from "@allurereport/core-api"; | ||
| import type { ExitCode } from "./plugin.js"; | ||
| import type { QualityGateValidationResult } from "./qualityGate.js"; | ||
| import type { ResultFile } from "./resultFile.js"; | ||
@@ -15,2 +17,6 @@ export type TestResultFilter = (testResult: TestResult) => boolean; | ||
| allNewTestResults: () => Promise<TestResult[]>; | ||
| qualityGateResults: () => Promise<QualityGateValidationResult[]>; | ||
| globalExitCode: () => Promise<ExitCode | undefined>; | ||
| allGlobalErrors: () => Promise<TestError[]>; | ||
| allGlobalAttachments: () => Promise<AttachmentLink[]>; | ||
| testCaseById: (tcId: string) => Promise<TestCase | undefined>; | ||
@@ -17,0 +23,0 @@ testResultById: (trId: string) => Promise<TestResult | undefined>; |
+3
-3
| { | ||
| "name": "@allurereport/plugin-api", | ||
| "version": "3.0.0-beta.17", | ||
| "version": "3.0.0-beta.18", | ||
| "description": "Allure Plugin API", | ||
@@ -29,3 +29,3 @@ "keywords": [ | ||
| "dependencies": { | ||
| "@allurereport/core-api": "3.0.0-beta.17" | ||
| "@allurereport/core-api": "3.0.0-beta.18" | ||
| }, | ||
@@ -40,3 +40,3 @@ "devDependencies": { | ||
| "@vitest/snapshot": "^2.1.9", | ||
| "allure-vitest": "^3.3.0", | ||
| "allure-vitest": "^3.3.3", | ||
| "eslint": "^8.57.0", | ||
@@ -43,0 +43,0 @@ "eslint-config-prettier": "^9.1.0", |
34408
69.25%26
30%753
74.31%+ Added
+ Added
+ Added
- Removed