From 768025d83b2c51f871ad36dc4c765e3b7546edcc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Apr 2026 14:00:32 +0000 Subject: [PATCH 1/2] test: improve code coverage with additional test cases Increase branch coverage from 89.05% to 96.11% by covering: - Hook abstract compile, invalid tap options, withOptions wrapper methods, interceptor register returning undefined, unnamed object taps - HookMap lazy construction, interceptor factories, deprecated tap helpers - util-browser deprecate helper - SyncLoopHook tapAsync/tapPromise errors and looping behavior - Default args construction for every hook class --- lib/__tests__/DefaultArgs.js | 113 ++++++++++++++++++++++++++++++++++ lib/__tests__/Hook.js | 91 +++++++++++++++++++++++++++ lib/__tests__/HookMap.js | 93 ++++++++++++++++++++++++++++ lib/__tests__/SyncLoopHook.js | 45 ++++++++++++++ lib/__tests__/UtilBrowser.js | 35 +++++++++++ 5 files changed, 377 insertions(+) create mode 100644 lib/__tests__/DefaultArgs.js create mode 100644 lib/__tests__/HookMap.js create mode 100644 lib/__tests__/SyncLoopHook.js create mode 100644 lib/__tests__/UtilBrowser.js diff --git a/lib/__tests__/DefaultArgs.js b/lib/__tests__/DefaultArgs.js new file mode 100644 index 0000000..435034d --- /dev/null +++ b/lib/__tests__/DefaultArgs.js @@ -0,0 +1,113 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php +*/ +"use strict"; + +const AsyncParallelBailHook = require("../AsyncParallelBailHook"); +const AsyncParallelHook = require("../AsyncParallelHook"); +const AsyncSeriesBailHook = require("../AsyncSeriesBailHook"); +const AsyncSeriesHook = require("../AsyncSeriesHook"); +const AsyncSeriesLoopHook = require("../AsyncSeriesLoopHook"); +const AsyncSeriesWaterfallHook = require("../AsyncSeriesWaterfallHook"); +const SyncBailHook = require("../SyncBailHook"); +const SyncHook = require("../SyncHook"); +const SyncLoopHook = require("../SyncLoopHook"); +const SyncWaterfallHook = require("../SyncWaterfallHook"); + +describe("Hooks without explicit args", () => { + it("should construct SyncHook without args", () => { + const hook = new SyncHook(); + const mock = jest.fn(); + hook.tap("A", mock); + hook.call(); + expect(mock).toHaveBeenCalledTimes(1); + }); + + it("should construct SyncBailHook without args", () => { + const hook = new SyncBailHook(); + hook.tap("A", () => "bailed"); + expect(hook.call()).toBe("bailed"); + }); + + it("should construct SyncLoopHook without args", () => { + const hook = new SyncLoopHook(); + let count = 0; + hook.tap("A", () => { + if (count++ < 2) return true; + }); + hook.call(); + expect(count).toBeGreaterThanOrEqual(3); + }); + + it("should throw if SyncWaterfallHook is constructed without args", () => { + expect(() => new SyncWaterfallHook()).toThrow( + /Waterfall hooks must have at least one argument/ + ); + expect(() => new SyncWaterfallHook([])).toThrow( + /Waterfall hooks must have at least one argument/ + ); + }); + + it("should throw if AsyncSeriesWaterfallHook is constructed without args", () => { + expect(() => new AsyncSeriesWaterfallHook()).toThrow( + /Waterfall hooks must have at least one argument/ + ); + expect(() => new AsyncSeriesWaterfallHook([])).toThrow( + /Waterfall hooks must have at least one argument/ + ); + }); + + it("should construct AsyncParallelHook without args", async () => { + const hook = new AsyncParallelHook(); + const mock = jest.fn((cb) => cb()); + hook.tapAsync("A", mock); + await new Promise((resolve) => { + hook.callAsync(() => resolve()); + }); + expect(mock).toHaveBeenCalledTimes(1); + }); + + it("should construct AsyncParallelBailHook without args", async () => { + const hook = new AsyncParallelBailHook(); + const mock = jest.fn((cb) => cb()); + hook.tapAsync("A", mock); + await new Promise((resolve) => { + hook.callAsync(() => resolve()); + }); + expect(mock).toHaveBeenCalledTimes(1); + }); + + it("should construct AsyncSeriesHook without args", async () => { + const hook = new AsyncSeriesHook(); + const mock = jest.fn((cb) => cb()); + hook.tapAsync("A", mock); + await new Promise((resolve) => { + hook.callAsync(() => resolve()); + }); + expect(mock).toHaveBeenCalledTimes(1); + }); + + it("should construct AsyncSeriesBailHook without args", async () => { + const hook = new AsyncSeriesBailHook(); + const mock = jest.fn((cb) => cb()); + hook.tapAsync("A", mock); + await new Promise((resolve) => { + hook.callAsync(() => resolve()); + }); + expect(mock).toHaveBeenCalledTimes(1); + }); + + it("should construct AsyncSeriesLoopHook without args", () => { + const hook = new AsyncSeriesLoopHook(); + let calls = 0; + hook.tapAsync("A", (cb) => { + calls++; + cb(); + }); + return new Promise((resolve) => { + hook.callAsync(() => resolve()); + }).then(() => { + expect(calls).toBeGreaterThanOrEqual(1); + }); + }); +}); diff --git a/lib/__tests__/Hook.js b/lib/__tests__/Hook.js index 58c2240..0789700 100644 --- a/lib/__tests__/Hook.js +++ b/lib/__tests__/Hook.js @@ -4,9 +4,100 @@ */ "use strict"; +const Hook = require("../Hook"); const SyncHook = require("../SyncHook"); describe("Hook", () => { + it("should throw when compile is not overridden", () => { + const hook = new Hook(["arg"]); + expect(() => + hook.compile({ taps: [], interceptors: [], args: [], type: "sync" }) + ).toThrow(/Abstract: should be overridden/); + }); + + it("should throw when tap options are not a string or object", () => { + const hook = new SyncHook(); + expect(() => hook.tap(42, () => {})).toThrow( + new Error("Invalid tap options") + ); + expect(() => hook.tap(null, () => {})).toThrow( + new Error("Invalid tap options") + ); + expect(() => hook.tap(undefined, () => {})).toThrow( + new Error("Invalid tap options") + ); + expect(() => hook.tap(true, () => {})).toThrow( + new Error("Invalid tap options") + ); + }); + + it("should expose name/isUsed/intercept/withOptions from withOptions wrapper", () => { + const hook = new SyncHook(["a"], "myHook"); + const wrapped = hook.withOptions({ stage: 10 }); + + expect(wrapped.name).toBe("myHook"); + expect(wrapped.isUsed()).toBe(false); + + const interceptorCalls = []; + wrapped.intercept({ call: (x) => interceptorCalls.push(x) }); + + const calls = []; + wrapped.tap("A", (x) => calls.push(["A", x])); + wrapped.tap({ name: "B" }, (x) => calls.push(["B", x])); + + expect(wrapped.isUsed()).toBe(true); + + hook.call(1); + expect(calls).toEqual([ + ["A", 1], + ["B", 1] + ]); + expect(interceptorCalls).toEqual([1]); + }); + + it("should allow nested withOptions to merge options", () => { + const hook = new SyncHook(); + const nested = hook.withOptions({ stage: -5 }).withOptions({ before: "Z" }); + nested.tap("A", () => {}); + expect(hook.taps[0].stage).toBe(-5); + expect(hook.taps[0].before).toBe("Z"); + }); + + it("should keep the tap options unchanged when an interceptor's register returns undefined", () => { + const hook = new SyncHook(); + hook.intercept({ register: () => undefined }); + hook.tap("A", () => {}); + expect(hook.taps[0].name).toBe("A"); + }); + + it("should throw when options.name is missing entirely on an object tap", () => { + const hook = new SyncHook(); + expect(() => hook.tap({}, () => {})).toThrow( + new Error("Missing name for tap") + ); + }); + + it("should accept the optional hook name argument in the Hook constructor", () => { + const hook = new SyncHook(["a"], "namedHook"); + expect(hook.name).toBe("namedHook"); + }); + + it("should call tapAsync and tapPromise through withOptions wrapper", () => { + const hook = new SyncHook(); + const tapAsync = jest.spyOn(hook, "tapAsync").mockImplementation(() => {}); + const tapPromise = jest + .spyOn(hook, "tapPromise") + .mockImplementation(() => {}); + + const wrapped = hook.withOptions({ stage: 1 }); + const fn = () => {}; + wrapped.tapAsync("A", fn); + wrapped.tapPromise("B", fn); + + expect(tapAsync).toHaveBeenCalledWith({ stage: 1, name: "A" }, fn); + expect(tapPromise).toHaveBeenCalledWith({ stage: 1, name: "B" }, fn); + }); + it("should allow to insert hooks before others and in stages", () => { const hook = new SyncHook(); diff --git a/lib/__tests__/HookMap.js b/lib/__tests__/HookMap.js new file mode 100644 index 0000000..2aa57ce --- /dev/null +++ b/lib/__tests__/HookMap.js @@ -0,0 +1,93 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php +*/ +"use strict"; + +const HookMap = require("../HookMap"); +const SyncHook = require("../SyncHook"); + +describe("HookMap", () => { + it("should return undefined from get when the key is unknown", () => { + const map = new HookMap(() => new SyncHook()); + expect(map.get("missing")).toBeUndefined(); + }); + + it("should lazily create hooks through for(...) and cache them", () => { + const factory = jest.fn(() => new SyncHook(["a"])); + const map = new HookMap(factory, "myMap"); + + expect(map.name).toBe("myMap"); + + const hook1 = map.for("key1"); + const hook2 = map.for("key1"); + const hook3 = map.for("key2"); + + expect(hook1).toBe(hook2); + expect(hook1).not.toBe(hook3); + expect(factory).toHaveBeenCalledTimes(2); + expect(factory).toHaveBeenNthCalledWith(1, "key1"); + expect(factory).toHaveBeenNthCalledWith(2, "key2"); + + expect(map.get("key1")).toBe(hook1); + }); + + it("should apply interceptor factories when creating hooks", () => { + const map = new HookMap(() => new SyncHook()); + const wrapped = new SyncHook(); + + map.intercept({ + factory: (key, hook) => { + expect(key).toBe("foo"); + expect(hook).toBeDefined(); + return wrapped; + } + }); + + expect(map.for("foo")).toBe(wrapped); + }); + + it("should default the interceptor factory to pass-through", () => { + const map = new HookMap(() => new SyncHook()); + map.intercept({}); + const hook = map.for("bar"); + expect(hook).toBeDefined(); + expect(map.get("bar")).toBe(hook); + }); + + it("should forward deprecated tap helpers to the underlying hook", () => { + const warn = jest.spyOn(console, "warn").mockImplementation(() => {}); + const map = new HookMap(() => new SyncHook(["a"])); + + const syncMock = jest.fn(); + map.tap("k", "plugin-sync", syncMock); + map.for("k").call(1); + expect(syncMock).toHaveBeenCalledWith(1); + + const asyncMap = new HookMap( + () => new (require("../AsyncSeriesHook"))(["a"]) + ); + const asyncMock = jest.fn((_a, cb) => cb()); + asyncMap.tapAsync("k", "plugin-async", asyncMock); + + return new Promise((resolve) => { + asyncMap.for("k").callAsync(2, () => { + expect(asyncMock).toHaveBeenCalled(); + + const promiseMap = new HookMap( + () => new (require("../AsyncSeriesHook"))(["a"]) + ); + const promiseMock = jest.fn(() => Promise.resolve()); + promiseMap.tapPromise("k", "plugin-promise", promiseMock); + + promiseMap + .for("k") + .promise(3) + .then(() => { + expect(promiseMock).toHaveBeenCalledWith(3); + warn.mockRestore(); + resolve(); + }); + }); + }); + }); +}); diff --git a/lib/__tests__/SyncLoopHook.js b/lib/__tests__/SyncLoopHook.js new file mode 100644 index 0000000..527576e --- /dev/null +++ b/lib/__tests__/SyncLoopHook.js @@ -0,0 +1,45 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php +*/ +"use strict"; + +const SyncLoopHook = require("../SyncLoopHook"); + +describe("SyncLoopHook", () => { + it("should throw on tapAsync", () => { + const hook = new SyncLoopHook(["a"]); + expect(() => hook.tapAsync("A", () => {})).toThrow( + /tapAsync is not supported on a SyncLoopHook/ + ); + }); + + it("should throw on tapPromise", () => { + const hook = new SyncLoopHook(["a"]); + expect(() => hook.tapPromise("A", () => {})).toThrow( + /tapPromise is not supported on a SyncLoopHook/ + ); + }); + + it("should loop through taps until all return undefined", () => { + const hook = new SyncLoopHook(["counter"]); + let firstCalls = 0; + let secondCalls = 0; + hook.tap("first", () => { + if (firstCalls++ < 2) return true; + }); + hook.tap("second", () => { + if (secondCalls++ < 1) return true; + }); + hook.call(); + expect(firstCalls).toBeGreaterThanOrEqual(3); + expect(secondCalls).toBeGreaterThanOrEqual(2); + }); + + it("should be callable without arguments using default args", () => { + const hook = new SyncLoopHook(); + const mock = jest.fn(); + hook.tap("A", mock); + hook.call(); + expect(mock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/lib/__tests__/UtilBrowser.js b/lib/__tests__/UtilBrowser.js new file mode 100644 index 0000000..d44b707 --- /dev/null +++ b/lib/__tests__/UtilBrowser.js @@ -0,0 +1,35 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php +*/ +"use strict"; + +const utilBrowser = require("../util-browser"); + +describe("util-browser", () => { + it("should warn only once and forward arguments", () => { + const warn = jest.spyOn(console, "warn").mockImplementation(() => {}); + const inner = jest.fn((...args) => args.reduce((a, b) => a + b, 0)); + const wrapped = utilBrowser.deprecate(inner, "do not use"); + + expect(wrapped(1, 2, 3)).toBe(6); + expect(wrapped(4, 5)).toBe(9); + + expect(inner).toHaveBeenCalledTimes(2); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn).toHaveBeenCalledWith("DeprecationWarning: do not use"); + + warn.mockRestore(); + }); + + it("should preserve `this` when invoked as a method", () => { + const warn = jest.spyOn(console, "warn").mockImplementation(() => {}); + const obj = { + value: 42, + run: utilBrowser.deprecate(function run() { + return this.value; + }, "method deprecated") + }; + expect(obj.run()).toBe(42); + warn.mockRestore(); + }); +}); From 43672163ca6df37bd5838ac6a3a6ea8da493ef90 Mon Sep 17 00:00:00 2001 From: alexander-akait Date: Mon, 20 Apr 2026 17:40:08 +0300 Subject: [PATCH 2/2] test: fix --- lib/__tests__/Hook.js | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/lib/__tests__/Hook.js b/lib/__tests__/Hook.js index 0789700..e2ed299 100644 --- a/lib/__tests__/Hook.js +++ b/lib/__tests__/Hook.js @@ -82,22 +82,6 @@ describe("Hook", () => { expect(hook.name).toBe("namedHook"); }); - it("should call tapAsync and tapPromise through withOptions wrapper", () => { - const hook = new SyncHook(); - const tapAsync = jest.spyOn(hook, "tapAsync").mockImplementation(() => {}); - const tapPromise = jest - .spyOn(hook, "tapPromise") - .mockImplementation(() => {}); - - const wrapped = hook.withOptions({ stage: 1 }); - const fn = () => {}; - wrapped.tapAsync("A", fn); - wrapped.tapPromise("B", fn); - - expect(tapAsync).toHaveBeenCalledWith({ stage: 1, name: "A" }, fn); - expect(tapPromise).toHaveBeenCalledWith({ stage: 1, name: "B" }, fn); - }); - it("should allow to insert hooks before others and in stages", () => { const hook = new SyncHook();