From a2b0bf3fd8332c36575973c77e75d491e34ad30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Tue, 24 Aug 2021 01:40:27 +0100 Subject: [PATCH] chore(tests): Use proxies to collect events (#920) Removes the need for a lot of repetitive code. Also refactors some parser tests to use `jest.fn()`, and simplifies tokenizer tests. __BREAKING:__ Removes the `CollectingHandler` and `MultiplexHandler` classes. --- src/CollectingHandler.ts | 28 ---- src/MultiplexHandler.ts | 63 -------- src/Parser.spec.ts | 50 +++--- src/Tokenizer.spec.ts | 195 ++++------------------- src/__fixtures__/test-helper.ts | 67 ++++---- src/__snapshots__/Tokenizer.spec.ts.snap | 169 ++++++++++++++++++++ 6 files changed, 257 insertions(+), 315 deletions(-) delete mode 100644 src/CollectingHandler.ts delete mode 100644 src/MultiplexHandler.ts create mode 100644 src/__snapshots__/Tokenizer.spec.ts.snap diff --git a/src/CollectingHandler.ts b/src/CollectingHandler.ts deleted file mode 100644 index 007f27008..000000000 --- a/src/CollectingHandler.ts +++ /dev/null @@ -1,28 +0,0 @@ -import MultiplexHandler from "./MultiplexHandler"; -import { Handler } from "./Parser"; - -type OptionalFunction = undefined | ((...args: unknown[]) => void); - -export class CollectingHandler extends MultiplexHandler { - public events: [keyof Handler, ...unknown[]][] = []; - - constructor(private readonly cbs: Partial = {}) { - super((name, ...args) => { - this.events.push([name, ...args]); - (this.cbs[name] as OptionalFunction)?.(...args); - }); - } - - onreset(): void { - this.events = []; - this.cbs.onreset?.(); - } - - restart(): void { - this.cbs.onreset?.(); - - for (const [name, ...args] of this.events) { - (this.cbs[name] as OptionalFunction)?.(...args); - } - } -} diff --git a/src/MultiplexHandler.ts b/src/MultiplexHandler.ts deleted file mode 100644 index 7665af737..000000000 --- a/src/MultiplexHandler.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Parser, Handler } from "./Parser"; - -/** - * Calls a specific handler function for all events that are encountered. - */ -export default class MultiplexHandler implements Handler { - /** - * @param func The function to multiplex all events to. - */ - constructor( - private readonly func: ( - event: keyof Handler, - ...args: unknown[] - ) => void - ) {} - - onattribute( - name: string, - value: string, - quote: string | null | undefined - ): void { - this.func("onattribute", name, value, quote); - } - oncdatastart(): void { - this.func("oncdatastart"); - } - oncdataend(): void { - this.func("oncdataend"); - } - ontext(text: string): void { - this.func("ontext", text); - } - onprocessinginstruction(name: string, value: string): void { - this.func("onprocessinginstruction", name, value); - } - oncomment(comment: string): void { - this.func("oncomment", comment); - } - oncommentend(): void { - this.func("oncommentend"); - } - onclosetag(name: string): void { - this.func("onclosetag", name); - } - onopentag(name: string, attribs: { [key: string]: string }): void { - this.func("onopentag", name, attribs); - } - onopentagname(name: string): void { - this.func("onopentagname", name); - } - onerror(error: Error): void { - this.func("onerror", error); - } - onend(): void { - this.func("onend"); - } - onparserinit(parser: Parser): void { - this.func("onparserinit", parser); - } - onreset(): void { - this.func("onreset"); - } -} diff --git a/src/Parser.spec.ts b/src/Parser.spec.ts index 17727b2c6..639009f86 100644 --- a/src/Parser.spec.ts +++ b/src/Parser.spec.ts @@ -1,8 +1,9 @@ import { Parser, Tokenizer } from "."; +import type { Handler } from "./Parser"; describe("API", () => { test("should work without callbacks", () => { - const cbs: Record void> = {}; + const cbs: Partial = { onerror: jest.fn() }; const p = new Parser(cbs, { xmlMode: true, lowerCaseAttributeNames: true, @@ -13,71 +14,64 @@ describe("API", () => { // Check for an error p.end(); - let err = false; - cbs.onerror = () => (err = true); p.write("foo"); - expect(err).toBeTruthy(); - err = false; + expect(cbs.onerror).toHaveBeenLastCalledWith( + new Error(".write() after done!") + ); p.end(); - expect(err).toBeTruthy(); + expect(cbs.onerror).toHaveBeenLastCalledWith( + new Error(".end() after done!") + ); p.reset(); // Remove method - cbs.onopentag = () => { - /* Ignore */ - }; + cbs.onopentag = jest.fn(); p.write(""); // Pause/resume - let processed = false; - cbs.ontext = (t) => { - expect(t).toBe("foo"); - processed = true; - }; + const onText = jest.fn(); + cbs.ontext = onText; p.pause(); p.write("foo"); - expect(processed).toBeFalsy(); + expect(onText).not.toHaveBeenCalled(); p.resume(); - expect(processed).toBeTruthy(); - processed = false; + expect(onText).toHaveBeenLastCalledWith("foo"); p.pause(); - expect(processed).toBeFalsy(); + expect(onText).toHaveBeenCalledTimes(1); p.resume(); - expect(processed).toBeFalsy(); + expect(onText).toHaveBeenCalledTimes(1); p.pause(); p.end("foo"); - expect(processed).toBeFalsy(); + expect(onText).toHaveBeenCalledTimes(1); p.resume(); - expect(processed).toBeTruthy(); + expect(onText).toHaveBeenCalledTimes(2); + expect(onText).toHaveBeenLastCalledWith("foo"); }); test("should back out of numeric entities (#125)", () => { - let finished = false; + const onend = jest.fn(); let text = ""; const p = new Parser({ ontext(data) { text += data; }, - onend() { - finished = true; - }, + onend, }); p.end("id=770&#anchor"); - expect(finished).toBeTruthy(); + expect(onend).toHaveBeenCalledTimes(1); expect(text).toBe("id=770&#anchor"); p.reset(); text = ""; - finished = false; p.end("0&#xn"); - expect(finished).toBeTruthy(); + expect(onend).toHaveBeenCalledTimes(2); expect(text).toBe("0&#xn"); }); diff --git a/src/Tokenizer.spec.ts b/src/Tokenizer.spec.ts index db65578ca..5a71ee48f 100644 --- a/src/Tokenizer.spec.ts +++ b/src/Tokenizer.spec.ts @@ -1,171 +1,44 @@ import { Tokenizer } from "."; -class CallbackLogger { - log: string[] = []; - - onattribdata(value: string) { - this.log.push(`onattribdata: '${value}'`); - } - onattribend() { - this.log.push(`onattribend`); - } - onattribname(name: string) { - this.log.push(`onattribname: '${name}'`); - } - oncdata(data: string) { - this.log.push(`oncdata: '${data}'`); - } - onclosetag(name: string) { - this.log.push(`onclosetag: '${name}'`); - } - oncomment(data: string) { - this.log.push(`oncomment: '${data}'`); - } - ondeclaration(content: string) { - this.log.push(`ondeclaration: '${content}'`); - } - onend() { - this.log.push(`onend`); - } - onerror(error: Error, state?: unknown) { - this.log.push(`onerror: '${error}', '${state}'`); - } - onopentagend() { - this.log.push(`onopentagend`); - } - onopentagname(name: string) { - this.log.push(`onopentagname: '${name}'`); - } - onprocessinginstruction(instruction: string) { - this.log.push(`onprocessinginstruction: '${instruction}'`); - } - onselfclosingtag() { - this.log.push(`onselfclosingtag`); - } - ontext(value: string) { - this.log.push(`ontext: '${value}'`); - } -} - -describe("Tokenizer", () => { - test("should support self-closing special tags", () => { - const logger = new CallbackLogger(); - const tokenizer = new Tokenizer( - { - xmlMode: false, - decodeEntities: false, +function tokenize(str: string) { + const log: unknown[][] = []; + const tokenizer = new Tokenizer( + {}, + new Proxy({} as any, { + get(_, prop) { + return (...args: unknown[]) => log.push([prop, ...args]); }, - logger - ); + }) + ); - const selfClosingScriptInput = "
"; - const normalScriptOutput = [ - "onopentagname: 'script'", - "onopentagend", - "onclosetag: 'script'", - "onopentagname: 'div'", - "onopentagend", - "onclosetag: 'div'", - "onend", - ]; - - tokenizer.write(normalScriptInput); - tokenizer.end(); - expect(logger.log).toEqual(normalScriptOutput); - tokenizer.reset(); - logger.log = []; - - const normalStyleInput = "
"; - const normalStyleOutput = [ - "onopentagname: 'style'", - "onopentagend", - "onclosetag: 'style'", - "onopentagname: 'div'", - "onopentagend", - "onclosetag: 'div'", - "onend", - ]; - - tokenizer.write(normalStyleInput); - tokenizer.end(); - expect(logger.log).toEqual(normalStyleOutput); - tokenizer.reset(); - logger.log = []; - - const normalTitleInput = "
"; - const normalTitleOutput = [ - "onopentagname: 'title'", - "onopentagend", - "onclosetag: 'title'", - "onopentagname: 'div'", - "onopentagend", - "onclosetag: 'div'", - "onend", - ]; - - tokenizer.write(normalTitleInput); - tokenizer.end(); - expect(logger.log).toEqual(normalTitleOutput); - tokenizer.reset(); - logger.log = []; + describe("should support standard special tags", () => { + it("for normal script tag", () => { + expect(tokenize("
")).toMatchSnapshot(); + }); + it("for normal style tag", () => { + expect(tokenize("
")).toMatchSnapshot(); + }); + it("for normal sitle tag", () => { + expect(tokenize("
")).toMatchSnapshot(); + }); }); }); diff --git a/src/__fixtures__/test-helper.ts b/src/__fixtures__/test-helper.ts index ae48592cf..a94577e4d 100644 --- a/src/__fixtures__/test-helper.ts +++ b/src/__fixtures__/test-helper.ts @@ -1,5 +1,4 @@ import { Parser, Handler, ParserOptions } from "../Parser"; -import { CollectingHandler } from "../CollectingHandler"; import fs from "fs"; import path from "path"; @@ -33,51 +32,49 @@ interface Event { } /** - * Creates a `CollectingHandler` that calls the supplied - * callback with simplified events on completion. + * Creates a handler that calls the supplied callback with simplified events on + * completion. * * @internal * @param cb Function to call with all events. */ export function getEventCollector( cb: (error: Error | null, events?: Event[]) => void -): CollectingHandler { - const handler = new CollectingHandler({ - onerror: cb, - onend() { - cb(null, handler.events.reduce(eventReducer, [])); - }, - }); +): Partial { + const events: Event[] = []; - return handler; -} + function handle(event: string, ...data: unknown[]): void { + if (event === "onerror") { + cb(data[0] as Error); + } else if (event === "onend") { + cb(null, events); + } else if (event === "onreset") { + events.length = 0; + } else if (event === "onparserinit") { + // Ignore + } else if ( + event === "ontext" && + events[events.length - 1]?.event === "text" + ) { + // Combine text nodes + (events[events.length - 1].data[0] as string) += data[0]; + } else { + // Remove `undefined`s from attribute responses, as they cannot be represented in JSON. + if (event === "onattribute" && data[2] === undefined) { + data.pop(); + } -function eventReducer( - events: Event[], - [event, ...data]: [string, ...unknown[]] -): Event[] { - if (event === "onerror" || event === "onend" || event === "onparserinit") { - // Ignore - } else if ( - event === "ontext" && - events.length && - events[events.length - 1].event === "text" - ) { - // Combine text nodes - (events[events.length - 1].data[0] as string) += data[0]; - } else { - // Remove `undefined`s from attribute responses, as they cannot be represented in JSON. - if (event === "onattribute" && data[2] === undefined) { - data.pop(); + events.push({ + event: event.substr(2), + data, + }); } - - events.push({ - event: event.substr(2), - data, - }); } - return events; + return new Proxy( + {}, + { get: (_, event) => handle.bind(null, event as string) } + ); } /** diff --git a/src/__snapshots__/Tokenizer.spec.ts.snap b/src/__snapshots__/Tokenizer.spec.ts.snap new file mode 100644 index 000000000..5b8d9202c --- /dev/null +++ b/src/__snapshots__/Tokenizer.spec.ts.snap @@ -0,0 +1,169 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Tokenizer should support self-closing special tags for self-closing script tag 1`] = ` +Array [ + Array [ + "onopentagname", + "script", + ], + Array [ + "onselfclosingtag", + ], + Array [ + "onopentagname", + "div", + ], + Array [ + "onopentagend", + ], + Array [ + "onclosetag", + "div", + ], + Array [ + "onend", + ], +] +`; + +exports[`Tokenizer should support self-closing special tags for self-closing style tag 1`] = ` +Array [ + Array [ + "onopentagname", + "style", + ], + Array [ + "onselfclosingtag", + ], + Array [ + "onopentagname", + "div", + ], + Array [ + "onopentagend", + ], + Array [ + "onclosetag", + "div", + ], + Array [ + "onend", + ], +] +`; + +exports[`Tokenizer should support self-closing special tags for self-closing title tag 1`] = ` +Array [ + Array [ + "onopentagname", + "title", + ], + Array [ + "onselfclosingtag", + ], + Array [ + "onopentagname", + "div", + ], + Array [ + "onopentagend", + ], + Array [ + "onclosetag", + "div", + ], + Array [ + "onend", + ], +] +`; + +exports[`Tokenizer should support standard special tags for normal script tag 1`] = ` +Array [ + Array [ + "onopentagname", + "script", + ], + Array [ + "onopentagend", + ], + Array [ + "onclosetag", + "script", + ], + Array [ + "onopentagname", + "div", + ], + Array [ + "onopentagend", + ], + Array [ + "onclosetag", + "div", + ], + Array [ + "onend", + ], +] +`; + +exports[`Tokenizer should support standard special tags for normal sitle tag 1`] = ` +Array [ + Array [ + "onopentagname", + "title", + ], + Array [ + "onopentagend", + ], + Array [ + "onclosetag", + "title", + ], + Array [ + "onopentagname", + "div", + ], + Array [ + "onopentagend", + ], + Array [ + "onclosetag", + "div", + ], + Array [ + "onend", + ], +] +`; + +exports[`Tokenizer should support standard special tags for normal style tag 1`] = ` +Array [ + Array [ + "onopentagname", + "style", + ], + Array [ + "onopentagend", + ], + Array [ + "onclosetag", + "style", + ], + Array [ + "onopentagname", + "div", + ], + Array [ + "onopentagend", + ], + Array [ + "onclosetag", + "div", + ], + Array [ + "onend", + ], +] +`;