Commit 9ed54744 authored by David Sveningsson's avatar David Sveningsson
Browse files

feat(rules): new rule `no-autoplay`

fixes #84
parent 9ec3d3a4
Pipeline #146168868 passed with stages
in 9 minutes and 34 seconds
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/no-autoplay.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/no-autoplay.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 8,
"context": Object {
"tagName": "video",
},
"line": 1,
"message": "The autoplay attribute is not allowed on <video>",
"offset": 7,
"ruleId": "no-autoplay",
"selector": "video",
"severity": 2,
"size": 8,
},
],
"source": "<video autoplay></video>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<video autoplay></video>`;
markup["correct"] = `<video></video>`;
describe("docs/rules/no-autoplay.md", () => {
it("inline validation: incorrect", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"no-autoplay":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"no-autoplay":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: no-autoplay
category: a17y
summary: Disallow autoplaying media elements
---
# Disallow autoplaying media elements (`no-autoplay`)
Autoplaying content can be disruptive for users and has accessibility issues.
This rule disallows `<audio>` and `<video>` with autoplay enabled.
Unless the user is expecting media to play automatically it is better to let the user control playback.
The media might be too loud or the user might be in a location where audio is discouraged.
Users with assistive technology might find it hard to pause as they must first navigate to the controls.
Media can be distracting for users with cognitive or concentration issues and if the video contains flashing or blinking sequences it can cause epilepsy.
There are also issues where some browsers use heurestics to prevent autoplaying so results may vary when used.
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="no-autoplay">
<video autoplay></video>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="no-autoplay">
<video></video>
</validate>
## Options
This rule takes an optional object:
```javascript
{
"include": ["audio", "video"],
"exclude": [],
}
```
### `include`
If set only elements listed in this array generates errors.
### `exclude`
If set elements listed in this array is ignored.
......@@ -21,6 +21,7 @@ module.exports = {
"empty-title": "error",
"long-title": "error",
"meta-refresh": "error",
"no-autoplay": ["error", { include: ["audio", "video"] }],
"no-conditional-comment": "error",
"no-deprecated-attr": "error",
"no-dup-attr": "error",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule no-autoplay should contain contextual documentation 1`] = `
Object {
"description": "The autoplay attribute is not allowed on <video>.
Autoplaying content can be disruptive for users and has accessibilty concerns.
Prefer to let the user control playback.",
"url": "https://html-validate.org/rules/no-autoplay.html",
}
`;
exports[`rule no-autoplay should contain documentation 1`] = `
Object {
"description": "The autoplay attribute is not allowed.
Autoplaying content can be disruptive for users and has accessibilty concerns.
Prefer to let the user control playback.",
"url": "https://html-validate.org/rules/no-autoplay.html",
}
`;
......@@ -25,6 +25,7 @@ import InputMissingLabel from "./input-missing-label";
import LongTitle from "./long-title";
import MetaRefresh from "./meta-refresh";
import MissingDoctype from "./missing-doctype";
import NoAutoplay from "./no-autoplay";
import NoConditionalComment from "./no-conditional-comment";
import NoDeprecatedAttr from "./no-deprecated-attr";
import NoDupAttr from "./no-dup-attr";
......@@ -79,6 +80,7 @@ const bundledRules: Record<string, RuleConstructor<any, any>> = {
"long-title": LongTitle,
"meta-refresh": MetaRefresh,
"missing-doctype": MissingDoctype,
"no-autoplay": NoAutoplay,
"no-conditional-comment": NoConditionalComment,
"no-deprecated-attr": NoDeprecatedAttr,
"no-dup-attr": NoDupAttr,
......
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
describe("rule no-autoplay", () => {
describe("default config", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "no-autoplay": "error" },
});
});
it.each(["audio", "video"])(
"should not report error when <%s> does not have autoplay",
(tagName) => {
expect.assertions(1);
const report = htmlvalidate.validateString(`<${tagName}></${tagName}>`);
expect(report).toBeValid();
}
);
it("should not report error when autoplay attribute is dynamic", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<video dynamic-autoplay="enableAutoplay">',
null,
{
processAttribute,
}
);
expect(report).toBeValid();
});
it.each(["audio", "video"])(
"should report error when <%s> have autoplay",
(tagName) => {
expect.assertions(1);
const report = htmlvalidate.validateString(
`<${tagName} autoplay></${tagName}>`
);
expect(report).toBeInvalid();
}
);
});
it("should not report error when role is excluded", () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate({
rules: { "no-autoplay": ["error", { exclude: ["video"] }] },
});
const valid = htmlvalidate.validateString("<video autoplay></video>");
const invalid = htmlvalidate.validateString("<audio autoplay></audio>");
expect(valid).toBeValid();
expect(invalid).toBeInvalid();
});
it("should report error only for included roles", () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate({
rules: { "no-autoplay": ["error", { include: ["video"] }] },
});
const valid = htmlvalidate.validateString("<audio autoplay></audio>");
const invalid = htmlvalidate.validateString("<video autoplay></video>");
expect(valid).toBeValid();
expect(invalid).toBeInvalid();
});
it("should contain documentation", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({
rules: { "no-autoplay": "error" },
});
expect(htmlvalidate.getRuleDocumentation("no-autoplay")).toMatchSnapshot();
});
it("should contain contextual documentation", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({
rules: { "no-autoplay": "error" },
});
const context = {
tagName: "video",
};
expect(
htmlvalidate.getRuleDocumentation("no-autoplay", null, context)
).toMatchSnapshot();
});
});
import { DynamicValue } from "../dom";
import { AttributeEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
interface RuleContext {
tagName: string;
}
interface RuleOptions {
include: string[] | null;
exclude: string[] | null;
}
const defaults: RuleOptions = {
include: null,
exclude: null,
};
export default class NoAutoplay extends Rule<RuleContext, RuleOptions> {
public constructor(options: RuleOptions) {
super(Object.assign({}, defaults, options));
}
public documentation(context: RuleContext): RuleDocumentation {
return {
description: [
`The autoplay attribute is not allowed${
context ? ` on <${context.tagName}>` : ""
}.`,
"Autoplaying content can be disruptive for users and has accessibilty concerns.",
"Prefer to let the user control playback.",
].join("\n"),
url: ruleDocumentationUrl(__filename),
};
}
public setup(): void {
this.on("attr", (event: AttributeEvent) => {
/* only handle autoplay attribute */
if (event.key.toLowerCase() !== "autoplay") {
return;
}
/* ignore dynamic values */
if (event.value && event.value instanceof DynamicValue) {
return;
}
/* ignore tagnames configured to be ignored */
const tagName = event.target.tagName;
if (this.isIgnored(tagName)) {
return;
}
/* report error */
const context: RuleContext = { tagName };
const location = event.location;
this.report(
event.target,
`The autoplay attribute is not allowed on <${tagName}>`,
location,
context
);
});
}
private isIgnored(tagName: string): boolean {
const { include, exclude } = this.options;
/* ignore tagnames not present in "include" */
if (include && !include.includes(tagName)) {
return true;
}
/* ignore tagnames present in "excludes" */
if (exclude && exclude.includes(tagName)) {
return true;
}
return false;
}
}
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