- 上下文清单、任务列表与详情、恢复控制、运行观察、创作对话与消息流、正式变更决策面板、正文写作与候选审阅、资料研究等页面同步本轮后端能力。 - 事件标签表补齐后端事件类型;查询键与生成客户端随 HTTP 契约重新生成。 - 依赖调整后 lockfile 同步;交互用例与浏览器旅程 spec 一并更新。 - 证据:make 前端检查(lint + typecheck + Pi 工具桥 tsc)exit 0;make 前端测试 37 文件 / 120 用例通过。
182 lines
6.4 KiB
TypeScript
182 lines
6.4 KiB
TypeScript
import {
|
||
cleanup,
|
||
fireEvent,
|
||
render,
|
||
screen,
|
||
waitFor,
|
||
} from "@testing-library/react";
|
||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||
import { MemoryRouter } from "react-router-dom";
|
||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||
import { Ui有限返修 } from "../../src/功能/审校修订/有限返修";
|
||
|
||
const api = vi.hoisted(() => ({ GET: vi.fn(), POST: vi.fn() }));
|
||
vi.mock("../../src/接口/生成/客户端", () => ({ 客户端: api }));
|
||
vi.mock("../../src/应用/作者会话", () => ({
|
||
useMuse作者会话: () => ({ data: { author_id: "a" } }),
|
||
}));
|
||
const base = {
|
||
session_id: "s1",
|
||
state: "awaiting_author",
|
||
current_round: 1,
|
||
max_rounds: 2,
|
||
stop_reason: null,
|
||
author_choice: null,
|
||
target: { work_id: "w1", chapter_id: "c1", branch_id: "main", revision: 7 },
|
||
rounds: [
|
||
{
|
||
round: 1,
|
||
task_id: "t1",
|
||
task_state: "completed",
|
||
outcome: "candidate",
|
||
candidate_id: "candidate-1",
|
||
},
|
||
],
|
||
};
|
||
function show() {
|
||
return render(
|
||
<QueryClientProvider
|
||
client={
|
||
new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||
}
|
||
>
|
||
<MemoryRouter>
|
||
<Ui有限返修 sessionId="s1" />
|
||
</MemoryRouter>
|
||
</QueryClientProvider>,
|
||
);
|
||
}
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
api.GET.mockResolvedValue({ data: base });
|
||
});
|
||
afterEach(cleanup);
|
||
|
||
// @muse-case {"case_id":"NC-ui-revision-session-wait","environment":"前端","given":"合成资料、真实S02配置与明确作者授权","when":"通过实际返修会话和作者入口执行并重新读取","then":["NC-ui-revision-session-wait:任务尚未完成不能点击下一轮"],"contract":"docs/系统架构/新版设计/模块设计/B06-审校修订.md"}
|
||
it("NC-ui-revision-session-wait:任务尚未完成不能点击下一轮", async () => {
|
||
api.GET.mockResolvedValue({
|
||
data: {
|
||
...base,
|
||
state: "running",
|
||
rounds: [
|
||
{
|
||
...base.rounds[0],
|
||
task_state: "running",
|
||
candidate_id: null,
|
||
outcome: null,
|
||
},
|
||
],
|
||
},
|
||
});
|
||
show();
|
||
expect(await screen.findByText(/尚未交付结果/)).toBeTruthy();
|
||
expect(
|
||
screen.getByRole("link", { name: "查看本轮任务" }).getAttribute("href"),
|
||
).toBe("/tasks?task=t1");
|
||
expect(screen.queryByRole("button", { name: "请写手再试一轮" })).toBeNull();
|
||
expect(api.POST).not.toHaveBeenCalled();
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-revision-session-retry","environment":"前端","given":"合成资料、真实S02配置与明确作者授权","when":"通过实际返修会话和作者入口执行并重新读取","then":["NC-ui-revision-session-retry:明确重试提交精确轮次和候选后重读"],"contract":"docs/系统架构/新版设计/模块设计/B06-审校修订.md"}
|
||
it("NC-ui-revision-session-retry:明确重试提交精确轮次和候选后重读", async () => {
|
||
show();
|
||
const button = await screen.findByRole("button", { name: "请写手再试一轮" });
|
||
api.POST.mockImplementation(async () => {
|
||
api.GET.mockResolvedValue({
|
||
data: {
|
||
...base,
|
||
state: "running",
|
||
current_round: 2,
|
||
rounds: [
|
||
...base.rounds,
|
||
{
|
||
round: 2,
|
||
task_id: "t2",
|
||
task_state: "pending",
|
||
outcome: null,
|
||
candidate_id: null,
|
||
},
|
||
],
|
||
},
|
||
});
|
||
return { data: {} };
|
||
});
|
||
fireEvent.click(button);
|
||
await screen.findByText(/第 2 \/ 2 轮/);
|
||
expect(api.POST).toHaveBeenCalledWith(
|
||
"/api/v1/revision-sessions/{session_id}/decisions",
|
||
{
|
||
params: { path: { session_id: "s1" } },
|
||
body: {
|
||
session_id: "s1",
|
||
expected_round: 1,
|
||
candidate_id: "candidate-1",
|
||
choice: "retry",
|
||
command_id: expect.any(String),
|
||
},
|
||
},
|
||
);
|
||
expect(screen.queryByRole("button", { name: "请写手再试一轮" })).toBeNull();
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-revision-session-limit","environment":"前端","given":"合成资料、真实S02配置与明确作者授权","when":"通过实际返修会话和作者入口执行并重新读取","then":["NC-ui-revision-session-limit:轮数用完仍能选择原文且不自动采纳"],"contract":"docs/系统架构/新版设计/模块设计/B06-审校修订.md"}
|
||
it("NC-ui-revision-session-limit:轮数用完仍能选择原文且不自动采纳", async () => {
|
||
api.GET.mockResolvedValue({
|
||
data: {
|
||
...base,
|
||
state: "closed",
|
||
max_rounds: 1,
|
||
stop_reason: "round_limit",
|
||
},
|
||
});
|
||
show();
|
||
expect(await screen.findByText(/已达到预先约定/)).toBeTruthy();
|
||
expect(
|
||
(
|
||
screen.getByRole("button", {
|
||
name: "请写手再试一轮",
|
||
}) as HTMLButtonElement
|
||
).disabled,
|
||
).toBe(true);
|
||
api.POST.mockImplementation(async () => {
|
||
api.GET.mockResolvedValue({
|
||
data: {
|
||
...base,
|
||
state: "closed",
|
||
max_rounds: 1,
|
||
stop_reason: "round_limit",
|
||
author_choice: "original",
|
||
},
|
||
});
|
||
return { data: {} };
|
||
});
|
||
fireEvent.click(screen.getByRole("button", { name: "保留原文并停止" }));
|
||
await screen.findByText("你已选择保留原文。");
|
||
expect(api.POST.mock.calls[0][1].body.choice).toBe("original");
|
||
expect(api.POST.mock.calls[0][0]).not.toContain("writing-candidates");
|
||
});
|
||
|
||
// @muse-case {"case_id":"NC-ui-revision-session-replay","environment":"前端","given":"合成资料、真实S02配置与明确作者授权","when":"通过实际返修会话和作者入口执行并重新读取","then":["NC-ui-revision-session-replay:保存失败保留错误并用原命令重试"],"contract":"docs/系统架构/新版设计/模块设计/B06-审校修订.md"}
|
||
it("NC-ui-revision-session-replay:保存失败保留错误并用原命令重试", async () => {
|
||
api.POST.mockRejectedValue(new Error("连接中断"));
|
||
show();
|
||
fireEvent.click(
|
||
await screen.findByRole("button", { name: "保留原文并停止" }),
|
||
);
|
||
await screen.findByRole("alert");
|
||
await waitFor(() =>
|
||
expect(
|
||
(
|
||
screen.getByRole("button", {
|
||
name: "保留原文并停止",
|
||
}) as HTMLButtonElement
|
||
).disabled,
|
||
).toBe(false),
|
||
);
|
||
fireEvent.click(screen.getByRole("button", { name: "保留原文并停止" }));
|
||
await waitFor(() => expect(api.POST).toHaveBeenCalledTimes(2));
|
||
expect(api.POST.mock.calls[0][1].body.command_id).toBe(
|
||
api.POST.mock.calls[1][1].body.command_id,
|
||
);
|
||
});
|