@allurereport/core-api 3.2.0 → 3.4.0

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,108 @@
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
+ id?: string;
42
+ name: string;
43
+ matchers?: CategoryMatcher;
44
+ groupBy?: readonly CategoryGroupSelector[];
45
+ groupByMessage?: boolean;
46
+ groupEnvironments?: boolean;
47
+ expand?: boolean;
48
+ hide?: boolean;
49
+ matchedStatuses?: readonly TestStatus[];
50
+ messageRegex?: string;
51
+ traceRegex?: string;
52
+ flaky?: boolean;
53
+ };
54
+ export type CategoriesStore = {
55
+ roots: string[];
56
+ nodes: Record<string, CategoryNode>;
57
+ };
58
+ export interface CategoryDefinition extends Pick<CategoryRule, "name" | "expand" | "hide" | "groupEnvironments"> {
59
+ id: string;
60
+ matchers: Matcher[];
61
+ groupBy: CategoryGroupSelector[];
62
+ groupByMessage: boolean;
63
+ index: number;
64
+ }
65
+ export type CategoryNodeProps = {
66
+ nodeId: string;
67
+ store: CategoriesStore;
68
+ activeNodeId?: string;
69
+ depth?: number;
70
+ };
71
+ export type CategoriesConfig = false | CategoryRule[] | {
72
+ rules: CategoryRule[];
73
+ };
74
+ export type CategoryNodeType = "category" | "group" | "history" | "message" | "tr";
75
+ export type CategoryNodeItem = {
76
+ id: string;
77
+ type: CategoryNodeType;
78
+ name: string;
79
+ key?: string;
80
+ value?: string;
81
+ historyId?: string;
82
+ retriesCount?: number;
83
+ transition?: TestStatusTransition;
84
+ tooltips?: Record<string, string>;
85
+ statistic?: Statistic;
86
+ childrenIds?: string[];
87
+ testId?: string;
88
+ expand?: boolean;
89
+ };
90
+ export interface CategoryTr extends Pick<TestResult, "name" | "status" | "duration" | "id" | "flaky" | "transition"> {
91
+ }
92
+ export type CategoryNode = Partial<CategoryTr> & CategoryNodeItem;
93
+ type GroupSortKey = {
94
+ missingRank: number;
95
+ primaryRank: number;
96
+ alphaKey: string;
97
+ };
98
+ export declare const normalizeCategoriesConfig: (cfg?: CategoriesConfig) => CategoryDefinition[];
99
+ export declare const matchCategoryMatcher: (matcher: Matcher, d: CategoryMatchingData) => boolean;
100
+ export declare const matchCategory: (categories: CategoryDefinition[], d: CategoryMatchingData) => CategoryDefinition | undefined;
101
+ export declare const extractErrorMatchingData: (tr: Pick<TestResult, "status" | "labels" | "error" | "flaky" | "duration" | "transition" | "environment">) => CategoryMatchingData;
102
+ export declare const buildEnvironmentSortOrder: (environmentNames: string[], defaultEnvironmentName: string) => Map<string, number>;
103
+ export declare const compareNumbers: (left: number, right: number) => 0 | 1 | -1;
104
+ export declare const compareStrings: (left: string, right: string) => number;
105
+ export declare const isMissingValue: (value: string | undefined) => boolean;
106
+ export declare const getGroupSortKey: (groupKey: string | undefined, groupValue: string | undefined, environmentOrderMap?: Map<string, number>) => GroupSortKey;
107
+ export declare const compareChildNodes: (leftNodeId: string, rightNodeId: string, nodesById: Record<string, CategoryNode>, environmentOrderMap?: Map<string, number>) => number;
108
+ export {};
@@ -0,0 +1,359 @@
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 hasControlChars = (value) => {
36
+ for (let index = 0; index < value.length; index++) {
37
+ const code = value.charCodeAt(index);
38
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
39
+ return true;
40
+ }
41
+ }
42
+ return false;
43
+ };
44
+ const normalizeCategoryId = (id) => {
45
+ if (typeof id !== "string") {
46
+ return { valid: false, reason: "id must be a string" };
47
+ }
48
+ const normalized = id.trim();
49
+ if (normalized.length === 0) {
50
+ return { valid: false, reason: "id must not be empty" };
51
+ }
52
+ if (hasControlChars(normalized)) {
53
+ return { valid: false, reason: "id must not contain control characters" };
54
+ }
55
+ return { valid: true, normalized };
56
+ };
57
+ const normalizeMatchers = (rule, index) => {
58
+ const compatKeysUsed = rule.matchedStatuses !== undefined ||
59
+ rule.messageRegex !== undefined ||
60
+ rule.traceRegex !== undefined ||
61
+ rule.flaky !== undefined;
62
+ if (rule.matchers !== undefined && compatKeysUsed) {
63
+ throw new Error(`categories[${index}] mixes canonical keys with compatibility keys`);
64
+ }
65
+ let matchers = [];
66
+ if (rule.matchers !== undefined) {
67
+ if (isMatcherArray(rule.matchers)) {
68
+ matchers = [...rule.matchers];
69
+ }
70
+ else {
71
+ matchers = [rule.matchers];
72
+ }
73
+ }
74
+ else if (compatKeysUsed) {
75
+ const compatMatcher = {};
76
+ if (rule.matchedStatuses) {
77
+ compatMatcher.statuses = rule.matchedStatuses;
78
+ }
79
+ if (rule.messageRegex !== undefined) {
80
+ compatMatcher.message = rule.messageRegex;
81
+ }
82
+ if (rule.traceRegex !== undefined) {
83
+ compatMatcher.trace = rule.traceRegex;
84
+ }
85
+ if (rule.flaky !== undefined) {
86
+ compatMatcher.flaky = rule.flaky;
87
+ }
88
+ matchers = [compatMatcher];
89
+ }
90
+ if (matchers.length === 0) {
91
+ throw new Error(`categories[${index}] must define matchers`);
92
+ }
93
+ for (let i = 0; i < matchers.length; i++) {
94
+ const m = matchers[i];
95
+ const ok = typeof m === "function" || isPlainObject(m);
96
+ if (!ok) {
97
+ throw new Error(`categories[${index}].matchers[${i}] must be object|function`);
98
+ }
99
+ }
100
+ return matchers;
101
+ };
102
+ export const normalizeCategoriesConfig = (cfg) => {
103
+ if (cfg === false) {
104
+ return [];
105
+ }
106
+ const rawRules = Array.isArray(cfg) ? cfg : (cfg?.rules ?? []);
107
+ const rules = rawRules.length ? rawRules : [];
108
+ const normalized = [];
109
+ const seen = new Map();
110
+ const sourceIdsByNormalizedId = new Map();
111
+ const applyRule = (rule, index) => {
112
+ if (!isPlainObject(rule)) {
113
+ throw new Error(`categories[${index}] must be an object`);
114
+ }
115
+ if (typeof rule.name !== "string" || !rule.name.trim()) {
116
+ throw new Error(`categories[${index}].name must be non-empty string`);
117
+ }
118
+ const idValidationResult = normalizeCategoryId(rule.id ?? rule.name);
119
+ if (!idValidationResult.valid) {
120
+ throw new Error(`categories[${index}].id ${idValidationResult.reason}`);
121
+ }
122
+ const normalizedId = idValidationResult.normalized;
123
+ const sourceIds = sourceIdsByNormalizedId.get(normalizedId) ?? new Set();
124
+ sourceIds.add(rule.id ?? rule.name);
125
+ sourceIdsByNormalizedId.set(normalizedId, sourceIds);
126
+ const matchers = normalizeMatchers(rule, index);
127
+ const existing = seen.get(normalizedId);
128
+ if (existing) {
129
+ existing.matchers.push(...matchers);
130
+ return;
131
+ }
132
+ const BUILT_IN_GROUP_SELECTORS = new Set([
133
+ "flaky",
134
+ "owner",
135
+ "severity",
136
+ "transition",
137
+ "status",
138
+ "environment",
139
+ "layer",
140
+ ]);
141
+ const groupBy = Array.isArray(rule.groupBy) ? [...rule.groupBy] : [];
142
+ for (const selector of groupBy) {
143
+ const isBuiltIn = typeof selector === "string" && BUILT_IN_GROUP_SELECTORS.has(selector);
144
+ const isCustom = isPlainObject(selector) &&
145
+ typeof selector.label === "string" &&
146
+ selector.label.trim().length > 0;
147
+ if (!isBuiltIn && !isCustom) {
148
+ throw new Error(`categories[${index}].groupBy contains invalid selector`);
149
+ }
150
+ }
151
+ const norm = {
152
+ id: normalizedId,
153
+ name: rule.name,
154
+ matchers,
155
+ groupBy,
156
+ groupByMessage: rule.groupByMessage ?? true,
157
+ groupEnvironments: rule.groupEnvironments,
158
+ expand: rule.expand ?? false,
159
+ hide: rule.hide ?? false,
160
+ index,
161
+ };
162
+ seen.set(normalizedId, norm);
163
+ normalized.push(norm);
164
+ };
165
+ rules.forEach(applyRule);
166
+ DEFAULT_ERROR_CATEGORIES.forEach((rule, index) => applyRule(rule, rules.length + index));
167
+ sourceIdsByNormalizedId.forEach((sourceIds, normalizedId) => {
168
+ if (sourceIds.size <= 1) {
169
+ return;
170
+ }
171
+ throw new Error(`categories: normalized id ${JSON.stringify(normalizedId)} is produced by source ids [${Array.from(sourceIds)
172
+ .map((id) => JSON.stringify(id))
173
+ .join(",")}]`);
174
+ });
175
+ return normalized;
176
+ };
177
+ const matchObjectMatcher = (m, d) => {
178
+ if (m.statuses && !m.statuses.includes(d.status)) {
179
+ return false;
180
+ }
181
+ if (m.flaky !== undefined && m.flaky !== d.flaky) {
182
+ return false;
183
+ }
184
+ if (m.labels) {
185
+ for (const [labelName, expected] of Object.entries(m.labels)) {
186
+ const re = toRegExp(expected);
187
+ const values = d.labels.filter((l) => l.name === labelName).map((l) => l.value ?? "");
188
+ if (!values.some((v) => re.test(v))) {
189
+ return false;
190
+ }
191
+ }
192
+ }
193
+ if (m.message !== undefined) {
194
+ const re = toRegExp(m.message);
195
+ if (!re.test(d.message ?? "")) {
196
+ return false;
197
+ }
198
+ }
199
+ if (m.trace !== undefined) {
200
+ const re = toRegExp(m.trace);
201
+ if (!re.test(d.trace ?? "")) {
202
+ return false;
203
+ }
204
+ }
205
+ if (m.transitions && !m.transitions.includes(d.transition)) {
206
+ return false;
207
+ }
208
+ if (m.environments && !m.environments.includes(d.environment ?? EMPTY_VALUE)) {
209
+ return false;
210
+ }
211
+ return true;
212
+ };
213
+ export const matchCategoryMatcher = (matcher, d) => {
214
+ if (typeof matcher === "function") {
215
+ return matcher(d);
216
+ }
217
+ if (isPlainObject(matcher)) {
218
+ return matchObjectMatcher(matcher, d);
219
+ }
220
+ return false;
221
+ };
222
+ export const matchCategory = (categories, d) => {
223
+ for (const c of categories) {
224
+ if (c.matchers.some((m) => matchCategoryMatcher(m, d))) {
225
+ return c;
226
+ }
227
+ }
228
+ return undefined;
229
+ };
230
+ export const extractErrorMatchingData = (tr) => {
231
+ const { message, trace } = tr.error ?? {};
232
+ const labels = Array.isArray(tr.labels)
233
+ ? tr.labels.map((l) => ({ name: l.name, value: l.value ?? "" }))
234
+ : [];
235
+ return {
236
+ status: tr.status,
237
+ labels,
238
+ message,
239
+ trace,
240
+ flaky: tr.flaky,
241
+ duration: tr.duration,
242
+ transition: tr.transition,
243
+ environment: tr.environment,
244
+ };
245
+ };
246
+ export const buildEnvironmentSortOrder = (environmentNames, defaultEnvironmentName) => {
247
+ const orderMap = new Map();
248
+ for (let index = 0; index < environmentNames.length; index++) {
249
+ orderMap.set(environmentNames[index], index);
250
+ }
251
+ const missingEnvironmentRank = environmentNames.length;
252
+ const defaultEnvironmentRank = environmentNames.length + 1;
253
+ orderMap.set(EMPTY_VALUE, missingEnvironmentRank);
254
+ orderMap.set(defaultEnvironmentName, defaultEnvironmentRank);
255
+ return orderMap;
256
+ };
257
+ export const compareNumbers = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
258
+ export const compareStrings = (left, right) => left.localeCompare(right);
259
+ export const isMissingValue = (value) => (value ?? EMPTY_VALUE) === EMPTY_VALUE;
260
+ export const getGroupSortKey = (groupKey, groupValue, environmentOrderMap) => {
261
+ const normalizedValue = groupValue ?? EMPTY_VALUE;
262
+ const missingRank = normalizedValue === EMPTY_VALUE ? 1 : 0;
263
+ if (groupKey === "status") {
264
+ const primaryRank = STATUS_ORDER[normalizedValue] ?? 999;
265
+ return { primaryRank, missingRank, alphaKey: normalizedValue };
266
+ }
267
+ if (groupKey === "severity") {
268
+ const primaryRank = SEVERITY_ORDER[normalizedValue] ?? 999;
269
+ return { primaryRank, missingRank, alphaKey: normalizedValue };
270
+ }
271
+ if (groupKey === "transition") {
272
+ const primaryRank = TRANSITION_ORDER[normalizedValue] ?? 999;
273
+ return { primaryRank, missingRank, alphaKey: normalizedValue };
274
+ }
275
+ if (groupKey === "flaky") {
276
+ const primaryRank = normalizedValue === "true" ? 0 : 1;
277
+ return { primaryRank, missingRank, alphaKey: normalizedValue };
278
+ }
279
+ if (groupKey === "environment") {
280
+ if (environmentOrderMap) {
281
+ const primaryRank = environmentOrderMap.get(normalizedValue) ?? 1000;
282
+ return { primaryRank, missingRank: 0, alphaKey: normalizedValue };
283
+ }
284
+ return { primaryRank: 0, missingRank, alphaKey: normalizedValue };
285
+ }
286
+ return { primaryRank: 0, missingRank, alphaKey: normalizedValue };
287
+ };
288
+ export const compareChildNodes = (leftNodeId, rightNodeId, nodesById, environmentOrderMap) => {
289
+ const leftNode = nodesById[leftNodeId];
290
+ const rightNode = nodesById[rightNodeId];
291
+ const leftType = leftNode?.type ?? "";
292
+ const rightType = rightNode?.type ?? "";
293
+ if (leftType === "message" && rightType === "message") {
294
+ const leftTotal = leftNode.statistic?.total ?? 0;
295
+ const rightTotal = rightNode.statistic?.total ?? 0;
296
+ const byCountDescending = compareNumbers(rightTotal, leftTotal);
297
+ if (byCountDescending !== 0) {
298
+ return byCountDescending;
299
+ }
300
+ const byNameMessage = compareStrings(leftNode.name ?? "", rightNode.name ?? "");
301
+ if (byNameMessage !== 0) {
302
+ return byNameMessage;
303
+ }
304
+ return compareStrings(leftNodeId, rightNodeId);
305
+ }
306
+ if (leftType === "tr" && rightType === "tr") {
307
+ const leftKey = leftNode.key;
308
+ const rightKey = rightNode.key;
309
+ if (leftKey === "environment" && rightKey === "environment") {
310
+ const leftSortKey = getGroupSortKey("environment", leftNode.value, environmentOrderMap);
311
+ const rightSortKey = getGroupSortKey("environment", rightNode.value, environmentOrderMap);
312
+ const byPrimaryRank = compareNumbers(leftSortKey.primaryRank, rightSortKey.primaryRank);
313
+ if (byPrimaryRank !== 0) {
314
+ return byPrimaryRank;
315
+ }
316
+ const byMissingLast = compareNumbers(leftSortKey.missingRank, rightSortKey.missingRank);
317
+ if (byMissingLast !== 0) {
318
+ return byMissingLast;
319
+ }
320
+ const byAlpha = compareStrings(leftSortKey.alphaKey, rightSortKey.alphaKey);
321
+ if (byAlpha !== 0) {
322
+ return byAlpha;
323
+ }
324
+ return compareStrings(leftNodeId, rightNodeId);
325
+ }
326
+ }
327
+ if (leftType === "group" && rightType === "group") {
328
+ const leftGroupKey = leftNode.key ?? "";
329
+ const rightGroupKey = rightNode.key ?? "";
330
+ const byGroupKey = compareStrings(leftGroupKey, rightGroupKey);
331
+ if (byGroupKey !== 0) {
332
+ return byGroupKey;
333
+ }
334
+ const leftSortKey = getGroupSortKey(leftGroupKey, leftNode.value, environmentOrderMap);
335
+ const rightSortKey = getGroupSortKey(rightGroupKey, rightNode.value, environmentOrderMap);
336
+ const byPrimaryRank = compareNumbers(leftSortKey.primaryRank, rightSortKey.primaryRank);
337
+ if (byPrimaryRank !== 0) {
338
+ return byPrimaryRank;
339
+ }
340
+ const byMissingLast = compareNumbers(leftSortKey.missingRank, rightSortKey.missingRank);
341
+ if (byMissingLast !== 0) {
342
+ return byMissingLast;
343
+ }
344
+ const byAlpha = compareStrings(leftSortKey.alphaKey, rightSortKey.alphaKey);
345
+ if (byAlpha !== 0) {
346
+ return byAlpha;
347
+ }
348
+ return compareStrings(leftNodeId, rightNodeId);
349
+ }
350
+ const byType = compareStrings(leftType, rightType);
351
+ if (byType !== 0) {
352
+ return byType;
353
+ }
354
+ const byName = compareStrings(leftNode?.name ?? "", rightNode?.name ?? "");
355
+ if (byName !== 0) {
356
+ return byName;
357
+ }
358
+ return compareStrings(leftNodeId, rightNodeId);
359
+ };
@@ -3,6 +3,7 @@ import type { SeverityLevel, TestStatus } from "./model.js";
3
3
  export declare const statusesList: readonly TestStatus[];
4
4
  export declare const severityLevels: readonly SeverityLevel[];
5
5
  export declare const severityLabelName = "severity";
6
+ export declare const fallbackTestCaseIdLabelName = "_fallbackTestCaseId";
6
7
  export declare const unsuccessfulStatuses: Set<TestStatus>;
7
8
  export declare const successfulStatuses: Set<TestStatus>;
8
9
  export declare const includedInSuccessRate: Set<TestStatus>;
package/dist/constants.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export const statusesList = ["failed", "broken", "passed", "skipped", "unknown"];
2
2
  export const severityLevels = ["blocker", "critical", "normal", "minor", "trivial"];
3
3
  export const severityLabelName = "severity";
4
+ export const fallbackTestCaseIdLabelName = "_fallbackTestCaseId";
4
5
  export const unsuccessfulStatuses = new Set(["failed", "broken"]);
5
6
  export const successfulStatuses = new Set(["passed"]);
6
7
  export const includedInSuccessRate = new Set([...unsuccessfulStatuses, ...successfulStatuses]);
@@ -3,11 +3,16 @@ export interface EnvironmentItem {
3
3
  name: string;
4
4
  values: string[];
5
5
  }
6
+ export interface EnvironmentIdentity {
7
+ id: string;
8
+ name: string;
9
+ }
6
10
  export type ReportVariables = Record<string, string>;
7
11
  export type EnvironmentMatcherPayload = {
8
12
  labels: TestLabel[];
9
13
  };
10
14
  export type EnvironmentDescriptor = {
15
+ name?: string;
11
16
  variables?: ReportVariables;
12
17
  matcher: (payload: EnvironmentMatcherPayload) => boolean;
13
18
  };
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,6 @@ 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";
26
+ export * from "./utils/path.js";
27
+ export * from "./utils/url.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,6 @@ 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";
16
+ export * from "./utils/path.js";
17
+ export * from "./utils/url.js";
package/dist/static.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare const createStylesLinkTag: (src: string) => string;
6
6
  export declare const createFontLinkTag: (src: string) => string;
7
7
  export declare const createFaviconLinkTag: (src: string) => string;
8
8
  export declare const createBaseUrlScript: () => string;
9
+ export declare const stringifyForInlineScript: (value: unknown) => string;
9
10
  export declare const createReportDataScript: (reportFiles?: {
10
11
  name: string;
11
12
  value: string;
package/dist/static.js CHANGED
@@ -23,6 +23,14 @@ export const createBaseUrlScript = () => {
23
23
  </script>
24
24
  `;
25
25
  };
26
+ export const stringifyForInlineScript = (value) => {
27
+ return JSON.stringify(value)
28
+ .replaceAll("<", "\\u003C")
29
+ .replaceAll(">", "\\u003E")
30
+ .replaceAll("&", "\\u0026")
31
+ .replaceAll("\u2028", "\\u2028")
32
+ .replaceAll("\u2029", "\\u2029");
33
+ };
26
34
  export const createReportDataScript = (reportFiles = []) => {
27
35
  if (!reportFiles?.length) {
28
36
  return `
@@ -31,7 +39,9 @@ export const createReportDataScript = (reportFiles = []) => {
31
39
  </script>
32
40
  `;
33
41
  }
34
- const reportFilesDeclaration = reportFiles.map(({ name, value }) => `d('${name}','${value}')`).join(",");
42
+ const reportFilesDeclaration = reportFiles
43
+ .map(({ name, value }) => `d(${JSON.stringify(name)},${JSON.stringify(value)})`)
44
+ .join(",");
35
45
  return `
36
46
  <script async>
37
47
  window.allureReportDataReady = false;
@@ -0,0 +1 @@
1
+ export declare const createDictionary: <T>() => Record<string, T>;
@@ -0,0 +1 @@
1
+ export const createDictionary = () => Object.create(null);
@@ -1,5 +1,18 @@
1
- import type { EnvironmentsConfig } from "../environment.js";
2
- import type { TestEnvGroup, TestResult } from "../model.js";
1
+ import type { EnvironmentIdentity } from "../environment.js";
2
+ import type { TestEnvGroup } from "../model.js";
3
3
  export declare const DEFAULT_ENVIRONMENT = "default";
4
- export declare const matchEnvironment: (envConfig: EnvironmentsConfig, tr: Pick<TestResult, "labels">) => string;
4
+ export declare const MAX_ENVIRONMENT_NAME_LENGTH = 64;
5
+ export declare const MAX_ENVIRONMENT_ID_LENGTH = 64;
6
+ export declare const DEFAULT_ENVIRONMENT_IDENTITY: EnvironmentIdentity;
7
+ export type EnvironmentValidationResult = {
8
+ valid: true;
9
+ normalized: string;
10
+ } | {
11
+ valid: false;
12
+ reason: string;
13
+ };
14
+ export declare const validateEnvironmentName: (name: unknown) => EnvironmentValidationResult;
15
+ export declare const validateEnvironmentId: (environmentId: unknown) => EnvironmentValidationResult;
16
+ export declare const assertValidEnvironmentName: (name: unknown, source?: string) => string;
17
+ export declare const formatNormalizedEnvironmentCollision: (sourcePath: string, normalized: string, originalKeys: string[]) => string;
5
18
  export declare const getRealEnvsCount: (group: TestEnvGroup) => number;
@@ -1,10 +1,80 @@
1
1
  export const DEFAULT_ENVIRONMENT = "default";
2
- export const matchEnvironment = (envConfig, tr) => {
3
- return (Object.entries(envConfig).find(([, { matcher }]) => matcher({ labels: tr.labels }))?.[0] ?? DEFAULT_ENVIRONMENT);
2
+ export const MAX_ENVIRONMENT_NAME_LENGTH = 64;
3
+ export const MAX_ENVIRONMENT_ID_LENGTH = 64;
4
+ export const DEFAULT_ENVIRONMENT_IDENTITY = {
5
+ id: DEFAULT_ENVIRONMENT,
6
+ name: DEFAULT_ENVIRONMENT,
4
7
  };
8
+ const hasControlChars = (value) => {
9
+ for (let i = 0; i < value.length; i++) {
10
+ const code = value.charCodeAt(i);
11
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) {
12
+ return true;
13
+ }
14
+ }
15
+ return false;
16
+ };
17
+ const hasPathLikeSegments = (value) => {
18
+ if (value.includes("/") || value.includes("\\")) {
19
+ return true;
20
+ }
21
+ return value === "." || value === "..";
22
+ };
23
+ export const validateEnvironmentName = (name) => {
24
+ if (typeof name !== "string") {
25
+ return { valid: false, reason: "name must be a string" };
26
+ }
27
+ const normalized = name.trim();
28
+ if (normalized.length === 0) {
29
+ return { valid: false, reason: "name must not be empty" };
30
+ }
31
+ if (normalized.length > MAX_ENVIRONMENT_NAME_LENGTH) {
32
+ return {
33
+ valid: false,
34
+ reason: `name must not exceed ${MAX_ENVIRONMENT_NAME_LENGTH} characters`,
35
+ };
36
+ }
37
+ if (hasControlChars(normalized)) {
38
+ return { valid: false, reason: "name must not contain control characters" };
39
+ }
40
+ if (hasPathLikeSegments(normalized)) {
41
+ return { valid: false, reason: "name must not contain path-like segments" };
42
+ }
43
+ return { valid: true, normalized };
44
+ };
45
+ export const validateEnvironmentId = (environmentId) => {
46
+ if (typeof environmentId !== "string") {
47
+ return { valid: false, reason: "id must be a string" };
48
+ }
49
+ const normalized = environmentId.trim();
50
+ if (normalized.length === 0) {
51
+ return { valid: false, reason: "id must not be empty" };
52
+ }
53
+ if (normalized.length > MAX_ENVIRONMENT_ID_LENGTH) {
54
+ return {
55
+ valid: false,
56
+ reason: `id must not exceed ${MAX_ENVIRONMENT_ID_LENGTH} characters`,
57
+ };
58
+ }
59
+ if (!/^[A-Za-z0-9_-]+$/.test(normalized)) {
60
+ return {
61
+ valid: false,
62
+ reason: "id must contain only latin letters, digits, underscores, and hyphens",
63
+ };
64
+ }
65
+ return { valid: true, normalized };
66
+ };
67
+ export const assertValidEnvironmentName = (name, source = "environment name") => {
68
+ const validationResult = validateEnvironmentName(name);
69
+ if (!validationResult.valid) {
70
+ throw new Error(`Invalid ${source} ${JSON.stringify(name)}: ${validationResult.reason}`);
71
+ }
72
+ return validationResult.normalized;
73
+ };
74
+ export const formatNormalizedEnvironmentCollision = (sourcePath, normalized, originalKeys) => `${sourcePath}: normalized key ${JSON.stringify(normalized)} is produced by original keys [${originalKeys.map((key) => JSON.stringify(key)).join(",")}]`;
5
75
  export const getRealEnvsCount = (group) => {
6
76
  const { testResultsByEnv = {} } = group ?? {};
7
- const envsCount = Object.keys(testResultsByEnv).length ?? 0;
77
+ const envsCount = Object.keys(testResultsByEnv).length;
8
78
  if (envsCount <= 1 && DEFAULT_ENVIRONMENT in testResultsByEnv) {
9
79
  return 0;
10
80
  }
@@ -1,3 +1,10 @@
1
1
  import type { HistoryDataPoint, HistoryTestResult } from "../history.js";
2
+ import type { TestParameter } from "../metadata.js";
2
3
  import type { TestResult } from "../model.js";
4
+ export declare const stringifyHistoryParams: (parameters?: TestParameter[]) => string;
5
+ export declare const getFallbackHistoryId: (tr: Pick<TestResult, "labels" | "parameters">) => string | undefined;
6
+ export declare const getHistoryIdCandidates: (tr: Pick<TestResult, "historyId" | "labels" | "parameters">) => string[];
7
+ export declare const filterUnknownByKnownIssues: (trs: TestResult[], knownIssueHistoryIds: ReadonlySet<string>) => TestResult[];
8
+ export declare const normalizeHistoryDataPointUrls: (historyDataPoint: HistoryDataPoint) => HistoryDataPoint;
9
+ export declare const selectHistoryTestResults: (historyDataPoints: HistoryDataPoint[], historyIdCandidates: readonly string[]) => HistoryTestResult[];
3
10
  export declare const htrsByTr: (hdps: HistoryDataPoint[], tr: TestResult | HistoryTestResult) => HistoryTestResult[];
@@ -1,22 +1,89 @@
1
- export const htrsByTr = (hdps, tr) => {
2
- if (!tr?.historyId) {
1
+ import { createHash } from "node:crypto";
2
+ import { fallbackTestCaseIdLabelName } from "../constants.js";
3
+ import { findLastByLabelName } from "./label.js";
4
+ const md5 = (data) => createHash("md5").update(data).digest("hex");
5
+ const parametersCompare = (a, b) => {
6
+ return (a.name ?? "").localeCompare(b.name ?? "") || (a.value ?? "").localeCompare(b.value ?? "");
7
+ };
8
+ export const stringifyHistoryParams = (parameters = []) => {
9
+ return [...parameters]
10
+ .filter((parameter) => !parameter?.excluded)
11
+ .sort(parametersCompare)
12
+ .map((parameter) => `${parameter.name}:${parameter.value}`)
13
+ .join(",");
14
+ };
15
+ export const getFallbackHistoryId = (tr) => {
16
+ const fallbackTestCaseId = findLastByLabelName(tr.labels ?? [], fallbackTestCaseIdLabelName);
17
+ if (!fallbackTestCaseId) {
18
+ return undefined;
19
+ }
20
+ return `${fallbackTestCaseId}.${md5(stringifyHistoryParams(tr.parameters ?? []))}`;
21
+ };
22
+ export const getHistoryIdCandidates = (tr) => {
23
+ const result = [];
24
+ if (tr.historyId) {
25
+ result.push(tr.historyId);
26
+ }
27
+ const fallbackHistoryId = getFallbackHistoryId(tr);
28
+ if (fallbackHistoryId && !result.includes(fallbackHistoryId)) {
29
+ result.push(fallbackHistoryId);
30
+ }
31
+ return result;
32
+ };
33
+ export const filterUnknownByKnownIssues = (trs, knownIssueHistoryIds) => {
34
+ return trs.filter((tr) => {
35
+ const historyIdCandidates = getHistoryIdCandidates(tr);
36
+ if (historyIdCandidates.length === 0) {
37
+ return true;
38
+ }
39
+ return historyIdCandidates.every((historyId) => !knownIssueHistoryIds.has(historyId));
40
+ });
41
+ };
42
+ export const normalizeHistoryDataPointUrls = (historyDataPoint) => {
43
+ const { url } = historyDataPoint;
44
+ if (!url) {
45
+ return historyDataPoint;
46
+ }
47
+ let testResults = historyDataPoint.testResults;
48
+ for (const [historyId, historyTestResult] of Object.entries(historyDataPoint.testResults)) {
49
+ if (historyTestResult.url) {
50
+ continue;
51
+ }
52
+ if (testResults === historyDataPoint.testResults) {
53
+ testResults = { ...historyDataPoint.testResults };
54
+ }
55
+ testResults[historyId] = {
56
+ ...historyTestResult,
57
+ url,
58
+ };
59
+ }
60
+ if (testResults === historyDataPoint.testResults) {
61
+ return historyDataPoint;
62
+ }
63
+ return {
64
+ ...historyDataPoint,
65
+ testResults,
66
+ };
67
+ };
68
+ export const selectHistoryTestResults = (historyDataPoints, historyIdCandidates) => {
69
+ if (historyIdCandidates.length === 0) {
3
70
  return [];
4
71
  }
5
- return hdps.reduce((acc, dp) => {
6
- const htr = dp.testResults[tr.historyId];
7
- if (htr) {
8
- if (dp.url) {
9
- const url = new URL(dp.url);
10
- url.hash = tr.id;
11
- acc.push({
12
- ...htr,
13
- url: url.toString(),
14
- });
15
- }
16
- else {
17
- acc.push(htr);
72
+ return historyDataPoints.reduce((acc, historyDataPoint) => {
73
+ for (const historyId of historyIdCandidates) {
74
+ const historyTestResult = historyDataPoint.testResults[historyId];
75
+ if (!historyTestResult) {
76
+ continue;
18
77
  }
78
+ acc.push(historyTestResult);
79
+ break;
19
80
  }
20
81
  return acc;
21
82
  }, []);
22
83
  };
84
+ export const htrsByTr = (hdps, tr) => {
85
+ if (!tr?.historyId) {
86
+ return [];
87
+ }
88
+ return selectHistoryTestResults(hdps, [tr.historyId]);
89
+ };
@@ -1,2 +1,4 @@
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;
4
+ export declare const shouldHideLabel: (labelName: string, matchers?: readonly (string | RegExp)[]) => boolean;
@@ -1,3 +1,22 @@
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
+ };
12
+ export const shouldHideLabel = (labelName, matchers = []) => {
13
+ if (labelName.startsWith("_")) {
14
+ return true;
15
+ }
16
+ return matchers.some((matcher) => {
17
+ if (typeof matcher === "string") {
18
+ return matcher === labelName;
19
+ }
20
+ return new RegExp(matcher.source, matcher.flags).test(labelName);
21
+ });
22
+ };
@@ -0,0 +1,2 @@
1
+ export declare const toPosixPath: (path: string) => string;
2
+ export declare const joinPosixPath: (...parts: string[]) => string;
@@ -0,0 +1,11 @@
1
+ export const toPosixPath = (path) => path.replace(/\\/g, "/");
2
+ export const joinPosixPath = (...parts) => {
3
+ const segments = parts.map(toPosixPath).join("/").split("/");
4
+ const nonEmptySegments = [];
5
+ for (const segment of segments) {
6
+ if (segment.length > 0) {
7
+ nonEmptySegments.push(segment);
8
+ }
9
+ }
10
+ return nonEmptySegments.join("/");
11
+ };
@@ -0,0 +1 @@
1
+ export declare const sanitizeExternalUrl: (value: unknown) => string | undefined;
@@ -0,0 +1,24 @@
1
+ const ALLOWED_EXTERNAL_URL_PROTOCOLS = new Set(["http:", "https:", "mailto:", "tel:"]);
2
+ export const sanitizeExternalUrl = (value) => {
3
+ if (typeof value !== "string") {
4
+ return undefined;
5
+ }
6
+ const normalized = value.trim();
7
+ if (normalized.length === 0) {
8
+ return undefined;
9
+ }
10
+ const schemeMatch = normalized.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
11
+ if (!schemeMatch) {
12
+ return undefined;
13
+ }
14
+ const protocol = `${schemeMatch[1].toLowerCase()}:`;
15
+ if (!ALLOWED_EXTERNAL_URL_PROTOCOLS.has(protocol)) {
16
+ return undefined;
17
+ }
18
+ try {
19
+ return new URL(normalized).toString();
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ };
package/package.json CHANGED
@@ -1,50 +1,39 @@
1
1
  {
2
2
  "name": "@allurereport/core-api",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Allure Core API",
5
5
  "keywords": [
6
6
  "allure"
7
7
  ],
8
- "repository": "https://github.com/allure-framework/allure3",
9
8
  "license": "Apache-2.0",
10
9
  "author": "Qameta Software",
10
+ "repository": "https://github.com/allure-framework/allure3",
11
+ "files": [
12
+ "dist"
13
+ ],
11
14
  "type": "module",
12
- "exports": {
13
- ".": "./dist/index.js"
14
- },
15
15
  "main": "./dist/index.js",
16
16
  "module": "./dist/index.js",
17
17
  "types": "./dist/index.d.ts",
18
- "files": [
19
- "dist"
20
- ],
18
+ "exports": {
19
+ ".": "./dist/index.js"
20
+ },
21
21
  "scripts": {
22
22
  "build": "run clean && tsc --project ./tsconfig.json",
23
23
  "clean": "rimraf ./dist",
24
- "eslint": "eslint ./src/**/*.{js,jsx,ts,tsx}",
25
- "eslint:format": "eslint --fix ./src/**/*.{js,jsx,ts,tsx}",
26
- "test": "rimraf ./out && vitest run"
24
+ "test": "rimraf ./out && vitest run",
25
+ "lint": "oxlint --import-plugin src test features stories",
26
+ "lint:fix": "oxlint --import-plugin --fix src test features stories"
27
27
  },
28
28
  "dependencies": {
29
29
  "d3-shape": "^3.2.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@stylistic/eslint-plugin": "^2.6.1",
33
32
  "@types/d3-shape": "^3.1.6",
34
- "@types/eslint": "^8.56.11",
35
33
  "@types/node": "^20.17.9",
36
- "@typescript-eslint/eslint-plugin": "^8.0.0",
37
- "@typescript-eslint/parser": "^8.0.0",
38
34
  "@vitest/runner": "^2.1.9",
39
35
  "@vitest/snapshot": "^2.1.9",
40
36
  "allure-vitest": "^3.3.3",
41
- "eslint": "^8.57.0",
42
- "eslint-config-prettier": "^9.1.0",
43
- "eslint-plugin-import": "^2.29.1",
44
- "eslint-plugin-jsdoc": "^50.0.0",
45
- "eslint-plugin-n": "^17.10.1",
46
- "eslint-plugin-no-null": "^1.0.2",
47
- "eslint-plugin-prefer-arrow": "^1.2.3",
48
37
  "rimraf": "^6.0.1",
49
38
  "typescript": "^5.6.3",
50
39
  "vitest": "^2.1.9"