Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement bypass rule #34

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 2 additions & 3 deletions generate-act-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ const rulesToIgnore = [
"off6ek",
"oj04fd",
"ucwvc8",
"ye5d6e",
"bf051a",
];

Expand Down Expand Up @@ -172,9 +171,9 @@ const parser = new DOMParser();

describe("[${ruleId}]${ruleName}", function () {
it("${testcaseTitle} (${exampleURL})", async () => {
const document = parser.parseFromString(\`${html}\`, 'text/html');
const el = parser.parseFromString(\`${html}\`, 'text/html');

const results = (await scan(document.body)).map(({ text, url }) => {
const results = (await scan(el)).map(({ text, url }) => {
return { text, url };
});

Expand Down
2 changes: 1 addition & 1 deletion src/rules/area-alt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const text = "Elements must only use allowed ARIA attributes";
const url =
"https://dequeuniversity.com/rules/axe/4.4/area-alt?application=RuleDescription";

export function areaAlt(el: Element): AccessibilityError[] {
export function areaAlt(el: Document | Element): AccessibilityError[] {
const errors = [];

for (const element of querySelectorAll("map area[href]", el)) {
Expand Down
2 changes: 1 addition & 1 deletion src/rules/aria-allowed-attr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const ariaMappings: Record<string, string | undefined> = {
I: undefined,
};

export function ariaAllowedAttr(el: Element): AccessibilityError[] {
export function ariaAllowedAttr(el: Document | Element): AccessibilityError[] {
const errors = [];
const selector = Object.keys(ariaMappings).join(",");
for (const element of querySelectorAll(selector, el)) {
Expand Down
8 changes: 4 additions & 4 deletions src/rules/aria-hidden-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ const text = 'aria-hidden="true" must not be present on the document <body>';
const url =
"https://dequeuniversity.com/rules/axe/4.4/aria-hidden-body?application=RuleDescription";

export function ariaHiddenBody(el: Element): AccessibilityError[] {
const element = el.ownerDocument.body;
if (element.getAttribute("aria-hidden") === "true") {
export function ariaHiddenBody(el: Document | Element): AccessibilityError[] {
const body = el instanceof Document ? el.body : el.ownerDocument.body;
if (body.getAttribute("aria-hidden") === "true") {
return [
{
element,
element: body,
text,
url,
},
Expand Down
2 changes: 1 addition & 1 deletion src/rules/aria-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const validRoles = [
"contentinfo",
];

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
for (const element of querySelectorAll("[role]", el)) {
const role = element.getAttribute("role");
Expand Down
3 changes: 2 additions & 1 deletion src/rules/aria-tooltip-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ function hasAccessibleText(el: Element): boolean {
return true;
}

export function ariaTooltipName(el: Element): AccessibilityError[] {
export function ariaTooltipName(el: Element | Document): AccessibilityError[] {
el = el instanceof Document ? el.documentElement : el;
const errors = [];
const tooltips = querySelectorAll("[role=tooltip]", el);
if (el.matches("[role=tooltip]")) tooltips.push(el);
Expand Down
5 changes: 4 additions & 1 deletion src/rules/aria-valid-attr-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,10 @@ function valid(el: Element, attribute: string, info: Info) {
return false;
}

export function ariaValidAttrValue(el: Element): AccessibilityError[] {
export function ariaValidAttrValue(
el: Element | Document,
): AccessibilityError[] {
el = el instanceof Document ? el.documentElement : el;
const errors = [];
const selector = Object.keys(ariaAttributes)
.map((attributeName) => `[${attributeName}]`)
Expand Down
3 changes: 2 additions & 1 deletion src/rules/aria-valid-attr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ const url = `https://dequeuniversity.com/rules/axe/4.4/${id}`;

// TODO: Maybe use https://github.com/A11yance/aria-query for this?

export default function (el: Element): AccessibilityError[] {
export default function (el: Element | Document): AccessibilityError[] {
el = el instanceof Document ? el.documentElement : el;
const errors = [];
const selector = "*";
const elements = querySelectorAll(selector, el);
Expand Down
3 changes: 2 additions & 1 deletion src/rules/audio-caption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const id = "audio-caption";
const text = "<audio> elements must have a captions <track>";
const url = `https://dequeuniversity.com/rules/axe/4.4/${id}`;

export default function (el: Element): AccessibilityError[] {
export default function (el: Element | Document): AccessibilityError[] {
el = el instanceof Document ? el.documentElement : el;
const errors = [];
const elements = querySelectorAll("audio", el);
if (el.matches("audio")) {
Expand Down
4 changes: 2 additions & 2 deletions src/rules/button-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ function getElementText(element: Element): string {
return label.trim();
}

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
const elements = querySelectorAll("button", el);
if (el.matches("button")) {
if (el instanceof HTMLElement && el.matches("button")) {
elements.push(el as HTMLButtonElement);
}
for (const element of elements) {
Expand Down
31 changes: 31 additions & 0 deletions src/rules/bypass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { AccessibilityError } from "../scanner";
import { querySelector, querySelectorAll } from "../utils";

const id = "ye5d6e";
const url = `https://act-rules.github.io/rules/${id}`;
const text = "Document has an instrument to move focus to non-repeated content";

// TODO: The tests don't work yet because the document doesn't load correctly in the test harness.
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
const document = el instanceof Document ? el : el.ownerDocument;
const links = querySelectorAll("a[href^='#']", document);

let hasInstrument = false;

// Loop over all the links and see if there's a instrucment to skip to the main content.
// If there's a link
for (const link of links) {
const href = link.getAttribute("href")!;
const target = querySelector(href, document);
if (target?.parentElement?.isSameNode(document.body)) hasInstrument = true;
}
if (!hasInstrument) {
errors.push({
element: document.body,
text,
url,
});
}
return errors;
}
3 changes: 2 additions & 1 deletion src/rules/duplicate-id-aria.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const id = "duplicate-id-aria";
const text = "IDs used in ARIA and labels must be unique";
const url = `https://dequeuniversity.com/rules/axe/4.4/${id}`;

export default function (el: Element): AccessibilityError[] {
export default function (el: Element | Document): AccessibilityError[] {
el = el instanceof Document ? el.documentElement : el;
const selector = "[aria-labelledby]";
const errors = [];
const elements = querySelectorAll(selector, el);
Expand Down
7 changes: 5 additions & 2 deletions src/rules/html-has-lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ const id = "html-has-lang";
const text = "<html> element must have a lang attribute";
const url = `https://dequeuniversity.com/rules/axe/4.4/${id}`;

export default function (el: Element): AccessibilityError[] {
const htmlElement = el.ownerDocument.documentElement;
export default function (el: Element | Document): AccessibilityError[] {
const htmlElement =
el instanceof Document
? el.documentElement
: el.ownerDocument.documentElement;
const langAttribute = htmlElement.getAttribute("lang");

// Report error if the `lang` attribute is not on the element or if it's just whitespace
Expand Down
4 changes: 2 additions & 2 deletions src/rules/image-alt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ const text = "Images must have alternate text";
const url =
"https://dequeuniversity.com/rules/axe/4.4/image-alt?application=RuleDescription";

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
const elements = querySelectorAll("img", el) as HTMLImageElement[];
if (el.matches("img")) {
if (el instanceof HTMLElement && el.matches("img")) {
elements.push(el as HTMLImageElement);
}
for (const element of elements) {
Expand Down
3 changes: 2 additions & 1 deletion src/rules/input-button-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { querySelectorAll, labelledByIsValid } from "../utils";
const text = "Input buttons must have discernible text";
const url = "https://dequeuniversity.com/rules/axe/4.4/input-button-name";

export default function (el: Element): AccessibilityError[] {
export default function (el: Element | Document): AccessibilityError[] {
el = el instanceof Document ? el.documentElement : el;
const selector =
'input[type="button"],input[type="submit"],input[type="reset"]';
const errors = [];
Expand Down
3 changes: 2 additions & 1 deletion src/rules/input-image-alt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const text = "Image buttons must have alternate text";
const url =
"https://dequeuniversity.com/rules/axe/4.4/input-image-alt?application=RuleDescription";

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
el = el instanceof Document ? el.documentElement : el;
const selector = "input[type=image]";
const errors = [];
const elements = querySelectorAll(selector, el) as HTMLImageElement[];
Expand Down
4 changes: 2 additions & 2 deletions src/rules/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ const text = "Form <input> elements must have labels";
const url =
"https://dequeuniversity.com/rules/axe/4.4/label?application=RuleDescription";

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
const selector = ["input", "textarea"].map((x) => `form ${x}`).join(", ");
const elements = querySelectorAll(selector, el) as HTMLInputElement[];

if (el.matches(selector)) {
if (el instanceof HTMLElement && el.matches(selector)) {
elements.push(el as HTMLInputElement);
}
for (const element of elements) {
Expand Down
4 changes: 2 additions & 2 deletions src/rules/link-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ const id = "c487ae";
const url = `https://act-rules.github.io/rules/${id}`;
const text = "Link has non-empty accessible name";

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
const elements = querySelectorAll("a", el) as HTMLAnchorElement[];
if (el.matches("a")) {
if (el instanceof HTMLElement && el.matches("a")) {
elements.push(el as HTMLAnchorElement);
}
for (const element of elements) {
Expand Down
4 changes: 2 additions & 2 deletions src/rules/meta-refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ const text = "Timed refresh must not exist";
const url =
"https://dequeuniversity.com/rules/axe/4.4/meta-refresh?application=RuleDescription";

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
const elements = Array.from(el.querySelectorAll<HTMLMetaElement>("meta"));
if (el.matches("meta")) {
if (el instanceof HTMLElement && el.matches("meta")) {
elements.push(el as HTMLMetaElement);
}
for (const element of elements) {
Expand Down
5 changes: 3 additions & 2 deletions src/rules/meta-viewport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ function parseContent(content: string): Record<string, string> {
return obj;
}

export default function metaViewport(el: Element) {
export default function metaViewport(el: Document | Element) {
const errors = [];

// TODO: Do the same for all the other rules
const selector = "meta[name=viewport]";
const elements = Array.from(el.querySelectorAll<HTMLMetaElement>(selector));
if (el.matches(selector)) elements.push(el as HTMLMetaElement);
if (el instanceof Element && el.matches(selector))
elements.push(el as HTMLMetaElement);
for (const element of elements) {
const content = parseContent(element.content);
if (content["user-scalable"] === "no" || content["user-scalable"] === "0") {
Expand Down
4 changes: 2 additions & 2 deletions src/rules/nested-interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ const interactiveSelector = [
"[role=radio]",
].join(",");

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
const elements = Array.from(querySelectorAll(interactiveSelector, el));
if (el.matches(interactiveSelector)) {
if (el instanceof HTMLElement && el.matches(interactiveSelector)) {
elements.push(el as HTMLImageElement);
}

Expand Down
12 changes: 7 additions & 5 deletions src/rules/scope-attr-valid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ const text = "Scope attribute should be used correctly on tables";
const url =
"https://dequeuniversity.com/rules/axe/4.4/scope-attr-valid?application=RuleDescription";

function checkScopeElements(el: Element) {
function checkScopeElements(el: Document | Element) {
const errors = [];
const selector = "[scope]";
const elements = querySelectorAll(selector, el);
if (el.matches(selector)) elements.push(el as HTMLElement);
if (el instanceof Element && el.matches(selector))
elements.push(el as HTMLElement);
for (const element of elements) {
if (element.tagName !== "TH") {
errors.push({
Expand All @@ -34,11 +35,12 @@ function checkScopeElements(el: Element) {
return errors;
}

function checkTableHeaderElements(el: Element) {
function checkTableHeaderElements(el: Document | Element) {
const errors = [];
const selector = "th:not([scope])";
const elements = querySelectorAll(selector, el);
if (el.matches(selector)) elements.push(el as HTMLElement);
if (el instanceof HTMLElement && el.matches(selector))
elements.push(el as HTMLElement);
for (const element of elements) {
errors.push({
element,
Expand All @@ -49,7 +51,7 @@ function checkTableHeaderElements(el: Element) {
return errors;
}

export default function metaViewport(el: Element) {
export default function metaViewport(el: Document | Element) {
const errors = [];

errors.push(...checkScopeElements(el), ...checkTableHeaderElements(el));
Expand Down
4 changes: 2 additions & 2 deletions src/rules/select-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ const text = "select element must have an accessible name";
const url =
"https://dequeuniversity.com/rules/axe/4.4/select-name?application=RuleDescription";

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
const elements = querySelectorAll("select", el) as HTMLSelectElement[];
if (el.matches("select")) {
if (el instanceof HTMLElement && el.matches("select")) {
elements.push(el as HTMLSelectElement);
}
for (const element of elements) {
Expand Down
4 changes: 2 additions & 2 deletions src/rules/valid-lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ function getTexts(element: Element): string[] {
return [...labels, ...alts];
}

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
const elements = querySelectorAll("[lang]", el);
if (el.matches("[lang]")) {
if (el instanceof HTMLElement && el.matches("[lang]")) {
elements.push(el);
}

Expand Down
4 changes: 2 additions & 2 deletions src/rules/video-caption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ const text = "<video> elements must have a <track> for captions";
const url =
"https://dequeuniversity.com/rules/axe/4.4/video-caption?application=RuleDescription";

export default function (el: Element): AccessibilityError[] {
export default function (el: Document | Element): AccessibilityError[] {
const errors = [];
const elements = querySelectorAll("video", el);
if (el.matches("video")) {
if (el instanceof HTMLElement && el.matches("video")) {
elements.push(el as HTMLElement);
}
for (const element of elements) {
Expand Down
5 changes: 4 additions & 1 deletion src/scanner.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ export interface AccessibilityError {
url: string;
element: HTMLElement;
}
export declare function scan(element: HTMLElement): Promise<void>;

type Scannable = HTMLElement | Document;

export declare function scan(element: Scannable): Promise<void>;
6 changes: 4 additions & 2 deletions src/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import label from "./rules/label";
import linkName from "./rules/link-name";
import nestedInteractive from "./rules/nested-interactive";
import validLang from "./rules/valid-lang";
import bypass from "./rules/bypass";

import { Logger } from "./logger";

Expand All @@ -22,7 +23,7 @@ export interface AccessibilityError {

const logger = new Logger();

type Rule = (el: Element) => AccessibilityError[];
type Rule = (el: Document | Element) => AccessibilityError[];

export const allRules: Rule[] = [
areaAlt,
Expand All @@ -38,6 +39,7 @@ export const allRules: Rule[] = [
linkName,
nestedInteractive,
validLang,
bypass,
];

export async function requestIdleScan(
Expand Down Expand Up @@ -70,7 +72,7 @@ export async function requestIdleScan(
}

export async function scan(
element: Element,
element: Element | Document,
enabledRules?: Rule[],
): Promise<AccessibilityError[]> {
const errors: AccessibilityError[] = [];
Expand Down
Loading
Loading