@allurereport/core 3.17.0 → 3.19.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.
package/dist/config.d.ts CHANGED
@@ -9,11 +9,11 @@ export interface ConfigOverride {
9
9
  port?: Config["port"];
10
10
  hideLabels?: Config["hideLabels"];
11
11
  historyPath?: Config["historyPath"];
12
+ historyBaseUrl?: Config["historyBaseUrl"];
12
13
  historyLimit?: Config["historyLimit"];
13
14
  resolutions?: Pick<NonNullable<Config["resolutions"]>, "knownIssuesPath">;
14
15
  plugins?: Config["plugins"];
15
16
  }
16
- export declare const parseIntegerConfigValue: (value: unknown, defaultValue: number, minValue: number) => number;
17
17
  export declare const isAgentDescriptor: (value: string | undefined) => value is "agent" | "@allurereport/plugin-agent";
18
18
  export declare const hasConfiguredAgent: (plugins: Record<string, PluginDescriptor>) => boolean;
19
19
  export declare const assertValidPluginId: (id: string) => void;
package/dist/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { extname, resolve } from "node:path";
3
3
  import * as process from "node:process";
4
- import { validateEnvironmentName } from "@allurereport/core-api";
4
+ import { parseIntegerConfigValue, validateEnvironmentName } from "@allurereport/core-api";
5
5
  import { createJiti } from "jiti";
6
6
  import { parse } from "yaml";
7
7
  import { FileSystemReportFiles } from "./plugin.js";
@@ -23,16 +23,8 @@ const CONFIG_FILENAMES = [
23
23
  "allurerc.yml",
24
24
  ];
25
25
  const DEFAULT_CONFIG = {};
26
- const DEFAULT_ALLURE_SERVICE_UPLOAD_CONCURRENCY = 100;
27
26
  const DEFAULT_ALLURE_SERVICE_UPLAOD_MAX_ATTEMPTS = 5;
28
27
  const DEFAULT_ALLURE_SERVICE_UPLOAD_MAX_SIMULTANEOUS_FAILURES = 5;
29
- export const parseIntegerConfigValue = (value, defaultValue, minValue) => {
30
- if (typeof value !== "number" || !Number.isFinite(value)) {
31
- return defaultValue;
32
- }
33
- const normalized = Math.floor(value);
34
- return normalized >= minValue ? normalized : defaultValue;
35
- };
36
28
  export const isAgentDescriptor = (value) => {
37
29
  return value === "agent" || value === "@allurereport/plugin-agent";
38
30
  };
@@ -97,7 +89,9 @@ export const validateConfig = (config) => {
97
89
  "port",
98
90
  "hideLabels",
99
91
  "historyPath",
92
+ "historyBaseUrl",
100
93
  "historyLimit",
94
+ "flakyDetection",
101
95
  "resolutions",
102
96
  "plugins",
103
97
  "defaultLabels",
@@ -112,6 +106,7 @@ export const validateConfig = (config) => {
112
106
  "categories",
113
107
  "globalAttachments",
114
108
  "resultsDir",
109
+ "dump",
115
110
  ];
116
111
  const unsupportedFields = Object.keys(config).filter((key) => !supportedFields.includes(key));
117
112
  return {
@@ -190,19 +185,34 @@ const resolveConfigEnvironments = (config) => {
190
185
  allowedEnvironments,
191
186
  };
192
187
  };
188
+ const validateFlakyDetectionConfig = (config) => {
189
+ if (config === undefined) {
190
+ return;
191
+ }
192
+ const { historyDepth, includePassedTests } = config;
193
+ if (historyDepth !== undefined && (!Number.isInteger(historyDepth) || historyDepth < -1)) {
194
+ throw new Error("flakyDetection.historyDepth must be an integer greater than or equal to -1");
195
+ }
196
+ if (includePassedTests !== undefined && typeof includePassedTests !== "boolean") {
197
+ throw new TypeError("flakyDetection.includePassedTests must be a boolean");
198
+ }
199
+ };
193
200
  export const resolveConfig = async (config, override = {}) => {
194
201
  const validationResult = validateConfig(config);
195
202
  if (!validationResult.valid) {
196
203
  throw new Error(`The provided Allure config contains unsupported fields: ${validationResult.fields.join(", ")}`);
197
204
  }
198
205
  validateResolutionsConfig(config.resolutions);
206
+ validateFlakyDetectionConfig(config.flakyDetection);
199
207
  const { environments, environment, allowedEnvironments } = resolveConfigEnvironments(config);
200
208
  const name = override.name ?? config.name ?? "Allure Report";
201
209
  const open = override.open ?? config.open ?? false;
202
210
  const port = override.port ?? config.port ?? undefined;
203
211
  const hideLabels = override.hideLabels ?? config.hideLabels;
204
212
  const historyPath = override.historyPath ?? config.historyPath;
213
+ const historyBaseUrl = override.historyBaseUrl ?? config.historyBaseUrl;
205
214
  const historyLimit = override.historyLimit ?? config.historyLimit;
215
+ const dump = config.dump;
206
216
  const appendHistory = config.appendHistory ?? true;
207
217
  const configuredKnownIssuesPath = override.resolutions?.knownIssuesPath ?? config.resolutions?.knownIssuesPath;
208
218
  const knownIssuesPathInput = configuredKnownIssuesPath ?? (config.resolutions ? DEFAULT_KNOWN_ISSUES_PATH : undefined);
@@ -235,6 +245,7 @@ export const resolveConfig = async (config, override = {}) => {
235
245
  };
236
246
  pluginInstances = await resolvePlugins(pluginsWithAgent);
237
247
  }
248
+ const uploadConcurrency = parseIntegerConfigValue(config.allureService?.uploadConcurrency, 1);
238
249
  return {
239
250
  name,
240
251
  cwd,
@@ -249,7 +260,10 @@ export const resolveConfig = async (config, override = {}) => {
249
260
  environments,
250
261
  appendHistory,
251
262
  historyLimit,
263
+ flakyDetection: config.flakyDetection,
252
264
  historyPath: historyPath ? resolve(historyPath) : undefined,
265
+ historyBaseUrl,
266
+ dump,
253
267
  reportFiles: new FileSystemReportFiles(output),
254
268
  plugins: pluginInstances,
255
269
  defaultLabels: config.defaultLabels ?? {},
@@ -259,9 +273,11 @@ export const resolveConfig = async (config, override = {}) => {
259
273
  ? {
260
274
  accessToken: config.allureService.accessToken,
261
275
  private: config.allureService.private,
262
- uploadConcurrency: parseIntegerConfigValue(config.allureService.uploadConcurrency, DEFAULT_ALLURE_SERVICE_UPLOAD_CONCURRENCY, 1),
263
- uploadMaxAttempts: parseIntegerConfigValue(config.allureService.uploadMaxAttempts, DEFAULT_ALLURE_SERVICE_UPLAOD_MAX_ATTEMPTS, 1),
264
- uploadMaxSimultaneousFailures: parseIntegerConfigValue(config.allureService.uploadMaxSimultaneousFailures, DEFAULT_ALLURE_SERVICE_UPLOAD_MAX_SIMULTANEOUS_FAILURES, 0),
276
+ ...(uploadConcurrency === undefined ? {} : { uploadConcurrency }),
277
+ uploadMaxAttempts: parseIntegerConfigValue(config.allureService.uploadMaxAttempts, 1) ??
278
+ DEFAULT_ALLURE_SERVICE_UPLAOD_MAX_ATTEMPTS,
279
+ uploadMaxSimultaneousFailures: parseIntegerConfigValue(config.allureService.uploadMaxSimultaneousFailures, 0) ??
280
+ DEFAULT_ALLURE_SERVICE_UPLOAD_MAX_SIMULTANEOUS_FAILURES,
265
281
  }
266
282
  : undefined,
267
283
  categories: config.categories,
package/dist/history.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { type AllureHistory, type HistoryDataPoint, type MetricSample, type TestCase, type TestResult } from "@allurereport/core-api";
2
+ export declare const normalizeHistoryBaseUrl: (historyBaseUrl: string) => string;
3
+ export declare const setHistoryDataPointUrl: (point: HistoryDataPoint, url: string) => HistoryDataPoint;
2
4
  export declare const createHistory: (reportUuid: string, reportName: string | undefined, testCases: TestCase[], testResults: TestResult[], remoteUrl?: string, metrics?: MetricSample[]) => HistoryDataPoint;
3
5
  export declare class AllureLocalHistory implements AllureHistory {
4
6
  #private;
@@ -7,6 +9,7 @@ export declare class AllureLocalHistory implements AllureHistory {
7
9
  historyPath: string;
8
10
  limit?: number;
9
11
  });
12
+ resolveTestResultUrl(historyUrl: string, pluginId: string, historicalResultId: string): string;
10
13
  readHistory(): Promise<HistoryDataPoint[]>;
11
14
  appendHistory(data: HistoryDataPoint): Promise<void>;
12
15
  }
package/dist/history.js CHANGED
@@ -19,8 +19,8 @@ import { normalizeHistoryDataPointUrls, } from "@allurereport/core-api";
19
19
  import { isFileNotFoundError } from "./utils/misc.js";
20
20
  const createHistoryItems = (testResults, remoteUrl) => {
21
21
  return testResults
22
- .filter((tr) => tr.historyId)
23
- .map(({ id, name, fullName, environment, historyId, status, error: { message, trace } = {}, start, stop, duration, labels, }) => {
22
+ .filter((tr) => tr.retryHash)
23
+ .map(({ id, name, fullName, environment, retryHash, status, error: { message, trace } = {}, start, stop, duration, labels, }) => {
24
24
  return {
25
25
  id,
26
26
  name,
@@ -34,12 +34,12 @@ const createHistoryItems = (testResults, remoteUrl) => {
34
34
  duration,
35
35
  labels,
36
36
  url: remoteUrl,
37
- historyId: historyId,
37
+ retryHash: retryHash,
38
38
  reportLinks: [],
39
39
  };
40
40
  })
41
41
  .reduce((acc, item) => {
42
- acc[item.historyId] = item;
42
+ acc[item.retryHash] = item;
43
43
  return acc;
44
44
  }, {});
45
45
  };
@@ -53,6 +53,28 @@ const metricsToHistoryValues = (metrics) => {
53
53
  }
54
54
  return Object.fromEntries([...grouped.entries()].map(([key, values]) => [key, values.reduce((acc, value) => acc + value, 0) / values.length]));
55
55
  };
56
+ export const normalizeHistoryBaseUrl = (historyBaseUrl) => {
57
+ let url;
58
+ try {
59
+ url = new URL(historyBaseUrl);
60
+ }
61
+ catch (cause) {
62
+ throw new Error(`Invalid historyBaseUrl ${JSON.stringify(historyBaseUrl)}: expected an absolute URL`, { cause });
63
+ }
64
+ if (url.href.includes("#")) {
65
+ throw new Error(`Invalid historyBaseUrl ${JSON.stringify(historyBaseUrl)}: fragments are not allowed`);
66
+ }
67
+ if (/\.html\/?$/iu.test(url.pathname)) {
68
+ throw new Error(`Invalid historyBaseUrl ${JSON.stringify(historyBaseUrl)}: expected a base URL, not an HTML document`);
69
+ }
70
+ url.pathname = `${url.pathname.replace(/\/+$/u, "")}/`;
71
+ return url.toString();
72
+ };
73
+ export const setHistoryDataPointUrl = (point, url) => ({
74
+ ...point,
75
+ url,
76
+ testResults: Object.fromEntries(Object.entries(point.testResults).map(([historyId, item]) => [historyId, { ...item, url }])),
77
+ });
56
78
  export const createHistory = (reportUuid, reportName = "Allure Report", testCases, testResults, remoteUrl = "", metrics = []) => {
57
79
  const knownTestCaseIds = testCases.map((tc) => tc.id);
58
80
  return {
@@ -138,6 +160,17 @@ export class AllureLocalHistory {
138
160
  `doesn't match the actual number ${actualBytes}`);
139
161
  });
140
162
  }
163
+ resolveTestResultUrl(historyUrl, pluginId, historicalResultId) {
164
+ if (!historyUrl) {
165
+ return "";
166
+ }
167
+ const url = new URL(historyUrl);
168
+ if (url.pathname.endsWith("/")) {
169
+ url.pathname = `${url.pathname}${pluginId}/index.html`;
170
+ }
171
+ url.hash = historicalResultId;
172
+ return url.toString();
173
+ }
141
174
  async readHistory() {
142
175
  if (__classPrivateFieldGet(this, _AllureLocalHistory_cachedHistory, "f").length > 0) {
143
176
  return __classPrivateFieldGet(this, _AllureLocalHistory_cachedHistory, "f");
@@ -174,15 +207,14 @@ export class AllureLocalHistory {
174
207
  const { limit } = this.params;
175
208
  await mkdir(parentDir, { recursive: true });
176
209
  const { file: historyFile, exists: historyExists } = await __classPrivateFieldGet(this, _AllureLocalHistory_ensureFileOpenedToAppend, "f").call(this, fullPath);
210
+ let dst;
177
211
  try {
178
- const dst = historyFile.createWriteStream({ encoding: "utf-8", start: 0, autoClose: false });
179
- if (limit === 0 && historyExists) {
180
- await historyFile.truncate(0);
181
- return;
182
- }
183
- if (limit === 0 && !historyExists) {
212
+ if (limit === 0) {
213
+ if (historyExists)
214
+ await historyFile.truncate(0);
184
215
  return;
185
216
  }
217
+ dst = historyFile.createWriteStream({ encoding: "utf-8", start: 0, autoClose: false });
186
218
  if (historyExists) {
187
219
  const start = await __classPrivateFieldGet(this, _AllureLocalHistory_findFirstEntryAddress, "f").call(this, historyFile, limit ? limit - 1 : undefined);
188
220
  const src = historyFile.createReadStream({ start, autoClose: false });
@@ -195,7 +227,11 @@ export class AllureLocalHistory {
195
227
  }
196
228
  }
197
229
  finally {
198
- await historyFile.close();
230
+ const closing = historyFile.close();
231
+ if (historyFile.fd !== -1) {
232
+ dst?.destroy();
233
+ }
234
+ await closing;
199
235
  if (limit !== 0) {
200
236
  __classPrivateFieldGet(this, _AllureLocalHistory_cachedHistory, "f").push(normalizedData);
201
237
  }
package/dist/index.d.ts CHANGED
@@ -11,4 +11,4 @@ export * from "./resolutions.js";
11
11
  export { resolveConfig, readConfig, readRawConfig, getPluginInstance } from "./config.js";
12
12
  export * from "./report.js";
13
13
  export * from "./plugin.js";
14
- export { QualityGateState, qualityGateDefaultRules, maxFailuresRule, minTestsCountRule, successRateRule, maxDurationRule, allTestsContainEnvRule, environmentsTestedRule, metricMaxRule, metricMinRule, metricMaxDeltaRule, metricMaxDeltaPercentRule, convertQualityGateResultsToTestErrors, stringifyQualityGateResults, } from "./qualityGate/index.js";
14
+ export { QualityGateState, qualityGateDefaultRules, maxFailuresRule, minTestsCountRule, successRateRule, maxDurationRule, allTestsContainEnvRule, environmentsTestedRule, metricMaxRule, metricMinRule, metricMaxDeltaRule, metricMaxDeltaPercentRule, convertQualityGateResultsToTestErrors, filterFailedQualityGateResults, stringifyQualityGateResults, } from "./qualityGate/index.js";
package/dist/index.js CHANGED
@@ -10,4 +10,4 @@ export * from "./resolutions.js";
10
10
  export { resolveConfig, readConfig, readRawConfig, getPluginInstance } from "./config.js";
11
11
  export * from "./report.js";
12
12
  export * from "./plugin.js";
13
- export { QualityGateState, qualityGateDefaultRules, maxFailuresRule, minTestsCountRule, successRateRule, maxDurationRule, allTestsContainEnvRule, environmentsTestedRule, metricMaxRule, metricMinRule, metricMaxDeltaRule, metricMaxDeltaPercentRule, convertQualityGateResultsToTestErrors, stringifyQualityGateResults, } from "./qualityGate/index.js";
13
+ export { QualityGateState, qualityGateDefaultRules, maxFailuresRule, minTestsCountRule, successRateRule, maxDurationRule, allTestsContainEnvRule, environmentsTestedRule, metricMaxRule, metricMinRule, metricMaxDeltaRule, metricMaxDeltaPercentRule, convertQualityGateResultsToTestErrors, filterFailedQualityGateResults, stringifyQualityGateResults, } from "./qualityGate/index.js";
@@ -1,5 +1,6 @@
1
1
  import { type MetricSample, type TestError, type TestResult } from "@allurereport/core-api";
2
2
  import type { QualityGateConfig, QualityGateMetricHistoryPoint, QualityGateValidationResult } from "@allurereport/plugin-api";
3
+ export declare const filterFailedQualityGateResults: (results: QualityGateValidationResult[]) => QualityGateValidationResult[];
3
4
  export declare const stringifyQualityGateResults: (results: QualityGateValidationResult[]) => string;
4
5
  export declare const convertQualityGateResultsToTestErrors: (results: QualityGateValidationResult[]) => TestError[];
5
6
  export declare class QualityGateState {
@@ -4,25 +4,29 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
4
4
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
5
  };
6
6
  var _QualityGateState_state, _QualityGateState_processedTestResultIds;
7
- import { gray, red } from "yoctocolors";
7
+ import { bold, gray, red } from "yoctocolors";
8
8
  import { isIgnoredFailure } from "../resolutions.js";
9
9
  import { qualityGateDefaultRules } from "./rules.js";
10
+ export const filterFailedQualityGateResults = (results) => results.filter(({ success }) => !success);
11
+ const formatQualityGateValue = (value) => (Array.isArray(value) ? value.join(", ") : String(value));
12
+ const defaultSuccessMessage = ({ actual, expected }) => `The rule has been passed; actual ${bold(formatQualityGateValue(actual))}, expected ${bold(formatQualityGateValue(expected))}`;
10
13
  export const stringifyQualityGateResults = (results) => {
11
- if (results.length === 0) {
14
+ const failedResults = filterFailedQualityGateResults(results);
15
+ if (failedResults.length === 0) {
12
16
  return "";
13
17
  }
14
18
  const lines = [red("Quality Gate failed with following issues:")];
15
- const maxMessageLength = Math.max(...results.map((r) => r.message.length));
19
+ const maxMessageLength = Math.max(...failedResults.map((r) => r.message.length));
16
20
  lines.push("");
17
- results.forEach((result) => {
21
+ failedResults.forEach((result) => {
18
22
  lines.push(` ${red("⨯")} ${result.message.padEnd(maxMessageLength, " ")} ${gray(result.rule)}`);
19
23
  });
20
24
  lines.push("");
21
- lines.push(red(`${results.length} quality gate rules have been failed.`));
25
+ lines.push(red(`${failedResults.length} quality gate rules have been failed.`));
22
26
  return lines.join("\n");
23
27
  };
24
28
  export const convertQualityGateResultsToTestErrors = (results) => {
25
- return results.map((result) => ({
29
+ return filterFailedQualityGateResults(results).map((result) => ({
26
30
  message: `Quality Gate (${result.rule}): ${result.message}`,
27
31
  actual: result.actual,
28
32
  expected: result.expected,
@@ -67,7 +71,7 @@ export class QualityGate {
67
71
  }
68
72
  trsToValidateById.set(tr.id, tr);
69
73
  }
70
- const trsToValidate = trsToValidateById.values().toArray();
74
+ const trsToValidate = [...trsToValidateById.values()];
71
75
  const { rules, use = [...qualityGateDefaultRules] } = this.config;
72
76
  const results = [];
73
77
  let fastFailed = false;
@@ -102,21 +106,21 @@ export class QualityGate {
102
106
  previousHistory,
103
107
  environment,
104
108
  });
105
- if (result.success) {
106
- continue;
107
- }
109
+ const messagePayload = {
110
+ actual: result.actual,
111
+ expected,
112
+ };
108
113
  results.push({
109
114
  ...result,
110
115
  expected,
111
116
  rule: ruleId,
112
- message: rule.message({
113
- actual: result.actual,
114
- expected,
115
- }),
117
+ message: result.success
118
+ ? (rule.successMessage ?? defaultSuccessMessage)(messagePayload)
119
+ : rule.message(messagePayload),
116
120
  environment,
117
121
  testResults: [...new Set([...(state?.getTestResults(ruleId) ?? []), ...result.testResults])],
118
122
  });
119
- if (ruleset.fastFail) {
123
+ if (!result.success && ruleset.fastFail) {
120
124
  fastFailed = true;
121
125
  break;
122
126
  }
@@ -1,4 +1,4 @@
1
- import { filterSuccessful, filterUnsuccessful } from "@allurereport/core-api";
1
+ import { filterIncludedInSuccessRate, filterSuccessful, filterUnsuccessful, getSuccessRate, } from "@allurereport/core-api";
2
2
  import { bold } from "yoctocolors";
3
3
  const numberStateValue = (value) => (Number.isFinite(value) ? Number(value) : 0);
4
4
  const stringArrayStateValue = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
@@ -41,12 +41,17 @@ const metricActual = (metrics, key, value) => {
41
41
  ...(sample?.unit ? { unit: sample.unit } : {}),
42
42
  };
43
43
  };
44
+ const formatQualityGateNumber = (value) => {
45
+ if (!Number.isFinite(value)) {
46
+ return String(value);
47
+ }
48
+ return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(3)));
49
+ };
44
50
  const formatMetricValue = (value, unit) => {
45
51
  if (!Number.isFinite(value)) {
46
52
  return "n/a";
47
53
  }
48
- const finiteValue = value;
49
- const formatted = Number.isInteger(finiteValue) ? String(finiteValue) : String(Number(finiteValue.toFixed(3)));
54
+ const formatted = formatQualityGateNumber(value);
50
55
  return unit ? `${formatted} ${unit}` : formatted;
51
56
  };
52
57
  const metricRuleTitle = (actual) => actual.title ?? actual.key;
@@ -73,6 +78,7 @@ const metricDeltaWithinThreshold = (delta, threshold, better) => {
73
78
  export const maxFailuresRule = {
74
79
  rule: "maxFailures",
75
80
  message: ({ actual, expected }) => `The number of failed tests ${bold(String(actual))} exceeds the allowed threshold value ${bold(String(expected))}`,
81
+ successMessage: ({ actual, expected }) => `The number of failed tests ${bold(String(actual))} is within the allowed threshold value ${bold(String(expected))}`,
76
82
  validate: async ({ trs, expected, state }) => {
77
83
  const previous = numberStateValue(state.getResult());
78
84
  const failedTrs = trs.filter(filterUnsuccessful);
@@ -89,6 +95,7 @@ export const maxFailuresRule = {
89
95
  export const minTestsCountRule = {
90
96
  rule: "minTestsCount",
91
97
  message: ({ actual, expected }) => `The total number of tests ${bold(String(actual))} is less than the expected threshold value ${bold(String(expected))}`,
98
+ successMessage: ({ actual, expected }) => `The total number of tests ${bold(String(actual))} meets the expected threshold value ${bold(String(expected))}`,
92
99
  validate: async ({ trs, expected, state }) => {
93
100
  const actual = trs.length + numberStateValue(state.getResult());
94
101
  state.setResult(actual, []);
@@ -101,15 +108,17 @@ export const minTestsCountRule = {
101
108
  };
102
109
  export const successRateRule = {
103
110
  rule: "successRate",
104
- message: ({ actual, expected }) => `Success rate ${bold(String(actual))} is less, than expected ${bold(String(expected))}`,
111
+ message: ({ actual, expected }) => `Success rate ${bold(formatQualityGateNumber(actual))} is less, than expected ${bold(formatQualityGateNumber(expected))}`,
112
+ successMessage: ({ actual, expected }) => `Success rate ${bold(formatQualityGateNumber(actual))} is not less, than expected ${bold(formatQualityGateNumber(expected))}`,
105
113
  validate: async ({ trs, expected, state }) => {
106
114
  const previous = successRateStateValue(state.getResult());
107
- const passedTrs = trs.filter(filterSuccessful);
108
- const notPassedTrs = trs.filter((tr) => !filterSuccessful(tr));
109
- const totalCount = previous.totalCount + trs.length;
115
+ const eligibleTrs = trs.filter(filterIncludedInSuccessRate);
116
+ const passedTrs = eligibleTrs.filter(filterSuccessful);
117
+ const notPassedTrs = eligibleTrs.filter(filterUnsuccessful);
118
+ const totalCount = previous.totalCount + eligibleTrs.length;
110
119
  const passedCount = previous.passedCount + passedTrs.length;
111
120
  const testResults = notPassedTrs.map((tr) => tr.id);
112
- const rate = totalCount === 0 ? 0 : passedCount / totalCount;
121
+ const rate = getSuccessRate({ passed: passedCount, failed: totalCount - passedCount });
113
122
  state.setResult({ totalCount, passedCount }, testResults);
114
123
  return {
115
124
  success: rate >= expected,
@@ -121,6 +130,7 @@ export const successRateRule = {
121
130
  export const maxDurationRule = {
122
131
  rule: "maxDuration",
123
132
  message: ({ actual, expected }) => `Maximum duration of some tests exceed the defined limit; actual ${bold(String(actual))}, expected ${bold(String(expected))}`,
133
+ successMessage: ({ actual, expected }) => `Maximum duration of the tests is within the defined limit; actual ${bold(String(actual))}, expected ${bold(String(expected))}`,
124
134
  validate: async ({ trs, expected, state }) => {
125
135
  const previous = numberStateValue(state.getResult());
126
136
  const actual = Math.max(previous, ...trs.map((tr) => tr.duration ?? 0));
@@ -137,6 +147,7 @@ export const maxDurationRule = {
137
147
  export const allTestsContainEnvRule = {
138
148
  rule: "allTestsContainEnv",
139
149
  message: ({ actual, expected }) => `Not all tests contain the required "${bold(expected)}" environment, ${bold(String(actual))} tests have different or missing environment`,
150
+ successMessage: ({ expected }) => `All tests contain the required "${bold(expected)}" environment`,
140
151
  validate: async ({ trs, expected, state }) => {
141
152
  const previous = numberStateValue(state.getResult());
142
153
  const testsWithoutEnv = trs.filter((tr) => (tr.environment ?? "") !== expected);
@@ -153,6 +164,7 @@ export const allTestsContainEnvRule = {
153
164
  export const environmentsTestedRule = {
154
165
  rule: "environmentsTested",
155
166
  message: ({ actual, expected }) => `The following environments were not tested: "${actual.join('", "')}"; expected all of: "${expected.join('", "')}"`,
167
+ successMessage: ({ expected }) => `All expected environments were tested: "${expected.join('", "')}"`,
156
168
  validate: async ({ trs, expected, state }) => {
157
169
  const previouslyTested = new Set(stringArrayStateValue(state.getResult()));
158
170
  const batchTested = trs.map((tr) => tr.environment).filter((env) => env != null && env !== "");