@allurereport/core-api 3.2.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ import type { Statistic } from "./aggregate.js";
2
+ import type { TestLabel } from "./metadata.js";
3
+ import type { TestResult, TestStatus, TestStatusTransition } from "./model.js";
4
+ export declare const EMPTY_VALUE = "<Empty>";
5
+ export declare const STATUS_ORDER: Record<string, number>;
6
+ export declare const SEVERITY_ORDER: Record<string, number>;
7
+ export declare const TRANSITION_ORDER: Record<string, number>;
8
+ export declare const DEFAULT_ERROR_CATEGORIES: CategoryRule[];
9
+ export type TestCategories = {
10
+ roots: string[];
11
+ nodes: Record<string, CategoryNode>;
12
+ };
13
+ export type CategoryMatchingData = {
14
+ status: TestStatus;
15
+ labels: readonly TestLabel[];
16
+ message?: string;
17
+ trace?: string;
18
+ flaky: boolean;
19
+ duration?: number;
20
+ transition?: TestStatusTransition;
21
+ environment?: string;
22
+ };
23
+ export type ObjectMatcher = {
24
+ statuses?: readonly TestStatus[];
25
+ labels?: Record<string, string | RegExp>;
26
+ message?: string | RegExp;
27
+ trace?: string | RegExp;
28
+ flaky?: boolean;
29
+ transitions?: readonly TestStatusTransition[];
30
+ environments?: readonly string[];
31
+ };
32
+ export type PredicateMatcher = (d: CategoryMatchingData) => boolean;
33
+ export type Matcher = ObjectMatcher | PredicateMatcher;
34
+ export type CategoryMatcher = Matcher | readonly Matcher[];
35
+ export type CategoryGroupBuiltInSelector = "flaky" | "owner" | "severity" | "transition" | "status" | "environment" | "layer";
36
+ export type CategoryGroupCustomSelector = {
37
+ label: string;
38
+ };
39
+ export type CategoryGroupSelector = CategoryGroupBuiltInSelector | CategoryGroupCustomSelector;
40
+ export type CategoryRule = {
41
+ name: string;
42
+ matchers?: CategoryMatcher;
43
+ groupBy?: readonly CategoryGroupSelector[];
44
+ groupByMessage?: boolean;
45
+ groupEnvironments?: boolean;
46
+ expand?: boolean;
47
+ hide?: boolean;
48
+ matchedStatuses?: readonly TestStatus[];
49
+ messageRegex?: string;
50
+ traceRegex?: string;
51
+ flaky?: boolean;
52
+ };
53
+ export type CategoriesStore = {
54
+ roots: string[];
55
+ nodes: Record<string, CategoryNode>;
56
+ };
57
+ export interface CategoryDefinition extends Pick<CategoryRule, "name" | "expand" | "hide" | "groupEnvironments"> {
58
+ matchers: Matcher[];
59
+ groupBy: CategoryGroupSelector[];
60
+ groupByMessage: boolean;
61
+ index: number;
62
+ }
63
+ export type CategoryNodeProps = {
64
+ nodeId: string;
65
+ store: CategoriesStore;
66
+ activeNodeId?: string;
67
+ depth?: number;
68
+ };
69
+ export type CategoriesConfig = false | CategoryRule[] | {
70
+ rules: CategoryRule[];
71
+ };
72
+ export type CategoryNodeType = "category" | "group" | "history" | "message" | "tr";
73
+ export type CategoryNodeItem = {
74
+ id: string;
75
+ type: CategoryNodeType;
76
+ name: string;
77
+ key?: string;
78
+ value?: string;
79
+ historyId?: string;
80
+ retriesCount?: number;
81
+ transition?: TestStatusTransition;
82
+ tooltips?: Record<string, string>;
83
+ statistic?: Statistic;
84
+ childrenIds?: string[];
85
+ testId?: string;
86
+ expand?: boolean;
87
+ };
88
+ export interface CategoryTr extends Pick<TestResult, "name" | "status" | "duration" | "id" | "flaky" | "transition"> {
89
+ }
90
+ export type CategoryNode = Partial<CategoryTr> & CategoryNodeItem;
91
+ type GroupSortKey = {
92
+ missingRank: number;
93
+ primaryRank: number;
94
+ alphaKey: string;
95
+ };
96
+ export declare const normalizeCategoriesConfig: (cfg?: CategoriesConfig) => CategoryDefinition[];
97
+ export declare const matchCategoryMatcher: (matcher: Matcher, d: CategoryMatchingData) => boolean;
98
+ export declare const matchCategory: (categories: CategoryDefinition[], d: CategoryMatchingData) => CategoryDefinition | undefined;
99
+ export declare const extractErrorMatchingData: (tr: Pick<TestResult, "status" | "labels" | "error" | "flaky" | "duration" | "transition" | "environment">) => CategoryMatchingData;
100
+ export declare const buildEnvironmentSortOrder: (environmentNames: string[], defaultEnvironmentName: string) => Map<string, number>;
101
+ export declare const compareNumbers: (left: number, right: number) => 0 | 1 | -1;
102
+ export declare const compareStrings: (left: string, right: string) => number;
103
+ export declare const isMissingValue: (value: string | undefined) => boolean;
104
+ export declare const getGroupSortKey: (groupKey: string | undefined, groupValue: string | undefined, environmentOrderMap?: Map<string, number>) => GroupSortKey;
105
+ export declare const compareChildNodes: (leftNodeId: string, rightNodeId: string, nodesById: Record<string, CategoryNode>, environmentOrderMap?: Map<string, number>) => number;
106
+ export {};
@@ -0,0 +1,319 @@
1
+ export const EMPTY_VALUE = "<Empty>";
2
+ export const STATUS_ORDER = {
3
+ failed: 0,
4
+ broken: 1,
5
+ passed: 2,
6
+ skipped: 3,
7
+ unknown: 4,
8
+ };
9
+ export const SEVERITY_ORDER = {
10
+ blocker: 0,
11
+ critical: 1,
12
+ normal: 2,
13
+ minor: 3,
14
+ trivial: 4,
15
+ };
16
+ export const TRANSITION_ORDER = {
17
+ regressed: 0,
18
+ malfunctioned: 1,
19
+ new: 2,
20
+ fixed: 3,
21
+ };
22
+ export const DEFAULT_ERROR_CATEGORIES = [
23
+ {
24
+ name: "Product errors",
25
+ matchers: { statuses: ["failed"] },
26
+ },
27
+ {
28
+ name: "Test errors",
29
+ matchers: { statuses: ["broken"] },
30
+ },
31
+ ];
32
+ const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
33
+ const toRegExp = (v) => (v instanceof RegExp ? v : new RegExp(v));
34
+ const isMatcherArray = (value) => Array.isArray(value);
35
+ const normalizeMatchers = (rule, index) => {
36
+ const compatKeysUsed = rule.matchedStatuses !== undefined ||
37
+ rule.messageRegex !== undefined ||
38
+ rule.traceRegex !== undefined ||
39
+ rule.flaky !== undefined;
40
+ if (rule.matchers !== undefined && compatKeysUsed) {
41
+ throw new Error(`categories[${index}] mixes canonical keys with compatibility keys`);
42
+ }
43
+ let matchers = [];
44
+ if (rule.matchers !== undefined) {
45
+ if (isMatcherArray(rule.matchers)) {
46
+ matchers = [...rule.matchers];
47
+ }
48
+ else {
49
+ matchers = [rule.matchers];
50
+ }
51
+ }
52
+ else if (compatKeysUsed) {
53
+ const compatMatcher = {};
54
+ if (rule.matchedStatuses) {
55
+ compatMatcher.statuses = rule.matchedStatuses;
56
+ }
57
+ if (rule.messageRegex !== undefined) {
58
+ compatMatcher.message = rule.messageRegex;
59
+ }
60
+ if (rule.traceRegex !== undefined) {
61
+ compatMatcher.trace = rule.traceRegex;
62
+ }
63
+ if (rule.flaky !== undefined) {
64
+ compatMatcher.flaky = rule.flaky;
65
+ }
66
+ matchers = [compatMatcher];
67
+ }
68
+ if (matchers.length === 0) {
69
+ throw new Error(`categories[${index}] must define matchers`);
70
+ }
71
+ for (let i = 0; i < matchers.length; i++) {
72
+ const m = matchers[i];
73
+ const ok = typeof m === "function" || isPlainObject(m);
74
+ if (!ok) {
75
+ throw new Error(`categories[${index}].matchers[${i}] must be object|function`);
76
+ }
77
+ }
78
+ return matchers;
79
+ };
80
+ export const normalizeCategoriesConfig = (cfg) => {
81
+ if (cfg === false) {
82
+ return [];
83
+ }
84
+ const rawRules = Array.isArray(cfg) ? cfg : (cfg?.rules ?? []);
85
+ const rules = rawRules.length ? rawRules : [];
86
+ const normalized = [];
87
+ const seen = new Map();
88
+ const applyRule = (rule, index) => {
89
+ if (!isPlainObject(rule)) {
90
+ throw new Error(`categories[${index}] must be an object`);
91
+ }
92
+ if (typeof rule.name !== "string" || !rule.name.trim()) {
93
+ throw new Error(`categories[${index}].name must be non-empty string`);
94
+ }
95
+ const matchers = normalizeMatchers(rule, index);
96
+ const existing = seen.get(rule.name);
97
+ if (existing) {
98
+ existing.matchers.push(...matchers);
99
+ return;
100
+ }
101
+ const BUILT_IN_GROUP_SELECTORS = new Set([
102
+ "flaky",
103
+ "owner",
104
+ "severity",
105
+ "transition",
106
+ "status",
107
+ "environment",
108
+ "layer",
109
+ ]);
110
+ const groupBy = Array.isArray(rule.groupBy) ? [...rule.groupBy] : [];
111
+ for (const selector of groupBy) {
112
+ const isBuiltIn = typeof selector === "string" && BUILT_IN_GROUP_SELECTORS.has(selector);
113
+ const isCustom = isPlainObject(selector) &&
114
+ typeof selector.label === "string" &&
115
+ selector.label.trim().length > 0;
116
+ if (!isBuiltIn && !isCustom) {
117
+ throw new Error(`categories[${index}].groupBy contains invalid selector`);
118
+ }
119
+ }
120
+ const norm = {
121
+ name: rule.name,
122
+ matchers,
123
+ groupBy,
124
+ groupByMessage: rule.groupByMessage ?? true,
125
+ groupEnvironments: rule.groupEnvironments,
126
+ expand: rule.expand ?? false,
127
+ hide: rule.hide ?? false,
128
+ index,
129
+ };
130
+ seen.set(rule.name, norm);
131
+ normalized.push(norm);
132
+ };
133
+ rules.forEach(applyRule);
134
+ DEFAULT_ERROR_CATEGORIES.forEach((rule, index) => applyRule(rule, rules.length + index));
135
+ return normalized;
136
+ };
137
+ const matchObjectMatcher = (m, d) => {
138
+ if (m.statuses && !m.statuses.includes(d.status)) {
139
+ return false;
140
+ }
141
+ if (m.flaky !== undefined && m.flaky !== d.flaky) {
142
+ return false;
143
+ }
144
+ if (m.labels) {
145
+ for (const [labelName, expected] of Object.entries(m.labels)) {
146
+ const re = toRegExp(expected);
147
+ const values = d.labels.filter((l) => l.name === labelName).map((l) => l.value ?? "");
148
+ if (!values.some((v) => re.test(v))) {
149
+ return false;
150
+ }
151
+ }
152
+ }
153
+ if (m.message !== undefined) {
154
+ const re = toRegExp(m.message);
155
+ if (!re.test(d.message ?? "")) {
156
+ return false;
157
+ }
158
+ }
159
+ if (m.trace !== undefined) {
160
+ const re = toRegExp(m.trace);
161
+ if (!re.test(d.trace ?? "")) {
162
+ return false;
163
+ }
164
+ }
165
+ if (m.transitions && !m.transitions.includes(d.transition)) {
166
+ return false;
167
+ }
168
+ if (m.environments && !m.environments.includes(d.environment ?? EMPTY_VALUE)) {
169
+ return false;
170
+ }
171
+ return true;
172
+ };
173
+ export const matchCategoryMatcher = (matcher, d) => {
174
+ if (typeof matcher === "function") {
175
+ return matcher(d);
176
+ }
177
+ if (isPlainObject(matcher)) {
178
+ return matchObjectMatcher(matcher, d);
179
+ }
180
+ return false;
181
+ };
182
+ export const matchCategory = (categories, d) => {
183
+ for (const c of categories) {
184
+ if (c.matchers.some((m) => matchCategoryMatcher(m, d))) {
185
+ return c;
186
+ }
187
+ }
188
+ return undefined;
189
+ };
190
+ export const extractErrorMatchingData = (tr) => {
191
+ const { message, trace } = tr.error ?? {};
192
+ const labels = Array.isArray(tr.labels)
193
+ ? tr.labels.map((l) => ({ name: l.name, value: l.value ?? "" }))
194
+ : [];
195
+ return {
196
+ status: tr.status,
197
+ labels,
198
+ message,
199
+ trace,
200
+ flaky: tr.flaky,
201
+ duration: tr.duration,
202
+ transition: tr.transition,
203
+ environment: tr.environment,
204
+ };
205
+ };
206
+ export const buildEnvironmentSortOrder = (environmentNames, defaultEnvironmentName) => {
207
+ const orderMap = new Map();
208
+ for (let index = 0; index < environmentNames.length; index++) {
209
+ orderMap.set(environmentNames[index], index);
210
+ }
211
+ const missingEnvironmentRank = environmentNames.length;
212
+ const defaultEnvironmentRank = environmentNames.length + 1;
213
+ orderMap.set(EMPTY_VALUE, missingEnvironmentRank);
214
+ orderMap.set(defaultEnvironmentName, defaultEnvironmentRank);
215
+ return orderMap;
216
+ };
217
+ export const compareNumbers = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
218
+ export const compareStrings = (left, right) => left.localeCompare(right);
219
+ export const isMissingValue = (value) => (value ?? EMPTY_VALUE) === EMPTY_VALUE;
220
+ export const getGroupSortKey = (groupKey, groupValue, environmentOrderMap) => {
221
+ const normalizedValue = groupValue ?? EMPTY_VALUE;
222
+ const missingRank = normalizedValue === EMPTY_VALUE ? 1 : 0;
223
+ if (groupKey === "status") {
224
+ const primaryRank = STATUS_ORDER[normalizedValue] ?? 999;
225
+ return { primaryRank, missingRank, alphaKey: normalizedValue };
226
+ }
227
+ if (groupKey === "severity") {
228
+ const primaryRank = SEVERITY_ORDER[normalizedValue] ?? 999;
229
+ return { primaryRank, missingRank, alphaKey: normalizedValue };
230
+ }
231
+ if (groupKey === "transition") {
232
+ const primaryRank = TRANSITION_ORDER[normalizedValue] ?? 999;
233
+ return { primaryRank, missingRank, alphaKey: normalizedValue };
234
+ }
235
+ if (groupKey === "flaky") {
236
+ const primaryRank = normalizedValue === "true" ? 0 : 1;
237
+ return { primaryRank, missingRank, alphaKey: normalizedValue };
238
+ }
239
+ if (groupKey === "environment") {
240
+ if (environmentOrderMap) {
241
+ const primaryRank = environmentOrderMap.get(normalizedValue) ?? 1000;
242
+ return { primaryRank, missingRank: 0, alphaKey: normalizedValue };
243
+ }
244
+ return { primaryRank: 0, missingRank, alphaKey: normalizedValue };
245
+ }
246
+ return { primaryRank: 0, missingRank, alphaKey: normalizedValue };
247
+ };
248
+ export const compareChildNodes = (leftNodeId, rightNodeId, nodesById, environmentOrderMap) => {
249
+ const leftNode = nodesById[leftNodeId];
250
+ const rightNode = nodesById[rightNodeId];
251
+ const leftType = leftNode?.type ?? "";
252
+ const rightType = rightNode?.type ?? "";
253
+ if (leftType === "message" && rightType === "message") {
254
+ const leftTotal = leftNode.statistic?.total ?? 0;
255
+ const rightTotal = rightNode.statistic?.total ?? 0;
256
+ const byCountDescending = compareNumbers(rightTotal, leftTotal);
257
+ if (byCountDescending !== 0) {
258
+ return byCountDescending;
259
+ }
260
+ const byNameMessage = compareStrings(leftNode.name ?? "", rightNode.name ?? "");
261
+ if (byNameMessage !== 0) {
262
+ return byNameMessage;
263
+ }
264
+ return compareStrings(leftNodeId, rightNodeId);
265
+ }
266
+ if (leftType === "tr" && rightType === "tr") {
267
+ const leftKey = leftNode.key;
268
+ const rightKey = rightNode.key;
269
+ if (leftKey === "environment" && rightKey === "environment") {
270
+ const leftSortKey = getGroupSortKey("environment", leftNode.value, environmentOrderMap);
271
+ const rightSortKey = getGroupSortKey("environment", rightNode.value, environmentOrderMap);
272
+ const byPrimaryRank = compareNumbers(leftSortKey.primaryRank, rightSortKey.primaryRank);
273
+ if (byPrimaryRank !== 0) {
274
+ return byPrimaryRank;
275
+ }
276
+ const byMissingLast = compareNumbers(leftSortKey.missingRank, rightSortKey.missingRank);
277
+ if (byMissingLast !== 0) {
278
+ return byMissingLast;
279
+ }
280
+ const byAlpha = compareStrings(leftSortKey.alphaKey, rightSortKey.alphaKey);
281
+ if (byAlpha !== 0) {
282
+ return byAlpha;
283
+ }
284
+ return compareStrings(leftNodeId, rightNodeId);
285
+ }
286
+ }
287
+ if (leftType === "group" && rightType === "group") {
288
+ const leftGroupKey = leftNode.key ?? "";
289
+ const rightGroupKey = rightNode.key ?? "";
290
+ const byGroupKey = compareStrings(leftGroupKey, rightGroupKey);
291
+ if (byGroupKey !== 0) {
292
+ return byGroupKey;
293
+ }
294
+ const leftSortKey = getGroupSortKey(leftGroupKey, leftNode.value, environmentOrderMap);
295
+ const rightSortKey = getGroupSortKey(rightGroupKey, rightNode.value, environmentOrderMap);
296
+ const byPrimaryRank = compareNumbers(leftSortKey.primaryRank, rightSortKey.primaryRank);
297
+ if (byPrimaryRank !== 0) {
298
+ return byPrimaryRank;
299
+ }
300
+ const byMissingLast = compareNumbers(leftSortKey.missingRank, rightSortKey.missingRank);
301
+ if (byMissingLast !== 0) {
302
+ return byMissingLast;
303
+ }
304
+ const byAlpha = compareStrings(leftSortKey.alphaKey, rightSortKey.alphaKey);
305
+ if (byAlpha !== 0) {
306
+ return byAlpha;
307
+ }
308
+ return compareStrings(leftNodeId, rightNodeId);
309
+ }
310
+ const byType = compareStrings(leftType, rightType);
311
+ if (byType !== 0) {
312
+ return byType;
313
+ }
314
+ const byName = compareStrings(leftNode?.name ?? "", rightNode?.name ?? "");
315
+ if (byName !== 0) {
316
+ return byName;
317
+ }
318
+ return compareStrings(leftNodeId, rightNodeId);
319
+ };
package/dist/history.d.ts CHANGED
@@ -25,6 +25,8 @@ export interface HistoryDataPoint {
25
25
  url: string;
26
26
  }
27
27
  export interface AllureHistory {
28
- readHistory(branch?: string): Promise<HistoryDataPoint[]>;
29
- appendHistory(history: HistoryDataPoint, branch?: string): Promise<void>;
28
+ readHistory(params?: {
29
+ branch?: string;
30
+ }): Promise<HistoryDataPoint[]>;
31
+ appendHistory(history: HistoryDataPoint): Promise<void>;
30
32
  }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export type * from "./testCase.js";
10
10
  export type * from "./testPlan.js";
11
11
  export type * from "./config.js";
12
12
  export * from "./static.js";
13
+ export * from "./categories.js";
13
14
  export * from "./utils/step.js";
14
15
  export type * from "./utils/tree.js";
15
16
  export * from "./utils/time.js";
@@ -21,3 +22,4 @@ export * from "./utils/status.js";
21
22
  export * from "./utils/environment.js";
22
23
  export * from "./utils/history.js";
23
24
  export * from "./utils/strings.js";
25
+ export * from "./utils/dictionary.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./constants.js";
2
2
  export * from "./ci.js";
3
3
  export * from "./static.js";
4
+ export * from "./categories.js";
4
5
  export * from "./utils/step.js";
5
6
  export * from "./utils/time.js";
6
7
  export * from "./utils/comparator.js";
@@ -11,3 +12,4 @@ export * from "./utils/status.js";
11
12
  export * from "./utils/environment.js";
12
13
  export * from "./utils/history.js";
13
14
  export * from "./utils/strings.js";
15
+ export * from "./utils/dictionary.js";
@@ -0,0 +1 @@
1
+ export declare const createDictionary: <T>() => Record<string, T>;
@@ -0,0 +1 @@
1
+ export const createDictionary = () => Object.create(null);
@@ -1,2 +1,3 @@
1
1
  import type { TestLabel } from "../index.js";
2
2
  export declare const findByLabelName: (labels: TestLabel[], name: string) => string | undefined;
3
+ export declare const findLastByLabelName: (labels: TestLabel[], name: string) => string | undefined;
@@ -1,3 +1,11 @@
1
1
  export const findByLabelName = (labels, name) => {
2
2
  return labels.find((label) => label.name === name)?.value;
3
3
  };
4
+ export const findLastByLabelName = (labels, name) => {
5
+ for (let i = labels.length - 1; i >= 0; i -= 1) {
6
+ if (labels[i].name === name) {
7
+ return labels[i].value;
8
+ }
9
+ }
10
+ return undefined;
11
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/core-api",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
4
4
  "description": "Allure Core API",
5
5
  "keywords": [
6
6
  "allure"