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

feat(rules): add `include` and `exclude` options to `prefer-button`

refs #90
parent 87c81140
......@@ -10,7 +10,9 @@ Array [
"messages": Array [
Object {
"column": 14,
"context": undefined,
"context": Object {
"type": "button",
},
"line": 1,
"message": "Prefer to use <button> instead of <input type=\\"button\\"> when adding buttons",
"offset": 13,
......
......@@ -4,20 +4,52 @@ name: prefer-button
summary: Prefer to use <button> instead of <input> for buttons
---
# prefer to use `<button>` (`prefer-button`)
# Prefer to use `<button>` (`prefer-button`)
HTML5 introduces the generic `<button>` element which replaces `<input type="button">` and similar constructs.
The `<button>` elements has some advantages:
- It can contain markup as content compared to the `value` attribute of `<input>` which can only hold text. Especially useful to add `<svg>` icons.
- The button text is a regular text node, no need to quote characters in the `value` attribute.
- Styling is easier, compare the selector `button` to `input[type="submit"], input[type="button"], ...`.
This rule will target the following input types:
- `<input type="button">`
- `<input type="submit">`
- `<input type="reset">`
- `<input type="image">`
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="prefer-button">
<input type="button">
<input type="button">
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="prefer-button">
<button type="button"></button>
<button type="button"></button>
</validate>
## Options
This rule takes an optional object:
```javascript
{
"include": [],
"exclude": [],
}
```
### `include`
If set only types listed in this array generates errors.
### `exclude`
If set types listed in this array is ignored.
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule prefer-button should contain documentation 1`] = `
Object {
"description": "Prefer to use the generic \`<button>\` element instead of \`<input>\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button smoketest 1`] = `
exports[`rule prefer-button default config smoketest 1`] = `
Array [
Object {
"errorCount": 4,
......@@ -15,7 +8,9 @@ Array [
"messages": Array [
Object {
"column": 14,
"context": undefined,
"context": Object {
"type": "button",
},
"line": 5,
"message": "Prefer to use <button> instead of <input type=\\"button\\"> when adding buttons",
"offset": 64,
......@@ -26,7 +21,9 @@ Array [
},
Object {
"column": 14,
"context": undefined,
"context": Object {
"type": "submit",
},
"line": 8,
"message": "Prefer to use <button> instead of <input type=\\"submit\\"> when adding buttons",
"offset": 119,
......@@ -37,7 +34,9 @@ Array [
},
Object {
"column": 14,
"context": undefined,
"context": Object {
"type": "reset",
},
"line": 11,
"message": "Prefer to use <button> instead of <input type=\\"reset\\"> when adding buttons",
"offset": 174,
......@@ -48,7 +47,9 @@ Array [
},
Object {
"column": 14,
"context": undefined,
"context": Object {
"type": "image",
},
"line": 14,
"message": "Prefer to use <button> instead of <input type=\\"image\\"> when adding buttons",
"offset": 227,
......@@ -78,3 +79,45 @@ Array [
},
]
`;
exports[`rule prefer-button should contain contextual documentation for type "button" 1`] = `
Object {
"description": "Prefer to use \`<button type=\\"button\\">\` instead of \`\\"<input type=\\"button\\">\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button should contain contextual documentation for type "image" 1`] = `
Object {
"description": "Prefer to use \`<button type=\\"button\\">\` instead of \`\\"<input type=\\"image\\">\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button should contain contextual documentation for type "reset" 1`] = `
Object {
"description": "Prefer to use \`<button type=\\"reset\\">\` instead of \`\\"<input type=\\"reset\\">\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button should contain contextual documentation for type "submit" 1`] = `
Object {
"description": "Prefer to use \`<button type=\\"submit\\">\` instead of \`\\"<input type=\\"submit\\">\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button should contain contextual documentation for type "unknown" 1`] = `
Object {
"description": "Prefer to use \`<button>\` instead of \`\\"<input type=\\"unknown\\">\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
exports[`rule prefer-button should contain documentation 1`] = `
Object {
"description": "Prefer to use the generic \`<button>\` element instead of \`<input>\`.",
"url": "https://html-validate.org/rules/prefer-button.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
import { types } from "./prefer-button";
describe("rule prefer-button", () => {
let htmlvalidate: HtmlValidate;
describe("default config", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "prefer-button": "error" },
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "prefer-button": "error" },
});
});
});
it("should not report error when type attribute is missing type attribute", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<input>");
expect(report).toBeValid();
});
it("should not report error when type attribute is missing type attribute", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<input>");
expect(report).toBeValid();
});
it("should not report error when type attribute is missing value", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<input type>");
expect(report).toBeValid();
});
it("should not report error when type attribute is missing value", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<input type>");
expect(report).toBeValid();
});
it("should not report error when using regular input fields", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<input type="text">');
expect(report).toBeValid();
});
it("should not report error for dynamic attributes", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<input dynamic-type="inputType">',
null,
{
processAttribute,
}
);
expect(report).toBeValid();
});
it("should report error when using type button", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="button">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="button"> when adding buttons'
);
});
it("should not report error when using regular input fields", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<input type="text">');
expect(report).toBeValid();
});
it("should report error when using type submit", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="submit">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="submit"> when adding buttons'
);
});
it("should report error when using type button", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="button">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="button"> when adding buttons'
);
});
it("should report error when using type reset", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="reset">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="reset"> when adding buttons'
);
it("should report error when using type submit", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="submit">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="submit"> when adding buttons'
);
});
it("should report error when using type reset", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="reset">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="reset"> when adding buttons'
);
});
it("should report error when using type image", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="image">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="image"> when adding buttons'
);
});
it("smoketest", () => {
expect.assertions(1);
const report = htmlvalidate.validateFile(
"test-files/rules/prefer-button.html"
);
expect(report.results).toMatchSnapshot();
});
});
it("should report error when using type image", () => {
it("should not report error when type is excluded", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<input type="image">');
expect(report).toBeInvalid();
expect(report).toHaveError(
"prefer-button",
'Prefer to use <button> instead of <input type="image"> when adding buttons'
);
const htmlvalidate = new HtmlValidate({
rules: { "prefer-button": ["error", { exclude: ["submit"] }] },
});
const valid = htmlvalidate.validateString('<input type="submit">');
const invalid = htmlvalidate.validateString('<input type="reset">');
expect(valid).toBeValid();
expect(invalid).toBeInvalid();
});
it("smoketest", () => {
expect.assertions(1);
const report = htmlvalidate.validateFile(
"test-files/rules/prefer-button.html"
);
expect(report.results).toMatchSnapshot();
it("should report error only for included types", () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate({
rules: { "prefer-button": ["error", { include: ["submit"] }] },
});
const valid = htmlvalidate.validateString('<input type="reset">');
const invalid = htmlvalidate.validateString('<input type="submit">');
expect(valid).toBeValid();
expect(invalid).toBeInvalid();
});
it("should contain documentation", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
const htmlvalidate = new HtmlValidate({
rules: { "prefer-button": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("prefer-button")
).toMatchSnapshot();
});
describe("should contain contextual documentation", () => {
it.each([...types, "unknown"])('for type "%s"', (type) => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({
rules: { "prefer-button": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("prefer-button", null, { type })
).toMatchSnapshot();
});
});
});
import { TagCloseEvent } from "../event";
import { AttributeEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
import { DynamicValue } from "../dom";
export default class PreferButton extends Rule {
public documentation(): RuleDocumentation {
return {
interface RuleContext {
type: string;
}
interface RuleOptions {
include: string[] | null;
exclude: string[] | null;
}
export const types = ["button", "submit", "reset", "image"];
const replacement: Record<string, string> = {
button: '<button type="button">',
submit: '<button type="submit">',
reset: '<button type="reset">',
image: '<button type="button">',
};
const defaults: RuleOptions = {
include: null,
exclude: null,
};
export default class PreferButton extends Rule<RuleContext, RuleOptions> {
public constructor(options: RuleOptions) {
super({ ...defaults, ...options });
}
public documentation(context: RuleContext): RuleDocumentation {
const doc: RuleDocumentation = {
description: `Prefer to use the generic \`<button>\` element instead of \`<input>\`.`,
url: ruleDocumentationUrl(__filename),
};
if (context) {
const src = `<input type="${context.type}">`;
const dst = replacement[context.type] || `<button>`;
doc.description = `Prefer to use \`${dst}\` instead of \`"${src}\`.`;
}
return doc;
}
public setup(): void {
this.on("tag:close", (event: TagCloseEvent) => {
const node = event.previous;
this.on("attr", (event: AttributeEvent) => {
const node = event.target;
/* only handle input elements */
if (node.tagName !== "input") {
return;
}
const type = node.getAttribute("type");
/* sanity check: handle missing, boolean and dynamic attributes */
if (!event.value || event.value instanceof DynamicValue) {
return;
}
/* sanity check: handle missing and boolean attributes */
if (!type || type.value === null) {
/* ignore types configured to be ignored */
if (this.isIgnored(event.value)) {
return;
}
if (type.valueMatches(/^(button|submit|reset|image)$/, false)) {
this.report(
node,
`Prefer to use <button> instead of <input type="${type.value}"> when adding buttons`,
type.valueLocation
);
/* only values matching known type triggers error */
if (!types.includes(event.value)) {
return;
}
const context: RuleContext = { type: event.value };
const message = `Prefer to use <button> instead of <input type="${event.value}"> when adding buttons`;
this.report(node, message, event.valueLocation, context);
});
}
private isIgnored(type: string): boolean {
const { include, exclude } = this.options;
/* ignore roles not present in "include" */
if (include && !include.includes(type)) {
return true;
}
/* ignore roles present in "excludes" */
if (exclude && exclude.includes(type)) {
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