Files
plainpages/src/plugin-host/hooks.test.ts
T
2026-08-18 23:12:13 +02:00

59 lines
2.9 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import type { RequestContext } from "../http/context.ts";
import type { Plugin, PluginHooks } from "./plugin.ts";
import { runBootHooks, runRequestHooks, runResponseHooks } from "./hooks.ts";
const ctx = {} as RequestContext; // the hooks only thread ctx through; they never read it
function plugin(id: string, hooks: PluginHooks): Plugin {
return { apiVersion: "1.0.0", hooks, id };
}
test("runBootHooks runs each onBoot in order, skips plugins without one, and a throw aborts", async () => {
const calls: string[] = [];
const scoped: string[] = []; // each hook is handed a context built for its own plugin
const bootContextFor = (built: Plugin) => { scoped.push(built.id); return {}; };
await runBootHooks([
plugin("a", { onBoot: () => void calls.push("a") }),
plugin("b", {}), // no onBoot → skipped
plugin("c", { onBoot: async () => void calls.push("c") }),
], bootContextFor);
assert.deepEqual(calls, ["a", "c"]);
assert.deepEqual(scoped, ["a", "c"]); // and built only for the plugins that have one
await assert.rejects(runBootHooks([plugin("x", { onBoot: () => { throw new Error("boom"); } })], () => ({})), /boom/);
});
test("runRequestHooks short-circuits on the first RouteResult (with its plugin); later hooks skipped", async () => {
const calls: string[] = [];
const scoped: string[] = []; // each hook is handed a context built for its own plugin
const contextFor = (pluginId: string) => { scoped.push(pluginId); return ctx; };
const short = await runRequestHooks([
plugin("a", { onRequest: () => void calls.push("a") }), // returns void → continue
plugin("b", { onRequest: () => { calls.push("b"); return { html: "stop" }; } }),
plugin("c", { onRequest: () => void calls.push("c") }), // never reached
], contextFor);
assert.deepEqual(short?.result, { html: "stop" });
assert.equal(short?.plugin.id, "b"); // the owning plugin (so a `view` result resolves correctly)
assert.equal(short?.ctx, ctx); // …and the context it ran on, for rendering its view
assert.deepEqual(calls, ["a", "b"]);
assert.deepEqual(scoped, ["a", "b"]); // a plugin without the hook never builds a context
// No hook short-circuits → null (proceed with normal routing).
assert.equal(await runRequestHooks([plugin("a", { onRequest: () => {} })], contextFor), null);
});
test("runResponseHooks runs every onResponse as an observer with the result; a throw fails", async () => {
const seen: unknown[] = [];
const contextFor = () => ctx; // each observer gets a context scoped to its own plugin
await runResponseHooks([
plugin("a", { onResponse: (_c, r) => void seen.push(r) }),
plugin("b", {}), // no onResponse → skipped
], contextFor, { html: "ok" });
assert.deepEqual(seen, [{ html: "ok" }]);
await assert.rejects(runResponseHooks([plugin("x", { onResponse: () => { throw new Error("boom"); } })], contextFor, null), /boom/);
});