import { describe, expect, it } from "vitest"; import { canAdvance, canSubmit, clampStep, emptyWizardForm, STEP_TITLES, toCreateRequest, WIZARD_STEPS, type WizardForm, } from "./wizard"; describe("wizard step metadata", () => { it("provides one title per step", () => { expect(STEP_TITLES).toHaveLength(WIZARD_STEPS); }); it("ends on a confirmation step rather than an empty world step", () => { expect(STEP_TITLES[WIZARD_STEPS - 1]).toBe("确认与提交"); }); }); describe("wizard step gating", () => { it("blocks advancing past step 1 without a title", () => { expect(canAdvance(1, emptyWizardForm)).toBe(false); }); it("allows advancing step 1 once title is set", () => { expect(canAdvance(1, { ...emptyWizardForm, title: "逐光而行" })).toBe(true); }); it("allows advancing later steps even when fields are empty", () => { expect(canAdvance(3, emptyWizardForm)).toBe(true); }); it("clamps step within bounds", () => { expect(clampStep(0)).toBe(1); expect(clampStep(WIZARD_STEPS + 3)).toBe(WIZARD_STEPS); expect(clampStep(3)).toBe(3); }); it("requires a title to submit", () => { expect(canSubmit(emptyWizardForm)).toBe(false); expect(canSubmit({ ...emptyWizardForm, title: "x" })).toBe(true); }); }); describe("toCreateRequest", () => { it("maps form to snake_case request, nulling empty optionals", () => { const form: WizardForm = { title: " 逐光而行 ", genre: "玄幻", logline: "", sellingPoints: ["逆袭", "系统流"], structure: "三幕", premise: " ", protagonist: "", theme: "抗争", }; expect(toCreateRequest(form)).toEqual({ title: "逐光而行", genre: "玄幻", logline: null, premise: null, theme: "抗争", selling_points: ["逆袭", "系统流"], structure: "三幕", }); }); // QA H1 回归:立意(premise)与主角/金手指(protagonist)是两个独立输入, // 不能互相覆盖——提交时各占一段合并进 premise。 it("merges premise and protagonist into premise without overwriting", () => { const form: WizardForm = { ...emptyWizardForm, title: "书", premise: "凡人逆袭的代价", protagonist: "主角林川,金手指=吞噬术,限制:每次吞噬折寿", }; const req = toCreateRequest(form); expect(req.premise).toBe( "凡人逆袭的代价\n\n主角/金手指:主角林川,金手指=吞噬术,限制:每次吞噬折寿", ); }); it("keeps protagonist alone when premise is empty", () => { const form: WizardForm = { ...emptyWizardForm, title: "书", protagonist: "主角设定", }; expect(toCreateRequest(form).premise).toBe("主角/金手指:主角设定"); }); }); // wizard→create 流程:归一后的请求体经 mock 客户端提交,返回新建项目 id。 describe("wizard create flow (mocked client)", () => { it("submits the normalized body and resolves a project id", async () => { const post = async ( _path: string, opts: { body: ReturnType }, ): Promise<{ data: { id: string }; error: null }> => { expect(opts.body.title).toBe("青冥录"); expect(opts.body.selling_points).toEqual(["群像"]); return { data: { id: "proj-123" }, error: null }; }; const form: WizardForm = { ...emptyWizardForm, title: "青冥录", sellingPoints: ["群像"], }; const { data } = await post("/projects", { body: toCreateRequest(form) }); expect(data.id).toBe("proj-123"); }); });