@allurereport/core-api 3.20.1 → 3.22.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.
@@ -12,6 +12,7 @@ export type Statistic = DiffStatistic & {
12
12
  unknown?: number;
13
13
  total: number;
14
14
  retries?: number;
15
+ retriesStatusChange?: number;
15
16
  flaky?: number;
16
17
  resolutions?: {
17
18
  issues?: number;
@@ -84,6 +84,7 @@ export type CategoryNodeItem = {
84
84
  value?: string;
85
85
  retryHash?: string;
86
86
  retriesCount?: number;
87
+ retriesStatusChange?: boolean;
87
88
  transition?: TestStatusTransition;
88
89
  tooltips?: Record<string, string>;
89
90
  statistic?: Statistic;
@@ -104,7 +105,7 @@ export declare const matchCategoryMatcher: (matcher: Matcher, d: CategoryMatchin
104
105
  export declare const matchCategory: (categories: CategoryDefinition[], d: CategoryMatchingData) => CategoryDefinition | undefined;
105
106
  export declare const extractErrorMatchingData: (tr: Pick<TestResult, "status" | "labels" | "error" | "flaky" | "duration" | "transition" | "environment">) => CategoryMatchingData;
106
107
  export declare const buildEnvironmentSortOrder: (environmentNames: string[], defaultEnvironmentName: string) => Map<string, number>;
107
- export declare const compareNumbers: (left: number, right: number) => 0 | 1 | -1;
108
+ export declare const compareNumbers: (left: number, right: number) => 1 | -1 | 0;
108
109
  export declare const compareStrings: (left: string, right: string) => number;
109
110
  export declare const isMissingValue: (value: string | undefined) => boolean;
110
111
  export declare const getGroupSortKey: (groupKey: string | undefined, groupValue: string | undefined, environmentOrderMap?: Map<string, number>) => GroupSortKey;
package/dist/ci.d.ts CHANGED
@@ -21,6 +21,8 @@ export interface CiDescriptor {
21
21
  jobRunUrl: string;
22
22
  jobRunName: string;
23
23
  jobRunBranch: string;
24
+ pipelineRunName?: string;
25
+ pipelineRunUrl?: string;
24
26
  pullRequestName: string;
25
27
  pullRequestUrl: string;
26
28
  provider?: GitProvider;
@@ -28,4 +30,6 @@ export interface CiDescriptor {
28
30
  sourceBranch?: string;
29
31
  targetBranch?: string;
30
32
  pullRequest?: GitPullRequestRef;
33
+ commitSha?: string;
34
+ commitUrl?: string;
31
35
  }
package/dist/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type DefaultLabelsConfig = Record<string, string | string[]>;
2
2
  export type FlakyDetectionConfig = {
3
3
  historyDepth?: number;
4
+ stabilizationPeriod?: number;
4
5
  includePassedTests?: boolean;
5
6
  };
6
7
  export declare const parseIntegerConfigValue: (value: unknown, minValue?: number) => number | undefined;
package/dist/history.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface HistoryTestResult {
6
6
  fullName?: string;
7
7
  environment?: string;
8
8
  status: TestStatus;
9
+ retries?: TestStatus[];
9
10
  error?: TestError;
10
11
  start?: number;
11
12
  stop?: number;
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export * from "./ci.js";
4
4
  export * from "./gitFlow.js";
5
5
  export type * from "./environment.js";
6
6
  export type * from "./history.js";
7
- export type * from "./resolutions.js";
7
+ export * from "./resolutions.js";
8
8
  export type * from "./metadata.js";
9
9
  export * from "./metrics.js";
10
10
  export type * from "./model.js";
@@ -25,6 +25,7 @@ export * from "./utils/status.js";
25
25
  export * from "./utils/successRate.js";
26
26
  export * from "./utils/environment.js";
27
27
  export * from "./utils/history.js";
28
+ export * from "./utils/flaky.js";
28
29
  export * from "./utils/identity.js";
29
30
  export * from "./utils/strings.js";
30
31
  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 "./gitFlow.js";
4
+ export * from "./resolutions.js";
4
5
  export * from "./metrics.js";
5
6
  export * from "./config.js";
6
7
  export * from "./static.js";
@@ -16,6 +17,7 @@ export * from "./utils/status.js";
16
17
  export * from "./utils/successRate.js";
17
18
  export * from "./utils/environment.js";
18
19
  export * from "./utils/history.js";
20
+ export * from "./utils/flaky.js";
19
21
  export * from "./utils/identity.js";
20
22
  export * from "./utils/strings.js";
21
23
  export * from "./utils/dictionary.js";
package/dist/model.d.ts CHANGED
@@ -57,6 +57,7 @@ export interface TestResult {
57
57
  known: boolean;
58
58
  resolution?: ResolutionCategory;
59
59
  resolutionComment?: string;
60
+ resolutionExpiresAt?: string;
60
61
  transition?: TestStatusTransition;
61
62
  retryHash: string | null;
62
63
  isRetry: boolean;
@@ -1,6 +1,7 @@
1
1
  import type { TestLink } from "./metadata.js";
2
- import type { TestError, TestStatus } from "./model.js";
2
+ import type { TestError, TestResult, TestStatus } from "./model.js";
3
3
  export type ResolutionCategory = "issue" | "muted" | "accepted";
4
+ export type MutedResolutionState = "active" | "outdated" | "permanent";
4
5
  export interface ResolutionLinkTemplate {
5
6
  nameTemplate?: string;
6
7
  urlTemplate: string;
@@ -22,8 +23,19 @@ export interface IssueResolutionRule extends ResolutionMatcher {
22
23
  export interface IgnoredResolutionRule extends ResolutionMatcher {
23
24
  resolution: "muted" | "accepted";
24
25
  comment: string;
26
+ expiresAt?: string;
27
+ }
28
+ export interface MutedResolutionRule extends IgnoredResolutionRule {
29
+ resolution: "muted";
30
+ }
31
+ export interface AcceptedResolutionRule extends IgnoredResolutionRule {
32
+ resolution: "accepted";
25
33
  }
26
34
  export type ResolutionRule = IssueResolutionRule | IgnoredResolutionRule;
35
+ type ResolutionStateSource = Pick<TestResult, "resolution" | "resolutionExpiresAt">;
36
+ export declare const parseResolutionExpiresAt: (value: string) => number | undefined;
37
+ export declare const getMutedResolutionState: (testResult: ResolutionStateSource, now?: number) => MutedResolutionState | undefined;
38
+ export declare const isIgnoredResolution: (testResult: ResolutionStateSource, now?: number) => boolean;
27
39
  export interface ResolutionsConfig {
28
40
  knownIssuesPath?: string;
29
41
  links?: Record<string, ResolutionLinkTemplate>;
@@ -48,3 +60,4 @@ export interface KnownIssueRecord extends ResolutionIssue {
48
60
  export interface KnownIssuesFile {
49
61
  resolutionIssues: KnownIssueRecord[];
50
62
  }
63
+ export {};
@@ -1 +1,39 @@
1
- export {};
1
+ export const parseResolutionExpiresAt = (value) => {
2
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/.exec(value);
3
+ if (!match) {
4
+ return undefined;
5
+ }
6
+ const [, yearValue, monthValue, dayValue, hourValue, minuteValue, secondValue, offsetHour, offsetMinute] = match;
7
+ const year = Number(yearValue);
8
+ const month = Number(monthValue);
9
+ const day = Number(dayValue);
10
+ const hour = Number(hourValue);
11
+ const minute = Number(minuteValue);
12
+ const second = Number(secondValue);
13
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
14
+ const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
15
+ if (year <= 0 ||
16
+ daysInMonth === undefined ||
17
+ day < 1 ||
18
+ day > daysInMonth ||
19
+ hour > 23 ||
20
+ minute > 59 ||
21
+ second > 59 ||
22
+ (offsetHour !== undefined && (Number(offsetHour) > 23 || Number(offsetMinute) > 59))) {
23
+ return undefined;
24
+ }
25
+ const timestamp = new Date(value).getTime();
26
+ return Number.isFinite(timestamp) ? timestamp : undefined;
27
+ };
28
+ export const getMutedResolutionState = (testResult, now = Date.now()) => {
29
+ if (testResult.resolution !== "muted") {
30
+ return undefined;
31
+ }
32
+ if (testResult.resolutionExpiresAt === undefined) {
33
+ return "permanent";
34
+ }
35
+ const expiresAt = parseResolutionExpiresAt(testResult.resolutionExpiresAt);
36
+ return expiresAt === undefined || now >= expiresAt ? "outdated" : "active";
37
+ };
38
+ export const isIgnoredResolution = (testResult, now = Date.now()) => testResult.resolution === "accepted" ||
39
+ (testResult.resolution === "muted" && getMutedResolutionState(testResult, now) !== "outdated");
package/dist/static.d.ts CHANGED
@@ -11,6 +11,11 @@ export declare const appLoaderStyles = "\n <style>\n .allure-app-loader {\n
11
11
  export declare const createAppLoaderMarkup: () => string;
12
12
  export declare const createBaseUrlScript: () => string;
13
13
  export declare const stringifyForInlineScript: (value: unknown) => string;
14
+ export declare const reportDataScriptPlaceholder = "<!-- allure-report-data -->";
15
+ export declare const injectReportDataScript: (html: string, reportFiles?: {
16
+ name: string;
17
+ value: string;
18
+ }[]) => Buffer;
14
19
  export declare const createReportDataScript: (reportFiles?: {
15
20
  name: string;
16
21
  value: string;
package/dist/static.js CHANGED
@@ -90,6 +90,58 @@ export const stringifyForInlineScript = (value) => {
90
90
  .replaceAll("\u2028", "\\u2028")
91
91
  .replaceAll("\u2029", "\\u2029");
92
92
  };
93
+ export const reportDataScriptPlaceholder = "<!-- allure-report-data -->";
94
+ const createReportDataScriptChunks = (reportFiles = []) => {
95
+ if (!reportFiles.length) {
96
+ return [
97
+ Buffer.from(`
98
+ <script async>
99
+ window.allureReportDataReady = true;
100
+ </script>
101
+ `),
102
+ ];
103
+ }
104
+ const chunks = [
105
+ Buffer.from(`
106
+ <script async>
107
+ window.allureReportDataReady = false;
108
+ window.allureReportData = window.allureReportData || {};
109
+
110
+ function d(name, value){
111
+ return new Promise(function (resolve) {
112
+ window.allureReportData[name] = value;
113
+
114
+ return resolve(true);
115
+ });
116
+ }
117
+ </script>
118
+ <script defer>
119
+ Promise.allSettled([`),
120
+ ];
121
+ reportFiles.forEach(({ name, value }, index) => {
122
+ chunks.push(Buffer.from(`${index === 0 ? "" : ","}d(${stringifyForInlineScript(name)},${stringifyForInlineScript(value)})`));
123
+ });
124
+ chunks.push(Buffer.from(`])
125
+ .then(function(){
126
+ window.allureReportDataReady = true;
127
+ })
128
+ </script>
129
+ `));
130
+ return chunks;
131
+ };
132
+ export const injectReportDataScript = (html, reportFiles = []) => {
133
+ const placeholderIndex = html.indexOf(reportDataScriptPlaceholder);
134
+ if (placeholderIndex === -1) {
135
+ throw new Error("Report data script placeholder is missing from the HTML template");
136
+ }
137
+ const prefix = html.slice(0, placeholderIndex);
138
+ const suffix = html.slice(placeholderIndex + reportDataScriptPlaceholder.length);
139
+ return Buffer.concat([
140
+ Buffer.from(prefix, "utf8"),
141
+ ...createReportDataScriptChunks(reportFiles),
142
+ Buffer.from(suffix, "utf8"),
143
+ ]);
144
+ };
93
145
  export const createReportDataScript = (reportFiles = []) => {
94
146
  if (!reportFiles?.length) {
95
147
  return `
@@ -99,7 +151,7 @@ export const createReportDataScript = (reportFiles = []) => {
99
151
  `;
100
152
  }
101
153
  const reportFilesDeclaration = reportFiles
102
- .map(({ name, value }) => `d(${JSON.stringify(name)},${JSON.stringify(value)})`)
154
+ .map(({ name, value }) => `d(${stringifyForInlineScript(name)},${stringifyForInlineScript(value)})`)
103
155
  .join(",");
104
156
  return `
105
157
  <script async>
@@ -0,0 +1,5 @@
1
+ import type { FlakyDetectionConfig } from "../config.js";
2
+ import type { HistoryTestResult } from "../history.js";
3
+ import type { TestResult } from "../model.js";
4
+ export type FlakinessOptions = Pick<FlakyDetectionConfig, "historyDepth" | "stabilizationPeriod">;
5
+ export declare const getTestFlakiness: (current: TestResult, history: (HistoryTestResult | undefined)[], options?: FlakinessOptions) => boolean | undefined;
@@ -0,0 +1,44 @@
1
+ const SIGNIFICANT_STATUSES = new Set(["passed", "failed", "broken"]);
2
+ export const getTestFlakiness = (current, history, options = {}) => {
3
+ const { historyDepth = 10, stabilizationPeriod = 5 } = options;
4
+ if (historyDepth === -1 || !SIGNIFICANT_STATUSES.has(current.status)) {
5
+ return undefined;
6
+ }
7
+ const statuses = [];
8
+ for (const result of history) {
9
+ if (result === undefined) {
10
+ break;
11
+ }
12
+ if (result.id === current.id || !SIGNIFICANT_STATUSES.has(result.status)) {
13
+ continue;
14
+ }
15
+ statuses.push(result.status);
16
+ if (historyDepth > 0 && statuses.length === historyDepth) {
17
+ break;
18
+ }
19
+ }
20
+ if (statuses.length === 0) {
21
+ return undefined;
22
+ }
23
+ statuses.reverse();
24
+ statuses.push(current.status);
25
+ const period = historyDepth > 0 ? Math.min(stabilizationPeriod, historyDepth + 1) : stabilizationPeriod;
26
+ let anchor = 0;
27
+ let streak = 0;
28
+ for (let index = 0; index < statuses.length; index++) {
29
+ streak = index > 0 && statuses[index] === statuses[index - 1] ? streak + 1 : 1;
30
+ if (streak >= period) {
31
+ anchor = index;
32
+ }
33
+ }
34
+ let changes = 0;
35
+ for (let index = anchor + 1; index < statuses.length; index++) {
36
+ if (statuses[index] !== statuses[index - 1]) {
37
+ changes++;
38
+ }
39
+ }
40
+ if (changes <= 1) {
41
+ return false;
42
+ }
43
+ return !(changes === 2 && statuses[anchor] === "passed" && current.status === "passed");
44
+ };
@@ -1,4 +1,5 @@
1
- import type { TestStatus } from "../model.js";
1
+ import type { TestResult, TestStatus } from "../model.js";
2
2
  export declare const StatusByPriority: TestStatus[];
3
3
  export declare const statusToPriority: (status: TestStatus | undefined) => number;
4
4
  export declare const getWorstStatus: (items: TestStatus[]) => TestStatus | undefined;
5
+ export declare const hasRetriesStatusChange: (current: Pick<TestResult, "status">, retries: readonly Pick<TestResult, "status">[]) => boolean;
@@ -1,3 +1,4 @@
1
+ const retryStatusChangeStatuses = new Set(["failed", "broken", "passed"]);
1
2
  export const StatusByPriority = ["failed", "broken", "passed", "skipped", "unknown"];
2
3
  export const statusToPriority = (status) => {
3
4
  if (!status) {
@@ -11,3 +12,5 @@ export const getWorstStatus = (items) => {
11
12
  }
12
13
  return items.sort((a, b) => statusToPriority(a) - statusToPriority(b))[0];
13
14
  };
15
+ export const hasRetriesStatusChange = (current, retries) => retryStatusChangeStatuses.has(current.status) &&
16
+ retries.some(({ status }) => retryStatusChangeStatuses.has(status) && status !== current.status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/core-api",
3
- "version": "3.20.1",
3
+ "version": "3.22.0",
4
4
  "description": "Allure Core API",
5
5
  "keywords": [
6
6
  "allure"
@@ -21,22 +21,27 @@
21
21
  "scripts": {
22
22
  "build": "run clean && tsc --project ./tsconfig.json",
23
23
  "clean": "rimraf ./dist",
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"
24
+ "test": "rimraf out && vp test run",
25
+ "test:unit": "yarn test",
26
+ "lint": "vp lint --import-plugin src test features stories",
27
+ "lint:fix": "vp lint --import-plugin --fix src test features stories"
27
28
  },
28
29
  "dependencies": {
29
30
  "d3-shape": "^3.2.0"
30
31
  },
31
32
  "devDependencies": {
33
+ "@allurereport/common-config": "3.22.0",
32
34
  "@types/d3-shape": "^3.1.8",
33
35
  "@types/node": "^20",
36
+ "@vitest/coverage-istanbul": "5.0.4",
34
37
  "@vitest/runner": "^2",
35
- "@vitest/snapshot": "^5.0.1",
38
+ "@vitest/snapshot": "5.0.4",
36
39
  "allure-js-commons": "^3",
37
40
  "allure-vitest": "^3",
38
41
  "rimraf": "^6",
39
42
  "typescript": "^5",
40
- "vitest": "^4.1.0"
43
+ "vite": "@voidzero-dev/vite-plus-core@1.2.0",
44
+ "vite-plus": "1.2.0",
45
+ "vitest": "5.0.4"
41
46
  }
42
47
  }