Files
buzz/components/form-submit.test.ts

120 lines
4.1 KiB
TypeScript

import { describe, expect, mock, test } from "bun:test";
import { z } from "zod";
import { defineFormHandler, formParse } from "./form-submit";
function form(entries: Record<string, string | Blob>) {
const data = new FormData();
for (const [key, value] of Object.entries(entries)) data.append(key, value);
return data;
}
const schema = z.object({
name: z.string().min(2, "Name is too short"),
age: z.coerce.number().int().positive("Age must be positive"),
});
describe("defineFormHandler", () => {
test("returns a schema-less action unchanged", async () => {
const action = mock(async (data: FormData) => ({
toast: String(data.get("name")),
}));
const handler = defineFormHandler(action);
const data = form({ name: "Ada" });
expect(handler).toBe(action);
expect(await handler(data)).toEqual({ toast: "Ada" });
expect(action).toHaveBeenCalledWith(data);
});
test("parses and transforms valid form data before calling the action", async () => {
const action = mock(async (input: z.output<typeof schema>) => ({
toast: `${input.name}:${input.age}`,
close: true,
}));
const handler = defineFormHandler(schema, action);
expect(await handler(form({ name: "Ada", age: "42" }))).toEqual({
toast: "Ada:42",
close: true,
});
expect(action).toHaveBeenCalledWith({ name: "Ada", age: 42 });
});
test("maps all field issues and does not call the action", async () => {
const action = mock(async () => ({ reset: true }));
const handler = defineFormHandler(schema, action);
expect(await handler(form({ name: "A", age: "0" }))).toEqual({
error: [
{ what: "Name is too short", where: "name" },
{ what: "Age must be positive", where: "age" },
],
});
expect(action).not.toHaveBeenCalled();
});
test("joins nested issue paths with dots", async () => {
const nested = z.object({ "profile.email": z.email("Bad email") });
const handler = defineFormHandler(nested, async () => undefined);
expect(await handler(form({ "profile.email": "no" }))).toEqual({
error: [{ what: "Bad email", where: "profile.email" }],
});
});
test("uses the last value when a field occurs more than once", async () => {
const data = form({ name: "first", age: "1" });
data.append("name", "last");
const handler = defineFormHandler(schema, async (input) => ({
toast: input.name,
}));
expect(await handler(data)).toEqual({ toast: "last" });
});
test("propagates errors thrown by the action", async () => {
const failure = new Error("action failed");
const handler = defineFormHandler(schema, async () => {
throw failure;
});
expect(handler(form({ name: "Ada", age: "42" }))).rejects.toBe(failure);
});
});
describe("formParse", () => {
test("returns transformed data for valid forms", () => {
const result = formParse(schema, form({ name: "Ada", age: "42" }));
expect(result.$).toEqual({ name: "Ada", age: 42 });
expect(result.error).toBeUndefined();
});
test("returns issue descriptors for field errors", () => {
const result = formParse(schema, form({ name: "A", age: "0" }));
expect(result.$).toBeUndefined();
expect(result.error).toEqual([
{ what: "Name is too short", where: "name" },
{ what: "Age must be positive", where: "age" },
]);
});
test("returns a string for a single root-level error", () => {
const rootSchema = z
.object({ code: z.string() })
.refine(({ code }) => code === "valid", "Invalid form");
const result = formParse(rootSchema, form({ code: "wrong" }));
expect(result.$).toBeUndefined();
expect(result.error).toBe("Invalid form");
});
test("keeps root errors in the issue list when there are multiple issues", () => {
const multiple = z
.object({ value: z.string().min(2, "Too short") })
.refine(() => false, "Invalid form");
const result = formParse(multiple, form({ value: "x" }));
expect(result.$).toBeUndefined();
expect(result.error).toEqual([
{ what: "Too short", where: "value" },
{ what: "Invalid form", where: "" },
]);
});
});