Skip to content

Commit 91e52de

Browse files
committed
feat(utils,plugin-axe): improve report formatting
1 parent 1281873 commit 91e52de

File tree

6 files changed

+119
-11
lines changed

6 files changed

+119
-11
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AxeResults, ImpactValue, NodeResult, Result } from 'axe-core';
2+
import type axe from 'axe-core';
23
import type {
34
AuditOutput,
45
AuditOutputs,
@@ -75,8 +76,15 @@ function formatSeverityCounts(issues: Issue[]): string {
7576
.join(', ');
7677
}
7778

79+
function formatSelector(selector: axe.CrossTreeSelector): string {
80+
if (typeof selector === 'string') {
81+
return selector;
82+
}
83+
return selector.join(' >> ');
84+
}
85+
7886
function toIssue(node: NodeResult, result: Result, url: string): Issue {
79-
const selector = node.target?.[0] || node.html;
87+
const selector = formatSelector(node.target?.[0] || node.html);
8088
const rawMessage = node.failureSummary || result.help;
8189
const cleanedMessage = rawMessage.replace(/\s+/g, ' ').trim();
8290

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,71 @@ describe('toAuditOutputs', () => {
228228
displayValue: '2 errors, 1 warning, 1 info',
229229
});
230230
});
231+
232+
it('should format shadow DOM selectors with >> notation', () => {
233+
const results = createMockAxeResults({
234+
violations: [
235+
createMockResult('color-contrast', [
236+
createMockNode({
237+
html: '<button></button>',
238+
target: [['#app', 'my-component', 'button']],
239+
impact: 'critical',
240+
failureSummary: 'Fix this: Element has insufficient color contrast',
241+
}),
242+
]),
243+
],
244+
});
245+
246+
expect(toAuditOutputs(results, testUrl)).toEqual<AuditOutput[]>([
247+
{
248+
slug: 'color-contrast',
249+
score: 0,
250+
value: 1,
251+
displayValue: '1 error',
252+
details: {
253+
issues: [
254+
{
255+
message:
256+
'[#app >> my-component >> button] Fix this: Element has insufficient color contrast (https://example.com)',
257+
severity: 'error',
258+
},
259+
],
260+
},
261+
},
262+
]);
263+
});
264+
265+
it('should fall back to html when target is missing', () => {
266+
const results = createMockAxeResults({
267+
violations: [
268+
createMockResult('aria-roles', [
269+
createMockNode({
270+
html: '<div role="invalid-role">Content</div>',
271+
target: undefined,
272+
impact: 'serious',
273+
failureSummary:
274+
'Fix this: Ensure all values assigned to role="" correspond to valid ARIA roles',
275+
}),
276+
]),
277+
],
278+
});
279+
280+
expect(toAuditOutputs(results, testUrl)).toEqual<AuditOutput[]>([
281+
{
282+
slug: 'aria-roles',
283+
score: 0,
284+
value: 1,
285+
displayValue: '1 error',
286+
details: {
287+
issues: [
288+
{
289+
message:
290+
'[<div role="invalid-role">Content</div>] Fix this: Ensure all values assigned to role="" correspond to valid ARIA roles (https://example.com)',
291+
severity: 'error',
292+
},
293+
],
294+
},
295+
},
296+
]);
297+
});
231298
});

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,14 @@ export function metaDescription(
7373
if (!description) {
7474
return docsLink;
7575
}
76-
const parsedDescription = description.endsWith('```')
77-
? `${description}\n\n`
78-
: `${description} `;
76+
const formattedDescription = wrapTags(description);
77+
const parsedDescription = formattedDescription.endsWith('```')
78+
? `${formattedDescription}\n\n`
79+
: `${formattedDescription} `;
7980
return md`${parsedDescription}${docsLink}`;
8081
}
8182
if (description && description.trim().length > 0) {
82-
return description;
83+
return wrapTags(description);
8384
}
8485
return '';
8586
}
@@ -171,3 +172,14 @@ export function formatFileLink(
171172
return relativePath;
172173
}
173174
}
175+
176+
/**
177+
* Wraps HTML tags in backticks to prevent markdown parsers
178+
* from interpreting them as actual HTML.
179+
*/
180+
export function wrapTags(text?: string): string {
181+
if (!text) {
182+
return '';
183+
}
184+
return text.replace(/<[a-z][a-z0-9]*[^>]*>/gi, '`$&`');
185+
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
linkToLocalSourceForIde,
99
metaDescription,
1010
tableSection,
11+
wrapTags,
1112
} from './formatting.js';
1213

1314
describe('tableSection', () => {
@@ -360,3 +361,21 @@ describe('formatFileLink', () => {
360361
).toBe('../src/index.ts');
361362
});
362363
});
364+
365+
describe('wrapTags', () => {
366+
it.each([
367+
['<label>', '`<label>`'],
368+
['<img src="test.jpg">', '`<img src="test.jpg">`'],
369+
[
370+
'<li> elements must be contained in a <ul> or <ol>',
371+
'`<li>` elements must be contained in a `<ul>` or `<ol>`',
372+
],
373+
['x < 5', 'x < 5'],
374+
['x < 5 and y > 3', 'x < 5 and y > 3'],
375+
['body > button', 'body > button'],
376+
['', ''],
377+
[undefined, ''],
378+
])('should transform %j to %j', (input, expected) => {
379+
expect(wrapTags(input)).toBe(expected);
380+
});
381+
});

packages/utils/src/lib/reports/generate-md-report-category-section.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type InlineText, MarkdownDocument, md } from 'build-md';
22
import type { AuditReport } from '@code-pushup/models';
33
import { slugify } from '../formatting.js';
44
import { HIERARCHY } from '../text-formats/index.js';
5-
import { metaDescription } from './formatting.js';
5+
import { metaDescription, wrapTags } from './formatting.js';
66
import { getSortableAuditByRef, getSortableGroupByRef } from './sorting.js';
77
import type { ScoreFilter, ScoredGroup, ScoredReport } from './types.js';
88
import {
@@ -90,7 +90,7 @@ export function categoryRef(
9090
): InlineText {
9191
const auditTitleAsLink = md.link(
9292
`#${slugify(title)}-${slugify(pluginTitle)}`,
93-
title,
93+
wrapTags(title),
9494
);
9595
const marker = scoreMarker(score, 'square');
9696
return md`${marker} ${auditTitleAsLink} (${md.italic(

packages/utils/src/lib/reports/generate-md-report.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
metaDescription,
1515
tableSection,
1616
treeSection,
17+
wrapTags,
1718
} from './formatting.js';
1819
import {
1920
categoriesDetailsSection,
@@ -86,16 +87,17 @@ export function auditDetailsIssues(
8687
],
8788
issues.map(({ severity: level, message, source }: Issue) => {
8889
const severity = md`${severityMarker(level)} ${md.italic(level)}`;
90+
const formattedMessage = wrapTags(message);
8991

9092
if (!source) {
91-
return [severity, message];
93+
return [severity, formattedMessage];
9294
}
9395
const file = linkToLocalSourceForIde(source, options);
9496
if (!source.position) {
95-
return [severity, message, file];
97+
return [severity, formattedMessage, file];
9698
}
9799
const line = formatSourceLine(source.position);
98-
return [severity, message, file, line];
100+
return [severity, formattedMessage, file, line];
99101
}),
100102
);
101103
}
@@ -143,7 +145,7 @@ export function auditsSection(
143145
.map(audit => ({ ...audit, plugin })),
144146
),
145147
(doc, { plugin, ...audit }) => {
146-
const auditTitle = `${audit.title} (${plugin.title})`;
148+
const auditTitle = `${wrapTags(audit.title)} (${plugin.title})`;
147149
const detailsContent = auditDetails(audit, options);
148150
const descriptionContent = metaDescription(audit);
149151

0 commit comments

Comments
 (0)