From 1ecc049c78849ee5b8a9bf9f55ca221487c847d7 Mon Sep 17 00:00:00 2001 From: Glen Keane Date: Thu, 8 Oct 2026 13:31:50 +0100 Subject: [PATCH] fix: reject invalid functions and asynchronous predicates --- test/test-validation.js | 110 ++++++++++++++++++++++++++++++++++++++++ when.js | 15 ++++++ 2 files changed, 125 insertions(+) create mode 100644 test/test-validation.js diff --git a/test/test-validation.js b/test/test-validation.js new file mode 100644 index 0000000..c8261da --- /dev/null +++ b/test/test-validation.js @@ -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, + ); +}); diff --git a/when.js b/when.js index 84b612a..a54fe42 100644 --- a/when.js +++ b/when.js @@ -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(); @@ -28,10 +35,12 @@ function when(condition, code) { } function setCondition(newCondition) { + assertFunction(newCondition, "condition"); condition = newCondition; } function setCode(newCode) { + assertFunction(newCode, "code"); code = newCode; } @@ -39,4 +48,10 @@ function when(condition, code) { return { clear, reset, setCondition, setCode }; } +function assertFunction(value, name) { + if (typeof value !== "function") { + throw new TypeError(`${name} must be a function`); + } +} + module.exports = when;