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",
},
},
},
],
};
}