Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions e2e/plugin-axe-e2e/tests/__snapshots__/collect.e2e.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ exports[`PLUGIN collect report with axe-plugin NPM package > should run plugin o
"details": {
"issues": [
{
"message": "[\`body > button\`] Fix any of the following: Element does not have inner text that is visible to screen readers aria-label attribute does not exist or is empty aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty Element has no title attribute Element does not have an implicit (wrapped) <label> Element does not have an explicit <label> Element's default semantics were not overridden with role="none" or role="presentation" ([/<TEST_DIR>/index.html](file:///<TEST_DIR>/index.html))",
"message": "[\`body > button\`] Fix any of the following: Element does not have inner text that is visible to screen readers aria-label attribute does not exist or is empty aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty Element has no title attribute Element does not have an implicit (wrapped) <label> Element does not have an explicit <label> Element's default semantics were not overridden with role="none" or role="presentation"",
"severity": "error",
},
],
Expand All @@ -523,7 +523,7 @@ exports[`PLUGIN collect report with axe-plugin NPM package > should run plugin o
"details": {
"issues": [
{
"message": "[\`.low-contrast\`] Fix any of the following: Element has insufficient color contrast of 1.57 (foreground color: #777777, background color: #999999, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1 ([/<TEST_DIR>/index.html](file:///<TEST_DIR>/index.html))",
"message": "[\`.low-contrast\`] Fix any of the following: Element has insufficient color contrast of 1.57 (foreground color: #777777, background color: #999999, font size: 12.0pt (16px), font weight: normal). Expected contrast ratio of 4.5:1",
"severity": "error",
},
],
Expand Down Expand Up @@ -612,7 +612,7 @@ exports[`PLUGIN collect report with axe-plugin NPM package > should run plugin o
"details": {
"issues": [
{
"message": "[\`div[role="button"]\`] Fix any of the following: Invalid ARIA attribute name: aria-invalid-attribute ([/<TEST_DIR>/index.html](file:///<TEST_DIR>/index.html))",
"message": "[\`div[role="button"]\`] Fix any of the following: Invalid ARIA attribute name: aria-invalid-attribute",
"severity": "error",
},
],
Expand All @@ -629,7 +629,7 @@ exports[`PLUGIN collect report with axe-plugin NPM package > should run plugin o
"details": {
"issues": [
{
"message": "[\`img\`] Fix any of the following: Element does not have an alt attribute aria-label attribute does not exist or is empty aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty Element has no title attribute Element's default semantics were not overridden with role="none" or role="presentation" ([/<TEST_DIR>/index.html](file:///<TEST_DIR>/index.html))",
"message": "[\`img\`] Fix any of the following: Element does not have an alt attribute aria-label attribute does not exist or is empty aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty Element has no title attribute Element's default semantics were not overridden with role="none" or role="presentation"",
"severity": "error",
},
],
Expand All @@ -646,7 +646,7 @@ exports[`PLUGIN collect report with axe-plugin NPM package > should run plugin o
"details": {
"issues": [
{
"message": "[\`a\`] Fix all of the following: Element is in tab order and does not have accessible text Fix any of the following: Element does not have text that is visible to screen readers aria-label attribute does not exist or is empty aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty Element has no title attribute ([/<TEST_DIR>/index.html](file:///<TEST_DIR>/index.html))",
"message": "[\`a\`] Fix all of the following: Element is in tab order and does not have accessible text Fix any of the following: Element does not have text that is visible to screen readers aria-label attribute does not exist or is empty aria-labelledby attribute does not exist, references elements that do not exist or references elements that are empty Element has no title attribute",
"severity": "error",
},
],
Expand Down
14 changes: 1 addition & 13 deletions e2e/plugin-axe-e2e/tests/collect.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,6 @@ import {
} from '@code-pushup/test-utils';
import { executeProcess, readJsonFile } from '@code-pushup/utils';

function sanitizeReportPaths(report: Report): Report {
// Convert to JSON, replace paths, and parse back
const reportJson = JSON.stringify(report);
const sanitized = reportJson.replace(
/\/(?:[^/\s"]+\/)+index\.html/g,
'/<TEST_DIR>/index.html',
);
return JSON.parse(sanitized);
}

describe('PLUGIN collect report with axe-plugin NPM package', () => {
const testFileDir = path.join(
E2E_ENVIRONMENTS_DIR,
Expand Down Expand Up @@ -53,8 +43,6 @@ describe('PLUGIN collect report with axe-plugin NPM package', () => {
);

expect(() => reportSchema.parse(report)).not.toThrow();
expect(
omitVariableReportData(sanitizeReportPaths(report)),
).toMatchSnapshot();
expect(omitVariableReportData(report)).toMatchSnapshot();
});
});
7 changes: 5 additions & 2 deletions packages/plugin-axe/src/lib/runner/run-axe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
logger,
pluralizeToken,
} from '@code-pushup/utils';
import { toAuditOutputs } from './transform.js';
import { createUrlSuffix, toAuditOutputs } from './transform.js';

/* eslint-disable functional/no-let */
let browser: Browser | undefined;
Expand Down Expand Up @@ -54,7 +54,10 @@ export async function runAxeForUrl(args: AxeUrlArgs): Promise<AxeUrlResult> {
const page = await context.newPage();
try {
const axeResults = await runAxeForPage(page, args);
const auditOutputs = toAuditOutputs(axeResults, url);
const auditOutputs = toAuditOutputs(
axeResults,
createUrlSuffix(url, urlsCount),
);
return {
message: `${prefix} Analyzed URL ${url}`,
result: { url, axeResults, auditOutputs },
Expand Down
40 changes: 26 additions & 14 deletions packages/plugin-axe/src/lib/runner/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,24 @@ import {
*/
export function toAuditOutputs(
{ passes, violations, incomplete, inapplicable }: axe.AxeResults,
url: string,
urlSuffix: string,
): AuditOutputs {
const auditMap = new Map<string, AuditOutput>([
...inapplicable.map(res => [res.id, toAuditOutput(res, url, 1)] as const),
...passes.map(res => [res.id, toAuditOutput(res, url, 1)] as const),
...incomplete.map(res => [res.id, toAuditOutput(res, url, 0)] as const),
...violations.map(res => [res.id, toAuditOutput(res, url, 0)] as const),
]);
const toEntries = (results: axe.Result[], score: number) =>
results.map(res => [res.id, toAuditOutput(res, urlSuffix, score)] as const);

return [...auditMap.values()];
return [
...new Map<string, AuditOutput>([
...toEntries(inapplicable, 1),
...toEntries(passes, 1),
...toEntries(incomplete, 0),
...toEntries(violations, 0),
]).values(),
];
}

/** Creates a URL suffix for issue messages, only included when analyzing multiple URLs. */
export function createUrlSuffix(url: string, urlsCount: number): string {
return urlsCount > 1 ? ` ([${getUrlIdentifier(url)}](${url}))` : '';
}

/**
Expand All @@ -36,7 +44,7 @@ export function toAuditOutputs(
*/
function toAuditOutput(
result: axe.Result,
url: string,
urlSuffix: string,
score: number,
): AuditOutput {
const base = {
Expand All @@ -46,7 +54,7 @@ function toAuditOutput(
};

if (score === 0 && result.nodes.length > 0) {
const issues = result.nodes.map(node => toIssue(node, result, url));
const issues = result.nodes.map(node => toIssue(node, result, urlSuffix));

return {
...base,
Expand All @@ -68,15 +76,19 @@ function formatSelector(selector: axe.CrossTreeSelector): string {
return selector.join(' >> ');
}

function toIssue(node: axe.NodeResult, result: axe.Result, url: string): Issue {
function toIssue(
node: axe.NodeResult,
result: axe.Result,
urlSuffix: string,
): Issue {
const selector = formatSelector(node.target?.[0] || node.html);
const rawMessage = node.failureSummary || result.help;
const cleanedMessage = rawMessage.replace(/\s+/g, ' ').trim();

const message = `[\`${selector}\`] ${cleanedMessage} ([${getUrlIdentifier(url)}](${url}))`;

return {
message: truncateIssueMessage(message),
message: truncateIssueMessage(
`[\`${selector}\`] ${cleanedMessage}${urlSuffix}`,
),
severity: impactToSeverity(node.impact),
};
}
Expand Down
50 changes: 29 additions & 21 deletions packages/plugin-axe/src/lib/runner/transform.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { AxeResults, NodeResult, Result } from 'axe-core';
import { describe, expect, it } from 'vitest';
import type { AuditOutput } from '@code-pushup/models';
import { toAuditOutputs } from './transform.js';
import { createUrlSuffix, toAuditOutputs } from './transform.js';

function createMockNode(overrides: Partial<NodeResult> = {}): NodeResult {
return {
Expand Down Expand Up @@ -33,8 +33,6 @@ function createMockAxeResults(overrides: Partial<AxeResults> = {}): AxeResults {
}

describe('toAuditOutputs', () => {
const testUrl = 'https://example.com';

it('should transform passes with score 1 and no issues', () => {
const results = createMockAxeResults({
passes: [
Expand All @@ -46,7 +44,7 @@ describe('toAuditOutputs', () => {
],
});

expect(toAuditOutputs(results, testUrl)).toEqual<AuditOutput[]>([
expect(toAuditOutputs(results, '')).toEqual<AuditOutput[]>([
{
slug: 'color-contrast',
score: 1,
Expand Down Expand Up @@ -81,7 +79,7 @@ describe('toAuditOutputs', () => {
],
});

expect(toAuditOutputs(results, testUrl)).toEqual<AuditOutput[]>([
expect(toAuditOutputs(results, '')).toEqual<AuditOutput[]>([
{
slug: 'image-alt',
score: 0,
Expand All @@ -91,17 +89,16 @@ describe('toAuditOutputs', () => {
issues: [
{
message:
'[`img`] Fix this: Element does not have an alt attribute ([example.com](https://example.com))',
'[`img`] Fix this: Element does not have an alt attribute',
severity: 'error',
},
{
message:
'[`.header > img:nth-child(2)`] Fix this: Element does not have an alt attribute ([example.com](https://example.com))',
'[`.header > img:nth-child(2)`] Fix this: Element does not have an alt attribute',
severity: 'error',
},
{
message:
'[`#main img`] Mock help for image-alt ([example.com](https://example.com))',
message: '[`#main img`] Mock help for image-alt',
severity: 'error',
},
],
Expand Down Expand Up @@ -130,7 +127,7 @@ describe('toAuditOutputs', () => {
],
});

expect(toAuditOutputs(results, testUrl)).toEqual<AuditOutput[]>([
expect(toAuditOutputs(results, '')).toEqual<AuditOutput[]>([
{
slug: 'color-contrast',
score: 0,
Expand All @@ -140,12 +137,11 @@ describe('toAuditOutputs', () => {
issues: [
{
message:
'[`button`] Fix this: Element has insufficient color contrast ([example.com](https://example.com))',
'[`button`] Fix this: Element has insufficient color contrast',
severity: 'warning',
},
{
message:
'[`a`] Review: Unable to determine contrast ratio ([example.com](https://example.com))',
message: '[`a`] Review: Unable to determine contrast ratio',
severity: 'warning',
},
],
Expand All @@ -159,7 +155,7 @@ describe('toAuditOutputs', () => {
inapplicable: [createMockResult('audio-caption', [])],
});

expect(toAuditOutputs(results, testUrl)).toEqual<AuditOutput[]>([
expect(toAuditOutputs(results, '')).toEqual<AuditOutput[]>([
{
slug: 'audio-caption',
score: 1,
Expand Down Expand Up @@ -192,7 +188,7 @@ describe('toAuditOutputs', () => {
],
});

const outputs = toAuditOutputs(results, testUrl);
const outputs = toAuditOutputs(results, '');

expect(outputs).toBeArrayOfSize(1);
expect(outputs[0]).toMatchObject({
Expand All @@ -204,7 +200,7 @@ describe('toAuditOutputs', () => {
});

it('should handle empty results', () => {
expect(toAuditOutputs(createMockAxeResults(), testUrl)).toBeEmpty();
expect(toAuditOutputs(createMockAxeResults(), '')).toBeEmpty();
});

it('should format severity counts when multiple impacts exist', () => {
Expand All @@ -219,7 +215,7 @@ describe('toAuditOutputs', () => {
],
});

const outputs = toAuditOutputs(results, testUrl);
const outputs = toAuditOutputs(results, '');

expect(outputs[0]).toMatchObject({
slug: 'color-contrast',
Expand All @@ -243,7 +239,7 @@ describe('toAuditOutputs', () => {
],
});

expect(toAuditOutputs(results, testUrl)).toEqual<AuditOutput[]>([
expect(toAuditOutputs(results, '')).toEqual<AuditOutput[]>([
{
slug: 'color-contrast',
score: 0,
Expand All @@ -253,7 +249,7 @@ describe('toAuditOutputs', () => {
issues: [
{
message:
'[`#app >> my-component >> button`] Fix this: Element has insufficient color contrast ([example.com](https://example.com))',
'[`#app >> my-component >> button`] Fix this: Element has insufficient color contrast',
severity: 'error',
},
],
Expand All @@ -277,7 +273,7 @@ describe('toAuditOutputs', () => {
],
});

expect(toAuditOutputs(results, testUrl)).toEqual<AuditOutput[]>([
expect(toAuditOutputs(results, '')).toEqual<AuditOutput[]>([
{
slug: 'aria-roles',
score: 0,
Expand All @@ -287,7 +283,7 @@ describe('toAuditOutputs', () => {
issues: [
{
message:
'[`<div role="invalid-role">Content</div>`] Fix this: Ensure all values assigned to role="" correspond to valid ARIA roles ([example.com](https://example.com))',
'[`<div role="invalid-role">Content</div>`] Fix this: Ensure all values assigned to role="" correspond to valid ARIA roles',
severity: 'error',
},
],
Expand All @@ -296,3 +292,15 @@ describe('toAuditOutputs', () => {
]);
});
});

describe('createUrlSuffix', () => {
it('should return empty string for single URL', () => {
expect(createUrlSuffix('https://example.com', 1)).toBe('');
});

it('should return formatted suffix for multiple URLs', () => {
expect(createUrlSuffix('https://example.com', 2)).toBe(
' ([example.com](https://example.com))',
);
});
});