muddle/tests/core.test.ts
2026-10-23 09:12:49 -04:00

148 lines
3.9 KiB
TypeScript

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<string, never>");
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 });
});
});