diff --git a/etc/browser.api.md b/etc/browser.api.md index 8c42d26803fe0086d04a84c3e9788793d1699215..e25c50de76ae2472a8ad2c7997152cacc4d6bbbc 100644 --- a/etc/browser.api.md +++ b/etc/browser.api.md @@ -122,7 +122,7 @@ export class Config { // @internal get(): ConfigData; // @internal - getMetaTable(): MetaTable; + getMetaTable(): MetaTable | Promise<MetaTable>; // @internal getPlugins(): Plugin_2[]; // @internal @@ -131,9 +131,9 @@ export class Config { getTransformers(): TransformerEntry[]; isRootFound(): boolean; merge(resolvers: Resolver[], rhs: Config): Config; - resolve(): ResolvedConfig; + resolve(): ResolvedConfig | Promise<ResolvedConfig>; // @internal - resolveData(): ResolvedConfigData; + resolveData(): ResolvedConfigData | Promise<ResolvedConfigData>; // @internal static validate(configData: ConfigData, filename?: string | null): void; } diff --git a/etc/index.api.md b/etc/index.api.md index 8b6779ed576d87f24ab238f3ae72f4dde423e940..b9e08b564c3e46eb1330e28b53d8ec9a1969d510 100644 --- a/etc/index.api.md +++ b/etc/index.api.md @@ -168,7 +168,7 @@ export class Config { // @internal get(): ConfigData; // @internal - getMetaTable(): MetaTable; + getMetaTable(): MetaTable | Promise<MetaTable>; // @internal getPlugins(): Plugin_2[]; // @internal @@ -177,9 +177,9 @@ export class Config { getTransformers(): TransformerEntry[]; isRootFound(): boolean; merge(resolvers: Resolver[], rhs: Config): Config; - resolve(): ResolvedConfig; + resolve(): ResolvedConfig | Promise<ResolvedConfig>; // @internal - resolveData(): ResolvedConfigData; + resolveData(): ResolvedConfigData | Promise<ResolvedConfigData>; // @internal static validate(configData: ConfigData, filename?: string | null): void; } diff --git a/src/config/config-loader.spec.ts b/src/config/config-loader.spec.ts index b3c29d990e0daba1a76d2e123f71e4da2285a0f5..97e9acd26c9439cde81d164b31ccaba7c693af5c 100644 --- a/src/config/config-loader.spec.ts +++ b/src/config/config-loader.spec.ts @@ -30,7 +30,7 @@ class SyncMockLoader extends ConfigLoader { public override getGlobalConfigSync(): Config { return super.getGlobalConfigSync(); } - public override getConfigFor(): ResolvedConfig { + public override getConfigFor(): ResolvedConfig | Promise<ResolvedConfig> { const config = this.getGlobalConfigSync(); return config.resolve(); } @@ -107,7 +107,7 @@ it("getGlobalConfigSync(..) should cache results", () => { it("getGlobalConfigSync(..) should throw an error if trying to use async results", () => { expect.assertions(2); class MockLoader extends ConfigLoader { - public override getConfigFor(): ResolvedConfig { + public override getConfigFor(): ResolvedConfig | Promise<ResolvedConfig> { const config = this.getGlobalConfigSync(); return config.resolve(); } diff --git a/src/config/config.spec.ts b/src/config/config.spec.ts index 388acc327cf2daf6039b51194bf62235d5e9f1cb..07fae54a0c74150257c3441762cd59acf25fe4b6 100644 --- a/src/config/config.spec.ts +++ b/src/config/config.spec.ts @@ -386,8 +386,9 @@ describe("config", () => { elements: ["order-c"], }); const elements = config.get().elements; + const metatable = await config.getMetaTable(); expect(elements).toEqual(["order-a", "order-b", "order-c"]); - expect(config.getMetaTable().getMetaFor("foo")).toEqual({ + expect(metatable.getMetaFor("foo")).toEqual({ tagName: "foo", aria: { implicitRole: expect.any(Function), @@ -398,7 +399,7 @@ describe("config", () => { implicitRole: expect.any(Function), permittedContent: ["baz"], }); - expect(config.getMetaTable().getMetaFor("bar")).toEqual({ + expect(metatable.getMetaFor("bar")).toEqual({ tagName: "bar", aria: { implicitRole: expect.any(Function), @@ -422,10 +423,10 @@ describe("config", () => { }); describe("getMetaTable()", () => { - it("should load metadata", () => { + it("should load metadata", async () => { expect.assertions(1); const config = Config.empty(); - const metatable = config.getMetaTable(); + const metatable = await config.getMetaTable(); expect(metatable.getMetaFor("div")).toBeDefined(); }); @@ -438,7 +439,7 @@ describe("config", () => { }, ], }); - const metatable = config.getMetaTable(); + const metatable = await config.getMetaTable(); expect(metatable.getMetaFor("div")).toBeNull(); expect(metatable.getMetaFor("foo")).not.toBeNull(); }); @@ -463,7 +464,7 @@ describe("config", () => { const config = await Config.fromObject(resolver, { elements: ["mock-elements"], }); - const metatable = config.getMetaTable(); + const metatable = await config.getMetaTable(); expect(metatable.getMetaFor("div")).toBeNull(); expect(metatable.getMetaFor("foo")).not.toBeNull(); }); diff --git a/src/config/config.ts b/src/config/config.ts index 61f3137f4c52fa5a784a336ea1ceddf0954a68ed..6ad50ced745658fa631d09d0de133e58a1e09611 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -310,14 +310,13 @@ export class Config { * * @internal */ - public getMetaTable(): MetaTable { + public getMetaTable(): MetaTable | Promise<MetaTable> { /* use cached table if it exists */ if (this.metaTable) { return this.metaTable; } const metaTable = new MetaTable(); - const source = this.config.elements ?? ["html5"]; /* extend validation schema from plugins */ for (const plugin of this.getPlugins()) { @@ -327,31 +326,35 @@ export class Config { } /* load from all entries */ - for (const entry of source) { - /* load meta directly from entry */ - if (typeof entry !== "string") { - metaTable.loadFromObject(entry as MetaDataTable); - continue; - } - - /* try searching builtin metadata */ - const bundled = bundledElements[entry] as MetaDataTable | undefined; - if (bundled) { - metaTable.loadFromObject(bundled); - continue; + const source = Array.from(this.config.elements ?? ["html5"]); + const loadEntry = (entry: string | Record<string, unknown>): void | Promise<void> => { + const result = this.getElementsFromEntry(entry); + if (isThenable(result)) { + return result.then((result) => { + const [obj, filename] = result; + metaTable.loadFromObject(obj, filename); + const next = source.shift(); + if (next) { + return loadEntry(next); + } + }); + } else { + const [obj, filename] = result; + metaTable.loadFromObject(obj, filename); + const next = source.shift(); + if (next) { + return loadEntry(next); + } } - - /* load with resolver */ - try { - const data = resolveElements(this.resolvers, entry, { cache: false }); - metaTable.loadFromObject(data, entry); - } catch (err: unknown) { - /* istanbul ignore next: only used as a fallback */ - const message = err instanceof Error ? err.message : String(err); - throw new ConfigError( - `Failed to load elements from "${entry}": ${message}`, - ensureError(err), - ); + }; + const next = source.shift(); + if (next) { + const result = loadEntry(next); + if (isThenable(result)) { + return result.then(() => { + metaTable.init(); + return (this.metaTable = metaTable); + }); } } @@ -359,6 +362,42 @@ export class Config { return (this.metaTable = metaTable); } + private getElementsFromEntry( + entry: string | Record<string, unknown>, + ): + | [obj: MetaDataTable, filename: string | null] + | Promise<[obj: MetaDataTable, filename: string | null]> { + /* load meta directly from entry */ + if (typeof entry !== "string") { + return [entry as MetaDataTable, null]; + } + + /* try searching builtin metadata */ + const bundled = bundledElements[entry] as MetaDataTable | undefined; + if (bundled) { + return [bundled, null]; + } + + /* load with resolver */ + try { + const obj = resolveElements(this.resolvers, entry, { cache: false }); + if (isThenable(obj)) { + return obj.then((obj) => { + return [obj, entry]; + }); + } else { + return [obj, entry]; + } + } catch (err: unknown) { + /* istanbul ignore next: only used as a fallback */ + const message = err instanceof Error ? err.message : String(err); + throw new ConfigError( + `Failed to load elements from "${entry}": ${message}`, + ensureError(err), + ); + } + } + /** * Get a copy of internal configuration data. * @@ -497,8 +536,15 @@ export class Config { * * @public */ - public resolve(): ResolvedConfig { - return new ResolvedConfig(this.resolveData(), this.get()); + public resolve(): ResolvedConfig | Promise<ResolvedConfig> { + const resolveData = this.resolveData(); + if (isThenable(resolveData)) { + return resolveData.then((resolveData) => { + return new ResolvedConfig(resolveData, this.get()); + }); + } else { + return new ResolvedConfig(resolveData, this.get()); + } } /** @@ -507,12 +553,24 @@ export class Config { * * @internal */ - public resolveData(): ResolvedConfigData { - return { - metaTable: this.getMetaTable(), - plugins: this.getPlugins(), - rules: this.getRules(), - transformers: this.transformers, - }; + public resolveData(): ResolvedConfigData | Promise<ResolvedConfigData> { + const metaTable = this.getMetaTable(); + if (isThenable(metaTable)) { + return metaTable.then((metaTable) => { + return { + metaTable, + plugins: this.getPlugins(), + rules: this.getRules(), + transformers: this.transformers, + }; + }); + } else { + return { + metaTable, + plugins: this.getPlugins(), + rules: this.getRules(), + transformers: this.transformers, + }; + } } } diff --git a/src/config/loaders/file-system.ts b/src/config/loaders/file-system.ts index b25f25c9eb7ad86015be7c1f7cb2a56b5c48dce1..d90661228ef8eb87812826b3dd38399a7adb6ae7 100644 --- a/src/config/loaders/file-system.ts +++ b/src/config/loaders/file-system.ts @@ -258,11 +258,11 @@ export class FileSystemConfigLoader extends ConfigLoader { return config; } - private _mergeSync( + private _merge( globalConfig: Config, override: Config, config: Config | null, - ): ResolvedConfig { + ): ResolvedConfig | Promise<ResolvedConfig> { const merged = config ? config.merge(this.resolvers, override) : globalConfig.merge(this.resolvers, override); @@ -302,10 +302,10 @@ export class FileSystemConfigLoader extends ConfigLoader { const config = this.fromFilename(filename); if (isThenable(config)) { return config.then((config) => { - return this._mergeSync(globalConfig, override, config); + return this._merge(globalConfig, override, config); }); } else { - return this._mergeSync(globalConfig, override, config); + return this._merge(globalConfig, override, config); } } @@ -324,7 +324,7 @@ export class FileSystemConfigLoader extends ConfigLoader { } const config = await this.fromFilenameAsync(filename); - return this._mergeSync(globalConfig, override, config); + return this._merge(globalConfig, override, config); } /** diff --git a/src/dom/domnode.spec.ts b/src/dom/domnode.spec.ts index 9791b18ce368eebebe2b3aedc87b907774f6b299..7d2e3577df1be7b0c5949e1aab2dc8f2bae00bc3 100644 --- a/src/dom/domnode.spec.ts +++ b/src/dom/domnode.spec.ts @@ -272,10 +272,11 @@ describe("DOMNode", () => { expect(root.textContent).toBe("foo bar baz"); }); - it("smoketest", () => { + it("smoketest", async () => { expect.assertions(1); const markup = `lorem <i>ipsum</i> <b>dolor <u>sit amet</u></b>`; - const parser = new Parser(Config.empty().resolve()); + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); const doc = parser.parseHtml(markup); expect(doc.textContent).toBe("lorem ipsum dolor sit amet"); }); diff --git a/src/dom/htmlelement.spec.ts b/src/dom/htmlelement.spec.ts index e6b20b1f2b9644ebf59bebc84a2581647e265a35..6277daf78dba3bef7547be481ed7f94e93f97d16 100644 --- a/src/dom/htmlelement.spec.ts +++ b/src/dom/htmlelement.spec.ts @@ -24,8 +24,13 @@ function createLocation({ column, size }: LocationSpec): Location { describe("HtmlElement", () => { let document: HtmlElement; + let parser: Parser; const location = createLocation({ column: 1, size: 4 }); - const parser = new Parser(Config.empty().resolve()); + + beforeAll(async () => { + const resolvedConfig = await Config.empty().resolve(); + parser = new Parser(resolvedConfig); + }); beforeEach(() => { const markup = /* HTML */ ` @@ -498,8 +503,9 @@ describe("HtmlElement", () => { describe("closest()", () => { let node: HtmlElement; - beforeAll(() => { - const parser = new Parser(Config.empty().resolve()); + beforeAll(async () => { + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); document = parser.parseHtml(` <div id="1" class="x"> <div id="2" class="x"> @@ -528,8 +534,9 @@ describe("HtmlElement", () => { describe("generateSelector()", () => { let parser: Parser; - beforeAll(() => { - parser = new Parser(Config.empty().resolve()); + beforeAll(async () => { + const resolvedConfig = await Config.empty().resolve(); + parser = new Parser(resolvedConfig); }); it("should generate a unique selector", () => { @@ -961,7 +968,7 @@ describe("HtmlElement", () => { expect(el.getAttributeValue("class")).toBe("baz"); }); - it("should find element with :scope", () => { + it("should find element with :scope", async () => { expect.assertions(1); const markup = ` <h1 id="first"></h1> @@ -971,7 +978,8 @@ describe("HtmlElement", () => { <div><h1 id="forth"></h1></div> </section> <h1 id="fifth"></h1>`; - const parser = new Parser(Config.empty().resolve()); + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); const document = parser.parseHtml(markup); const section = document.querySelector("section")!; const el = section.querySelectorAll(":scope > h1"); diff --git a/src/dom/selector.spec.ts b/src/dom/selector.spec.ts index 3824172565be0c658889f8a7f25f46d37eb69cd0..8568808b84cd80a82738bf9d7cba5307add4169a 100644 --- a/src/dom/selector.spec.ts +++ b/src/dom/selector.spec.ts @@ -240,8 +240,9 @@ describe("splitPattern()", () => { describe("Selector", () => { let doc: HtmlElement; - beforeEach(() => { - const parser = new Parser(Config.empty().resolve()); + beforeEach(async () => { + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); doc = parser.parseHtml(` <foo id="barney" test-id="foo-1">first foo</foo> <foo CLASS="fred" test-id="foo-2">second foo</foo> @@ -324,49 +325,55 @@ describe("Selector", () => { ]); }); - it("should match id with escaped colon", () => { + it("should match id with escaped colon", async () => { expect.assertions(1); - const parser = new Parser(Config.empty().resolve()); + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); const document = parser.parseHtml(`<div id="foo:"></div>`); const selector = new Selector("#foo\\:"); expect(fetch(selector.match(document))).toEqual([expect.objectContaining({ tagName: "div" })]); }); - it("should match id with escaped space", () => { + it("should match id with escaped space", async () => { expect.assertions(1); - const parser = new Parser(Config.empty().resolve()); + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); const document = parser.parseHtml(`<div id="foo "></div>`); const selector = new Selector("#foo\\ "); expect(fetch(selector.match(document))).toEqual([expect.objectContaining({ tagName: "div" })]); }); - it("should match id with escaped tab", () => { + it("should match id with escaped tab", async () => { expect.assertions(1); - const parser = new Parser(Config.empty().resolve()); + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); const document = parser.parseHtml(`<div id="foo\t"></div>`); const selector = new Selector("#foo\\9 "); expect(fetch(selector.match(document))).toEqual([expect.objectContaining({ tagName: "div" })]); }); - it("should match id with escaped newline (\\n)", () => { + it("should match id with escaped newline (\\n)", async () => { expect.assertions(1); - const parser = new Parser(Config.empty().resolve()); + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); const document = parser.parseHtml(`<div id="foo\n"></div>`); const selector = new Selector("#foo\\a "); expect(fetch(selector.match(document))).toEqual([expect.objectContaining({ tagName: "div" })]); }); - it("should match id with escaped newline (\\r)", () => { + it("should match id with escaped newline (\\r)", async () => { expect.assertions(1); - const parser = new Parser(Config.empty().resolve()); + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); const document = parser.parseHtml(`<div id="foo\r"></div>`); const selector = new Selector("#foo\\d "); expect(fetch(selector.match(document))).toEqual([expect.objectContaining({ tagName: "div" })]); }); - it("should match id with escaped bracket", () => { + it("should match id with escaped bracket", async () => { expect.assertions(1); - const parser = new Parser(Config.empty().resolve()); + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); const document = parser.parseHtml(`<div id="foo[bar]"></div>`); const selector = new Selector("#foo\\[bar\\]"); expect(fetch(selector.match(document))).toEqual([expect.objectContaining({ tagName: "div" })]); @@ -405,9 +412,10 @@ describe("Selector", () => { ]); }); - it('should match nested : in string ([id=":r1:"])', () => { + it('should match nested : in string ([id=":r1:"])', async () => { expect.assertions(1); - const parser = new Parser(Config.empty().resolve()); + const resolvedConfig = await Config.empty().resolve(); + const parser = new Parser(resolvedConfig); doc = parser.parseHtml(/* HTML */ ` <label id="#r1:"> lorem ipsum </label> `); const element = doc.querySelector("label")!; const id = element.id; diff --git a/src/engine/engine.spec.ts b/src/engine/engine.spec.ts index f9b2da136d3e4ad4031c6a4717630027c7f7e603..e8ad89c1ef36384dc26ee624ec289fdf0e8c06f1 100644 --- a/src/engine/engine.spec.ts +++ b/src/engine/engine.spec.ts @@ -81,6 +81,7 @@ class ExposedEngine<T extends Parser> extends Engine<T> { describe("Engine", () => { let config: Config; + let resolvedConfig: ResolvedConfig; let engine: ExposedEngine<Parser>; beforeEach(async () => { @@ -91,7 +92,8 @@ describe("Engine", () => { "no-unused-disable": "off", }, }); - engine = new ExposedEngine(config.resolve(), MockParser); + resolvedConfig = await config.resolve(); + engine = new ExposedEngine(resolvedConfig, MockParser); }); describe("lint()", () => { @@ -173,10 +175,10 @@ describe("Engine", () => { expect(report).toHaveError("close-order", expect.any(String)); }); - it("should generate config:ready event", () => { + it("should generate config:ready event", async () => { expect.assertions(5); const source: Source[] = [inline("<div></div>")]; - const resolved = config.resolve(); + const resolved = await config.resolve(); const parser = new Parser(resolved); const spy = jest.fn(); parser.on("config:ready", spy); @@ -198,10 +200,11 @@ describe("Engine", () => { expect(event.rules).toBeDefined(); }); - it("should generate source:ready event", () => { + it("should generate source:ready event", async () => { expect.assertions(3); const source: Source[] = [inline("<div></div>"), inline("<p></i>")]; - const parser = new Parser(config.resolve()); + const resolvedConfig = await config.resolve(); + const parser = new Parser(resolvedConfig); const spy = jest.fn(); parser.on("source:ready", spy); jest.spyOn(engine, "instantiateParser").mockReturnValue(parser); @@ -488,7 +491,7 @@ describe("Engine", () => { }); describe("plugins", () => { - it("should call init callback if present", () => { + it("should call init callback if present", async () => { expect.assertions(1); const plugin: Plugin = { @@ -499,12 +502,13 @@ describe("Engine", () => { (config as any).plugins = [plugin]; const source = inline(""); - const engine = new ExposedEngine(config.resolve(), MockParser); + const resolvedConfig = await config.resolve(); + const engine = new ExposedEngine(resolvedConfig, MockParser); engine.lint([source]); expect(plugin.init).toHaveBeenCalledWith(); }); - it("should call setup callback if present", () => { + it("should call setup callback if present", async () => { expect.assertions(1); const plugin: Plugin = { @@ -515,7 +519,8 @@ describe("Engine", () => { (config as any).plugins = [plugin]; const source = inline(""); - const engine = new ExposedEngine(config.resolve(), MockParser); + const resolvedConfig = await config.resolve(); + const engine = new ExposedEngine(resolvedConfig, MockParser); engine.lint([source]); expect(plugin.setup).toHaveBeenCalledWith(source, expect.any(EventHandler)); }); @@ -527,8 +532,8 @@ describe("Engine", () => { let reporter: Reporter; let mockRule: any; - beforeEach(() => { - parser = new MockParser(config.resolve()); + beforeEach(async () => { + parser = new MockParser(await config.resolve()); reporter = new Reporter(); mockRule = { init: jest.fn(), @@ -536,17 +541,11 @@ describe("Engine", () => { }; }); - it("should load and initialize rule", () => { + it("should load and initialize rule", async () => { expect.assertions(4); + const resolvedConfig = await config.resolve(); jest.spyOn(engine, "instantiateRule").mockReturnValueOnce(mockRule); - const rule = engine.loadRule( - "void", - config.resolve(), - Severity.ERROR, - {}, - parser, - reporter, - ); + const rule = engine.loadRule("void", resolvedConfig, Severity.ERROR, {}, parser, reporter); expect(rule).toBe(mockRule); expect(rule.init).toHaveBeenCalledWith( parser, @@ -558,9 +557,10 @@ describe("Engine", () => { expect(rule.name).toBe("void"); }); - it("should add error if rule cannot be found", () => { + it("should add error if rule cannot be found", async () => { expect.assertions(1); - engine.loadRule("foobar", config.resolve(), Severity.ERROR, {}, parser, reporter); + const resolvedConfig = await config.resolve(); + engine.loadRule("foobar", resolvedConfig, Severity.ERROR, {}, parser, reporter); const add = jest.spyOn(reporter, "add"); const location = { filename: "inline", @@ -584,7 +584,7 @@ describe("Engine", () => { ); }); - it("should load from plugins", () => { + it("should load from plugins", async () => { expect.assertions(2); class MyRule extends Rule { public setup(): void { @@ -601,10 +601,11 @@ describe("Engine", () => { }, ]; - const engine = new ExposedEngine<Parser>(config.resolve(), MockParser); + const resolvedConfig = await config.resolve(); + const engine = new ExposedEngine<Parser>(resolvedConfig, MockParser); const rule = engine.loadRule( "custom/my-rule", - config.resolve(), + resolvedConfig, Severity.ERROR, {}, parser, @@ -614,7 +615,7 @@ describe("Engine", () => { expect(rule.name).toBe("custom/my-rule"); }); - it("should handle plugin setting rule to null", () => { + it("should handle plugin setting rule to null", async () => { expect.assertions(1); /* mock loading of plugins */ @@ -626,13 +627,14 @@ describe("Engine", () => { }, ]; - const engine = new ExposedEngine<Parser>(config.resolve(), MockParser); + const resolvedConfig = await config.resolve(); + const engine = new ExposedEngine<Parser>(resolvedConfig, MockParser); const missingRule = jest.spyOn(engine, "missingRule"); - engine.loadRule("custom/my-rule", config.resolve(), Severity.ERROR, {}, parser, reporter); + engine.loadRule("custom/my-rule", resolvedConfig, Severity.ERROR, {}, parser, reporter); expect(missingRule).toHaveBeenCalledWith("custom/my-rule"); }); - it("should handle missing setup callback", () => { + it("should handle missing setup callback", async () => { expect.assertions(1); // @ts-expect-error: abstract method not implemented, but plugin might be vanilla js so want to handle the case class MyRule extends Rule {} @@ -646,10 +648,11 @@ describe("Engine", () => { }, ]; - const engine = new ExposedEngine<Parser>(config.resolve(), MockParser); + const resolvedConfig = await config.resolve(); + const engine = new ExposedEngine<Parser>(resolvedConfig, MockParser); const rule = engine.loadRule( "custom/my-rule", - config.resolve(), + resolvedConfig, Severity.ERROR, {}, parser, @@ -658,11 +661,12 @@ describe("Engine", () => { expect(rule).toBeInstanceOf(MyRule); }); - it("should handle plugin without rules", () => { + it("should handle plugin without rules", async () => { expect.assertions(1); /* mock loading of plugins */ (config as any).plugins = [{}]; - expect(() => new ExposedEngine(config.resolve(), MockParser)).not.toThrow(); + const resolvedConfig = await config.resolve(); + expect(() => new ExposedEngine(resolvedConfig, MockParser)).not.toThrow(); }); }); }); diff --git a/src/htmlvalidate.spec.ts b/src/htmlvalidate.spec.ts index 9583d4c553eb14852c2c68e710ec69bc39a947d0..fcbe43c64f38bd6e15030f119c51b342efe4afa6 100644 --- a/src/htmlvalidate.spec.ts +++ b/src/htmlvalidate.spec.ts @@ -6,6 +6,7 @@ import { UserError } from "./error"; import { HtmlValidate } from "./htmlvalidate"; import { type Message } from "./message"; import { Parser } from "./parser"; +import { isThenable } from "./utils"; const engine = { lint: jest.fn(), @@ -27,8 +28,8 @@ jest.mock("./parser"); function mockConfig(): Promise<ResolvedConfig> { const config = Config.empty(); const original = config.resolve; - jest.spyOn(config, "resolve").mockImplementation(() => { - const resolved = original.call(config); + jest.spyOn(config, "resolve").mockImplementation(async () => { + const resolved = await original.call(config); resolved.transformFilename = jest.fn( (_resolvers, filename): Promise<Source[]> => Promise.resolve([ @@ -48,7 +49,7 @@ function mockConfig(): Promise<ResolvedConfig> { function mockConfigSync(): ResolvedConfig { const config = Config.empty(); - const original = config.resolve; + const original = config.resolve as () => ResolvedConfig; jest.spyOn(config, "resolve").mockImplementation(() => { const resolved = original.call(config); resolved.transformFilename = jest.fn( @@ -74,7 +75,11 @@ function mockConfigSync(): ResolvedConfig { ]); return resolved; }); - return config.resolve(); + const resolvedConfig = config.resolve(); + if (isThenable(resolvedConfig)) { + throw new Error("Config is thenable when it shouldn't be"); + } + return resolvedConfig; } beforeEach(() => { @@ -909,14 +914,14 @@ describe("HtmlValidate", () => { ]); }); - it("dumpSources() should dump sources", () => { + it("dumpSources() should dump sources", async () => { expect.assertions(1); const htmlvalidate = new HtmlValidate(); const filename = "foo.html"; const config = Config.empty(); const original = config.resolve; - config.resolve = () => { - const resolved = original.call(config); + config.resolve = async () => { + const resolved = await original.call(config); resolved.transformFilenameSync = jest.fn((_resolvers, filename): Source[] => [ { data: `first markup`, @@ -948,7 +953,7 @@ describe("HtmlValidate", () => { ]); return resolved; }; - jest.spyOn(htmlvalidate, "getConfigForSync").mockImplementation(() => config.resolve()); + jest.spyOn(htmlvalidate, "getConfigForSync").mockReturnValue(await config.resolve()); const output = htmlvalidate.dumpSource(filename); expect(output).toMatchInlineSnapshot(` [ @@ -1110,7 +1115,7 @@ describe("HtmlValidate", () => { it("should use given configuration", () => { expect.assertions(1); const htmlvalidate = new HtmlValidate(); - const config = Config.empty().resolve(); + const config = mockConfigSync(); const getConfigFor = jest.spyOn(htmlvalidate, "getConfigForSync"); htmlvalidate.getContextualDocumentationSync({ ruleId: "foo" }, config); expect(getConfigFor).not.toHaveBeenCalled(); @@ -1120,7 +1125,7 @@ describe("HtmlValidate", () => { it("getRuleDocumentation() should delegate call to engine", async () => { expect.assertions(2); const htmlvalidate = new HtmlValidate(); - const config = Config.empty().resolve(); + const config = await mockConfig(); await htmlvalidate.getRuleDocumentation("foo"); await htmlvalidate.getRuleDocumentation("foo", config, { bar: "baz" }); expect(engine.getRuleDocumentation).toHaveBeenCalledWith({ ruleId: "foo", context: null }); @@ -1135,7 +1140,7 @@ describe("HtmlValidate", () => { it("getRuleDocumentationSync() should delegate call to engine", () => { expect.assertions(2); const htmlvalidate = new HtmlValidate(); - const config = Config.empty().resolve(); + const config = mockConfigSync(); htmlvalidate.getRuleDocumentationSync("foo"); htmlvalidate.getRuleDocumentationSync("foo", config, { bar: "baz" }); expect(engine.getRuleDocumentation).toHaveBeenCalledWith({ ruleId: "foo", context: null }); diff --git a/src/meta/helper.spec.ts b/src/meta/helper.spec.ts index 74876941d7906d331ab23927e66275842007acef..e6089732051a9629f39e9a27e6bd637688904e44 100644 --- a/src/meta/helper.spec.ts +++ b/src/meta/helper.spec.ts @@ -10,8 +10,12 @@ import { allowedIfParentIsPresent, } from "./helper"; -const config = Config.empty(); -const parser = new Parser(config.resolve()); +let parser: Parser; + +beforeAll(async () => { + const config = await Config.empty().resolve(); + parser = new Parser(config); +}); function parse(markup: string, selector: string = "div"): HtmlElement { const source: Source = { diff --git a/src/meta/validator.spec.ts b/src/meta/validator.spec.ts index 594c38416557e0646cdb869db9cbe59862d1a6a2..85cca4b54586e5775239f09096e73b0a42b854ec 100644 --- a/src/meta/validator.spec.ts +++ b/src/meta/validator.spec.ts @@ -485,8 +485,8 @@ describe("Meta validator", () => { describe("validateAncestors()", () => { let root: HtmlElement; - beforeAll(() => { - const parser = new Parser(Config.empty().resolve()); + beforeAll(async () => { + const parser = new Parser(await Config.empty().resolve()); root = parser.parseHtml(` <dl id="variant-1"> <dt></dt> @@ -533,8 +533,8 @@ describe("Meta validator", () => { describe("validateRequiredContent()", () => { let parser: Parser; - beforeAll(() => { - parser = new Parser(Config.empty().resolve()); + beforeAll(async () => { + parser = new Parser(await Config.empty().resolve()); }); it("should match if no rule is present", () => { diff --git a/src/parser/parser.spec.ts b/src/parser/parser.spec.ts index aba398f46e375110479eeaa81e103a0a167bdb64..e564abedefc595021634592e2dacee0534a4a5f5 100644 --- a/src/parser/parser.spec.ts +++ b/src/parser/parser.spec.ts @@ -115,9 +115,9 @@ describe("parser", () => { let events: any[]; let parser: ExposedParser; - beforeEach(() => { + beforeEach(async () => { events = []; - parser = new ExposedParser(Config.empty().resolve()); + parser = new ExposedParser(await Config.empty().resolve()); parser.on("*", (event: string, data: any) => { if (ignoredEvents.includes(event)) return; events.push(mergeEvent(event, data)); diff --git a/src/plugin/plugin.spec.ts b/src/plugin/plugin.spec.ts index 65136852dbe7b27b8bb5cdccc07d77e2c0341de7..bc345facdd3bb0042118b373c873cd55ac9c44ff 100644 --- a/src/plugin/plugin.spec.ts +++ b/src/plugin/plugin.spec.ts @@ -169,14 +169,14 @@ describe("Plugin", () => { }); }); - describe("extedMeta", () => { + describe("extendMeta", () => { it("should not throw error when schema isn't extended", async () => { expect.assertions(1); config = await Config.fromObject(resolvers, { plugins: ["mock-plugin"], }); - expect(() => { - const metaTable = config.getMetaTable(); + expect(async () => { + const metaTable = await config.getMetaTable(); return metaTable.getMetaFor("my-element"); }).not.toThrow(); }); @@ -217,7 +217,7 @@ describe("Plugin", () => { }, ], }); - const metaTable = config.getMetaTable(); + const metaTable = await config.getMetaTable(); const meta = metaTable.getMetaFor("my-element"); expect(meta).toEqual({ tagName: "my-element", @@ -256,7 +256,7 @@ describe("Plugin", () => { }, ], }); - const metaTable = config.getMetaTable(); + const metaTable = await config.getMetaTable(); const meta = metaTable.getMetaFor("my-element"); expect(meta).toEqual({ tagName: "my-element", @@ -313,7 +313,7 @@ describe("Plugin", () => { }, ], }); - const metaTable = config.getMetaTable(); + const metaTable = await config.getMetaTable(); const a = metaTable.getMetaFor("my-element"); const b = metaTable.getMetaFor("my-element:real"); const node = HtmlElement.createElement("my-element", location, { meta: a }); @@ -341,34 +341,35 @@ describe("Plugin", () => { }); }); - it("Engine should handle missing plugin callbacks", () => { + it("Engine should handle missing plugin callbacks", async () => { expect.assertions(1); - expect(() => new Engine(config.resolve(), Parser)).not.toThrow(); + const resolvedConfig = await config.resolve(); + expect(() => new Engine(resolvedConfig, Parser)).not.toThrow(); }); - it("Engine should call plugin init callback", () => { + it("Engine should call plugin init callback", async () => { expect.assertions(1); mockPlugin.init = jest.fn(); - const engine = new Engine(config.resolve(), Parser); + const engine = new Engine(await config.resolve(), Parser); engine.lint([source]); expect(mockPlugin.init).toHaveBeenCalledWith(); }); - it("Engine should call plugin setup callback", () => { + it("Engine should call plugin setup callback", async () => { expect.assertions(1); mockPlugin.setup = jest.fn(); - const engine = new Engine(config.resolve(), Parser); + const engine = new Engine(await config.resolve(), Parser); engine.lint([source]); expect(mockPlugin.setup).toHaveBeenCalledWith(source, expect.any(EventHandler)); }); - it("Parser events should trigger plugin eventhandler", () => { + it("Parser events should trigger plugin eventhandler", async () => { expect.assertions(1); const handler = jest.fn(); mockPlugin.setup = (source: Source, eventhandler: EventHandler) => { eventhandler.on("dom:ready", handler); }; - const engine = new Engine(config.resolve(), Parser); + const engine = new Engine(await config.resolve(), Parser); engine.lint([source]); expect(handler).toHaveBeenCalledWith("dom:ready", expect.anything()); }); @@ -385,7 +386,7 @@ describe("Plugin", () => { }); }); - it("Engine should call rule init callback", () => { + it("Engine should call rule init callback", async () => { expect.assertions(1); const mockRule: Rule = new (class extends Rule { public setup(): void { @@ -396,7 +397,7 @@ describe("Plugin", () => { "mock-rule": null /* instantiateRule is mocked, this can be anything */, }; const setup = jest.spyOn(mockRule, "setup"); - const engine = new Engine(config.resolve(), Parser); + const engine = new Engine(await config.resolve(), Parser); jest.spyOn(engine as any, "instantiateRule").mockImplementation(() => mockRule); engine.lint([source]); expect(setup).toHaveBeenCalledWith(); @@ -426,7 +427,7 @@ describe("Plugin", () => { ".*": "mock-plugin", }, }); - const resolvedConfig = config.resolve(); + const resolvedConfig = await config.resolve(); const sources = await resolvedConfig.transformSource(resolvers, { data: "original data", filename: "/path/to/mock.filename", @@ -475,7 +476,7 @@ describe("Plugin", () => { ".*": "mock-plugin:foobar", }, }); - const resolvedConfig = config.resolve(); + const resolvedConfig = await config.resolve(); const sources = await resolvedConfig.transformSource(resolvers, { data: "original data", filename: "/path/to/mock.filename", diff --git a/src/rule.spec.ts b/src/rule.spec.ts index d4eb61f70912e78d5568b90480aada7a5744d012..053205df2fafb6bd0d02ce0c0fd2e040d6b470d6 100644 --- a/src/rule.spec.ts +++ b/src/rule.spec.ts @@ -37,8 +37,9 @@ describe("rule base class", () => { let mockLocation: Location; let mockEvent: Event; - beforeEach(() => { - parser = new Parser(Config.empty().resolve()); + beforeEach(async () => { + const config = await Config.empty().resolve(); + parser = new Parser(config); parserOn = jest.spyOn(parser, "on"); reporter = new Reporter(); reporter.add = jest.fn(); diff --git a/src/rules/helper/a11y.spec.ts b/src/rules/helper/a11y.spec.ts index 26597dabc80c700740aead8b9c2510b4c0cce2b0..d58ad660c90d262ef933eec829e64498900bcae6 100644 --- a/src/rules/helper/a11y.spec.ts +++ b/src/rules/helper/a11y.spec.ts @@ -20,13 +20,14 @@ const location: Location = { size: 1, }; -describe("a11y helpers", () => { - let parser: Parser; +let parser: Parser; - beforeEach(() => { - parser = new Parser(Config.defaultConfig().resolve()); - }); +beforeAll(async () => { + const config = await Config.defaultConfig().resolve(); + parser = new Parser(config); +}); +describe("a11y helpers", () => { function parse(data: string): HtmlElement { return parser.parseHtml({ data, diff --git a/src/rules/helper/has-accessible-name.spec.ts b/src/rules/helper/has-accessible-name.spec.ts index 7f90b3134d737de3ac61437d5211b9a6d48b82e0..3512f239ba0da358c6f3fe914bea954080c72537 100644 --- a/src/rules/helper/has-accessible-name.spec.ts +++ b/src/rules/helper/has-accessible-name.spec.ts @@ -5,8 +5,12 @@ import { Parser } from "../../parser"; import { processAttribute } from "../../transform/mocks/attribute"; import { hasAccessibleName } from "./has-accessible-name"; -const config = Config.empty(); -const parser = new Parser(config.resolve()); +let parser: Parser; + +beforeAll(async () => { + const config = await Config.empty().resolve(); + parser = new Parser(config); +}); function processElement(node: HtmlElement): void { if (node.hasAttribute("bind-text")) { diff --git a/src/rules/helper/is-focusable.spec.ts b/src/rules/helper/is-focusable.spec.ts index 3eb19a17b1ac30da134de0daa2f0bf8d19841c10..a9077d7186825a6e805620030aca1e1f1ceb4d8b 100644 --- a/src/rules/helper/is-focusable.spec.ts +++ b/src/rules/helper/is-focusable.spec.ts @@ -4,7 +4,12 @@ import { Parser } from "../../parser"; import { processAttribute } from "../../transform/mocks/attribute"; import { isFocusable } from "./is-focusable"; -const parser = new Parser(Config.defaultConfig().resolve()); +let parser: Parser; + +beforeAll(async () => { + const config = await Config.defaultConfig().resolve(); + parser = new Parser(config); +}); function parse(data: string): HtmlElement { return parser.parseHtml({ diff --git a/src/rules/helper/text.spec.ts b/src/rules/helper/text.spec.ts index d581543bf703132311cd0eff2804f02f740ae172..3d6148cd4f7934dd67afc1fe292762418c0642fc 100644 --- a/src/rules/helper/text.spec.ts +++ b/src/rules/helper/text.spec.ts @@ -12,7 +12,12 @@ const location: Location = { size: 1, }; -const parser = new Parser(Config.empty().resolve()); +let parser: Parser; + +beforeAll(async () => { + const config = await Config.empty().resolve(); + parser = new Parser(config); +}); describe("classifyNodeText()", () => { it("should classify element with text as STATIC_TEXT", () => { diff --git a/src/utils/dump-tree.spec.ts b/src/utils/dump-tree.spec.ts index 433cf5c30b175d5f4457ea71a43a694df33a59b0..7b5f0ac5f0b3023974a67245377398e588e6299c 100644 --- a/src/utils/dump-tree.spec.ts +++ b/src/utils/dump-tree.spec.ts @@ -2,7 +2,7 @@ import { Config } from "../config"; import { Parser } from "../parser"; import { dumpTree } from "./dump-tree"; -const parser = new Parser(Config.empty().resolve()); +let parser: Parser; expect.addSnapshotSerializer({ serialize(value) { @@ -13,6 +13,11 @@ expect.addSnapshotSerializer({ }, }); +beforeAll(async () => { + const config = await Config.empty().resolve(); + parser = new Parser(config); +}); + describe("dumpTree()", () => { it("should dump DOM tree", () => { expect.assertions(1);