Commit a32b8162 authored by David Sveningsson's avatar David Sveningsson
Browse files

feat(rules): new rule no-redundant-role

fixes #65
parent ee354a00
Pipeline #112745935 passed with stages
in 10 minutes and 15 seconds
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/no-redundant-role.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/no-redundant-role.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 2,
"filePath": "inline",
"messages": Array [
Object {
"column": 13,
"context": Object {
"role": "main",
"tagname": "main",
},
"line": 1,
"message": "Redundant role \\"main\\" on <main>",
"offset": 12,
"ruleId": "no-redundant-role",
"selector": "main",
"severity": 2,
"size": 4,
},
Object {
"column": 15,
"context": Object {
"role": "listitem",
"tagname": "li",
},
"line": 3,
"message": "Redundant role \\"listitem\\" on <li>",
"offset": 40,
"ruleId": "no-redundant-role",
"selector": "main > ul > li",
"severity": 2,
"size": 8,
},
],
"source": "<main role=\\"main\\">
<ul>
<li role=\\"listitem\\">Lorem ipsum</li>
</ul>
</main>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<main role="main">
<ul>
<li role="listitem">Lorem ipsum</li>
</ul>
</main>`;
markup["correct"] = `<ul>
<li role="presentation">Lorem ipsum</li>
</ul>`;
describe("docs/rules/no-redundant-role.md", () => {
it("inline validation: incorrect", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-redundant-role":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-redundant-role":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: no-redundant-role
summary: Disallow usage of redundant roles
---
# Disallow usage of redundant roles (`no-redundant-role`)
Some HTML5 elements have implied [WAI-ARIA roles][wai-aria-roles] and this rule disallows setting the implied roles on those elements.
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="no-redundant-role">
<main role="main">
<ul>
<li role="listitem">Lorem ipsum</li>
</ul>
</main>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="no-redundant-role">
<ul>
<li role="presentation">Lorem ipsum</li>
</ul>
</validate>
......@@ -28,6 +28,7 @@ module.exports = {
"no-implicit-close": "error",
"no-inline-style": "error",
"no-raw-characters": "error",
"no-redundant-role": "error",
"no-trailing-whitespace": "error",
"prefer-button": "error",
"prefer-native-element": "error",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule no-redundant-role should contain contextual documentation 1`] = `
Object {
"description": "Using the \\"checkbox\\" role is redundant as it is already implied by the <input> element.",
"url": "https://html-validate.org/rules/no-redundant-role.html",
}
`;
exports[`rule no-redundant-role should contain documentation 1`] = `
Object {
"description": "Using this role is redundant as it is already implied by the element.",
"url": "https://html-validate.org/rules/no-redundant-role.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
describe("rule no-redundant-role", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "no-redundant-role": "error" },
});
});
it("should not report error when element has non-redundant role", () => {
const report = htmlvalidate.validateString('<li role="presentation"></li>');
expect(report).toBeValid();
});
it("should not report error element has no known roles", () => {
const report = htmlvalidate.validateString('<span role="main"></span>');
expect(report).toBeValid();
});
it("should not report error when role is boolean", () => {
const report = htmlvalidate.validateString("<div role></div>");
expect(report).toBeValid();
});
it("should not report error for dynamic attributes", () => {
const report = htmlvalidate.validateString(
'<input dynamic-role="main">',
null,
{
processAttribute,
}
);
expect(report).toBeValid();
});
it("should report error when element has redundant role", () => {
const htmlvalidate = new HtmlValidate({
rules: { "no-redundant-role": "error" },
});
const report = htmlvalidate.validateString('<li role="listitem"></li>');
expect(report).toBeInvalid();
});
it("should contain documentation", () => {
const htmlvalidate = new HtmlValidate({
rules: { "no-redundant-role": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("no-redundant-role")
).toMatchSnapshot();
});
it("should contain contextual documentation", () => {
const htmlvalidate = new HtmlValidate({
rules: { "no-redundant-role": "error" },
});
const context = {
role: "checkbox",
tagname: "input",
};
expect(
htmlvalidate.getRuleDocumentation("no-redundant-role", null, context)
).toMatchSnapshot();
});
});
import { DynamicValue } from "../dom";
import { AttributeEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
interface RuleContext {
tagname: string;
role: string;
}
const mapping: Record<string, string[]> = {
article: ["article"],
header: ["banner"],
button: ["button"],
td: ["cell"],
input: ["checkbox", "radio", "input"],
aside: ["complementary"],
footer: ["contentinfo"],
figure: ["figure"],
form: ["form"],
h1: ["heading"],
h2: ["heading"],
h3: ["heading"],
h4: ["heading"],
h5: ["heading"],
h6: ["heading"],
a: ["link"],
ul: ["list"],
select: ["listbox"],
li: ["listitem"],
main: ["main"],
nav: ["navigation"],
progress: ["progressbar"],
section: ["region"],
table: ["table"],
textarea: ["textbox"],
};
class NoRedundantRole extends Rule<RuleContext, void> {
public documentation(context: RuleContext): RuleDocumentation {
const doc: RuleDocumentation = {
description: `Using this role is redundant as it is already implied by the element.`,
url: ruleDocumentationUrl(__filename),
};
if (context) {
doc.description = `Using the "${context.role}" role is redundant as it is already implied by the <${context.tagname}> element.`;
}
return doc;
}
public setup(): void {
this.on("attr", (event: AttributeEvent) => {
const { target } = event;
/* ignore non-role attributes */
if (event.key.toLowerCase() !== "role") {
return;
}
/* ignore missing and dynamic values */
if (!event.value || event.value instanceof DynamicValue) {
return;
}
/* ignore elements without known redundant roles */
const redundant = mapping[target.tagName];
if (!redundant) {
return;
}
/* ignore elements with non-redundant roles */
if (!redundant.includes(event.value)) {
return;
}
/* report error */
const context: RuleContext = {
tagname: target.tagName,
role: event.value,
};
this.report(
event.target,
`Redundant role "${event.value}" on <${target.tagName}>`,
event.valueLocation,
context
);
});
}
}
module.exports = NoRedundantRole;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment