import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; import { adminApi } from "../api/client"; import type { AdminRole, OfficialSkillCatalog } from "../api/types"; import { App } from "../app"; afterEach(() => { cleanup(); vi.restoreAllMocks(); window.location.hash = ""; }); describe("内容管理", () => { it("SUPPORT 可查看但不能修改 Skill 与 Hint", async () => { mockSession("SUPPORT"); mockContent(); window.location.hash = "#/content"; render(); expect(await screen.findByRole("heading", { name: "内容管理" })).toBeTruthy(); expect(screen.getByRole("link", { name: /内容管理/ })).toBeTruthy(); expect(await screen.findByText("润色")).toBeTruthy(); expect(screen.getByText("只读访问")).toBeTruthy(); expect(screen.queryByRole("button", { name: "新增 Skill" })).toBeNull(); const editor = await screen.findByLabelText("zh JSON"); expect(editor).toHaveProperty("readOnly", true); expect(screen.queryByRole("button", { name: "保存" })).toBeNull(); expect(screen.queryByRole("button", { name: "立即生成" })).toBeNull(); expect(screen.queryByRole("button", { name: "保存生成设置" })).toBeNull(); }); it("SUPER_ADMIN 可启停官方 Skill", async () => { mockSession("SUPER_ADMIN"); mockContent(); const mutation = vi .spyOn(adminApi, "setContentSkillEnabled") .mockResolvedValue(undefined); window.location.hash = "#/content"; render(); await userEvent.click(await screen.findByRole("button", { name: "停用" })); await waitFor(() => { expect(mutation).toHaveBeenCalledWith("official.polish", false); }); expect(screen.getByRole("button", { name: "新增 Skill" })).toBeTruthy(); }); it("Skill 编辑器约束与客户端边界一致", async () => { mockSession("SUPER_ADMIN"); mockContent(); window.location.hash = "#/content"; render(); await userEvent.click(await screen.findByRole("button", { name: "新增 Skill" })); expect(screen.getByLabelText("Skill ID").getAttribute("maxlength")).toBe("100"); expect(screen.getByLabelText("SF Symbol").getAttribute("maxlength")).toBe("100"); const sortOrder = screen.getByLabelText("排序"); expect(sortOrder.getAttribute("min")).toBe("0"); expect(sortOrder.getAttribute("max")).toBe("100000"); screen.getAllByLabelText("名称").forEach((input) => { expect(input.getAttribute("maxlength")).toBe("40"); }); screen.getAllByLabelText("摘要").forEach((input) => { expect(input.getAttribute("maxlength")).toBe("200"); }); screen.getAllByLabelText("Prompt").forEach((input) => { expect(input.getAttribute("maxlength")).toBe("6000"); }); }); it("SUPER_ADMIN 可手动触发双语 Hint 原子生成", async () => { mockSession("SUPER_ADMIN"); mockContent(); vi.spyOn(window, "confirm").mockReturnValue(true); const regenerate = vi.spyOn(adminApi, "regenerateHintFeed").mockResolvedValue({ generationId: "00000000-0000-0000-0000-000000000001", generatedAt: "2026-08-21T06:00:00Z", zh: { version: 3, cardCount: 20 }, en: { version: 3, cardCount: 25 }, }); window.location.hash = "#/content"; render(); await userEvent.click(await screen.findByRole("button", { name: "立即生成" })); await waitFor(() => expect(regenerate).toHaveBeenCalledOnce()); }); it("SUPER_ADMIN 可编辑并保存当前生效的 Hint pack", async () => { mockSession("SUPER_ADMIN"); mockContent(); const save = vi.spyOn(adminApi, "updateContentHintPack").mockResolvedValue({ locale: "zh", generatedAt: "2026-08-21T06:00:00Z", intervalHours: 12, version: 3, cards: [], }); window.location.hash = "#/content"; render(); await userEvent.click(await screen.findByRole("button", { name: "保存" })); await waitFor(() => expect(save).toHaveBeenCalledOnce()); expect(await screen.findByText("version 3")).toBeTruthy(); }); }); function mockSession(role: AdminRole) { vi.spyOn(adminApi, "session").mockResolvedValue({ authenticated: true, operatorName: "owner", role, }); } function mockContent() { vi.spyOn(adminApi, "contentSkills").mockResolvedValue(catalog()); vi.spyOn(adminApi, "contentHintPack").mockResolvedValue({ locale: "zh", generatedAt: "2026-08-21T04:00:00Z", intervalHours: 12, version: 2, cards: [], }); vi.spyOn(adminApi, "hintFeedSettings").mockResolvedValue({ enabled: true, topHubApiKeyConfigured: false, generationIntervalHours: 12, holidayCountriesZh: "CN", holidayCountriesEn: "US,GB", weatherCitiesZh: "北京:39.90,116.40", weatherCitiesEn: "London:51.51,-0.13", googleTrendsGeos: "US,GB", }); vi.spyOn(adminApi, "hintFeedStatus").mockResolvedValue({ enabled: true, outcome: "SUCCEEDED", intervalHours: 12, topHubApiKeyConfigured: false, zhVersion: 2, zhCardCount: 20, enVersion: 2, enCardCount: 25, }); } function catalog(): OfficialSkillCatalog { return { revision: 3, generatedAt: "2026-08-21T04:00:00Z", skills: [ { id: "official.polish", systemImage: "wand.and.sparkles", sortOrder: 10, kind: "transform", thinkingEnabled: false, enabled: true, localizations: { "zh-Hans": { name: "润色", summary: "优化表达", prompt: "请润色", }, en: { name: "Polish", summary: "Improve wording", prompt: "Please polish", }, }, }, ], }; }