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
110 changes: 110 additions & 0 deletions test/test-validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const { setImmediate } = require("node:timers/promises");
const when = require("../");

test("rejects invalid callbacks before evaluating the condition", () => {
for (const value of [undefined, null, false, 1, "callback", {}]) {
let checks = 0;
assert.throws(() => when(() => checks++, value), {
name: "TypeError",
message: "code must be a function",
});
assert.equal(checks, 0);
}
});

test("rejects invalid conditions with a named error", () => {
for (const value of [undefined, null, false, 1, "condition", {}]) {
assert.throws(() => when(value, () => {}), {
name: "TypeError",
message: "condition must be a function",
});
}
});

test("invalid setters preserve the previous functions", (t) => {
let calls = 0;
const handle = when(
() => true,
() => calls++,
);
t.after(() => handle.clear());

assert.throws(
() => handle.setCondition(null),
/condition must be a function/,
);
assert.throws(() => handle.setCode(null), /code must be a function/);
handle.reset();

assert.equal(calls, 2);
});

test("rejects Promise and callable thenable results without invoking code", async () => {
const callable = Object.assign(() => {}, {
then: (resolve) => resolve(false),
});
for (const condition of [
async () => false,
async () => {
throw new Error("predicate rejected");
},
() => ({ then: (resolve) => resolve(false) }),
() => callable,
]) {
let calls = 0;
assert.throws(() => when(condition, () => calls++), {
name: "TypeError",
message: "condition must return a synchronous value",
});
assert.equal(calls, 0);
}
await setImmediate();
});

test("a non-callable then property remains an ordinary truthy value", () => {
let calls = 0;
when(
() => ({ then: 1 }),
() => calls++,
);
assert.equal(calls, 1);
});

test("reset validates the replacement predicate result", (t) => {
const handle = when(
() => false,
() => {},
);
t.after(() => handle.clear());
handle.setCondition(async () => false);
assert.throws(
() => handle.reset(),
/condition must return a synchronous value/,
);
});

test("synchronous predicate and callback errors retain their identity", () => {
const failure = new Error("original failure");
assert.throws(
() =>
when(
() => {
throw failure;
},
() => {},
),
(error) => error === failure,
);
assert.throws(
() =>
when(
() => true,
() => {
throw failure;
},
),
(error) => error === failure,
);
});
15 changes: 15 additions & 0 deletions when.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
require("setimmediate");

function when(condition, code) {
assertFunction(condition, "condition");
assertFunction(code, "code");
let immediate;
let generation = 0;

function checkCondition() {
const currentGeneration = generation;
const ready = condition();
const asynchronous = ready != null && typeof ready.then === "function";
if (asynchronous) Promise.resolve(ready).catch(() => {});
if (currentGeneration !== generation) return;
if (asynchronous) {
throw new TypeError("condition must return a synchronous value");
}

if (ready) {
clear();
Expand All @@ -28,15 +35,23 @@ function when(condition, code) {
}

function setCondition(newCondition) {
assertFunction(newCondition, "condition");
condition = newCondition;
}

function setCode(newCode) {
assertFunction(newCode, "code");
code = newCode;
}

checkCondition();
return { clear, reset, setCondition, setCode };
}

function assertFunction(value, name) {
if (typeof value !== "function") {
throw new TypeError(`${name} must be a function`);
}
}

module.exports = when;
Loading