import { describe, expect, test } from "bun:test"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { generate, renderProps } from "../contracts/typegen/generator.ts"; import { Container } from "../core/container.ts"; import { Module } from "../decorators/Module.ts"; import { Service } from "../decorators/Service.ts"; import { Controller } from "../decorators/Controller.ts"; import { Route } from "../decorators/Route.ts"; import { validate, toFormFields } from "../core/form.ts"; /** * Tests of the container, of the modules and of the type generator. */ describe("container", () => { test("resolves a dependency by type", () => { @Service() class UserService { find() { return "ada"; } } @Controller("/") class UserController { constructor(private readonly users: UserService) {} find() { return this.users.find(); } } const container = new Container(); container.registerClass(UserService); container.registerClass(UserController); const controller = container.resolve(UserController); expect(controller.find()).toBe("ada"); }); test("awaits an asynchronous provider", async () => { @Service() class AsyncService { async load() { return "loaded"; } } const container = new Container(); container.registerFactory( AsyncService, async () => new AsyncService(), ); const service = await container.resolveAsync(AsyncService); expect(await service.load()).toBe("loaded"); }); test("reports an unknown dependency", () => { const container = new Container(); expect(() => container.resolve("missing")).toThrow("Cannot resolve"); }); }); describe("modules", () => { test("collects the controllers of a module", () => { @Controller("/") class HomeController { @Route("/") index() { return "home"; } } @Module({ controllers: [HomeController] }) class AppModule {} const container = new Container(); container.registerClass(HomeController); expect(AppModule).toBeDefined(); }); }); describe("forms", () => { test("reads the fields of a form", () => { const fields = toFormFields({ title: "Hello", tags: ["a", "b"] }); expect(fields.get("title")).toBe("Hello"); expect(fields.has("tags")).toBe(true); expect(fields.all().tags).toEqual(["a", "b"]); }); test("validates the fields", () => { const fields = toFormFields({ title: "Hello", age: "42" }); const result = validate<{ title: string; age: number }>(fields, { title: { required: true, maxLength: 10 }, age: { type: "number", min: 18 }, }); expect(result.ok).toBe(true); if (result.ok) { expect(result.value.age).toBe(42); } }); test("reports the invalid fields", () => { const fields = toFormFields({ title: "", age: "abc" }); const result = validate(fields, { title: { required: true }, age: { type: "number" }, }); expect(result.ok).toBe(false); expect(result.errors.title).toBe("title is required"); expect(result.errors.age).toBe("age must be a number"); }); }); describe("typegen", () => { test("renders the props of a view", () => { expect(renderProps([])).toBe("Record"); expect( renderProps([ { name: "title", type: "string", optional: false }, { name: "body", type: "string", optional: true }, ]), ).toBe("{ title: string; body?: string }"); }); test("generates the contracts of the templates", async () => { const root = await mkdtemp(join(tmpdir(), "muddle-typegen-")); await writeFile( join(root, "index.spug"), "props\n title: string\n count?: number\n\np= title", "utf8", ); const result = await generate({ root, out: join(root, "out"), viewRoots: ["."], }); expect(result.views).toHaveLength(1); expect(result.views[0]?.key).toBe("index"); expect(result.views[0]?.props.map((prop) => prop.name)).toEqual([ "title", "count", ]); await rm(root, { recursive: true, force: true }); }); });