@allurereport/core-api
Advanced tools
| import type { Statistic } from "./aggregate.js"; | ||
| import type { TestLabel } from "./metadata.js"; | ||
| import type { TestResult, TestStatus, TestStatusTransition } from "./model.js"; | ||
| export declare const EMPTY_VALUE = "<Empty>"; | ||
| export declare const STATUS_ORDER: Record<string, number>; | ||
| export declare const SEVERITY_ORDER: Record<string, number>; | ||
| export declare const TRANSITION_ORDER: Record<string, number>; | ||
| export declare const DEFAULT_ERROR_CATEGORIES: CategoryRule[]; | ||
| export type TestCategories = { | ||
| roots: string[]; | ||
| nodes: Record<string, CategoryNode>; | ||
| }; | ||
| export type CategoryMatchingData = { | ||
| status: TestStatus; | ||
| labels: readonly TestLabel[]; | ||
| message?: string; | ||
| trace?: string; | ||
| flaky: boolean; | ||
| duration?: number; | ||
| transition?: TestStatusTransition; | ||
| environment?: string; | ||
| }; | ||
| export type ObjectMatcher = { | ||
| statuses?: readonly TestStatus[]; | ||
| labels?: Record<string, string | RegExp>; | ||
| message?: string | RegExp; | ||
| trace?: string | RegExp; | ||
| flaky?: boolean; | ||
| transitions?: readonly TestStatusTransition[]; | ||
| environments?: readonly string[]; | ||
| }; | ||
| export type PredicateMatcher = (d: CategoryMatchingData) => boolean; | ||
| export type Matcher = ObjectMatcher | PredicateMatcher; | ||
| export type CategoryMatcher = Matcher | readonly Matcher[]; | ||
| export type CategoryGroupBuiltInSelector = "flaky" | "owner" | "severity" | "transition" | "status" | "environment" | "layer"; | ||
| export type CategoryGroupCustomSelector = { | ||
| label: string; | ||
| }; | ||
| export type CategoryGroupSelector = CategoryGroupBuiltInSelector | CategoryGroupCustomSelector; | ||
| export type CategoryRule = { | ||
| name: string; | ||
| matchers?: CategoryMatcher; | ||
| groupBy?: readonly CategoryGroupSelector[]; | ||
| groupByMessage?: boolean; | ||
| groupEnvironments?: boolean; | ||
| expand?: boolean; | ||
| hide?: boolean; | ||
| matchedStatuses?: readonly TestStatus[]; | ||
| messageRegex?: string; | ||
| traceRegex?: string; | ||
| flaky?: boolean; | ||
| }; | ||
| export type CategoriesStore = { | ||
| roots: string[]; | ||
| nodes: Record<string, CategoryNode>; | ||
| }; | ||
| export interface CategoryDefinition extends Pick<CategoryRule, "name" | "expand" | "hide" | "groupEnvironments"> { | ||
| matchers: Matcher[]; | ||
| groupBy: CategoryGroupSelector[]; | ||
| groupByMessage: boolean; | ||
| index: number; | ||
| } | ||
| export type CategoryNodeProps = { | ||
| nodeId: string; | ||
| store: CategoriesStore; | ||
| activeNodeId?: string; | ||
| depth?: number; | ||
| }; | ||
| export type CategoriesConfig = false | CategoryRule[] | { | ||
| rules: CategoryRule[]; | ||
| }; | ||
| export type CategoryNodeType = "category" | "group" | "history" | "message" | "tr"; | ||
| export type CategoryNodeItem = { | ||
| id: string; | ||
| type: CategoryNodeType; | ||
| name: string; | ||
| key?: string; | ||
| value?: string; | ||
| historyId?: string; | ||
| retriesCount?: number; | ||
| transition?: TestStatusTransition; | ||
| tooltips?: Record<string, string>; | ||
| statistic?: Statistic; | ||
| childrenIds?: string[]; | ||
| testId?: string; | ||
| expand?: boolean; | ||
| }; | ||
| export interface CategoryTr extends Pick<TestResult, "name" | "status" | "duration" | "id" | "flaky" | "transition"> { | ||
| } | ||
| export type CategoryNode = Partial<CategoryTr> & CategoryNodeItem; | ||
| type GroupSortKey = { | ||
| missingRank: number; | ||
| primaryRank: number; | ||
| alphaKey: string; | ||
| }; | ||
| export declare const normalizeCategoriesConfig: (cfg?: CategoriesConfig) => CategoryDefinition[]; | ||
| export declare const matchCategoryMatcher: (matcher: Matcher, d: CategoryMatchingData) => boolean; | ||
| export declare const matchCategory: (categories: CategoryDefinition[], d: CategoryMatchingData) => CategoryDefinition | undefined; | ||
| export declare const extractErrorMatchingData: (tr: Pick<TestResult, "status" | "labels" | "error" | "flaky" | "duration" | "transition" | "environment">) => CategoryMatchingData; | ||
| export declare const buildEnvironmentSortOrder: (environmentNames: string[], defaultEnvironmentName: string) => Map<string, number>; | ||
| export declare const compareNumbers: (left: number, right: number) => 0 | 1 | -1; | ||
| export declare const compareStrings: (left: string, right: string) => number; | ||
| export declare const isMissingValue: (value: string | undefined) => boolean; | ||
| export declare const getGroupSortKey: (groupKey: string | undefined, groupValue: string | undefined, environmentOrderMap?: Map<string, number>) => GroupSortKey; | ||
| export declare const compareChildNodes: (leftNodeId: string, rightNodeId: string, nodesById: Record<string, CategoryNode>, environmentOrderMap?: Map<string, number>) => number; | ||
| export {}; |
| export const EMPTY_VALUE = "<Empty>"; | ||
| export const STATUS_ORDER = { | ||
| failed: 0, | ||
| broken: 1, | ||
| passed: 2, | ||
| skipped: 3, | ||
| unknown: 4, | ||
| }; | ||
| export const SEVERITY_ORDER = { | ||
| blocker: 0, | ||
| critical: 1, | ||
| normal: 2, | ||
| minor: 3, | ||
| trivial: 4, | ||
| }; | ||
| export const TRANSITION_ORDER = { | ||
| regressed: 0, | ||
| malfunctioned: 1, | ||
| new: 2, | ||
| fixed: 3, | ||
| }; | ||
| export const DEFAULT_ERROR_CATEGORIES = [ | ||
| { | ||
| name: "Product errors", | ||
| matchers: { statuses: ["failed"] }, | ||
| }, | ||
| { | ||
| name: "Test errors", | ||
| matchers: { statuses: ["broken"] }, | ||
| }, | ||
| ]; | ||
| const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v); | ||
| const toRegExp = (v) => (v instanceof RegExp ? v : new RegExp(v)); | ||
| const isMatcherArray = (value) => Array.isArray(value); | ||
| const normalizeMatchers = (rule, index) => { | ||
| const compatKeysUsed = rule.matchedStatuses !== undefined || | ||
| rule.messageRegex !== undefined || | ||
| rule.traceRegex !== undefined || | ||
| rule.flaky !== undefined; | ||
| if (rule.matchers !== undefined && compatKeysUsed) { | ||
| throw new Error(`categories[${index}] mixes canonical keys with compatibility keys`); | ||
| } | ||
| let matchers = []; | ||
| if (rule.matchers !== undefined) { | ||
| if (isMatcherArray(rule.matchers)) { | ||
| matchers = [...rule.matchers]; | ||
| } | ||
| else { | ||
| matchers = [rule.matchers]; | ||
| } | ||
| } | ||
| else if (compatKeysUsed) { | ||
| const compatMatcher = {}; | ||
| if (rule.matchedStatuses) { | ||
| compatMatcher.statuses = rule.matchedStatuses; | ||
| } | ||
| if (rule.messageRegex !== undefined) { | ||
| compatMatcher.message = rule.messageRegex; | ||
| } | ||
| if (rule.traceRegex !== undefined) { | ||
| compatMatcher.trace = rule.traceRegex; | ||
| } | ||
| if (rule.flaky !== undefined) { | ||
| compatMatcher.flaky = rule.flaky; | ||
| } | ||
| matchers = [compatMatcher]; | ||
| } | ||
| if (matchers.length === 0) { | ||
| throw new Error(`categories[${index}] must define matchers`); | ||
| } | ||
| for (let i = 0; i < matchers.length; i++) { | ||
| const m = matchers[i]; | ||
| const ok = typeof m === "function" || isPlainObject(m); | ||
| if (!ok) { | ||
| throw new Error(`categories[${index}].matchers[${i}] must be object|function`); | ||
| } | ||
| } | ||
| return matchers; | ||
| }; | ||
| export const normalizeCategoriesConfig = (cfg) => { | ||
| if (cfg === false) { | ||
| return []; | ||
| } | ||
| const rawRules = Array.isArray(cfg) ? cfg : (cfg?.rules ?? []); | ||
| const rules = rawRules.length ? rawRules : []; | ||
| const normalized = []; | ||
| const seen = new Map(); | ||
| const applyRule = (rule, index) => { | ||
| if (!isPlainObject(rule)) { | ||
| throw new Error(`categories[${index}] must be an object`); | ||
| } | ||
| if (typeof rule.name !== "string" || !rule.name.trim()) { | ||
| throw new Error(`categories[${index}].name must be non-empty string`); | ||
| } | ||
| const matchers = normalizeMatchers(rule, index); | ||
| const existing = seen.get(rule.name); | ||
| if (existing) { | ||
| existing.matchers.push(...matchers); | ||
| return; | ||
| } | ||
| const BUILT_IN_GROUP_SELECTORS = new Set([ | ||
| "flaky", | ||
| "owner", | ||
| "severity", | ||
| "transition", | ||
| "status", | ||
| "environment", | ||
| "layer", | ||
| ]); | ||
| const groupBy = Array.isArray(rule.groupBy) ? [...rule.groupBy] : []; | ||
| for (const selector of groupBy) { | ||
| const isBuiltIn = typeof selector === "string" && BUILT_IN_GROUP_SELECTORS.has(selector); | ||
| const isCustom = isPlainObject(selector) && | ||
| typeof selector.label === "string" && | ||
| selector.label.trim().length > 0; | ||
| if (!isBuiltIn && !isCustom) { | ||
| throw new Error(`categories[${index}].groupBy contains invalid selector`); | ||
| } | ||
| } | ||
| const norm = { | ||
| name: rule.name, | ||
| matchers, | ||
| groupBy, | ||
| groupByMessage: rule.groupByMessage ?? true, | ||
| groupEnvironments: rule.groupEnvironments, | ||
| expand: rule.expand ?? false, | ||
| hide: rule.hide ?? false, | ||
| index, | ||
| }; | ||
| seen.set(rule.name, norm); | ||
| normalized.push(norm); | ||
| }; | ||
| rules.forEach(applyRule); | ||
| DEFAULT_ERROR_CATEGORIES.forEach((rule, index) => applyRule(rule, rules.length + index)); | ||
| return normalized; | ||
| }; | ||
| const matchObjectMatcher = (m, d) => { | ||
| if (m.statuses && !m.statuses.includes(d.status)) { | ||
| return false; | ||
| } | ||
| if (m.flaky !== undefined && m.flaky !== d.flaky) { | ||
| return false; | ||
| } | ||
| if (m.labels) { | ||
| for (const [labelName, expected] of Object.entries(m.labels)) { | ||
| const re = toRegExp(expected); | ||
| const values = d.labels.filter((l) => l.name === labelName).map((l) => l.value ?? ""); | ||
| if (!values.some((v) => re.test(v))) { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| if (m.message !== undefined) { | ||
| const re = toRegExp(m.message); | ||
| if (!re.test(d.message ?? "")) { | ||
| return false; | ||
| } | ||
| } | ||
| if (m.trace !== undefined) { | ||
| const re = toRegExp(m.trace); | ||
| if (!re.test(d.trace ?? "")) { | ||
| return false; | ||
| } | ||
| } | ||
| if (m.transitions && !m.transitions.includes(d.transition)) { | ||
| return false; | ||
| } | ||
| if (m.environments && !m.environments.includes(d.environment ?? EMPTY_VALUE)) { | ||
| return false; | ||
| } | ||
| return true; | ||
| }; | ||
| export const matchCategoryMatcher = (matcher, d) => { | ||
| if (typeof matcher === "function") { | ||
| return matcher(d); | ||
| } | ||
| if (isPlainObject(matcher)) { | ||
| return matchObjectMatcher(matcher, d); | ||
| } | ||
| return false; | ||
| }; | ||
| export const matchCategory = (categories, d) => { | ||
| for (const c of categories) { | ||
| if (c.matchers.some((m) => matchCategoryMatcher(m, d))) { | ||
| return c; | ||
| } | ||
| } | ||
| return undefined; | ||
| }; | ||
| export const extractErrorMatchingData = (tr) => { | ||
| const { message, trace } = tr.error ?? {}; | ||
| const labels = Array.isArray(tr.labels) | ||
| ? tr.labels.map((l) => ({ name: l.name, value: l.value ?? "" })) | ||
| : []; | ||
| return { | ||
| status: tr.status, | ||
| labels, | ||
| message, | ||
| trace, | ||
| flaky: tr.flaky, | ||
| duration: tr.duration, | ||
| transition: tr.transition, | ||
| environment: tr.environment, | ||
| }; | ||
| }; | ||
| export const buildEnvironmentSortOrder = (environmentNames, defaultEnvironmentName) => { | ||
| const orderMap = new Map(); | ||
| for (let index = 0; index < environmentNames.length; index++) { | ||
| orderMap.set(environmentNames[index], index); | ||
| } | ||
| const missingEnvironmentRank = environmentNames.length; | ||
| const defaultEnvironmentRank = environmentNames.length + 1; | ||
| orderMap.set(EMPTY_VALUE, missingEnvironmentRank); | ||
| orderMap.set(defaultEnvironmentName, defaultEnvironmentRank); | ||
| return orderMap; | ||
| }; | ||
| export const compareNumbers = (left, right) => (left < right ? -1 : left > right ? 1 : 0); | ||
| export const compareStrings = (left, right) => left.localeCompare(right); | ||
| export const isMissingValue = (value) => (value ?? EMPTY_VALUE) === EMPTY_VALUE; | ||
| export const getGroupSortKey = (groupKey, groupValue, environmentOrderMap) => { | ||
| const normalizedValue = groupValue ?? EMPTY_VALUE; | ||
| const missingRank = normalizedValue === EMPTY_VALUE ? 1 : 0; | ||
| if (groupKey === "status") { | ||
| const primaryRank = STATUS_ORDER[normalizedValue] ?? 999; | ||
| return { primaryRank, missingRank, alphaKey: normalizedValue }; | ||
| } | ||
| if (groupKey === "severity") { | ||
| const primaryRank = SEVERITY_ORDER[normalizedValue] ?? 999; | ||
| return { primaryRank, missingRank, alphaKey: normalizedValue }; | ||
| } | ||
| if (groupKey === "transition") { | ||
| const primaryRank = TRANSITION_ORDER[normalizedValue] ?? 999; | ||
| return { primaryRank, missingRank, alphaKey: normalizedValue }; | ||
| } | ||
| if (groupKey === "flaky") { | ||
| const primaryRank = normalizedValue === "true" ? 0 : 1; | ||
| return { primaryRank, missingRank, alphaKey: normalizedValue }; | ||
| } | ||
| if (groupKey === "environment") { | ||
| if (environmentOrderMap) { | ||
| const primaryRank = environmentOrderMap.get(normalizedValue) ?? 1000; | ||
| return { primaryRank, missingRank: 0, alphaKey: normalizedValue }; | ||
| } | ||
| return { primaryRank: 0, missingRank, alphaKey: normalizedValue }; | ||
| } | ||
| return { primaryRank: 0, missingRank, alphaKey: normalizedValue }; | ||
| }; | ||
| export const compareChildNodes = (leftNodeId, rightNodeId, nodesById, environmentOrderMap) => { | ||
| const leftNode = nodesById[leftNodeId]; | ||
| const rightNode = nodesById[rightNodeId]; | ||
| const leftType = leftNode?.type ?? ""; | ||
| const rightType = rightNode?.type ?? ""; | ||
| if (leftType === "message" && rightType === "message") { | ||
| const leftTotal = leftNode.statistic?.total ?? 0; | ||
| const rightTotal = rightNode.statistic?.total ?? 0; | ||
| const byCountDescending = compareNumbers(rightTotal, leftTotal); | ||
| if (byCountDescending !== 0) { | ||
| return byCountDescending; | ||
| } | ||
| const byNameMessage = compareStrings(leftNode.name ?? "", rightNode.name ?? ""); | ||
| if (byNameMessage !== 0) { | ||
| return byNameMessage; | ||
| } | ||
| return compareStrings(leftNodeId, rightNodeId); | ||
| } | ||
| if (leftType === "tr" && rightType === "tr") { | ||
| const leftKey = leftNode.key; | ||
| const rightKey = rightNode.key; | ||
| if (leftKey === "environment" && rightKey === "environment") { | ||
| const leftSortKey = getGroupSortKey("environment", leftNode.value, environmentOrderMap); | ||
| const rightSortKey = getGroupSortKey("environment", rightNode.value, environmentOrderMap); | ||
| const byPrimaryRank = compareNumbers(leftSortKey.primaryRank, rightSortKey.primaryRank); | ||
| if (byPrimaryRank !== 0) { | ||
| return byPrimaryRank; | ||
| } | ||
| const byMissingLast = compareNumbers(leftSortKey.missingRank, rightSortKey.missingRank); | ||
| if (byMissingLast !== 0) { | ||
| return byMissingLast; | ||
| } | ||
| const byAlpha = compareStrings(leftSortKey.alphaKey, rightSortKey.alphaKey); | ||
| if (byAlpha !== 0) { | ||
| return byAlpha; | ||
| } | ||
| return compareStrings(leftNodeId, rightNodeId); | ||
| } | ||
| } | ||
| if (leftType === "group" && rightType === "group") { | ||
| const leftGroupKey = leftNode.key ?? ""; | ||
| const rightGroupKey = rightNode.key ?? ""; | ||
| const byGroupKey = compareStrings(leftGroupKey, rightGroupKey); | ||
| if (byGroupKey !== 0) { | ||
| return byGroupKey; | ||
| } | ||
| const leftSortKey = getGroupSortKey(leftGroupKey, leftNode.value, environmentOrderMap); | ||
| const rightSortKey = getGroupSortKey(rightGroupKey, rightNode.value, environmentOrderMap); | ||
| const byPrimaryRank = compareNumbers(leftSortKey.primaryRank, rightSortKey.primaryRank); | ||
| if (byPrimaryRank !== 0) { | ||
| return byPrimaryRank; | ||
| } | ||
| const byMissingLast = compareNumbers(leftSortKey.missingRank, rightSortKey.missingRank); | ||
| if (byMissingLast !== 0) { | ||
| return byMissingLast; | ||
| } | ||
| const byAlpha = compareStrings(leftSortKey.alphaKey, rightSortKey.alphaKey); | ||
| if (byAlpha !== 0) { | ||
| return byAlpha; | ||
| } | ||
| return compareStrings(leftNodeId, rightNodeId); | ||
| } | ||
| const byType = compareStrings(leftType, rightType); | ||
| if (byType !== 0) { | ||
| return byType; | ||
| } | ||
| const byName = compareStrings(leftNode?.name ?? "", rightNode?.name ?? ""); | ||
| if (byName !== 0) { | ||
| return byName; | ||
| } | ||
| return compareStrings(leftNodeId, rightNodeId); | ||
| }; |
| export declare const createDictionary: <T>() => Record<string, T>; |
| export const createDictionary = () => Object.create(null); |
@@ -28,4 +28,6 @@ import type { TestLabel } from "./metadata.js"; | ||
| export interface AllureHistory { | ||
| readHistory(branch?: string): Promise<HistoryDataPoint[]>; | ||
| appendHistory(history: HistoryDataPoint, branch?: string): Promise<void>; | ||
| readHistory(params?: { | ||
| branch?: string; | ||
| }): Promise<HistoryDataPoint[]>; | ||
| appendHistory(history: HistoryDataPoint): Promise<void>; | ||
| } |
+2
-0
@@ -13,2 +13,3 @@ export type * from "./aggregate.js"; | ||
| export * from "./static.js"; | ||
| export * from "./categories.js"; | ||
| export * from "./utils/step.js"; | ||
@@ -25,1 +26,2 @@ export type * from "./utils/tree.js"; | ||
| export * from "./utils/strings.js"; | ||
| export * from "./utils/dictionary.js"; |
+2
-0
| export * from "./constants.js"; | ||
| export * from "./ci.js"; | ||
| export * from "./static.js"; | ||
| export * from "./categories.js"; | ||
| export * from "./utils/step.js"; | ||
@@ -14,1 +15,2 @@ export * from "./utils/time.js"; | ||
| export * from "./utils/strings.js"; | ||
| export * from "./utils/dictionary.js"; |
| import type { TestLabel } from "../index.js"; | ||
| export declare const findByLabelName: (labels: TestLabel[], name: string) => string | undefined; | ||
| export declare const findLastByLabelName: (labels: TestLabel[], name: string) => string | undefined; |
| export const findByLabelName = (labels, name) => { | ||
| return labels.find((label) => label.name === name)?.value; | ||
| }; | ||
| export const findLastByLabelName = (labels, name) => { | ||
| for (let i = labels.length - 1; i >= 0; i -= 1) { | ||
| if (labels[i].name === name) { | ||
| return labels[i].value; | ||
| } | ||
| } | ||
| return undefined; | ||
| }; |
+1
-1
| { | ||
| "name": "@allurereport/core-api", | ||
| "version": "3.2.0", | ||
| "version": "3.3.1", | ||
| "description": "Allure Core API", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
40862
69.71%54
8%1083
68.95%