Commit 46533844 authored by David Sveningsson's avatar David Sveningsson

feat(rules): new rule no-missing-references

to validate elements referenced by attributes such as `for`
parent 5025692a
Pipeline #76035139 passed with stages
in 10 minutes and 7 seconds
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/no-missing-references.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/no-missing-references.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 3,
"filePath": "inline",
"messages": Array [
Object {
"column": 13,
"context": Object {
"key": "for",
"value": "missing-input",
},
"line": 1,
"message": "Element references missing id \\"missing-input\\"",
"offset": 12,
"ruleId": "no-missing-references",
"severity": 2,
"size": 13,
},
Object {
"column": 23,
"context": Object {
"key": "aria-labelledby",
"value": "missing-text",
},
"line": 2,
"message": "Element references missing id \\"missing-text\\"",
"offset": 58,
"ruleId": "no-missing-references",
"severity": 2,
"size": 12,
},
Object {
"column": 24,
"context": Object {
"key": "aria-describedby",
"value": "missing-text",
},
"line": 3,
"message": "Element references missing id \\"missing-text\\"",
"offset": 102,
"ruleId": "no-missing-references",
"severity": 2,
"size": 12,
},
],
"source": "<label for=\\"missing-input\\"></label>
<div aria-labelledby=\\"missing-text\\"></div>
<div aria-describedby=\\"missing-text\\"></div>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<label for="missing-input"></label>
<div aria-labelledby="missing-text"></div>
<div aria-describedby="missing-text"></div>`;
markup["correct"] = `<label for="my-input"></label>
<div id="verbose-text"></div>
<div aria-labelledby="verbose-text"></div>
<div aria-describedby="verbose-text"></div>
<input id="my-input">`;
describe("docs/rules/no-missing-references.md", () => {
it("inline validation: incorrect", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-missing-references":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
const htmlvalidate = new HtmlValidate({"rules":{"no-missing-references":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
@ngdoc rule
@module rules
@name no-missing-references
@category document
@summary Require all element references to exist
@description
# no missing references (`no-missing-references`)
Require all elements referenced by attributes such as `for` to exist in the
current document.
Checked attributes:
- `for`
- `aria-labelledby`
- `aria-describedby`
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="no-missing-references">
<label for="missing-input"></label>
<div aria-labelledby="missing-text"></div>
<div aria-describedby="missing-text"></div>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="no-missing-references">
<label for="my-input"></label>
<div id="verbose-text"></div>
<div aria-labelledby="verbose-text"></div>
<div aria-describedby="verbose-text"></div>
<input id="my-input">
</validate>
......@@ -3,6 +3,7 @@ module.exports = {
"input-missing-label": "error",
"heading-level": "error",
"missing-doctype": "error",
"no-missing-references": "error",
"require-sri": "error",
},
};
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule no-missing-references should contain contextual documentation 1`] = `
Object {
"description": "The element ID \\"my-id\\" referenced by the my-attribute attribute must point to an existing element.",
"url": "https://html-validate.org/rules/no-missing-references.html",
}
`;
exports[`rule no-missing-references should contain documentation 1`] = `
Object {
"description": "The element ID referenced by the attribute must point to an existing element.",
"url": "https://html-validate.org/rules/no-missing-references.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
describe("rule no-missing-references", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "no-missing-references": "error" },
});
});
it('should not report error when <label for=".."> is referencing existing element', () => {
const report = htmlvalidate.validateString(
'<label for="existing"></label><input id="existing">'
);
expect(report).toBeValid();
});
it('should not report error when <ANY aria-labelledby=".."> is referencing existing element', () => {
const report = htmlvalidate.validateString(
'<div aria-labelledby="existing"></div><span id="existing"></span>'
);
expect(report).toBeValid();
});
it('should not report error when <ANY aria-describedby=".."> is referencing existing element', () => {
const report = htmlvalidate.validateString(
'<div aria-describedby="existing"></div><span id="existing"></span>'
);
expect(report).toBeValid();
});
it('should report error when <label for=".."> is referencing missing element', () => {
const report = htmlvalidate.validateString('<label for="missing"></label>');
expect(report).toBeInvalid();
expect(report).toHaveError(
"no-missing-references",
'Element references missing id "missing"'
);
});
it('should report error when <ANY aria-labelledby=".."> is referencing missing element', () => {
const report = htmlvalidate.validateString(
'<div aria-labelledby="missing"></div>'
);
expect(report).toBeInvalid();
expect(report).toHaveError(
"no-missing-references",
'Element references missing id "missing"'
);
});
it('should report error when <ANY aria-describedby=".."> is referencing missing element', () => {
const report = htmlvalidate.validateString(
'<div aria-describedby="missing"></div>'
);
expect(report).toBeInvalid();
expect(report).toHaveError(
"no-missing-references",
'Element references missing id "missing"'
);
});
it("should contain documentation", () => {
htmlvalidate = new HtmlValidate({
rules: { "no-missing-references": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("no-missing-references")
).toMatchSnapshot();
});
it("should contain contextual documentation", () => {
htmlvalidate = new HtmlValidate({
rules: { "no-missing-references": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("no-missing-references", null, {
key: "my-attribute",
value: "my-id",
})
).toMatchSnapshot();
});
});
import { Attribute, DOMTree, DynamicValue, HtmlElement } from "../dom";
import { DOMReadyEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
interface Context {
key: string;
value: string;
}
class NoMissingReferences extends Rule<Context> {
public documentation(context: Context): RuleDocumentation {
if (context) {
return {
description: `The element ID "${context.value}" referenced by the ${context.key} attribute must point to an existing element.`,
url: ruleDocumentationUrl(__filename),
};
} else {
return {
description: `The element ID referenced by the attribute must point to an existing element.`,
url: ruleDocumentationUrl(__filename),
};
}
}
public setup(): void {
this.on("dom:ready", (event: DOMReadyEvent) => {
const document = event.document;
/* verify <label for=".."> */
for (const node of document.querySelectorAll("label[for]")) {
const attr = node.getAttribute("for");
this.validateReference(document, node, attr);
}
/* verify <ANY aria-labelledby=".."> */
for (const node of document.querySelectorAll("[aria-labelledby]")) {
const attr = node.getAttribute("aria-labelledby");
this.validateReference(document, node, attr);
}
/* verify <ANY aria-describedby=".."> */
for (const node of document.querySelectorAll("[aria-describedby]")) {
const attr = node.getAttribute("aria-describedby");
this.validateReference(document, node, attr);
}
});
}
protected validateReference(
document: DOMTree,
node: HtmlElement,
attr: Attribute
): void {
const id = attr.value;
if (id instanceof DynamicValue || id === "") {
return;
}
const nodes = document.querySelectorAll(`[id="${id}"]`);
if (nodes.length === 0) {
const context: Context = { key: attr.key, value: id };
this.report(
node,
`Element references missing id "${id}"`,
attr.valueLocation,
context
);
}
}
}
module.exports = NoMissingReferences;
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