b25f5ae6e9
Keep manual editing while making save the only user action and applying changes immediately without a separate publish workflow.
181 lines
5.7 KiB
TypeScript
181 lines
5.7 KiB
TypeScript
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(<App />);
|
|
|
|
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(<App />);
|
|
|
|
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(<App />);
|
|
|
|
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(<App />);
|
|
|
|
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(<App />);
|
|
|
|
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",
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
}
|