Skip to content

Commit b48dac9

Browse files
committed
refactor(plugin-axe,plugin-eslint,utils): streamline formatting and transformation
1 parent 0a8038f commit b48dac9

File tree

9 files changed

+125
-64
lines changed

9 files changed

+125
-64
lines changed

packages/plugin-axe/src/lib/meta/transform.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,15 @@ function getPresetTags(preset: AxePreset): string[] {
7979
}
8080
}
8181

82-
function createGroup(slug: string, title: string, ruleIds: string[]): Group {
82+
function createGroup(
83+
slug: string,
84+
title: string,
85+
rules: axe.RuleMetadata[],
86+
): Group {
8387
return {
8488
slug,
8589
title,
86-
refs: ruleIds.map(ruleId => ({ slug: ruleId, weight: 1 })),
90+
refs: rules.map(({ ruleId }) => ({ slug: ruleId, weight: 1 })),
8791
};
8892
}
8993

@@ -95,26 +99,26 @@ function createWcagGroups(
9599
const aaTags =
96100
version === '2.1' ? WCAG_LEVEL_AA_TAGS_21 : WCAG_LEVEL_AA_TAGS_22;
97101

98-
const levelARuleIds = rules
99-
.filter(({ tags }) => tags.some(tag => aTags.includes(tag)))
100-
.map(({ ruleId }) => ruleId);
102+
const levelARules = rules.filter(({ tags }) =>
103+
tags.some(tag => aTags.includes(tag)),
104+
);
101105

102-
const levelAARuleIds = rules
103-
.filter(({ tags }) => tags.some(tag => aaTags.includes(tag)))
104-
.map(({ ruleId }) => ruleId);
106+
const levelAARules = rules.filter(({ tags }) =>
107+
tags.some(tag => aaTags.includes(tag)),
108+
);
105109

106110
const versionSlug = version.replace('.', '');
107111

108112
return [
109113
createGroup(
110114
`wcag${versionSlug}-level-a`,
111115
`WCAG ${version} Level A`,
112-
levelARuleIds,
116+
levelARules,
113117
),
114118
createGroup(
115119
`wcag${versionSlug}-level-aa`,
116120
`WCAG ${version} Level AA`,
117-
levelAARuleIds,
121+
levelAARules,
118122
),
119123
];
120124
}
@@ -127,20 +131,15 @@ function createCategoryGroups(rules: axe.RuleMetadata[]): Group[] {
127131
return Array.from(categoryTags).map(tag => {
128132
const slug = tag.replace('cat.', '');
129133
const title = formatCategoryTitle(tag, slug);
130-
const ruleIds = rules
131-
.filter(({ tags }) => tags.includes(tag))
132-
.map(({ ruleId }) => ruleId);
134+
const categoryRules = rules.filter(({ tags }) => tags.includes(tag));
133135

134-
return createGroup(slug, title, ruleIds);
136+
return createGroup(slug, title, categoryRules);
135137
});
136138
}
137139

138140
function formatCategoryTitle(tag: string, slug: string): string {
139141
if (CATEGORY_TITLES[tag]) {
140142
return CATEGORY_TITLES[tag];
141143
}
142-
return slug
143-
.split('-')
144-
.map(word => capitalize(word))
145-
.join(' ');
144+
return slug.split('-').map(capitalize).join(' ');
146145
}

packages/plugin-axe/src/lib/runner/run-axe.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import AxeBuilder from '@axe-core/playwright';
22
import { type Browser, chromium } from 'playwright-core';
33
import type { AuditOutputs } from '@code-pushup/models';
4-
import { logger, stringifyError } from '@code-pushup/utils';
4+
import { logger, pluralizeToken, stringifyError } from '@code-pushup/utils';
55
import { toAuditOutputs } from './transform.js';
66

77
let browser: Browser | undefined;
@@ -35,9 +35,11 @@ export async function runAxeForUrl(
3535

3636
const results = await axeBuilder.analyze();
3737

38-
if (results.incomplete.length > 0) {
38+
const incompleteCount = results.incomplete.length;
39+
40+
if (incompleteCount > 0) {
3941
logger.warn(
40-
`Axe returned ${results.incomplete.length} incomplete result(s) for ${url}`,
42+
`Axe returned ${pluralizeToken('incomplete result', incompleteCount)} for ${url}`,
4143
);
4244
}
4345

packages/plugin-axe/src/lib/runner/runner.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
import {
77
addIndex,
88
logger,
9+
pluralizeToken,
910
shouldExpandForUrls,
1011
stringifyError,
1112
} from '@code-pushup/utils';
@@ -16,17 +17,18 @@ export function createRunnerFunction(
1617
ruleIds: string[],
1718
): RunnerFunction {
1819
return async (_runnerArgs?: RunnerArgs): Promise<AuditOutputs> => {
19-
const isSingleUrl = !shouldExpandForUrls(urls.length);
20+
const urlCount = urls.length;
21+
const isSingleUrl = !shouldExpandForUrls(urlCount);
2022

2123
logger.info(
22-
`Running Axe accessibility checks for ${urls.length} URL(s)...`,
24+
`Running Axe accessibility checks for ${pluralizeToken('URL', urlCount)}...`,
2325
);
2426

2527
try {
2628
const allResults = await urls.reduce(async (prev, url, index) => {
2729
const acc = await prev;
2830

29-
logger.debug(`Testing URL ${index + 1}/${urls.length}: ${url}`);
31+
logger.debug(`Testing URL ${index + 1}/${urlCount}: ${url}`);
3032

3133
try {
3234
const auditOutputs = await runAxeForUrl(url, ruleIds);
@@ -45,7 +47,9 @@ export function createRunnerFunction(
4547
}
4648
}, Promise.resolve<AuditOutputs>([]));
4749

48-
if (allResults.length === 0) {
50+
const totalAuditCount = allResults.length;
51+
52+
if (totalAuditCount === 0) {
4953
throw new Error(
5054
isSingleUrl
5155
? 'Axe did not produce any results.'
@@ -54,7 +58,7 @@ export function createRunnerFunction(
5458
}
5559

5660
logger.info(
57-
`Completed Axe accessibility checks with ${allResults.length} audit(s)`,
61+
`Completed Axe accessibility checks with ${pluralizeToken('audit', totalAuditCount)}`,
5862
);
5963

6064
return allResults;

packages/plugin-axe/src/lib/runner/transform.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import type {
77
IssueSeverity,
88
} from '@code-pushup/models';
99
import {
10-
countOccurrences,
11-
objectToEntries,
10+
formatIssueSeverities,
11+
getUrlIdentifier,
1212
pluralizeToken,
1313
truncateIssueMessage,
1414
} from '@code-pushup/utils';
@@ -51,7 +51,7 @@ function toAuditOutput(
5151

5252
return {
5353
...base,
54-
displayValue: formatSeverityCounts(issues),
54+
displayValue: formatIssueSeverities(issues),
5555
details: { issues },
5656
};
5757
}
@@ -62,20 +62,6 @@ function toAuditOutput(
6262
};
6363
}
6464

65-
function formatSeverityCounts(issues: Issue[]): string {
66-
const severityCounts = countOccurrences(
67-
issues.map(({ severity }) => severity),
68-
);
69-
70-
return objectToEntries(severityCounts)
71-
.toSorted(([a], [b]) => {
72-
const order = { error: 0, warning: 1, info: 2 };
73-
return order[a] - order[b];
74-
})
75-
.map(([severity, count = 0]) => pluralizeToken(severity, count))
76-
.join(', ');
77-
}
78-
7965
function formatSelector(selector: axe.CrossTreeSelector): string {
8066
if (typeof selector === 'string') {
8167
return selector;
@@ -88,7 +74,7 @@ function toIssue(node: NodeResult, result: Result, url: string): Issue {
8874
const rawMessage = node.failureSummary || result.help;
8975
const cleanedMessage = rawMessage.replace(/\s+/g, ' ').trim();
9076

91-
const message = `[${selector}] ${cleanedMessage} (${url})`;
77+
const message = `[\`${selector}\`] ${cleanedMessage} ([${getUrlIdentifier(url)}](${url}))`;
9278

9379
return {
9480
message: truncateIssueMessage(message),

packages/plugin-axe/src/lib/runner/transform.unit.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,17 @@ describe('toAuditOutputs', () => {
9191
issues: [
9292
{
9393
message:
94-
'[img] Fix this: Element does not have an alt attribute (https://example.com)',
94+
'[`img`] Fix this: Element does not have an alt attribute ([example.com](https://example.com))',
9595
severity: 'error',
9696
},
9797
{
9898
message:
99-
'[.header > img:nth-child(2)] Fix this: Element does not have an alt attribute (https://example.com)',
99+
'[`.header > img:nth-child(2)`] Fix this: Element does not have an alt attribute ([example.com](https://example.com))',
100100
severity: 'error',
101101
},
102102
{
103103
message:
104-
'[#main img] Mock help for image-alt (https://example.com)',
104+
'[`#main img`] Mock help for image-alt ([example.com](https://example.com))',
105105
severity: 'error',
106106
},
107107
],
@@ -140,12 +140,12 @@ describe('toAuditOutputs', () => {
140140
issues: [
141141
{
142142
message:
143-
'[button] Fix this: Element has insufficient color contrast (https://example.com)',
143+
'[`button`] Fix this: Element has insufficient color contrast ([example.com](https://example.com))',
144144
severity: 'warning',
145145
},
146146
{
147147
message:
148-
'[a] Review: Unable to determine contrast ratio (https://example.com)',
148+
'[`a`] Review: Unable to determine contrast ratio ([example.com](https://example.com))',
149149
severity: 'warning',
150150
},
151151
],
@@ -253,7 +253,7 @@ describe('toAuditOutputs', () => {
253253
issues: [
254254
{
255255
message:
256-
'[#app >> my-component >> button] Fix this: Element has insufficient color contrast (https://example.com)',
256+
'[`#app >> my-component >> button`] Fix this: Element has insufficient color contrast ([example.com](https://example.com))',
257257
severity: 'error',
258258
},
259259
],
@@ -287,7 +287,7 @@ describe('toAuditOutputs', () => {
287287
issues: [
288288
{
289289
message:
290-
'[<div role="invalid-role">Content</div>] Fix this: Ensure all values assigned to role="" correspond to valid ARIA roles (https://example.com)',
290+
'[`<div role=\"invalid-role\">Content</div>`] Fix this: Ensure all values assigned to role=\"\" correspond to valid ARIA roles ([example.com](https://example.com))',
291291
severity: 'error',
292292
},
293293
],

packages/plugin-eslint/src/lib/runner/transform.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import type { Linter } from 'eslint';
22
import type { AuditOutput, Issue, IssueSeverity } from '@code-pushup/models';
33
import {
4-
compareIssueSeverity,
5-
countOccurrences,
6-
objectToEntries,
7-
pluralizeToken,
4+
formatIssueSeverities,
85
truncateIssueMessage,
96
ui,
107
} from '@code-pushup/utils';
@@ -51,20 +48,12 @@ export function lintResultsToAudits({
5148

5249
function toAudit(slug: string, issues: LintIssue[]): AuditOutput {
5350
const auditIssues = issues.map(convertIssue);
54-
const severityCounts = countOccurrences(
55-
auditIssues.map(({ severity }) => severity),
56-
);
57-
const severities = objectToEntries(severityCounts);
58-
const summaryText = severities
59-
.toSorted((a, b) => -compareIssueSeverity(a[0], b[0]))
60-
.map(([severity, count = 0]) => pluralizeToken(severity, count))
61-
.join(', ');
6251

6352
return {
6453
slug,
6554
score: Number(auditIssues.length === 0),
6655
value: auditIssues.length,
67-
displayValue: summaryText,
56+
displayValue: formatIssueSeverities(auditIssues),
6857
details: {
6958
issues: auditIssues,
7059
},

packages/utils/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ export { Logger, logger } from './lib/logger.js';
9191
export { link, ui, type CliUi, type Column } from './lib/logging.js';
9292
export { mergeConfigs } from './lib/merge-configs.js';
9393
export {
94+
getUrlIdentifier,
9495
normalizeUrlInput,
9596
type PluginUrlContext,
9697
} from './lib/plugin-url-config.js';
@@ -123,6 +124,7 @@ export {
123124
listAuditsFromAllPlugins,
124125
listGroupsFromAllPlugins,
125126
} from './lib/reports/flatten-plugins.js';
127+
export { formatIssueSeverities } from './lib/reports/formatting.js';
126128
export { generateMdReport } from './lib/reports/generate-md-report.js';
127129
export {
128130
generateMdReportsDiff,

packages/utils/src/lib/reports/formatting.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,28 @@ import {
77
import path from 'node:path';
88
import type {
99
AuditReport,
10+
Issue,
11+
IssueSeverity,
1012
SourceFileLocation,
1113
Table,
1214
Tree,
1315
} from '@code-pushup/models';
16+
import { pluralizeToken } from '../formatting.js';
1417
import { formatAsciiTree } from '../text-formats/ascii/tree.js';
1518
import {
1619
columnsToStringArray,
1720
getColumnAlignments,
1821
rowToStringArray,
1922
} from '../text-formats/table.js';
23+
import { countOccurrences, objectToEntries } from '../transform.js';
2024
import { AUDIT_DETAILS_HEADING_LEVEL } from './constants.js';
2125
import {
2226
getEnvironmentType,
2327
getGitHubBaseUrl,
2428
getGitLabBaseUrl,
2529
} from './environment-type.js';
2630
import type { MdReportOptions } from './types.js';
31+
import { compareIssueSeverity } from './utils.js';
2732

2833
export function tableSection(
2934
table: Table,
@@ -173,6 +178,30 @@ export function formatFileLink(
173178
}
174179
}
175180

181+
export function formatSeverityCounts(
182+
severityCounts: Partial<Record<IssueSeverity, number>>,
183+
): string {
184+
return objectToEntries(severityCounts)
185+
.toSorted(([a], [b]) => -compareIssueSeverity(a, b))
186+
.map(([severity, count = 0]) => pluralizeToken(severity, count))
187+
.join(', ');
188+
}
189+
190+
/**
191+
* Formats issues into a human-readable severity summary string.
192+
*
193+
* @param issues - Array of issues with severity property
194+
* @returns Formatted string like "3 errors, 5 warnings, 2 infos"
195+
*/
196+
export function formatIssueSeverities(
197+
issues: Pick<Issue, 'severity'>[],
198+
): string {
199+
const severityCounts = countOccurrences(
200+
issues.map(({ severity }) => severity),
201+
);
202+
return formatSeverityCounts(severityCounts);
203+
}
204+
176205
/**
177206
* Wraps HTML tags in backticks to prevent markdown parsers
178207
* from interpreting them as actual HTML.

0 commit comments

Comments
 (0)