Skip to content

Commit d9e1e8f

Browse files
committed
Merge branch 'feature/reporter-async-merge' into 'master'
reporter async merge See merge request html-validate/html-validate!909
2 parents 6083b31 + 527f373 commit d9e1e8f

File tree

11 files changed

+206
-88
lines changed

11 files changed

+206
-88
lines changed

etc/browser.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,7 @@ export class Reporter {
935935
// (undocumented)
936936
protected isValid(): boolean;
937937
static merge(reports: Report_2[]): Report_2;
938+
static merge(reports: Promise<Report_2[]> | Array<Promise<Report_2>>): Promise<Report_2>;
938939
// (undocumented)
939940
protected result: Record<string, DeferredMessage[]>;
940941
// (undocumented)

etc/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,7 @@ export class Reporter {
10281028
// (undocumented)
10291029
protected isValid(): boolean;
10301030
static merge(reports: Report_2[]): Report_2;
1031+
static merge(reports: Promise<Report_2[]> | Array<Promise<Report_2>>): Promise<Report_2>;
10311032
// (undocumented)
10321033
protected result: Record<string, DeferredMessage[]>;
10331034
// (undocumented)

src/cli/actions/lint.spec.ts

Lines changed: 88 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,14 @@ beforeEach(() => {
4949

5050
it("should return successful if there where no errors", async () => {
5151
expect.assertions(2);
52-
jest.spyOn(htmlvalidate, "validateFileSync").mockReturnValue({
53-
valid: true,
54-
results: [],
55-
errorCount: 0,
56-
warningCount: 0,
57-
});
52+
jest.spyOn(htmlvalidate, "validateFile").mockImplementation(() =>
53+
Promise.resolve({
54+
valid: true,
55+
results: [],
56+
errorCount: 0,
57+
warningCount: 0,
58+
}),
59+
);
5860
const files = ["foo.html", "bar.html"];
5961
const success = await lint(htmlvalidate, stdout, files, defaultOptions);
6062
expect(success).toBeTruthy();
@@ -63,20 +65,22 @@ it("should return successful if there where no errors", async () => {
6365

6466
it("should return success if there where only warnings", async () => {
6567
expect.assertions(2);
66-
jest.spyOn(htmlvalidate, "validateFileSync").mockImplementation((filePath: string) => ({
67-
valid: true,
68-
results: [
69-
{
70-
messages: [mockWarning("mock-rule", "lorem ipsum")],
71-
filePath,
72-
errorCount: 0,
73-
warningCount: 1,
74-
source: null,
75-
},
76-
],
77-
errorCount: 0,
78-
warningCount: 1,
79-
}));
68+
jest.spyOn(htmlvalidate, "validateFile").mockImplementation((filePath: string) =>
69+
Promise.resolve({
70+
valid: true,
71+
results: [
72+
{
73+
messages: [mockWarning("mock-rule", "lorem ipsum")],
74+
filePath,
75+
errorCount: 0,
76+
warningCount: 1,
77+
source: null,
78+
},
79+
],
80+
errorCount: 0,
81+
warningCount: 1,
82+
}),
83+
);
8084
const files = ["foo.html", "bar.html"];
8185
const success = await lint(htmlvalidate, stdout, files, defaultOptions);
8286
expect(success).toBeTruthy();
@@ -89,20 +93,22 @@ it("should return success if there where only warnings", async () => {
8993

9094
it("should return failure if there where any errors", async () => {
9195
expect.assertions(2);
92-
jest.spyOn(htmlvalidate, "validateFileSync").mockImplementation((filePath: string) => ({
93-
valid: false,
94-
results: [
95-
{
96-
messages: [mockError("mock-rule", "lorem ipsum")],
97-
filePath,
98-
errorCount: 1,
99-
warningCount: 0,
100-
source: null,
101-
},
102-
],
103-
errorCount: 1,
104-
warningCount: 0,
105-
}));
96+
jest.spyOn(htmlvalidate, "validateFile").mockImplementation((filePath: string) =>
97+
Promise.resolve({
98+
valid: false,
99+
results: [
100+
{
101+
messages: [mockError("mock-rule", "lorem ipsum")],
102+
filePath,
103+
errorCount: 1,
104+
warningCount: 0,
105+
source: null,
106+
},
107+
],
108+
errorCount: 1,
109+
warningCount: 0,
110+
}),
111+
);
106112
const files = ["foo.html", "bar.html"];
107113
const success = await lint(htmlvalidate, stdout, files, defaultOptions);
108114
expect(success).toBeFalsy();
@@ -115,20 +121,22 @@ it("should return failure if there where any errors", async () => {
115121

116122
it("should return failure if there are too many warnings", async () => {
117123
expect.assertions(2);
118-
jest.spyOn(htmlvalidate, "validateFileSync").mockImplementation((filePath: string) => ({
119-
valid: true,
120-
results: [
121-
{
122-
messages: [mockWarning("mock-rule", "lorem ipsum")],
123-
filePath,
124-
errorCount: 0,
125-
warningCount: 1,
126-
source: null,
127-
},
128-
],
129-
errorCount: 0,
130-
warningCount: 1,
131-
}));
124+
jest.spyOn(htmlvalidate, "validateFile").mockImplementation((filePath: string) =>
125+
Promise.resolve({
126+
valid: true,
127+
results: [
128+
{
129+
messages: [mockWarning("mock-rule", "lorem ipsum")],
130+
filePath,
131+
errorCount: 0,
132+
warningCount: 1,
133+
source: null,
134+
},
135+
],
136+
errorCount: 0,
137+
warningCount: 1,
138+
}),
139+
);
132140
const files = ["foo.html", "bar.html"];
133141
const success = await lint(htmlvalidate, stdout, files, {
134142
...defaultOptions,
@@ -146,20 +154,22 @@ it("should return failure if there are too many warnings", async () => {
146154

147155
it("should retain /dev/stdin when stdinFilename is not given", async () => {
148156
expect.assertions(2);
149-
jest.spyOn(htmlvalidate, "validateFileSync").mockImplementation((filePath: string) => ({
150-
valid: false,
151-
results: [
152-
{
153-
messages: [mockError("mock-rule", "lorem ipsum")],
154-
filePath,
155-
errorCount: 1,
156-
warningCount: 0,
157-
source: null,
158-
},
159-
],
160-
errorCount: 1,
161-
warningCount: 0,
162-
}));
157+
jest.spyOn(htmlvalidate, "validateFile").mockImplementation((filePath: string) =>
158+
Promise.resolve({
159+
valid: false,
160+
results: [
161+
{
162+
messages: [mockError("mock-rule", "lorem ipsum")],
163+
filePath,
164+
errorCount: 1,
165+
warningCount: 0,
166+
source: null,
167+
},
168+
],
169+
errorCount: 1,
170+
warningCount: 0,
171+
}),
172+
);
163173
const files = ["/dev/stdin"];
164174
const success = await lint(htmlvalidate, stdout, files, {
165175
...defaultOptions,
@@ -173,20 +183,22 @@ it("should retain /dev/stdin when stdinFilename is not given", async () => {
173183

174184
it("should rename stdin if stdinFilename is given", async () => {
175185
expect.assertions(2);
176-
jest.spyOn(htmlvalidate, "validateFileSync").mockImplementation((filePath: string) => ({
177-
valid: false,
178-
results: [
179-
{
180-
messages: [mockError("mock-rule", "lorem ipsum")],
181-
filePath,
182-
errorCount: 1,
183-
warningCount: 0,
184-
source: null,
185-
},
186-
],
187-
errorCount: 1,
188-
warningCount: 0,
189-
}));
186+
jest.spyOn(htmlvalidate, "validateFile").mockImplementation((filePath: string) =>
187+
Promise.resolve({
188+
valid: false,
189+
results: [
190+
{
191+
messages: [mockError("mock-rule", "lorem ipsum")],
192+
filePath,
193+
errorCount: 1,
194+
warningCount: 0,
195+
source: null,
196+
},
197+
],
198+
errorCount: 1,
199+
warningCount: 0,
200+
}),
201+
);
190202
const files = ["/dev/stdin"];
191203
const success = await lint(htmlvalidate, stdout, files, {
192204
...defaultOptions,

src/cli/actions/lint.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,23 @@ function renameStdin(report: Report, filename: string): void {
1515
}
1616
}
1717

18-
export function lint(
18+
export async function lint(
1919
htmlvalidate: HtmlValidate,
2020
output: WritableStreamLike,
2121
files: string[],
2222
options: LintOptions,
2323
): Promise<boolean> {
24-
const reports = files.map((filename: string) => {
24+
const reports = files.map(async (filename: string) => {
2525
try {
26-
return htmlvalidate.validateFileSync(filename);
26+
return await htmlvalidate.validateFile(filename);
2727
} catch (err) {
2828
const message = kleur.red(`Validator crashed when parsing "${filename}"`);
2929
output.write(`${message}\n`);
3030
throw err;
3131
}
3232
});
3333

34-
const merged = Reporter.merge(reports);
34+
const merged = await Reporter.merge(reports);
3535

3636
/* rename stdin if an explicit filename was passed */
3737
if (options.stdinFilename) {
@@ -42,8 +42,8 @@ export function lint(
4242

4343
if (options.maxWarnings >= 0 && merged.warningCount > options.maxWarnings) {
4444
output.write(`\nhtml-validate found too many warnings (maximum: ${options.maxWarnings}).\n`);
45-
return Promise.resolve(false);
45+
return false;
4646
}
4747

48-
return Promise.resolve(merged.valid);
48+
return merged.valid;
4949
}

src/config/resolver/nodejs/nodejs-resolver.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,15 @@ export function nodejsResolver(options: { rootDir?: string } = {}): NodeJSResolv
8383
const cwd = path.dirname(id);
8484
const expand = <T>(value: string | T): string | T => expandRelativePath(value, { cwd });
8585

86-
if (configData.elements) {
86+
if (Array.isArray(configData.elements)) {
8787
configData.elements = configData.elements.map(expand);
8888
}
8989

90-
if (configData.extends) {
90+
if (Array.isArray(configData.extends)) {
9191
configData.extends = configData.extends.map(expand);
9292
}
9393

94-
if (configData.plugins) {
94+
if (Array.isArray(configData.plugins)) {
9595
configData.plugins = configData.plugins.map(expand);
9696
}
9797

src/jest/utils/is-thenable.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1-
export function isThenable<T>(src: T | Promise<T>): src is Promise<T> {
2-
return src && typeof src === "object" && typeof (src as any).then === "function";
1+
/**
2+
* @internal
3+
*/
4+
export function isThenable<T>(value: T | Promise<T>): value is Promise<T> {
5+
return value && typeof value === "object" && "then" in value && typeof value.then === "function";
36
}

src/reporter.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,56 @@ describe("Reporter", () => {
4848
expect(merged.errorCount).toBe(4);
4949
expect(merged.warningCount).toBe(0);
5050
});
51+
52+
it("should handle promise with results", async () => {
53+
expect.assertions(3);
54+
const merged = await Reporter.merge(
55+
Promise.resolve([
56+
{
57+
valid: false,
58+
results: [createResult("foo", ["fred", "barney"])],
59+
errorCount: 1,
60+
warningCount: 0,
61+
},
62+
{
63+
valid: true,
64+
results: [createResult("bar", ["wilma"])],
65+
errorCount: 0,
66+
warningCount: 1,
67+
},
68+
]),
69+
);
70+
expect(merged.valid).toBeFalsy();
71+
expect(merged.results[0].filePath).toBe("foo");
72+
expect(merged.results[1].filePath).toBe("bar");
73+
});
74+
75+
it("should handle array of promises", async () => {
76+
expect.assertions(3);
77+
const merged = await Reporter.merge([
78+
Promise.resolve({
79+
valid: false,
80+
results: [createResult("foo", ["fred", "barney"])],
81+
errorCount: 1,
82+
warningCount: 0,
83+
}),
84+
Promise.resolve({
85+
valid: true,
86+
results: [createResult("bar", ["wilma"])],
87+
errorCount: 0,
88+
warningCount: 1,
89+
}),
90+
]);
91+
expect(merged.valid).toBeFalsy();
92+
expect(merged.results[0].filePath).toBe("foo");
93+
expect(merged.results[1].filePath).toBe("bar");
94+
});
95+
96+
it("should handle empty array", async () => {
97+
expect.assertions(1);
98+
const merged = Reporter.merge([]);
99+
expect(merged.results).toHaveLength(0);
100+
});
51101
});
52102

53103
describe("save()", () => {

src/reporter.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Severity } from "./config";
22
import { type Location, type Source } from "./context";
33
import { type Rule } from "./rule";
44
import { type DOMNode } from "./dom";
5+
import { isThenable } from "./utils";
56

67
/**
78
* Reported error message.
@@ -58,6 +59,13 @@ function freeze(src: DeferredMessage): Message {
5859
};
5960
}
6061

62+
function isThenableArray<T>(value: T[] | Array<Promise<T>>): value is Array<Promise<T>> {
63+
if (value.length === 0) {
64+
return false;
65+
}
66+
return isThenable(value[0]);
67+
}
68+
6169
/**
6270
* @public
6371
*/
@@ -100,8 +108,29 @@ export class Reporter {
100108

101109
/**
102110
* Merge two or more reports into a single one.
111+
*
112+
* @param reports- Reports to merge.
113+
* @returns A merged report.
114+
*/
115+
public static merge(reports: Report[]): Report;
116+
117+
/**
118+
* Merge two or more reports into a single one.
119+
*
120+
* @param reports- Reports to merge.
121+
* @returns A promise resolved with the merged report.
103122
*/
104-
public static merge(reports: Report[]): Report {
123+
public static merge(reports: Promise<Report[]> | Array<Promise<Report>>): Promise<Report>;
124+
125+
public static merge(
126+
reports: Report[] | Promise<Report[]> | Array<Promise<Report>>,
127+
): Report | Promise<Report> {
128+
if (isThenable(reports)) {
129+
return reports.then((reports) => this.merge(reports));
130+
}
131+
if (isThenableArray(reports)) {
132+
return Promise.all(reports).then((reports) => this.merge(reports));
133+
}
105134
const valid = reports.every((report) => report.valid);
106135
const merged: Record<string, Result> = {};
107136
reports.forEach((report: Report) => {

0 commit comments

Comments
 (0)