test: refactor and format test files
- Update `game.test.ts` to use `_ctx` when `this` is not required - Fix type assertion in `boop.test.ts` - Update `command-runner.test.ts` to use `parseCommandSchema` in prompt tests - Reformat `middleware.test.ts` to use double quotes and consistent indentation
This commit is contained in:
parent
974c1a828c
commit
34cb828f60
|
|
@ -60,15 +60,12 @@ describe("createGameContext", () => {
|
|||
const registry = createGameCommandRegistry();
|
||||
const ctx = createGameContext(registry);
|
||||
|
||||
registry.register(
|
||||
"test <value>",
|
||||
async function (this: CommandRunnerContext<IGameContext>, _ctx, value) {
|
||||
return this.prompt(
|
||||
createPromptDef("prompt <answer>"),
|
||||
(answer) => answer,
|
||||
);
|
||||
},
|
||||
);
|
||||
registry.register("test <value>", async function (_ctx, value) {
|
||||
return _ctx.prompt(
|
||||
createPromptDef("prompt <answer>"),
|
||||
(answer) => answer,
|
||||
);
|
||||
});
|
||||
|
||||
const promptPromise = new Promise<PromptEvent>((resolve) => {
|
||||
ctx._commands.on("prompt", resolve);
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ describe("Boop Game", () => {
|
|||
ctx.produce((state) => {
|
||||
const whiteKitten = state.pieces["white-kitten-1"];
|
||||
if (whiteKitten && whiteKitten.regionId === "board") {
|
||||
whiteKitten.type = "cat";
|
||||
(whiteKitten as { type: string }).type = "cat";
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -464,7 +464,7 @@ describe("Boop Game", () => {
|
|||
const k3 = state.pieces["white-kitten-3"];
|
||||
|
||||
if (k1) {
|
||||
k1.type = "cat";
|
||||
(k1 as { type: string }).type = "cat";
|
||||
k1.regionId = "board";
|
||||
k1.position = [0, 0];
|
||||
state.regions.board.partMap["0,0"] = k1.id;
|
||||
|
|
@ -473,7 +473,7 @@ describe("Boop Game", () => {
|
|||
);
|
||||
}
|
||||
if (k2) {
|
||||
k2.type = "cat";
|
||||
(k2 as { type: string }).type = "cat";
|
||||
k2.regionId = "board";
|
||||
k2.position = [0, 1];
|
||||
state.regions.board.partMap["0,1"] = k2.id;
|
||||
|
|
@ -482,7 +482,7 @@ describe("Boop Game", () => {
|
|||
);
|
||||
}
|
||||
if (k3) {
|
||||
k3.type = "cat";
|
||||
(k3 as { type: string }).type = "cat";
|
||||
k3.regionId = "board";
|
||||
k3.position = [0, 2];
|
||||
state.regions.board.partMap["0,2"] = k3.id;
|
||||
|
|
@ -510,7 +510,7 @@ describe("Boop Game", () => {
|
|||
const k3 = state.pieces["black-kitten-3"];
|
||||
|
||||
if (k1) {
|
||||
k1.type = "cat";
|
||||
(k1 as { type: string }).type = "cat";
|
||||
k1.regionId = "board";
|
||||
k1.position = [0, 0];
|
||||
state.regions.board.partMap["0,0"] = k1.id;
|
||||
|
|
@ -519,7 +519,7 @@ describe("Boop Game", () => {
|
|||
);
|
||||
}
|
||||
if (k2) {
|
||||
k2.type = "cat";
|
||||
(k2 as { type: string }).type = "cat";
|
||||
k2.regionId = "board";
|
||||
k2.position = [1, 0];
|
||||
state.regions.board.partMap["1,0"] = k2.id;
|
||||
|
|
@ -528,7 +528,7 @@ describe("Boop Game", () => {
|
|||
);
|
||||
}
|
||||
if (k3) {
|
||||
k3.type = "cat";
|
||||
(k3 as { type: string }).type = "cat";
|
||||
k3.regionId = "board";
|
||||
k3.position = [2, 0];
|
||||
state.regions.board.partMap["2,0"] = k3.id;
|
||||
|
|
@ -556,7 +556,7 @@ describe("Boop Game", () => {
|
|||
const k3 = state.pieces["white-kitten-3"];
|
||||
|
||||
if (k1) {
|
||||
k1.type = "cat";
|
||||
(k1 as { type: string }).type = "cat";
|
||||
k1.regionId = "board";
|
||||
k1.position = [0, 0];
|
||||
state.regions.board.partMap["0,0"] = k1.id;
|
||||
|
|
@ -565,7 +565,7 @@ describe("Boop Game", () => {
|
|||
);
|
||||
}
|
||||
if (k2) {
|
||||
k2.type = "cat";
|
||||
(k2 as { type: string }).type = "cat";
|
||||
k2.regionId = "board";
|
||||
k2.position = [1, 1];
|
||||
state.regions.board.partMap["1,1"] = k2.id;
|
||||
|
|
@ -574,7 +574,7 @@ describe("Boop Game", () => {
|
|||
);
|
||||
}
|
||||
if (k3) {
|
||||
k3.type = "cat";
|
||||
(k3 as { type: string }).type = "cat";
|
||||
k3.regionId = "board";
|
||||
k3.position = [2, 2];
|
||||
state.regions.board.partMap["2,2"] = k3.id;
|
||||
|
|
@ -740,7 +740,7 @@ describe("Boop Game", () => {
|
|||
const c3 = state.pieces["white-cat-3"];
|
||||
|
||||
if (c1) {
|
||||
c1.type = "cat";
|
||||
(c1 as { type: string }).type = "cat";
|
||||
c1.regionId = "board";
|
||||
c1.position = [0, 0];
|
||||
state.regions.board.partMap["0,0"] = c1.id;
|
||||
|
|
@ -749,7 +749,7 @@ describe("Boop Game", () => {
|
|||
);
|
||||
}
|
||||
if (c2) {
|
||||
c2.type = "cat";
|
||||
(c2 as { type: string }).type = "cat";
|
||||
c2.regionId = "board";
|
||||
c2.position = [0, 1];
|
||||
state.regions.board.partMap["0,1"] = c2.id;
|
||||
|
|
@ -758,7 +758,7 @@ describe("Boop Game", () => {
|
|||
);
|
||||
}
|
||||
if (c3) {
|
||||
c3.type = "cat";
|
||||
(c3 as { type: string }).type = "cat";
|
||||
c3.regionId = "board";
|
||||
c3.position = [0, 2];
|
||||
state.regions.board.partMap["0,2"] = c3.id;
|
||||
|
|
|
|||
|
|
@ -339,7 +339,10 @@ describe("prompt", () => {
|
|||
schema: parseCommandSchema("choose"),
|
||||
run: async function () {
|
||||
try {
|
||||
await this.prompt("select <card>", (card) => card as string);
|
||||
await this.prompt(
|
||||
parseCommandSchema("select <card>"),
|
||||
(card) => card as string,
|
||||
);
|
||||
return "unexpected success";
|
||||
} catch (e) {
|
||||
return (e as Error).message;
|
||||
|
|
@ -420,8 +423,14 @@ describe("prompt", () => {
|
|||
const multiPromptRunner: CommandRunner<TestContext, string[]> = {
|
||||
schema: parseCommandSchema("multi"),
|
||||
run: async function () {
|
||||
const first = await this.prompt("first <a>", (a) => a as string);
|
||||
const second = await this.prompt("second <b>", (b) => b as string);
|
||||
const first = await this.prompt(
|
||||
parseCommandSchema("first <a>"),
|
||||
(a) => a as string,
|
||||
);
|
||||
const second = await this.prompt(
|
||||
parseCommandSchema("second <b>"),
|
||||
(b) => b as string,
|
||||
);
|
||||
return [first, second];
|
||||
},
|
||||
};
|
||||
|
|
@ -475,13 +484,16 @@ describe("prompt", () => {
|
|||
const chooseRunner: CommandRunner<TestContext, string> = {
|
||||
schema: parseCommandSchema("choose"),
|
||||
run: async function () {
|
||||
const result = await this.prompt("select <card>", (card) => {
|
||||
const cardStr = card as string;
|
||||
if (!["Ace", "King", "Queen"].includes(cardStr)) {
|
||||
throw `Invalid card: ${cardStr}. Must be Ace, King, or Queen.`;
|
||||
}
|
||||
return cardStr;
|
||||
});
|
||||
const result = await this.prompt(
|
||||
parseCommandSchema("select <card>"),
|
||||
(card) => {
|
||||
const cardStr = card as string;
|
||||
if (!["Ace", "King", "Queen"].includes(cardStr)) {
|
||||
throw `Invalid card: ${cardStr}. Must be Ace, King, or Queen.`;
|
||||
}
|
||||
return cardStr;
|
||||
},
|
||||
);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
|
@ -533,7 +545,10 @@ describe("prompt", () => {
|
|||
schema: parseCommandSchema("choose"),
|
||||
run: async function () {
|
||||
try {
|
||||
await this.prompt("select <card>", (cmd) => cmd.params[0] as string);
|
||||
await this.prompt(
|
||||
parseCommandSchema("select <card>"),
|
||||
(cmd) => cmd.params[0] as string,
|
||||
);
|
||||
return "unexpected success";
|
||||
} catch (e) {
|
||||
return (e as Error).message;
|
||||
|
|
|
|||
|
|
@ -1,436 +1,445 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { createMiddlewareChain, type MiddlewareChain } from '@/utils/middleware';
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
createMiddlewareChain,
|
||||
type MiddlewareChain,
|
||||
} from "@/utils/middleware";
|
||||
|
||||
describe('createMiddlewareChain', () => {
|
||||
describe('basic execution', () => {
|
||||
it('should return context when no middlewares and no fallback', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
const result = await chain.execute({ value: 42 });
|
||||
describe("createMiddlewareChain", () => {
|
||||
describe("basic execution", () => {
|
||||
it("should return context when no middlewares and no fallback", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
const result = await chain.execute({ value: 42 });
|
||||
|
||||
expect(result).toEqual({ value: 42 });
|
||||
});
|
||||
|
||||
it('should call fallback when no middlewares', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, string>(
|
||||
async ctx => `value is ${ctx.value}`
|
||||
);
|
||||
const result = await chain.execute({ value: 42 });
|
||||
|
||||
expect(result).toBe('value is 42');
|
||||
});
|
||||
|
||||
it('should pass context to fallback', async () => {
|
||||
const chain = createMiddlewareChain<{ a: number; b: number }, number>(
|
||||
async ctx => ctx.a + ctx.b
|
||||
);
|
||||
const result = await chain.execute({ a: 3, b: 7 });
|
||||
|
||||
expect(result).toBe(10);
|
||||
});
|
||||
expect(result).toEqual({ value: 42 });
|
||||
});
|
||||
|
||||
describe('single middleware', () => {
|
||||
it('should execute a single middleware', async () => {
|
||||
const chain = createMiddlewareChain<{ count: number }>();
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.count *= 2;
|
||||
return next();
|
||||
});
|
||||
it("should call fallback when no middlewares", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, string>(
|
||||
async (ctx) => `value is ${ctx.value}`,
|
||||
);
|
||||
const result = await chain.execute({ value: 42 });
|
||||
|
||||
const result = await chain.execute({ count: 5 });
|
||||
|
||||
expect(result.count).toBe(10);
|
||||
});
|
||||
|
||||
it('should allow middleware to modify return value', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, number>(
|
||||
async ctx => ctx.value
|
||||
);
|
||||
chain.use(async (ctx, next) => {
|
||||
const result = await next();
|
||||
return result * 2;
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 21 });
|
||||
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
|
||||
it('should allow middleware to short-circuit without calling next', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
chain.use(async (_ctx, _next) => {
|
||||
return { value: 999 };
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 1 });
|
||||
|
||||
expect(result.value).toBe(999);
|
||||
});
|
||||
expect(result).toBe("value is 42");
|
||||
});
|
||||
|
||||
describe('multiple middlewares', () => {
|
||||
it('should execute middlewares in order', async () => {
|
||||
const order: number[] = [];
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
it("should pass context to fallback", async () => {
|
||||
const chain = createMiddlewareChain<{ a: number; b: number }, number>(
|
||||
async (ctx) => ctx.a + ctx.b,
|
||||
);
|
||||
const result = await chain.execute({ a: 3, b: 7 });
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
order.push(1);
|
||||
const result = await next();
|
||||
order.push(4);
|
||||
return result;
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
order.push(2);
|
||||
const result = await next();
|
||||
order.push(3);
|
||||
return result;
|
||||
});
|
||||
expect(result).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
await chain.execute({ value: 0 });
|
||||
describe("single middleware", () => {
|
||||
it("should execute a single middleware", async () => {
|
||||
const chain = createMiddlewareChain<{ count: number }>();
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.count *= 2;
|
||||
return next();
|
||||
});
|
||||
|
||||
expect(order).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
const result = await chain.execute({ count: 5 });
|
||||
|
||||
it('should accumulate modifications through the chain', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.value += 1;
|
||||
return next();
|
||||
});
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.value *= 2;
|
||||
return next();
|
||||
});
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.value += 3;
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 0 });
|
||||
|
||||
expect(result.value).toBe(5);
|
||||
});
|
||||
|
||||
it('should allow middleware to modify result on the way back', async () => {
|
||||
const chain = createMiddlewareChain<{ base: number }, number>(
|
||||
async ctx => ctx.base
|
||||
);
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
const result = await next();
|
||||
return result + 10;
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
const result = await next();
|
||||
return result * 2;
|
||||
});
|
||||
|
||||
const result = await chain.execute({ base: 5 });
|
||||
|
||||
expect(result).toBe(20);
|
||||
});
|
||||
|
||||
it('should allow middleware to short-circuit in the middle', async () => {
|
||||
const executed: number[] = [];
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
executed.push(1);
|
||||
return next();
|
||||
});
|
||||
chain.use(async () => {
|
||||
executed.push(2);
|
||||
return { value: -1 };
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
executed.push(3);
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 100 });
|
||||
|
||||
expect(result.value).toBe(-1);
|
||||
expect(executed).toEqual([1, 2]);
|
||||
});
|
||||
expect(result.count).toBe(10);
|
||||
});
|
||||
|
||||
describe('nested next calls', () => {
|
||||
it('should advance index on each next call, skipping remaining middlewares', async () => {
|
||||
const chain = createMiddlewareChain<{ counter: number }>();
|
||||
it("should allow middleware to modify return value", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, number>(
|
||||
async (ctx) => ctx.value,
|
||||
);
|
||||
chain.use(async (ctx, next) => {
|
||||
const result = await next();
|
||||
return result * 2;
|
||||
});
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
await next();
|
||||
await next();
|
||||
});
|
||||
const result = await chain.execute({ value: 21 });
|
||||
|
||||
const result = await chain.execute({ counter: 0 });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow middleware to call next conditionally', async () => {
|
||||
const chain = createMiddlewareChain<{ skip: boolean; value: number }>();
|
||||
|
||||
chain.use(async (ctx, next) => {
|
||||
if (ctx.skip) {
|
||||
return { value: -1 };
|
||||
}
|
||||
return next();
|
||||
});
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.value += 10;
|
||||
return next();
|
||||
});
|
||||
|
||||
const resultA = await chain.execute({ skip: true, value: 0 });
|
||||
const resultB = await chain.execute({ skip: false, value: 0 });
|
||||
|
||||
expect(resultA.value).toBe(-1);
|
||||
expect(resultB.value).toBe(10);
|
||||
});
|
||||
|
||||
it('should handle middleware that awaits next multiple times with a fallback', async () => {
|
||||
const log: string[] = [];
|
||||
const chain = createMiddlewareChain<{ value: number }, string[]>(
|
||||
async _ctx => log
|
||||
);
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
log.push('before');
|
||||
await next();
|
||||
log.push('after-first');
|
||||
await next();
|
||||
log.push('after-second');
|
||||
return log;
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
log.push('mw2');
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 0 });
|
||||
|
||||
expect(result).toEqual(['before', 'mw2', 'after-first', 'after-second']);
|
||||
});
|
||||
|
||||
it('should return fallback result on second next call when no more middlewares remain', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, number>(
|
||||
async ctx => ctx.value * 10
|
||||
);
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
await next();
|
||||
return await next();
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 5 });
|
||||
|
||||
expect(result).toBe(50);
|
||||
});
|
||||
|
||||
it('should return fallback result on second next call when no more middlewares remain', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, number>(
|
||||
async ctx => ctx.value * 10
|
||||
);
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
await next();
|
||||
return await next();
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 5 });
|
||||
|
||||
expect(result).toBe(50);
|
||||
});
|
||||
expect(result).toBe(42);
|
||||
});
|
||||
|
||||
describe('async behavior', () => {
|
||||
it('should handle async middlewares', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
it("should allow middleware to short-circuit without calling next", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
chain.use(async (_ctx, _next) => {
|
||||
return { value: 999 };
|
||||
});
|
||||
|
||||
chain.use(async (ctx, next) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
ctx.value += 1;
|
||||
return next();
|
||||
});
|
||||
chain.use(async (ctx, next) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
ctx.value += 2;
|
||||
return next();
|
||||
});
|
||||
const result = await chain.execute({ value: 1 });
|
||||
|
||||
const result = await chain.execute({ value: 0 });
|
||||
expect(result.value).toBe(999);
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.value).toBe(3);
|
||||
});
|
||||
describe("multiple middlewares", () => {
|
||||
it("should execute middlewares in order", async () => {
|
||||
const order: number[] = [];
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
it('should handle async fallback', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, number>(
|
||||
async ctx => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
return ctx.value * 10;
|
||||
}
|
||||
);
|
||||
chain.use(async (_ctx, next) => {
|
||||
order.push(1);
|
||||
const result = await next();
|
||||
order.push(4);
|
||||
return result;
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
order.push(2);
|
||||
const result = await next();
|
||||
order.push(3);
|
||||
return result;
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 5 });
|
||||
await chain.execute({ value: 0 });
|
||||
|
||||
expect(result).toBe(50);
|
||||
});
|
||||
expect(order).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should propagate errors from middleware', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
it("should accumulate modifications through the chain", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
chain.use(async () => {
|
||||
throw new Error('middleware error');
|
||||
});
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.value += 1;
|
||||
return next();
|
||||
});
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.value *= 2;
|
||||
return next();
|
||||
});
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.value += 3;
|
||||
return next();
|
||||
});
|
||||
|
||||
await expect(chain.execute({ value: 1 })).rejects.toThrow('middleware error');
|
||||
});
|
||||
const result = await chain.execute({ value: 0 });
|
||||
|
||||
it('should propagate errors from fallback', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, number>(
|
||||
async () => {
|
||||
throw new Error('fallback error');
|
||||
}
|
||||
);
|
||||
|
||||
await expect(chain.execute({ value: 1 })).rejects.toThrow('fallback error');
|
||||
});
|
||||
|
||||
it('should allow middleware to catch errors from downstream', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
try {
|
||||
return await next();
|
||||
} catch {
|
||||
return { value: -1 };
|
||||
}
|
||||
});
|
||||
chain.use(async () => {
|
||||
throw new Error('downstream error');
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 1 });
|
||||
|
||||
expect(result.value).toBe(-1);
|
||||
});
|
||||
expect(result.value).toBe(5);
|
||||
});
|
||||
|
||||
describe('return type override', () => {
|
||||
it('should support different TReturn type than TContext', async () => {
|
||||
const chain = createMiddlewareChain<{ name: string }, string>(
|
||||
async ctx => `Hello, ${ctx.name}!`
|
||||
);
|
||||
it("should allow middleware to modify result on the way back", async () => {
|
||||
const chain = createMiddlewareChain<{ base: number }, number>(
|
||||
async (ctx) => ctx.base,
|
||||
);
|
||||
|
||||
const result = await chain.execute({ name: 'World' });
|
||||
chain.use(async (_ctx, next) => {
|
||||
const result = await next();
|
||||
return result + 10;
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
const result = await next();
|
||||
return result * 2;
|
||||
});
|
||||
|
||||
expect(result).toBe('Hello, World!');
|
||||
});
|
||||
const result = await chain.execute({ base: 5 });
|
||||
|
||||
it('should allow middleware to transform return type', async () => {
|
||||
const chain = createMiddlewareChain<{ items: number[] }, number>(
|
||||
async ctx => ctx.items.reduce((a, b) => a + b, 0)
|
||||
);
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
const sum = await next();
|
||||
return sum * 2;
|
||||
});
|
||||
|
||||
const result = await chain.execute({ items: [1, 2, 3] });
|
||||
|
||||
expect(result).toBe(12);
|
||||
});
|
||||
expect(result).toBe(20);
|
||||
});
|
||||
|
||||
describe('reusability', () => {
|
||||
it('should reset index on each execute call', async () => {
|
||||
const chain = createMiddlewareChain<{ count: number }>();
|
||||
it("should allow middleware to short-circuit in the middle", async () => {
|
||||
const executed: number[] = [];
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.count += 1;
|
||||
return next();
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
executed.push(1);
|
||||
return next();
|
||||
});
|
||||
chain.use(async () => {
|
||||
executed.push(2);
|
||||
return { value: -1 };
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
executed.push(3);
|
||||
return next();
|
||||
});
|
||||
|
||||
const resultA = await chain.execute({ count: 0 });
|
||||
const resultB = await chain.execute({ count: 0 });
|
||||
const result = await chain.execute({ value: 100 });
|
||||
|
||||
expect(resultA.count).toBe(1);
|
||||
expect(resultB.count).toBe(1);
|
||||
});
|
||||
expect(result.value).toBe(-1);
|
||||
expect(executed).toEqual([1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should share middlewares across execute calls', async () => {
|
||||
const chain = createMiddlewareChain<{ log: string[] }>();
|
||||
describe("nested next calls", () => {
|
||||
it("should advance index on each next call, skipping remaining middlewares", async () => {
|
||||
const chain = createMiddlewareChain<{ counter: number }>();
|
||||
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.log.push('always');
|
||||
return next();
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
await next();
|
||||
await next();
|
||||
return undefined as unknown as { counter: number };
|
||||
});
|
||||
|
||||
await chain.execute({ log: [] });
|
||||
await chain.execute({ log: [] });
|
||||
const result = await chain.execute({ counter: 0 });
|
||||
|
||||
expect(chain).toBeDefined();
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle empty context object', async () => {
|
||||
const chain = createMiddlewareChain<Record<string, never>>();
|
||||
const result = await chain.execute({});
|
||||
it("should allow middleware to call next conditionally", async () => {
|
||||
const chain = createMiddlewareChain<{ skip: boolean; value: number }>();
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
chain.use(async (ctx, next) => {
|
||||
if (ctx.skip) {
|
||||
return { skip: ctx.skip, value: -1 };
|
||||
}
|
||||
return next();
|
||||
});
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.value += 10;
|
||||
return next();
|
||||
});
|
||||
|
||||
it('should handle middleware that returns a completely different object', async () => {
|
||||
const chain = createMiddlewareChain<{ x: number }, { y: string }>(
|
||||
async () => ({ y: 'default' })
|
||||
);
|
||||
const resultA = await chain.execute({ skip: true, value: 0 });
|
||||
const resultB = await chain.execute({ skip: false, value: 0 });
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ x: 42 });
|
||||
|
||||
expect(result).toEqual({ y: 'default' });
|
||||
});
|
||||
|
||||
it('should handle middleware that mutates context without returning', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>(
|
||||
async ctx => ctx
|
||||
);
|
||||
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.value = 100;
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 0 });
|
||||
|
||||
expect(result.value).toBe(100);
|
||||
});
|
||||
|
||||
it('should return undefined when middleware does not call next or return', async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
chain.use(async (ctx) => {
|
||||
ctx.value = 100;
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 0 });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
expect(resultA.value).toBe(-1);
|
||||
expect(resultB.value).toBe(10);
|
||||
});
|
||||
|
||||
it("should handle middleware that awaits next multiple times with a fallback", async () => {
|
||||
const log: string[] = [];
|
||||
const chain = createMiddlewareChain<{ value: number }, string[]>(
|
||||
async (_ctx) => log,
|
||||
);
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
log.push("before");
|
||||
await next();
|
||||
log.push("after-first");
|
||||
await next();
|
||||
log.push("after-second");
|
||||
return log;
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
log.push("mw2");
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 0 });
|
||||
|
||||
expect(result).toEqual(["before", "mw2", "after-first", "after-second"]);
|
||||
});
|
||||
|
||||
it("should return fallback result on second next call when no more middlewares remain", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, number>(
|
||||
async (ctx) => ctx.value * 10,
|
||||
);
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
await next();
|
||||
return await next();
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 5 });
|
||||
|
||||
expect(result).toBe(50);
|
||||
});
|
||||
|
||||
it("should return fallback result on second next call when no more middlewares remain", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, number>(
|
||||
async (ctx) => ctx.value * 10,
|
||||
);
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
await next();
|
||||
return await next();
|
||||
});
|
||||
chain.use(async (_ctx, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 5 });
|
||||
|
||||
expect(result).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe("async behavior", () => {
|
||||
it("should handle async middlewares", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
chain.use(async (ctx, next) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
ctx.value += 1;
|
||||
return next();
|
||||
});
|
||||
chain.use(async (ctx, next) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
ctx.value += 2;
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 0 });
|
||||
|
||||
expect(result.value).toBe(3);
|
||||
});
|
||||
|
||||
it("should handle async fallback", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, number>(
|
||||
async (ctx) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
return ctx.value * 10;
|
||||
},
|
||||
);
|
||||
|
||||
const result = await chain.execute({ value: 5 });
|
||||
|
||||
expect(result).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should propagate errors from middleware", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
chain.use(async () => {
|
||||
throw new Error("middleware error");
|
||||
});
|
||||
|
||||
await expect(chain.execute({ value: 1 })).rejects.toThrow(
|
||||
"middleware error",
|
||||
);
|
||||
});
|
||||
|
||||
it("should propagate errors from fallback", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }, number>(
|
||||
async () => {
|
||||
throw new Error("fallback error");
|
||||
},
|
||||
);
|
||||
|
||||
await expect(chain.execute({ value: 1 })).rejects.toThrow(
|
||||
"fallback error",
|
||||
);
|
||||
});
|
||||
|
||||
it("should allow middleware to catch errors from downstream", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
try {
|
||||
return await next();
|
||||
} catch {
|
||||
return { value: -1 };
|
||||
}
|
||||
});
|
||||
chain.use(async () => {
|
||||
throw new Error("downstream error");
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 1 });
|
||||
|
||||
expect(result.value).toBe(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("return type override", () => {
|
||||
it("should support different TReturn type than TContext", async () => {
|
||||
const chain = createMiddlewareChain<{ name: string }, string>(
|
||||
async (ctx) => `Hello, ${ctx.name}!`,
|
||||
);
|
||||
|
||||
const result = await chain.execute({ name: "World" });
|
||||
|
||||
expect(result).toBe("Hello, World!");
|
||||
});
|
||||
|
||||
it("should allow middleware to transform return type", async () => {
|
||||
const chain = createMiddlewareChain<{ items: number[] }, number>(
|
||||
async (ctx) => ctx.items.reduce((a, b) => a + b, 0),
|
||||
);
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
const sum = await next();
|
||||
return sum * 2;
|
||||
});
|
||||
|
||||
const result = await chain.execute({ items: [1, 2, 3] });
|
||||
|
||||
expect(result).toBe(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reusability", () => {
|
||||
it("should reset index on each execute call", async () => {
|
||||
const chain = createMiddlewareChain<{ count: number }>();
|
||||
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.count += 1;
|
||||
return next();
|
||||
});
|
||||
|
||||
const resultA = await chain.execute({ count: 0 });
|
||||
const resultB = await chain.execute({ count: 0 });
|
||||
|
||||
expect(resultA.count).toBe(1);
|
||||
expect(resultB.count).toBe(1);
|
||||
});
|
||||
|
||||
it("should share middlewares across execute calls", async () => {
|
||||
const chain = createMiddlewareChain<{ log: string[] }>();
|
||||
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.log.push("always");
|
||||
return next();
|
||||
});
|
||||
|
||||
await chain.execute({ log: [] });
|
||||
await chain.execute({ log: [] });
|
||||
|
||||
expect(chain).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty context object", async () => {
|
||||
const chain = createMiddlewareChain<Record<string, never>>();
|
||||
const result = await chain.execute({});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it("should handle middleware that returns a completely different object", async () => {
|
||||
const chain = createMiddlewareChain<{ x: number }, { y: string }>(
|
||||
async () => ({ y: "default" }),
|
||||
);
|
||||
|
||||
chain.use(async (_ctx, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ x: 42 });
|
||||
|
||||
expect(result).toEqual({ y: "default" });
|
||||
});
|
||||
|
||||
it("should handle middleware that mutates context without returning", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>(
|
||||
async (ctx) => ctx,
|
||||
);
|
||||
|
||||
chain.use(async (ctx, next) => {
|
||||
ctx.value = 100;
|
||||
return next();
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 0 });
|
||||
|
||||
expect(result.value).toBe(100);
|
||||
});
|
||||
|
||||
it("should return undefined when middleware does not call next or return", async () => {
|
||||
const chain = createMiddlewareChain<{ value: number }>();
|
||||
|
||||
chain.use(async (ctx) => {
|
||||
ctx.value = 100;
|
||||
return undefined as unknown as { value: number };
|
||||
});
|
||||
|
||||
const result = await chain.execute({ value: 0 });
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue