Skip to content

Commit 512c0fe

Browse files
committed
refactor(plugin-axe): use structured check messages
1 parent 0279e87 commit 512c0fe

File tree

2 files changed

+136
-31
lines changed

2 files changed

+136
-31
lines changed

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

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type {
44
AuditOutputs,
55
Issue,
66
IssueSeverity,
7-
SourceUrlLocation,
87
} from '@code-pushup/models';
98
import {
109
formatIssueSeverities,
@@ -71,26 +70,35 @@ function formatSelector(selector: axe.CrossTreeSelector): string {
7170
return selector.join(' >> ');
7271
}
7372

73+
/**
74+
* Joins `none`/`all` check messages (each must be fixed).
75+
* Falls back to first `any` check (OR-ed, one represents the group).
76+
*/
77+
function formatNodeMessage(node: axe.NodeResult, fallback: string): string {
78+
const requiredMessages = [...node.none, ...node.all].map(
79+
check => check.message,
80+
);
81+
if (requiredMessages.length > 0) {
82+
return requiredMessages.join('. ');
83+
}
84+
return node.any[0]?.message ?? fallback;
85+
}
86+
7487
function toIssue(node: axe.NodeResult, result: axe.Result, url: string): Issue {
7588
const selector = node.target?.[0]
7689
? formatSelector(node.target[0])
7790
: undefined;
78-
const rawMessage = node.failureSummary || result.help;
79-
const cleanMessage = rawMessage.replace(/\s+/g, ' ').trim();
80-
81-
// TODO: Remove selector prefix from message once Portal supports URL sources
82-
const message = selector ? `[\`${selector}\`] ${cleanMessage}` : cleanMessage;
8391

84-
const source: SourceUrlLocation = {
85-
url,
86-
...(node.html && { snippet: node.html }),
87-
...(selector && { selector }),
88-
};
92+
const message = formatNodeMessage(node, result.help);
8993

9094
return {
9195
message: truncateIssueMessage(message),
9296
severity: impactToSeverity(node.impact),
93-
source,
97+
source: {
98+
url,
99+
...(node.html && { snippet: node.html }),
100+
...(selector && { selector }),
101+
},
94102
};
95103
}
96104

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

Lines changed: 116 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
1-
import type { AxeResults, NodeResult, Result } from 'axe-core';
1+
import type { AxeResults, CheckResult, NodeResult, Result } from 'axe-core';
22
import type { AuditOutput } from '@code-pushup/models';
33
import { toAuditOutputs } from './transform.js';
44

5+
function createMockCheck(overrides: Partial<CheckResult> = {}): CheckResult {
6+
return {
7+
id: 'mock-check',
8+
data: null,
9+
relatedNodes: [],
10+
impact: 'serious',
11+
message: 'Mock check message',
12+
...overrides,
13+
} as CheckResult;
14+
}
15+
516
function createMockNode(overrides: Partial<NodeResult> = {}): NodeResult {
617
return {
718
html: '<div></div>',
819
target: ['div'],
20+
all: [],
21+
any: [],
22+
none: [],
923
...overrides,
1024
} as NodeResult;
1125
}
@@ -61,13 +75,23 @@ describe('toAuditOutputs', () => {
6175
html: '<img src="logo.png">',
6276
target: ['img'],
6377
impact: 'critical',
64-
failureSummary: 'Fix this: Element does not have an alt attribute',
78+
any: [
79+
createMockCheck({
80+
id: 'has-alt',
81+
message: 'Element does not have an alt attribute',
82+
}),
83+
],
6584
}),
6685
createMockNode({
6786
html: '<img src="icon.svg">',
6887
target: ['.header > img:nth-child(2)'],
6988
impact: 'serious',
70-
failureSummary: 'Fix this: Element does not have an alt attribute',
89+
any: [
90+
createMockCheck({
91+
id: 'has-alt',
92+
message: 'Element does not have an alt attribute',
93+
}),
94+
],
7195
}),
7296
createMockNode({
7397
html: '<img src="banner.jpg">',
@@ -89,8 +113,7 @@ describe('toAuditOutputs', () => {
89113
details: {
90114
issues: [
91115
{
92-
message:
93-
'[`img`] Fix this: Element does not have an alt attribute',
116+
message: 'Element does not have an alt attribute',
94117
severity: 'error',
95118
source: {
96119
url: 'https://example.com',
@@ -99,8 +122,7 @@ describe('toAuditOutputs', () => {
99122
},
100123
},
101124
{
102-
message:
103-
'[`.header > img:nth-child(2)`] Fix this: Element does not have an alt attribute',
125+
message: 'Element does not have an alt attribute',
104126
severity: 'error',
105127
source: {
106128
url: 'https://example.com',
@@ -109,7 +131,7 @@ describe('toAuditOutputs', () => {
109131
},
110132
},
111133
{
112-
message: '[`#main img`] Mock help for image-alt',
134+
message: 'Mock help for image-alt',
113135
severity: 'error',
114136
source: {
115137
url: 'https://example.com',
@@ -131,13 +153,23 @@ describe('toAuditOutputs', () => {
131153
html: '<button>Click me</button>',
132154
target: ['button'],
133155
impact: 'moderate',
134-
failureSummary: 'Fix this: Element has insufficient color contrast',
156+
any: [
157+
createMockCheck({
158+
id: 'color-contrast',
159+
message: 'Element has insufficient color contrast',
160+
}),
161+
],
135162
}),
136163
createMockNode({
137164
html: '<a href="#">Link</a>',
138165
target: ['a'],
139166
impact: 'moderate',
140-
failureSummary: 'Review: Unable to determine contrast ratio',
167+
any: [
168+
createMockCheck({
169+
id: 'color-contrast',
170+
message: 'Unable to determine contrast ratio',
171+
}),
172+
],
141173
}),
142174
]),
143175
],
@@ -154,8 +186,7 @@ describe('toAuditOutputs', () => {
154186
details: {
155187
issues: [
156188
{
157-
message:
158-
'[`button`] Fix this: Element has insufficient color contrast',
189+
message: 'Element has insufficient color contrast',
159190
severity: 'warning',
160191
source: {
161192
url: 'https://example.com',
@@ -164,7 +195,7 @@ describe('toAuditOutputs', () => {
164195
},
165196
},
166197
{
167-
message: '[`a`] Review: Unable to determine contrast ratio',
198+
message: 'Unable to determine contrast ratio',
168199
severity: 'warning',
169200
source: {
170201
url: 'https://example.com',
@@ -261,7 +292,12 @@ describe('toAuditOutputs', () => {
261292
html: '<button></button>',
262293
target: [['#app', 'my-component', 'button']],
263294
impact: 'critical',
264-
failureSummary: 'Fix this: Element has insufficient color contrast',
295+
any: [
296+
createMockCheck({
297+
id: 'color-contrast',
298+
message: 'Element has insufficient color contrast',
299+
}),
300+
],
265301
}),
266302
]),
267303
],
@@ -278,8 +314,7 @@ describe('toAuditOutputs', () => {
278314
details: {
279315
issues: [
280316
{
281-
message:
282-
'[`#app >> my-component >> button`] Fix this: Element has insufficient color contrast',
317+
message: 'Element has insufficient color contrast',
283318
severity: 'error',
284319
source: {
285320
url: 'https://example.com',
@@ -293,6 +328,63 @@ describe('toAuditOutputs', () => {
293328
]);
294329
});
295330

331+
it('should use none/all check messages over any checks', () => {
332+
const results = createMockAxeResults({
333+
violations: [
334+
createMockResult('link-name', [
335+
createMockNode({
336+
html: '<a href="/page"><img src="icon.png"></a>',
337+
target: ['a'],
338+
impact: 'serious',
339+
none: [
340+
createMockCheck({
341+
id: 'focusable-no-name',
342+
message:
343+
'Element is in tab order and does not have accessible text',
344+
}),
345+
],
346+
any: [
347+
createMockCheck({
348+
id: 'has-visible-text',
349+
message:
350+
'Element does not have text that is visible to screen readers',
351+
}),
352+
createMockCheck({
353+
id: 'aria-label',
354+
message: 'aria-label attribute does not exist or is empty',
355+
}),
356+
],
357+
}),
358+
]),
359+
],
360+
});
361+
362+
expect(toAuditOutputs(results, 'https://example.com')).toStrictEqual<
363+
AuditOutput[]
364+
>([
365+
{
366+
slug: 'link-name',
367+
score: 0,
368+
value: 1,
369+
displayValue: '1 error',
370+
details: {
371+
issues: [
372+
{
373+
message:
374+
'Element is in tab order and does not have accessible text',
375+
severity: 'error',
376+
source: {
377+
url: 'https://example.com',
378+
snippet: '<a href="/page"><img src="icon.png"></a>',
379+
selector: 'a',
380+
},
381+
},
382+
],
383+
},
384+
},
385+
]);
386+
});
387+
296388
it('should omit selector when target is missing', () => {
297389
const results = createMockAxeResults({
298390
violations: [
@@ -301,8 +393,13 @@ describe('toAuditOutputs', () => {
301393
html: '<div role="invalid-role">Content</div>',
302394
target: undefined,
303395
impact: 'serious',
304-
failureSummary:
305-
'Fix this: Ensure all values assigned to role="" correspond to valid ARIA roles',
396+
all: [
397+
createMockCheck({
398+
id: 'aria-allowed-role',
399+
message:
400+
'Ensure all values assigned to role="" correspond to valid ARIA roles',
401+
}),
402+
],
306403
}),
307404
]),
308405
],
@@ -320,7 +417,7 @@ describe('toAuditOutputs', () => {
320417
issues: [
321418
{
322419
message:
323-
'Fix this: Ensure all values assigned to role="" correspond to valid ARIA roles',
420+
'Ensure all values assigned to role="" correspond to valid ARIA roles',
324421
severity: 'error',
325422
source: {
326423
url: 'https://example.com',

0 commit comments

Comments
 (0)