Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions lib/__tests__/DefaultArgs.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
75 changes: 75 additions & 0 deletions lib/__tests__/Hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,84 @@
*/
"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 allow to insert hooks before others and in stages", () => {
const hook = new SyncHook();

Expand Down
93 changes: 93 additions & 0 deletions lib/__tests__/HookMap.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
});
});
45 changes: 45 additions & 0 deletions lib/__tests__/SyncLoopHook.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
35 changes: 35 additions & 0 deletions lib/__tests__/UtilBrowser.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading