blob: 3c8221d85b30efbb5265b5d5a9071cf29464fa12 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../third_party/axe-core/axe.js';
// These rules are disabled for one or more of the following reasons:
// * The rule is slow enough to cause flaky timeouts.
// * The rule has known issues.
// * The rule is low value so we disable it to improve overall test time.
export const DISABLED_RULES: Record<string, {enabled: boolean}> = {
// Slow rules:
// https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#section-4-performance
// https://github.com/dequelabs/axe-core/pull/1503
'aria-hidden-focus': {
enabled: false,
},
'aria-input-field-name': {
enabled: false,
},
'aria-toggle-field-name': {
enabled: false,
},
'color-contrast': {
enabled: false,
},
'image-redundant-alt': {
enabled: false,
},
// Rules with issues:
// https://github.com/dequelabs/axe-core/issues/1444
'aria-required-children': {
enabled: false,
},
// Low value rules:
'aria-allowed-role': {
enabled: false,
},
'aria-dpub-role-fallback': {
enabled: false,
},
'audio-caption': {
enabled: false,
},
blink: {
enabled: false,
},
'form-field-multiple-labels': {
enabled: false,
},
'frame-tested': {
enabled: false,
},
'html-has-lang': {
enabled: false,
},
'html-lang-valid': {
enabled: false,
},
'html-xml-lang-mismatch': {
enabled: false,
},
'landmark-banner-is-top-level': {
enabled: false,
},
'landmark-complementary-is-top-level': {
enabled: false,
},
'landmark-contentinfo-is-top-level': {
enabled: false,
},
'landmark-main-is-top-level': {
enabled: false,
},
'landmark-no-duplicate-banner': {
enabled: false,
},
'landmark-no-duplicate-contentinfo': {
enabled: false,
},
'landmark-one-main': {
enabled: false,
},
'landmark-unique': {
enabled: false,
},
marquee: {
enabled: false,
},
'meta-refresh': {
enabled: false,
},
'meta-viewport': {
enabled: false,
},
'meta-viewport-large': {
enabled: false,
},
'object-alt': {
enabled: false,
},
'page-has-heading-one': {
enabled: false,
},
'scrollable-region-focusable': {
enabled: false,
},
'video-caption': {
enabled: false,
},
'video-description': {
enabled: false,
},
'valid-lang': {
enabled: false,
},
};
export interface AxeConfig {
checks: Array<{id: string, options: string[]}>;
runOnly: {type: string, values: {include: string[], exclude: string[]}};
}
export const DEFAULT_CONFIG: AxeConfig = {
checks: [
// Any attribute included in the options array will be
// ignored by the 'aria-valid-attr' rule.
// axe-core supports aria-description, but the bundled version does not.
// Remove this exception once the bundled version supports aria-description.
// See: https://github.com/dequelabs/axe-core/issues/3121
{id: 'aria-valid-attr', options: ['aria-description']},
],
runOnly: {type: 'tags', values: {include: ['wcag2a', 'best-practice'], exclude: ['experimental']}},
};
export interface AxeFailedNode {
target: string[];
html: string;
failureSummary?: string;
}
export interface AxeViolation {
ruleId: string;
ruleDescription: string;
impact?: string;
failedNodes: AxeFailedNode[];
}
interface AxeNodeResult {
target: string[];
html: string;
failureSummary?: string;
}
interface AxeRuleResult {
id: string;
description: string;
impact?: string;
nodes: AxeNodeResult[];
}
interface AxeResults {
violations: AxeRuleResult[];
}
interface AxeCore {
configure(spec: unknown): void;
run(element: Element, options: unknown): Promise<AxeResults>;
}
export async function getAxeViolations(
element: Element|ShadowRoot,
rules?: Record<string, {enabled: boolean}>,
): Promise<AxeViolation[]> {
const axeInstance = (globalThis as unknown as {axe?: AxeCore}).axe;
if (!axeInstance) {
throw new Error('axe-core is not loaded.');
}
axeInstance.configure({...DEFAULT_CONFIG});
const target = element instanceof ShadowRoot ? element.host : element;
const results = await axeInstance.run(target, {rules: {...DISABLED_RULES, ...rules}});
return results.violations.map(violation => ({
ruleId: violation.id,
ruleDescription: violation.description,
impact: violation.impact,
failedNodes: violation.nodes.map(node => ({
target: node.target,
html: node.html,
failureSummary: node.failureSummary,
})),
}));
}