@allurereport/ci 3.19.0 → 3.19.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.
@@ -27,6 +27,7 @@ const emptyResolutionStats = () => ({
27
27
  });
28
28
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29
29
  const isTestStatus = (value) => typeof value === "string" && TEST_STATUSES.includes(value);
30
+ const getFiniteNumber = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
30
31
  const normalizePath = (filePath) => filePath.split(sep).join("/");
31
32
  const readOptionalJson = async (filePath, onError) => {
32
33
  if (!existsSync(filePath)) {
@@ -40,12 +41,43 @@ const readOptionalJson = async (filePath, onError) => {
40
41
  return undefined;
41
42
  }
42
43
  };
44
+ const getReportName = (summary) => {
45
+ if (typeof summary.name === "string" && summary.name.trim()) {
46
+ return summary.name;
47
+ }
48
+ return "Allure Report";
49
+ };
50
+ const hasPluginSummarySignal = (value) => typeof value.name === "string" ||
51
+ typeof value.plugin === "string" ||
52
+ typeof value.href === "string" ||
53
+ typeof value.remoteHref === "string" ||
54
+ isRecord(value.stats);
43
55
  const isPluginSummary = (value) => {
44
- if (!isRecord(value) || typeof value.name !== "string" || !isRecord(value.stats)) {
56
+ if (!isRecord(value) || !hasPluginSummarySignal(value)) {
45
57
  return false;
46
58
  }
47
- return typeof value.duration === "number" && isTestStatus(value.status);
59
+ return true;
60
+ };
61
+ const normalizePluginSummaryStats = (summary) => {
62
+ const rawStats = isRecord(summary.stats) ? summary.stats : {};
63
+ const stats = {
64
+ ...rawStats,
65
+ failed: getFiniteNumber(rawStats.failed),
66
+ broken: getFiniteNumber(rawStats.broken),
67
+ passed: getFiniteNumber(rawStats.passed),
68
+ skipped: getFiniteNumber(rawStats.skipped),
69
+ unknown: getFiniteNumber(rawStats.unknown),
70
+ };
71
+ stats.total = getFiniteNumber(rawStats.total, TEST_STATUSES.reduce((acc, status) => acc + getFiniteNumber(stats[status]), 0));
72
+ return stats;
48
73
  };
74
+ const normalizePluginSummary = (summary) => ({
75
+ ...summary,
76
+ name: getReportName(summary),
77
+ stats: normalizePluginSummaryStats(summary),
78
+ status: isTestStatus(summary.status) ? summary.status : "passed",
79
+ duration: getFiniteNumber(summary.duration),
80
+ });
49
81
  const getReportPath = (reportDir, summaryFile) => normalizePath(relative(resolve(reportDir), dirname(resolve(summaryFile))));
50
82
  const readPluginSummary = async (reportDir, filePath, onError) => {
51
83
  const value = await readOptionalJson(filePath, onError);
@@ -57,7 +89,7 @@ const readPluginSummary = async (reportDir, filePath, onError) => {
57
89
  return undefined;
58
90
  }
59
91
  return {
60
- ...value,
92
+ ...normalizePluginSummary(value),
61
93
  summaryFile: filePath,
62
94
  reportPath: getReportPath(reportDir, filePath),
63
95
  };
@@ -204,8 +236,10 @@ const createEnvironmentContext = (registry, summaries) => {
204
236
  });
205
237
  return [...environmentsByName.values()].toSorted((left, right) => left.name.localeCompare(right.name));
206
238
  };
207
- const createReport = (summary) => ({ ...summary });
208
- const sortReports = (reports) => reports.toSorted((left, right) => left.name.localeCompare(right.name));
239
+ const createReport = (summary) => normalizePluginSummary(summary);
240
+ const sortReports = (reports) => reports
241
+ .map((report) => normalizePluginSummary(report))
242
+ .toSorted((left, right) => left.name.localeCompare(right.name));
209
243
  const getResolutionStats = (summaries) => {
210
244
  const stats = emptyResolutionStats();
211
245
  summaries.forEach((summary) => {
@@ -1,6 +1,16 @@
1
- import type { ReportContext } from "./reportContext.js";
1
+ import type { ReportContext, ReportContextFlagStats, ReportContextTestReport, ReportContextResolutionStats, ReportContextStatusStats } from "./reportContext.js";
2
+ export type ReportSummaryMarkdownRow = {
3
+ kind: "total" | "environment" | "report";
4
+ name: string;
5
+ duration: number;
6
+ stats: ReportContextStatusStats;
7
+ flags: ReportContextFlagStats;
8
+ resolutions?: ReportContextResolutionStats;
9
+ report?: ReportContextTestReport;
10
+ };
2
11
  export type RenderReportSummaryMarkdownOptions = {
3
12
  title?: string;
4
13
  includeArtifacts?: boolean;
14
+ getReportFilterHref?: (filter: keyof ReportContextFlagStats, row: ReportSummaryMarkdownRow) => string | undefined;
5
15
  };
6
16
  export declare const renderReportSummaryMarkdown: (context: ReportContext, options?: RenderReportSummaryMarkdownOptions) => string;
@@ -10,6 +10,7 @@ const STATUS_ICON_BASE_URL = "https://allurecharts.qameta.workers.dev/dot";
10
10
  const STATUS_PIE_BASE_URL = "https://allurecharts.qameta.workers.dev/pie";
11
11
  const escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
12
12
  const tableCell = (value) => escapeHtml(String(value)).replaceAll("|", "&#124;").replaceAll("\n", "<br>");
13
+ const inlineCode = (value) => `<code>${escapeHtml(value)}</code>`;
13
14
  const isSafeHref = (href) => {
14
15
  try {
15
16
  const url = new URL(href);
@@ -82,11 +83,19 @@ const formatResolutions = (resolutions) => {
82
83
  return lines.join("<br>");
83
84
  };
84
85
  const hasResolutions = (resolutions) => resolutions.issues > 0 || resolutions.muted > 0 || resolutions.accepted > 0;
85
- const formatFlag = (value) => (value > 0 ? String(value) : "0");
86
- const renderTable = (rows, includeResolutions) => {
86
+ const formatFlag = (flag, row, options) => {
87
+ const value = row.flags[flag];
88
+ if (value <= 0) {
89
+ return "0";
90
+ }
91
+ const label = String(value);
92
+ const href = options.getReportFilterHref?.(flag, row);
93
+ return href ? link(label, href) : label;
94
+ };
95
+ const renderTable = (rows, includeResolutions, options) => {
87
96
  const headers = [
88
97
  "&nbsp;&nbsp;&nbsp;&nbsp;",
89
- "Scope",
98
+ "Environment",
90
99
  "Duration",
91
100
  "Stats",
92
101
  ...(includeResolutions ? ["Resolutions"] : []),
@@ -101,14 +110,31 @@ const renderTable = (rows, includeResolutions) => {
101
110
  tableCell(formatDuration(row.duration)),
102
111
  formatStats(row.stats),
103
112
  ...(includeResolutions ? [formatResolutions(row.resolutions)] : []),
104
- tableCell(formatFlag(row.flags.new)),
105
- tableCell(formatFlag(row.flags.flaky)),
106
- tableCell(formatFlag(row.flags.retry)),
113
+ formatFlag("new", row, options),
114
+ formatFlag("flaky", row, options),
115
+ formatFlag("retry", row, options),
107
116
  ].join(" | "));
108
117
  return [`| ${headers.join(" | ")} |`, `| ${divider.join(" | ")} |`, ...body.map((row) => `| ${row} |`)].join("\n");
109
118
  };
110
119
  const reportHref = (report) => report.remoteHref ?? report.href;
111
120
  const reportLabel = (report) => report.plugin ?? report.name;
121
+ const isAwesomeReport = (report) => report?.pluginId?.toLowerCase() === "awesome" || report?.plugin?.toLowerCase() === "awesome";
122
+ const appendReportFilter = (href, filter) => {
123
+ const hashIndex = href.indexOf("#");
124
+ const base = hashIndex === -1 ? href : href.slice(0, hashIndex);
125
+ const hash = hashIndex === -1 ? "" : href.slice(hashIndex);
126
+ const separator = base.includes("?") ? "&" : "?";
127
+ const query = filter === "new" ? "transition=new" : `${filter}=true`;
128
+ return `${base}${separator}${query}${hash}`;
129
+ };
130
+ const createDefaultReportFilterHref = (reports) => {
131
+ const defaultReport = reports.find(isAwesomeReport);
132
+ return (filter, row) => {
133
+ const report = isAwesomeReport(row.report) ? row.report : defaultReport;
134
+ const href = report ? reportHref(report) : undefined;
135
+ return href ? appendReportFilter(href, filter) : undefined;
136
+ };
137
+ };
112
138
  const reportLinkKind = (report) => report.plugin?.toLowerCase() === "testops" ? "testops" : "report";
113
139
  const toReportLink = (report) => {
114
140
  const href = reportHref(report);
@@ -151,43 +177,46 @@ const pluginSummaryToResolutions = (report) => ({
151
177
  muted: report.stats.resolutions?.muted ?? 0,
152
178
  accepted: report.stats.resolutions?.accepted ?? 0,
153
179
  });
154
- const renderFilteredReports = (reports) => {
180
+ const renderFilteredReports = (reports, options) => {
155
181
  const rows = reports.map((report) => ({
182
+ kind: "report",
156
183
  name: report.name,
157
184
  duration: report.duration,
158
185
  stats: pluginSummaryToStatusStats(report),
159
186
  flags: pluginSummaryToFlags(report),
160
187
  resolutions: pluginSummaryToResolutions(report),
188
+ report,
161
189
  }));
162
190
  if (!rows.length) {
163
191
  return undefined;
164
192
  }
165
193
  const includeResolutions = rows.some(({ resolutions }) => resolutions && hasResolutions(resolutions));
166
194
  const links = renderReportLinks(reports);
167
- return ["**Filtered Reports**", renderTable(rows, includeResolutions), ...links].join("\n\n");
195
+ return ["**Filtered Reports**", renderTable(rows, includeResolutions, options), ...links].join("\n\n");
168
196
  };
169
197
  const renderArtifacts = (artifacts) => {
170
198
  if (!artifacts.length) {
171
199
  return undefined;
172
200
  }
173
- const rows = artifacts.map(({ name, path }) => `| ${tableCell(name)} | ${tableCell(path)} |`);
174
- return [
175
- `<details>`,
176
- `<summary>Artifacts used (${artifacts.length})</summary>`,
177
- "",
178
- "| Name | Path |",
179
- "| --- | --- |",
180
- ...rows,
181
- "",
182
- "</details>",
183
- ].join("\n");
201
+ const rows = artifacts.map(({ name, path }) => {
202
+ const normalizedPath = path.replaceAll("\\", "/");
203
+ const isDuplicateName = name === path || normalizedPath.endsWith(`/${name}`);
204
+ const label = isDuplicateName ? inlineCode(path) : `${inlineCode(name)} &mdash; ${inlineCode(path)}`;
205
+ return `- ${label}`;
206
+ });
207
+ return [`<details>`, `<summary>Artifacts used (${artifacts.length})</summary>`, "", ...rows, "", "</details>"].join("\n");
184
208
  };
185
209
  export const renderReportSummaryMarkdown = (context, options = {}) => {
186
210
  const { title = "Allure Report Summary", includeArtifacts = true } = options;
187
211
  const regularReports = context.reports.filter((report) => report.filtered !== true);
188
212
  const filteredReports = context.reports.filter((report) => report.filtered === true);
213
+ const renderOptions = {
214
+ ...options,
215
+ getReportFilterHref: options.getReportFilterHref ?? createDefaultReportFilterHref(regularReports),
216
+ };
189
217
  const aggregateRows = [
190
218
  {
219
+ kind: "total",
191
220
  name: "All tests",
192
221
  duration: context.totals.duration,
193
222
  stats: context.totals.stats,
@@ -195,6 +224,7 @@ export const renderReportSummaryMarkdown = (context, options = {}) => {
195
224
  resolutions: context.totals.resolutions,
196
225
  },
197
226
  ...context.environments.map((environment) => ({
227
+ kind: "environment",
198
228
  name: environment.name,
199
229
  duration: environment.duration,
200
230
  stats: environment.stats,
@@ -204,9 +234,9 @@ export const renderReportSummaryMarkdown = (context, options = {}) => {
204
234
  const includeResolutions = hasResolutions(context.totals.resolutions);
205
235
  const sections = [
206
236
  `# ${escapeHtml(title)}`,
207
- renderTable(aggregateRows, includeResolutions),
237
+ renderTable(aggregateRows, includeResolutions, renderOptions),
208
238
  ...renderReportLinks(regularReports),
209
- renderFilteredReports(filteredReports),
239
+ renderFilteredReports(filteredReports, renderOptions),
210
240
  includeArtifacts ? renderArtifacts(context.artifacts) : undefined,
211
241
  ].filter((section) => Boolean(section));
212
242
  return `${sections.join("\n\n")}\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/ci",
3
- "version": "3.19.0",
3
+ "version": "3.19.1",
4
4
  "description": "Utilities set to get useful report data from different CI",
5
5
  "keywords": [
6
6
  "allure",
@@ -32,15 +32,15 @@
32
32
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
33
33
  },
34
34
  "dependencies": {
35
- "@allurereport/core-api": "3.19.0",
36
- "@allurereport/plugin-api": "3.19.0",
35
+ "@allurereport/core-api": "3.19.1",
36
+ "@allurereport/plugin-api": "3.19.1",
37
37
  "axios": "^1.20.0",
38
38
  "glob": "^13.0.6"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^20",
42
42
  "@vitest/runner": "^2.1.8",
43
- "@vitest/snapshot": "^2.1.8",
43
+ "@vitest/snapshot": "^5.0.1",
44
44
  "allure-js-commons": "^3",
45
45
  "allure-vitest": "^3",
46
46
  "rimraf": "^6",