Commits (24)
......@@ -128,8 +128,12 @@ Jest compat:
before_script:
- npm ci
- npm install --force jest@${VERSION}.0.0 ts-jest@${VERSION}.0.0
- npm install
# workaround issue where node_modules/.bin/jest does not exist
- npm install --force jest@${VERSION}.0.0 ts-jest@${VERSION}.0.0
- npm ls jest ts-jest
script:
- npm exec jest -- --no-coverage matchers tests/jest
- npm exec jest -- --no-coverage --ci matchers tests/jest
Module:
stage: compatibility
......
# html-validate changelog
### [5.2.1](https://gitlab.com/html-validate/html-validate/compare/v5.2.0...v5.2.1) (2021-08-09)
### Bug Fixes
- **html5:** add `user` and `environment` to `capture` attribute ([d6b8f90](https://gitlab.com/html-validate/html-validate/commit/d6b8f9062d7ecbe8148c5ae2f801a09411b57213)), closes [#130](https://gitlab.com/html-validate/html-validate/issues/130)
## [5.2.0](https://gitlab.com/html-validate/html-validate/compare/v5.1.1...v5.2.0) (2021-07-23)
### Features
......
......@@ -3446,7 +3446,11 @@ Array [
Object {
"column": 29,
"context": Object {
"allowed": Array [],
"allowed": Array [
"",
"environment",
"user",
],
"attribute": "capture",
"element": "input",
"value": "foobar",
......
......@@ -550,7 +550,7 @@
"requiredAttributes": ["type"],
"attributes": {
"autofocus": [],
"capture": [],
"capture": ["", "environment", "user"],
"checked": [],
"disabled": [],
"inputmode": ["none", "text", "decimal", "numeric", "tel", "search", "email", "url"],
......
This diff is collapsed.
{
"name": "html-validate",
"version": "5.2.0",
"version": "5.2.1",
"description": "Offline html5 validator",
"keywords": [
"html",
......@@ -147,20 +147,20 @@
"semver": "^7.0.0"
},
"devDependencies": {
"@babel/core": "7.14.8",
"@babel/preset-env": "7.14.8",
"@commitlint/cli": "12.1.4",
"@babel/core": "7.15.0",
"@babel/preset-env": "7.15.0",
"@commitlint/cli": "13.1.0",
"@html-validate/commitlint-config": "2.0.0",
"@html-validate/eslint-config": "4.4.4",
"@html-validate/eslint-config-jest": "4.4.2",
"@html-validate/eslint-config-typescript": "4.4.0",
"@html-validate/jest-config": "2.2.0",
"@html-validate/prettier-config": "2.0.0",
"@html-validate/semantic-release-config": "2.0.1",
"@html-validate/semantic-release-config": "2.0.2",
"@lodder/grunt-postcss": "3.0.1",
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-replace": "3.0.0",
"@rollup/plugin-typescript": "8.2.3",
"@rollup/plugin-typescript": "8.2.5",
"@rollup/plugin-virtual": "2.0.3",
"@types/babar": "0.2.1",
"@types/babel__code-frame": "7.0.3",
......@@ -172,7 +172,7 @@
"@types/minimist": "1.2.2",
"@types/node": "11.15.54",
"@types/prompts": "2.0.14",
"@types/semver": "7.3.7",
"@types/semver": "7.3.8",
"autoprefixer": "10.3.1",
"babar": "0.2.0",
"babelify": "10.0.0",
......@@ -182,7 +182,7 @@
"dgeni": "0.4.14",
"dgeni-front-matter": "3.0.0",
"dgeni-packages": "0.29.1",
"eslint": "7.31.0",
"eslint": "7.32.0",
"font-awesome": "4.7.0",
"front-matter": "4.0.2",
"grunt": "1.4.1",
......@@ -191,22 +191,22 @@
"grunt-contrib-connect": "3.0.0",
"grunt-contrib-copy": "1.0.0",
"grunt-sass": "3.1.0",
"highlight.js": "11.1.0",
"highlight.js": "11.2.0",
"husky": "7.0.1",
"jest": "27.0.6",
"jest-diff": "27.0.6",
"jquery": "3.6.0",
"lint-staged": "11.1.0",
"lint-staged": "11.1.2",
"load-grunt-tasks": "5.1.0",
"marked": "2.1.3",
"minimatch": "3.0.4",
"npm-pkg-lint": "1.4.0",
"postcss": "8.3.6",
"prettier": "2.3.2",
"rollup": "2.53.3",
"rollup": "2.56.0",
"rollup-plugin-copy": "3.4.0",
"rollup-plugin-dts": "3.0.2",
"sass": "1.35.2",
"sass": "1.37.5",
"semantic-release": "17.4.4",
"serve-static": "1.14.1",
"stringmap": "0.2.2",
......
import path from "path";
import { Stats } from "fs";
let mockDirectory: string[] = [];
interface Entry {
type: "file" | "directory";
}
interface FileEntry extends Entry {
type: "file";
content: string;
}
interface DirectoryEntry extends Entry {
type: "directory";
}
class FSError extends Error {
public code: string;
public path: string;
public constructor(code: string, message: string, path: string) {
super(message);
this.code = code;
this.path = path;
}
public toString(): string {
return `${this.code}: ${this.message} '${this.path}'`;
}
}
class ENOENT extends FSError {
public constructor(filePath: string) {
super("ENOENT", "no such file or directory", filePath);
}
}
let mockData: Map<string, Entry> = new Map();
function isDirectory(entry: Entry): entry is DirectoryEntry {
return entry.type === "directory";
}
function isFile(entry: Entry): entry is FileEntry {
return entry.type === "file";
}
function setMockDirectories(directories: string[]): void {
mockDirectory = directories;
function normalize(filePath: string): string {
return path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
}
function statSync(dir: string): any {
/* slice the cwd away since it is prepended automatically and makes it harder
* to test with */
const suffix = dir.replace(`${process.cwd()}${path.sep}`, "");
function mockFile(filePath: string, content: string): void {
const normalized = normalize(filePath);
const fileEntry: FileEntry = {
type: "file",
content,
};
mockData.set(normalized, fileEntry);
let current = path.dirname(normalized);
while (current !== "/") {
if (mockData.has(current)) {
break;
}
const dirEntry: DirectoryEntry = {
type: "directory",
};
mockData.set(current, dirEntry);
current = path.dirname(current);
}
}
function mockReset(): void {
mockData = new Map();
const rootEntry: DirectoryEntry = {
type: "directory",
};
mockData.set("/", rootEntry);
}
function existsSync(filePath: string): boolean {
return mockData.has(normalize(filePath));
}
function readdirSync(
filePath: string,
options?: string | { encoding?: string; withFileTypes?: boolean }
): string[] | Buffer[] {
if (!filePath) {
throw new ENOENT(filePath);
}
const normalized = normalize(filePath);
const entry = mockData.get(normalized);
if (!entry) {
throw new ENOENT(filePath);
}
if (!isDirectory(entry)) {
throw new FSError("ENOTDIR", "not a directory", filePath);
}
const encoding = typeof options === "string" ? options : options?.encoding ?? "utf-8";
const entries = Array.from(mockData.keys())
.filter((it) => path.dirname(it) === normalized)
.map((it) => path.basename(it))
.sort();
if (typeof options !== "string" && options?.withFileTypes) {
throw new Error("NotImplemented: readdirSync option withFileTypes is not supported by mock");
}
if (encoding !== "utf-8") {
throw new Error("NotImplemented: readdirSync encoding only supports utf-8");
}
return entries;
}
function readFileSync(
filePath: string,
options?: string | { encoding?: string; flag?: string }
): Buffer | string {
if (!filePath) {
throw new ENOENT(filePath);
}
const normalized = normalize(filePath);
const entry = mockData.get(normalized);
if (!entry) {
throw new ENOENT(filePath);
}
if (!isFile(entry)) {
throw new FSError("EISDIR", "illegal operation on a directory", filePath);
}
if (typeof options === "string" || options?.encoding) {
return entry.content;
} else {
return Buffer.from(entry.content, "utf-8");
}
}
const found = mockDirectory.includes(suffix);
function statSync(filePath: string): Stats {
if (!filePath) {
throw new ENOENT(filePath);
}
const normalized = normalize(filePath);
const date = new Date();
const time = date.getTime();
const entry = mockData.get(normalized);
if (!entry) {
throw new ENOENT(filePath);
}
return {
isDirectory() {
return found;
},
isFile: () => isFile(entry),
isDirectory: () => isDirectory(entry),
isBlockDevice: () => false,
isCharacterDevice: () => false,
isSymbolicLink: () => false,
isFIFO: () => false,
isSocket: () => false,
dev: 1,
ino: 1,
mode: 0b110110110,
nlink: 1,
uid: 1,
gid: 1,
rdev: 1,
size: 1,
blksize: 1,
blocks: 1,
atimeMs: time,
mtimeMs: time,
ctimeMs: time,
birthtimeMs: time,
atime: date,
mtime: date,
ctime: date,
birthtime: date,
};
}
module.exports = {
setMockDirectories,
const original = jest.requireActual("fs");
const fs = {
__esModule: undefined,
mockFile,
mockReset,
existsSync,
realpath: original.realpath,
realpathSync: original.realpathSync,
readdirSync,
readFileSync,
statSync,
readFileSync: () => {
throw new Error("ENOENT");
},
lstatSync: statSync,
};
module.exports = new Proxy(fs, {
get(target, prop, receiver) {
if (Reflect.has(target, prop)) {
return Reflect.get(target, prop, receiver);
} else {
throw new Error(`fs mock does not implement "${prop.toString()}"`);
}
},
});
import path from "path";
import minimatch from "minimatch";
const glob: any = jest.requireActual("glob");
interface Options {
cwd?: string;
}
let mockFiles: string[] | null = null;
function setMockFiles(files: string[]): void {
mockFiles = files.map((cur) => path.normalize(cur));
}
function resetMock(): void {
mockFiles = null;
}
const originalSync = glob.sync;
function syncMock(pattern: string, options: Options = {}): string[] {
if (!mockFiles) {
return originalSync(pattern, options);
}
/* slice the cwd away since it is prepended automatically and makes it harder
* to test with */
const cwd = options.cwd || "";
const dir = cwd
.replace(process.cwd(), "")
.replace("\\", "/")
.replace(/^\/(.*)/, `$1${path.sep}`);
let src = mockFiles;
if (dir) {
src = src.filter((cur) => cur.startsWith(dir)).map((cur) => cur.slice(dir.length));
}
return src.filter((cur) => minimatch(cur, pattern));
}
glob.setMockFiles = setMockFiles;
glob.resetMock = resetMock;
glob.sync = syncMock;
module.exports = glob;
import fs from "fs";
import { ConfigData } from "../config";
import HtmlValidate from "../htmlvalidate";
import { CLI } from "./cli";
jest.disableAutomock();
declare module "fs" {
function mockFile(filePath: string, content: string): void;
function mockReset(): void;
}
jest.mock("fs");
jest.mock("../htmlvalidate");
describe("CLI", () => {
beforeEach(() => {
(HtmlValidate as any).mockClear();
fs.mockReset();
fs.mockFile("package.json", "{}");
});
describe("getValidator()", () => {
it("should create new HtmlValidate instance", () => {
expect.assertions(2);
expect.assertions(1);
const cli = new CLI();
const htmlvalidate = cli.getValidator();
expect(HtmlValidate).toHaveBeenCalledWith({
extends: ["html-validate:recommended"],
});
expect(htmlvalidate).toBeDefined();
});
});
describe("getConfig()", () => {
it("should use default configuration", () => {
expect.assertions(1);
const cli = new CLI();
expect(cli.getConfig()).toMatchInlineSnapshot(`
Object {
"extends": Array [
"html-validate:recommended",
],
}
`);
});
it("should use configuration file", () => {
expect.assertions(3);
const customConfig: ConfigData = {
rules: {
foo: "error",
},
};
const readFileSync = jest
.spyOn(fs, "readFileSync")
.mockImplementation(() => JSON.stringify(customConfig));
it("should use custom configuration file", () => {
expect.assertions(1);
fs.mockFile(
"config.json",
JSON.stringify({
rules: {
foo: "error",
},
})
);
const cli = new CLI({
configFile: "config.json",
});
const htmlvalidate = cli.getValidator();
expect(HtmlValidate).toHaveBeenCalledWith({
rules: {
foo: "error",
},
});
expect(htmlvalidate).toBeDefined();
expect(readFileSync).toHaveBeenCalledWith("config.json", "utf-8");
expect(cli.getConfig()).toMatchInlineSnapshot(`
Object {
"rules": Object {
"foo": "error",
},
}
`);
});
it("should configure single rule", () => {
expect.assertions(2);
expect.assertions(1);
const cli = new CLI({
rules: "foo:1",
});
const htmlvalidate = cli.getValidator();
expect(HtmlValidate).toHaveBeenCalledWith({
extends: [],
rules: {
foo: 1,
},
});
expect(htmlvalidate).toBeDefined();
expect(cli.getConfig()).toMatchInlineSnapshot(`
Object {
"extends": Array [],
"rules": Object {
"foo": 1,
},
}
`);
});
it("should configure multiple rule", () => {
expect.assertions(2);
expect.assertions(1);
const cli = new CLI({
rules: ["foo:1", "bar:0"],
});
const htmlvalidate = cli.getValidator();
expect(HtmlValidate).toHaveBeenCalledWith({
extends: [],
rules: {
foo: 1,
bar: 0,
},
});
expect(htmlvalidate).toBeDefined();
expect(cli.getConfig()).toMatchInlineSnapshot(`
Object {
"extends": Array [],
"rules": Object {
"bar": 0,
"foo": 1,
},
}
`);
});
});
});
......@@ -78,7 +78,10 @@ export class CLI {
return new HtmlValidate(this.config);
}
private getConfig(): ConfigData {
/**
* @internal
*/
public getConfig(): ConfigData {
const { options } = this;
const config: ConfigData = options.configFile
? JSON.parse(readFileSync(options.configFile, "utf-8"))
......
jest.mock("fs");
jest.mock("glob");
jest.mock("./is-ignored");
import fs from "fs";
import path from "path";
import glob from "glob";
import { CLI } from "./cli";
declare module "fs" {
function setMockDirectories(directories: string[]): void;
function mockFile(filePath: string, content: string): void;
function mockReset(): void;
}
declare module "glob" {
......@@ -19,61 +18,50 @@ declare module "glob" {
let cli: CLI;
beforeEach(() => {
fs.mockReset();
fs.mockFile("package.json", "{}");
fs.mockFile("foo.html", "");
fs.mockFile("bar/fred.html", "");
fs.mockFile("bar/fred.json", "");
fs.mockFile("bar/barney.html", "");
fs.mockFile("bar/barney.js", "");
fs.mockFile("baz/spam.html", "");
cli = new CLI();
glob.setMockFiles([
"/dev/stdin",
"foo.html",
"bar",
"bar/fred.html",
"bar/fred.json",
"bar/barney.html",
"bar/barney.js",
"baz",
"baz/spam.html",
]);
fs.setMockDirectories(["bar"]);
});
afterAll(() => {
glob.resetMock();
});
describe("expandFiles()", () => {
it("should expand globs", () => {
expect.assertions(3);
const spy = jest.spyOn(glob, "sync");
expect.assertions(1);
expect(cli.expandFiles(["foo.html", "bar/**/*.html"])).toEqual([
path.normalize("foo.html"),
path.normalize("bar/fred.html"),
path.normalize("bar/barney.html"),
path.normalize("bar/fred.html"),
]);
expect(spy).toHaveBeenCalledWith("foo.html", expect.anything());
expect(spy).toHaveBeenCalledWith("bar/**/*.html", expect.anything());
});
it("should expand directories (default extensions)", () => {
expect.assertions(1);
expect(cli.expandFiles(["bar"])).toEqual([
path.normalize("bar/fred.html"),
path.normalize("bar/barney.html"),
path.normalize("bar/fred.html"),
]);
});
it("should expand directories (explicit extensions)", () => {
expect.assertions(1);
expect(cli.expandFiles(["bar"], { extensions: ["js", "json"] })).toEqual([
path.normalize("bar/fred.json"),
path.normalize("bar/barney.js"),
path.normalize("bar/fred.json"),
]);
});
it("should expand directories (no extensions => all files)", () => {
expect.assertions(1);
expect(cli.expandFiles(["bar"], { extensions: [] })).toEqual([
path.normalize("bar/fred.html"),
path.normalize("bar/fred.json"),
path.normalize("bar/barney.html"),
path.normalize("bar/barney.js"),
path.normalize("bar/fred.html"),
path.normalize("bar/fred.json"),
]);
});
......
......@@ -4,6 +4,7 @@
<!-- should allow attributes -->
<input type="text" autofocus>
<input type="file" capture>
<input type="file" capture="environment">
<input type="checkbox" checked>
<input type="text" disabled>
<input type="file" multiple>
......@@ -69,6 +70,7 @@
<input type="range" autocomplete="on">
<input type="color" autocomplete="on">
<input type="file" capture>
<input type="file" capture="environment">
<input type="checkbox" checked>
<input type="radio" checked>
<input type="text" dirname="myname">
......