Commit 44388ea0 authored by David Sveningsson's avatar David Sveningsson

feat(jest): add `toHTMLValidate()`

parent 40b2d956
Pipeline #108608143 passed with stages
in 9 minutes and 17 seconds
......@@ -44,6 +44,7 @@
<li><a href="/usage/transformers.html">Transfomers</a></li>
<li role="separator" class="divider"></li>
<li><a href="/usage/grunt.html">Grunt</a></li>
<li><a href="/frameworks/jest.html">Jest</a></li>
<li><a href="/usage/protractor.html">Protractor</a></li>
<li><a href="/frameworks/vue.html">Vue.js</a></li>
</ul>
......
---
docType: content
title: Usage with Jest
---
# Usage with Jest
`html-validate` comes with Jest support built-in.
In you test import `matchers`:
```js
import "html-validate/build/matchers";
```
This makes all the custom matchers available.
## API
### `toHTMLValidate(config?: ConfigData, filename?: string)`
Validates a string of HTML and passes the assertion if the markup is valid.
```js
expect("<p></p>").toHTMLValidate();
expect("<p></i>").not.toHTMLValidate();
```
You can also pass jsdom elements:
```js
const elem = document.createElement("div");
expect(elem).toHTMLValidate();
```
If needed a custom configuration can be passed:
```js
expect("<p></i>").toHTMLValidate({
rules: {
"close-order": "off",
},
});
```
By default configuration is also read from `.htmlvalidate.json` files where the test-case filename is used to match.
If you need to override this (perhaps because the test-case isn't in the same folder) you can pass in a custom filename as the third argument:
```js
expect("<p></i>").toHTMLValidate(null, "path/to/my-file.html");
```
Additionally, the `root` configuration property can be used to skip loading from `.htmlvalidate.json` but remember to actually include the rules you need:
```js
expect("<p></i>").toHTMLValidate({
extends: ["html-validate:recommended"],
root: true,
});
```
### `toBeValid()`
Assert that a HTML-Validate report is valid.
```js
const htmlvalidate = new HtmlValidate();
const report = htmlvalidate.validateString("<p></p>");
expect(report).toBeValid();
```
### `toBeInvalid()`
Assert that a HTML-Validate report is invalid.
Inverse of `toBeValid()`.
```js
const htmlvalidate = new HtmlValidate();
const report = htmlvalidate.validateString("<p></i>");
expect(report).toBeInvalid();
```
### `toHaveError(ruleId: string, message: string, context?: any)`
Assert that a specific error is present in an HTML-Validate report.
```js
const htmlvalidate = new HtmlValidate();
const report = htmlvalidate.validateString("<p></i>");
expect(report).toHaveError(
"close-order",
"Mismatched close-tag, expected '</p>' but found '</i>'"
);
```
### `toHaveErrors(errors: Array<[string, string] | object>)`
Similar to `toHaveError` but but asserts multiple errors.
The passed list must have the same length as the report.
Each error must either be `[ruleId, message]` or an object passed to `expect.objectContaining`.
```js
const htmlvalidate = new HtmlValidate();
const report = htmlvalidate.validateString("<p></i>");
expect(report).toHaveErrors([
["close-order", "Mismatched close-tag, expected '</p>' but found '</i>'"],
]);
```
or with object syntax:
```js
expect(report).toHaveErrors([
{
ruleId: "close-order",
message: "Mismatched close-tag, expected '</p>' but found '</i>'",
},
]);
```
......@@ -188,3 +188,74 @@ describe("toBeToken()", () => {
expect(stripAnsi(error.message)).toMatchSnapshot();
});
});
describe("toHTMLValidate()", () => {
it("should pass if markup is valid", () => {
expect.assertions(1);
expect("<p></p>").toHTMLValidate();
});
it("should pass if markup is invalid but negated", () => {
expect.assertions(1);
expect("<p></i>").not.toHTMLValidate();
});
it("should fail if markup is invalid", async () => {
expect.assertions(3);
let error: Error;
try {
await expect("<a><button></i>").toHTMLValidate();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(stripAnsi(error.message)).toMatchInlineSnapshot(`
"Expected HTML to be valid but had the following errors:
Anchor link must have a text describing its purpose [WCAG/H30]
<button> is missing required \\"type\\" attribute [element-required-attributes]
Element <button> is not permitted as descendant of <a> [element-permitted-content]
Mismatched close-tag, expected '</button>' but found '</i>'. [close-order]
Missing close-tag, expected '</a>' but document ended before it was found. [close-order]"
`);
});
it("should fail if markup is valid but negated", async () => {
expect.assertions(3);
let error: Error;
try {
await expect("<p></p>").not.toHTMLValidate();
} catch (e) {
error = e;
}
expect(error).toBeDefined();
expect(stripAnsi(error.message)).toMatchInlineSnapshot(
`"HTML is valid when an error was expected"`
);
});
it("should support configuration object", () => {
expect.assertions(1);
expect("<p></i>").toHTMLValidate({
rules: {
"close-order": "off",
},
});
});
it("should support jsdom", () => {
/* eslint-disable-next-line @typescript-eslint/ban-ts-ignore */
// @ts-ignore DOM library not available
const doc = document;
expect.assertions(2);
/* should pass */
const p = doc.createElement("p");
expect(p).toHTMLValidate();
/* should fail (type not set) */
const button = doc.createElement("button");
expect(button).not.toHTMLValidate();
});
});
/* eslint-disable @typescript-eslint/no-namespace, prefer-template, sonarjs/no-duplicate-string */
/* eslint-disable @typescript-eslint/no-namespace, @typescript-eslint/ban-ts-ignore, prefer-template, sonarjs/no-duplicate-string */
import diff from "jest-diff";
import { TokenType } from "./lexer";
import { Message, Report, Result } from "./reporter";
import { ConfigData } from "./config";
import HtmlValidate from "./htmlvalidate";
interface TokenMatcher {
type: TokenType;
......@@ -18,6 +20,15 @@ declare global {
toBeToken(expected: TokenMatcher): R;
toHaveError(ruleId: string, message: string, context?: any): R;
toHaveErrors(errors: Array<[string, string] | {}>): R;
/**
* Validate string or HTMLElement.
*
* Test passes if result is valid.
*
* @param config - Optional HTML-Validate configuration object.
*/
toHTMLValidate(config?: ConfigData): R;
}
}
}
......@@ -129,6 +140,37 @@ function toHaveErrors(
return { pass, message: resultMessage };
}
function toHTMLValidate(
this: jest.MatcherUtils,
// @ts-ignore DOM library not available
actual: string | HTMLElement,
config?: ConfigData,
filename?: string
): jest.CustomMatcherResult {
// @ts-ignore DOM library not available
if (actual instanceof HTMLElement) {
actual = actual.outerHTML;
}
const htmlvalidate = new HtmlValidate(config);
const report = htmlvalidate.validateString(actual, filename || this.testPath);
const pass = report.valid;
if (pass) {
return { pass, message: () => "HTML is valid when an error was expected" };
} else {
const errors = report.results[0].messages.map(
message => ` ${message.message} [${message.ruleId}]`
);
return {
pass,
message: () =>
["Expected HTML to be valid but had the following errors:", ""]
.concat(errors)
.join("\n"),
};
}
}
function toBeToken(
this: jest.MatcherUtils,
actual: any,
......@@ -168,5 +210,6 @@ expect.extend({
toBeInvalid,
toHaveError,
toHaveErrors,
toHTMLValidate,
toBeToken,
});
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