...
 
Commits (15)
# html-validate changelog
# [1.14.0](https://gitlab.com/html-validate/html-validate/compare/v1.13.0...v1.14.0) (2019-10-20)
### Features
- **shim:** expose more types ([86bb78d](https://gitlab.com/html-validate/html-validate/commit/86bb78d))
- enable typescript strict mode (excect strict null) ([5d2b45e](https://gitlab.com/html-validate/html-validate/commit/5d2b45e))
- **htmlvalidate:** support passing filename to `validateString` ([c2e09a2](https://gitlab.com/html-validate/html-validate/commit/c2e09a2))
# [1.13.0](https://gitlab.com/html-validate/html-validate/compare/v1.12.0...v1.13.0) (2019-10-13)
### Features
......
This diff is collapsed.
{
"name": "html-validate",
"version": "1.13.0",
"version": "1.14.0",
"description": "html linter",
"keywords": [
"html",
......@@ -125,13 +125,13 @@
"@types/babel__code-frame": "7.0.1",
"@types/estree": "0.0.39",
"@types/glob": "7.1.1",
"@types/jest": "24.0.18",
"@types/jest": "24.0.19",
"@types/json-merge-patch": "0.0.4",
"@types/minimist": "1.2.0",
"@types/node": "11.13.22",
"@typescript-eslint/eslint-plugin": "2.3.3",
"@typescript-eslint/parser": "2.3.3",
"autoprefixer": "9.6.4",
"@typescript-eslint/eslint-plugin": "2.4.0",
"@typescript-eslint/parser": "2.4.0",
"autoprefixer": "9.6.5",
"babelify": "10.0.0",
"bootstrap-sass": "3.4.1",
"canonical-path": "1.0.0",
......@@ -156,7 +156,7 @@
"grunt-postcss": "0.9.0",
"grunt-sass": "3.1.0",
"highlight.js": "9.15.10",
"husky": "3.0.8",
"husky": "3.0.9",
"jest": "24.9.0",
"jest-diff": "24.9.0",
"jest-junit": "8.0.0",
......@@ -166,7 +166,7 @@
"minimatch": "3.0.4",
"prettier": "1.18.2",
"sass": "1.23.0",
"semantic-release": "15.13.24",
"semantic-release": "15.13.27",
"serve-static": "1.14.1",
"strip-ansi": "5.2.0",
"ts-jest": "24.1.0",
......
......@@ -42,9 +42,11 @@ describe("ConfigLoader", () => {
});
it("should load configuration", () => {
jest.spyOn(fs, "existsSync").mockImplementation((filename: string) => {
return filename === path.resolve("/path/to/.htmlvalidate.json");
});
jest
.spyOn(fs, "existsSync")
.mockImplementation((filename: fs.PathLike) => {
return filename === path.resolve("/path/to/.htmlvalidate.json");
});
const config = loader.fromTarget("/path/to/target.html");
expect(config.get()).toEqual(
expect.objectContaining({
......@@ -57,9 +59,11 @@ describe("ConfigLoader", () => {
});
it("should load configuration from parent directory", () => {
jest.spyOn(fs, "existsSync").mockImplementation((filename: string) => {
return filename === path.resolve("/path/.htmlvalidate.json");
});
jest
.spyOn(fs, "existsSync")
.mockImplementation((filename: fs.PathLike) => {
return filename === path.resolve("/path/.htmlvalidate.json");
});
const config = loader.fromTarget("/path/to/target.html");
expect(config.get()).toEqual(
expect.objectContaining({
......
import { Config, ConfigLoader } from "./config";
import { Config, ConfigLoader, Severity } from "./config";
import { Source } from "./context";
import { Engine } from "./engine";
import HtmlValidate from "./htmlvalidate";
import { Parser } from "./parser";
import { Message } from "./reporter";
jest.mock("./engine");
jest.mock("./parser");
const engine = {
lint: jest.fn(),
dumpEvents: jest.fn(),
dumpTree: jest.fn(),
dumpTokens: jest.fn(),
getRuleDocumentation: jest.fn(),
};
function engineInstance(): Engine {
return ((Engine as unknown) as jest.MockInstance<Engine, any>).mock
.instances[0];
}
jest.mock("./engine", () => {
return {
Engine: jest.fn().mockImplementation(() => engine),
};
});
jest.mock("./parser");
function mockConfig(): Config {
const config = Config.empty();
......@@ -27,7 +34,7 @@ function mockConfig(): Config {
}
beforeEach(() => {
((Engine as unknown) as jest.MockInstance<Engine, any>).mockClear();
jest.clearAllMocks();
});
describe("HtmlValidate", () => {
......@@ -49,22 +56,38 @@ describe("HtmlValidate", () => {
);
});
it("validateString() should lint given string", () => {
const htmlvalidate = new HtmlValidate();
const str = "foobar";
htmlvalidate.validateString(str);
const engine = engineInstance();
expect(engine.lint).toHaveBeenCalledWith([
{
column: 1,
data: str,
filename: "inline",
line: 1,
},
]);
describe("validateString()", () => {
it("should validate given string", () => {
const mockReport = "mock-report";
engine.lint.mockReturnValue(mockReport);
const htmlvalidate = new HtmlValidate();
const str = "foobar";
const report = htmlvalidate.validateString(str);
expect(report).toEqual(mockReport);
expect(engine.lint).toHaveBeenCalledWith([
{
column: 1,
data: str,
filename: "inline",
line: 1,
},
]);
});
it("should load configuration if filename is given", () => {
const mockReport = "mock-report";
engine.lint.mockReturnValue(mockReport);
const htmlvalidate = new HtmlValidate();
const spy = jest.spyOn(htmlvalidate, "getConfigFor");
const str = "foobar";
htmlvalidate.validateString(str, "my-file.html");
expect(spy).toHaveBeenCalledWith("my-file.html");
});
});
it("validateSource() should lint given source", () => {
const mockReport = "mock-report";
engine.lint.mockReturnValue(mockReport);
const htmlvalidate = new HtmlValidate();
const source: Source = {
data: "foo",
......@@ -72,17 +95,19 @@ describe("HtmlValidate", () => {
line: 1,
column: 1,
};
htmlvalidate.validateSource(source);
const engine = engineInstance();
const report = htmlvalidate.validateSource(source);
expect(report).toEqual(mockReport);
expect(engine.lint).toHaveBeenCalledWith([source]);
});
it("validateFile() should lint given file", () => {
const mockReport = "mock-report";
engine.lint.mockReturnValue(mockReport);
const htmlvalidate = new HtmlValidate();
const filename = "foo.html";
jest.spyOn(htmlvalidate, "getConfigFor").mockImplementation(mockConfig);
htmlvalidate.validateFile(filename);
const engine = engineInstance();
const report = htmlvalidate.validateFile(filename);
expect(report).toEqual(mockReport);
expect(engine.lint).toHaveBeenCalledWith([
{
column: 1,
......@@ -93,12 +118,125 @@ describe("HtmlValidate", () => {
]);
});
describe("validateMultipleFiles()", () => {
const warning: Message = {
ruleId: "mock-warning",
message: "mock warning message",
severity: Severity.WARN,
line: 1,
column: 1,
offset: 0,
size: 1,
};
const error: Message = {
ruleId: "mock-error",
message: "mock error message",
severity: Severity.ERROR,
line: 1,
column: 1,
offset: 0,
size: 1,
};
it("should call validateFile for each file", () => {
expect.assertions(3);
const htmlvalidate = new HtmlValidate();
const spy = jest.spyOn(htmlvalidate, "validateFile").mockReturnValue({
valid: true,
results: [],
errorCount: 0,
warningCount: 0,
});
htmlvalidate.validateMultipleFiles(["foo.html", "bar.html"]);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith("foo.html");
expect(spy).toHaveBeenCalledWith("bar.html");
});
it("should merge reports", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate();
jest
.spyOn(htmlvalidate, "validateFile")
.mockReturnValueOnce({
valid: true,
results: [
{
filePath: "foo.html",
messages: [warning],
errorCount: 0,
warningCount: 1,
},
],
errorCount: 0,
warningCount: 1,
})
.mockReturnValueOnce({
valid: false,
results: [
{
filePath: "bar.html",
messages: [error],
errorCount: 1,
warningCount: 0,
},
],
errorCount: 1,
warningCount: 0,
});
const report = htmlvalidate.validateMultipleFiles([
"foo.html",
"bar.html",
]);
expect(report).toMatchInlineSnapshot(`
Object {
"errorCount": 1,
"results": Array [
Object {
"errorCount": 0,
"filePath": "foo.html",
"messages": Array [
Object {
"column": 1,
"line": 1,
"message": "mock warning message",
"offset": 0,
"ruleId": "mock-warning",
"severity": 1,
"size": 1,
},
],
"warningCount": 1,
},
Object {
"errorCount": 1,
"filePath": "bar.html",
"messages": Array [
Object {
"column": 1,
"line": 1,
"message": "mock error message",
"offset": 0,
"ruleId": "mock-error",
"severity": 2,
"size": 1,
},
],
"warningCount": 0,
},
],
"valid": false,
"warningCount": 1,
}
`);
});
});
it("dumpTokens() should dump tokens", () => {
const htmlvalidate = new HtmlValidate();
const filename = "foo.html";
jest.spyOn(htmlvalidate, "getConfigFor").mockImplementation(mockConfig);
htmlvalidate.dumpTokens(filename);
const engine = engineInstance();
expect(engine.dumpTokens).toHaveBeenCalledWith([
{
column: 1,
......@@ -114,7 +252,6 @@ describe("HtmlValidate", () => {
const filename = "foo.html";
jest.spyOn(htmlvalidate, "getConfigFor").mockImplementation(mockConfig);
htmlvalidate.dumpEvents(filename);
const engine = engineInstance();
expect(engine.dumpEvents).toHaveBeenCalledWith([
{
column: 1,
......@@ -130,7 +267,6 @@ describe("HtmlValidate", () => {
const filename = "foo.html";
jest.spyOn(htmlvalidate, "getConfigFor").mockImplementation(mockConfig);
htmlvalidate.dumpTree(filename);
const engine = engineInstance();
expect(engine.dumpTree).toHaveBeenCalledWith([
{
column: 1,
......@@ -145,7 +281,6 @@ describe("HtmlValidate", () => {
const htmlvalidate = new HtmlValidate();
const config = Config.empty();
htmlvalidate.getRuleDocumentation("foo", config, { bar: "baz" });
const engine = engineInstance();
expect(engine.getRuleDocumentation).toHaveBeenCalledWith("foo", {
bar: "baz",
});
......
......@@ -33,14 +33,19 @@ class HtmlValidate {
* Parse and validate HTML from string.
*
* @param str - Text to parse.
* @param filename - If set configuration is loaded for given filename.
* @param hooks - Optional hooks (see [[Source]]) for definition.
* @returns Report output.
*/
public validateString(str: string, hooks?: SourceHooks): Report {
public validateString(
str: string,
filename?: string,
hooks?: SourceHooks
): Report {
const source = {
column: 1,
data: str,
filename: "inline",
filename: filename || "inline",
line: 1,
hooks,
};
......@@ -54,7 +59,7 @@ class HtmlValidate {
* @returns Report output.
*/
public validateSource(source: Source): Report {
const config = this.getConfigFor("inline");
const config = this.getConfigFor(source.filename);
const engine = new Engine(config, Parser);
return engine.lint([source]);
}
......
......@@ -31,7 +31,10 @@ function flattenMessages(report: Report): Message[] {
}, []);
}
function toBeValid(report: Report): jest.CustomMatcherResult {
function toBeValid(
this: jest.MatcherUtils,
report: Report
): jest.CustomMatcherResult {
if (report.valid) {
return {
pass: true,
......@@ -48,7 +51,10 @@ function toBeValid(report: Report): jest.CustomMatcherResult {
}
}
function toBeInvalid(report: Report): jest.CustomMatcherResult {
function toBeInvalid(
this: jest.MatcherUtils,
report: Report
): jest.CustomMatcherResult {
if (report.valid) {
return {
pass: false,
......@@ -64,6 +70,7 @@ function toBeInvalid(report: Report): jest.CustomMatcherResult {
}
function toHaveError(
this: jest.MatcherUtils,
report: Report,
ruleId: any,
message: any,
......@@ -93,6 +100,7 @@ function toHaveError(
}
function toHaveErrors(
this: jest.MatcherUtils,
report: Report,
errors: Array<[string, string] | {}>
): jest.CustomMatcherResult {
......@@ -121,7 +129,11 @@ function toHaveErrors(
return { pass, message: resultMessage };
}
function toBeToken(actual: any, expected: any): jest.CustomMatcherResult {
function toBeToken(
this: jest.MatcherUtils,
actual: any,
expected: any
): jest.CustomMatcherResult {
const token = actual.value;
// istanbul ignore next: TokenMatcher requires "type" property to be set, this is just a failsafe
......
......@@ -50,6 +50,7 @@ export interface MetaElement extends MetaData {
tagName: string;
[key: string]:
| undefined
| boolean
| PropertyExpression
| Permitted
......
......@@ -211,9 +211,13 @@ describe("rule attr-case", () => {
htmlvalidate = new HtmlValidate({
rules: { "attr-case": "error" },
});
const report = htmlvalidate.validateString('<input dynamic-fooBar="foo">', {
processAttribute,
});
const report = htmlvalidate.validateString(
'<input dynamic-fooBar="foo">',
null,
{
processAttribute,
}
);
expect(report).toBeInvalid();
expect(report).toHaveErrors([
{
......
......@@ -56,6 +56,7 @@ describe("rule attribute-allowed-values", () => {
it("should not report error when attribute is dynamic", () => {
const report = htmlvalidate.validateString(
'<input type="{{ interpolated }}" required="{{ interpolated }}"><input dynamic-type="dynamic" dynamic-required="dynamic">',
null,
{
processAttribute,
}
......
......@@ -51,6 +51,7 @@ describe("rule attribute-boolean-style", () => {
it("should report error when attribute is interpolated", () => {
const report = htmlvalidate.validateString(
'<input required="{{ dynamic }}">',
null,
{ processAttribute }
);
expect(report).toBeInvalid();
......@@ -63,6 +64,7 @@ describe("rule attribute-boolean-style", () => {
it("should not report error when attribute is dynamic", () => {
const report = htmlvalidate.validateString(
'<input dynamic-required="dynamic">',
null,
{ processAttribute }
);
expect(report).toBeValid();
......@@ -109,6 +111,7 @@ describe("rule attribute-boolean-style", () => {
it("should report error when attribute is dynamic", () => {
const report = htmlvalidate.validateString(
'<input required="{{ dynamic }}">',
null,
{ processAttribute }
);
expect(report).toBeInvalid();
......@@ -159,6 +162,7 @@ describe("rule attribute-boolean-style", () => {
it("should report error when attribute is dynamic", () => {
const report = htmlvalidate.validateString(
'<input required="{{ dynamic }}">',
null,
{ processAttribute }
);
expect(report).toBeInvalid();
......
......@@ -41,6 +41,18 @@ describe("rule deprecated-rule", () => {
expect(report).toBeValid();
});
it("should not report error when no deprecated rule is disabled", () => {
const htmlvalidate = new HtmlValidate({
plugins: ["my-plugin"],
rules: {
"custom/deprecated": "off",
"deprecated-rule": "error",
},
});
const report = htmlvalidate.validateString("<div></div>");
expect(report).toBeValid();
});
it("should report error when a rule is deprecated", () => {
const htmlvalidate = new HtmlValidate({
plugins: ["my-plugin"],
......
......@@ -27,7 +27,7 @@ describe("rule empty-heading", () => {
function processElement(node: HtmlElement): void {
node.appendText(new DynamicValue(""));
}
const report = htmlvalidate.validateString("<h1></h1>", {
const report = htmlvalidate.validateString("<h1></h1>", null, {
processElement,
});
expect(report).toBeValid();
......
......@@ -27,7 +27,7 @@ describe("rule empty-title", () => {
function processElement(node: HtmlElement): void {
node.appendText(new DynamicValue(""));
}
const report = htmlvalidate.validateString("<title></title>", {
const report = htmlvalidate.validateString("<title></title>", null, {
processElement,
});
expect(report).toBeValid();
......
......@@ -19,6 +19,7 @@ describe("rule id-pattern", () => {
it("should not report error when id is interpolated", () => {
const report = htmlvalidate.validateString(
'<p id="{{ interpolated }}"></p>',
null,
{ processAttribute }
);
expect(report).toBeValid();
......
......@@ -19,6 +19,7 @@ describe("rule no-dup-attr", () => {
it("should not report error when attribute is dynamic", () => {
const report = htmlvalidate.validateString(
'<input class="foo" dynamic-class="bar">',
null,
{ processAttribute }
);
expect(report).toBeValid();
......@@ -43,6 +44,7 @@ describe("rule no-dup-attr", () => {
it("should report error when dynamic element is used multiple times", () => {
const report = htmlvalidate.validateString(
'<input dynamic-class="foo" dynamic-class="bar">',
null,
{ processAttribute }
);
expect(report).toBeInvalid();
......
......@@ -3,9 +3,9 @@
export { default as HtmlValidate } from "./htmlvalidate";
export { AttributeData } from "./parser";
export { CLI } from "./cli/cli";
export { Config, ConfigData, ConfigLoader } from "./config";
export { Config, ConfigData, ConfigLoader, Severity } from "./config";
export { DynamicValue, HtmlElement } from "./dom";
export { Rule } from "./rule";
export { Source } from "./context";
export { Reporter } from "./reporter";
export { Source, Location } from "./context";
export { Reporter, Message, Result } from "./reporter";
export { TemplateExtractor } from "./transform/template";
......@@ -9,7 +9,9 @@
"noImplicitAny": true,
"outDir": "build",
"sourceMap": false,
"target": "es2017"
"target": "es2017",
"strict": true,
"strictNullChecks": false
},
"sourceMap": true,
"include": ["src/**/*.ts"],
......