muse-agent-example/web/tests/交互/候选审阅.test.tsx
zizi 98530437f2 W11 编辑器、动态表单与人审工作台:编辑器、动态表单与人审工作台(确认前正文不落正式库)。
按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
2026-09-10 19:25:41 +08:00

210 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { afterEach, expect, it, vi } from "vitest";
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import { Ui决策面板 } from "../../src/功能/正式变更/决策面板";
import type { 候选审阅 } from "../../src/功能/正式变更/数据";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { Ui依赖装配 } from "../../src/应用/依赖装配";
import { Ui候选审阅页 } from "../../src/功能/正文写作/候选审阅";
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
sessionStorage.clear();
});
const document = {
format_version: 1,
paragraphs: [
{
paragraph_id: "p1",
content: [{ type: "text" as const, text: "合成原文", marks: [] }],
},
],
};
const review: 候选审阅 = {
review_id: "review-original",
review_hash: "review-hash",
snapshot: {},
before: {
work_id: "work",
chapter_id: "chapter",
branch_id: "main",
document_id: "document",
revision: 3,
document,
document_hash: "base-hash",
visible_text_hash: "text-hash",
visible_text: "合成原文",
state: "saved",
},
candidate: {
candidate_id: "candidate",
revision: 2,
candidate_hash: "candidate-hash",
document_id: "document",
chapter_id: "chapter",
branch_id: "main",
origin: "author",
base_revision: 3,
current_document_revision: 3,
document,
visible_text: "合成改稿",
checks: { format: "passed", format_version: 1, quality: "not_applicable" },
decision: "undecided",
stale: false,
changes: [],
},
};
it("NC-review-fixed-request-retry:响应未知后保留原审阅与命令,刷新后仍重放同一决定", async () => {
const requests: unknown[] = [];
vi.stubGlobal(
"fetch",
vi.fn(async (request: Request) => {
requests.push(await request.json());
if (requests.length === 1) throw new Error("synthetic lost response");
return new Response(
JSON.stringify({
receipt_id: "receipt-one",
action: "adopt",
results: [],
}),
{ headers: { "Content-Type": "application/json" } },
);
}),
);
const view = render(
<Ui决策面板 review={review} author="author" onReopen={() => {}} />,
);
fireEvent.click(screen.getByRole("button", { name: "采纳为正文新版本" }));
await screen.findByRole("alert");
const retry = screen.getByRole("button", { name: "重试这次决定" });
expect(retry).toBeTruthy();
expect(screen.queryByRole("button", { name: "拒绝候选" })).toBeNull();
view.unmount();
render(
<Ui决策面板
review={{ ...review, review_id: "new-view-only" }}
author="author"
onReopen={() => {}}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "重试这次决定" }));
await screen.findByText("已采纳");
expect(requests).toHaveLength(2);
expect(requests[1]).toEqual(requests[0]);
expect(requests[0]).toMatchObject({
candidate_revision: 2,
candidate_hash: "candidate-hash",
expected_revision: 3,
author_review_id: "review-original",
approved_changes: ["document"],
});
expect(sessionStorage.length).toBe(0);
});
it("NC-review-conflict-reopen:明确冲突停止决定并重新展示,不以当前版本悄悄替换基线", async () => {
const calls = vi.fn(
async () =>
new Response(
JSON.stringify({ code: "REVISION_CONFLICT", message: "候选已经更新" }),
{ status: 409, headers: { "Content-Type": "application/json" } },
),
);
vi.stubGlobal("fetch", calls);
const reopen = vi.fn();
render(<Ui决策面板 review={review} author="author" onReopen={reopen} />);
fireEvent.click(screen.getByRole("button", { name: "采纳为正文新版本" }));
await screen.findByText("候选已经更新");
expect(screen.queryByRole("button", { name: "采纳为正文新版本" })).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "重新打开审阅" }));
await waitFor(() => expect(reopen).toHaveBeenCalledOnce());
expect(calls).toHaveBeenCalledOnce();
expect(sessionStorage.length).toBe(0);
});
it("NC-candidate-stale-buffer-choice:明确放弃旧缓冲后能基于当前正文另拟改稿", async () => {
const key = "muse:candidate-draft:author:work:chapter:new";
const current = { ...review.before, revision: 2 };
const draft = {
...document,
paragraphs: [
{
paragraph_id: "p1",
content: [{ type: "text", text: "尚未提交的旧改稿", marks: [] }],
},
],
};
sessionStorage.setItem(
key,
JSON.stringify({
document: draft,
base_revision: 1,
mode: "edit",
selected: [],
}),
);
const sent: unknown[] = [];
vi.stubGlobal(
"fetch",
vi.fn(async (request: Request) => {
const path = new URL(request.url).pathname;
if (request.method === "POST") sent.push(await request.json());
const data =
request.method === "POST"
? { receipt_id: "candidate-saved", results: [] }
: path.endsWith("session")
? { author_id: "author" }
: path.endsWith("writing-candidates")
? []
: current;
return new Response(JSON.stringify(data), {
headers: { "Content-Type": "application/json" },
});
}),
);
render(
<Ui依赖装配>
<MemoryRouter initialEntries={["/works/work/candidates/chapter"]}>
<Routes>
<Route
path="/works/:work_id/candidates/:chapter_id"
element={<Ui候选审阅页 />}
/>
</Routes>
</MemoryRouter>
</Ui依赖装配>,
);
fireEvent.click(await screen.findByRole("button", { name: "另拟改稿" }));
expect(
screen.getByRole("textbox", { name: "候选改稿" }).textContent,
).toContain("尚未提交的旧改稿");
expect(
(screen.getByRole("button", { name: "保存候选版本" }) as HTMLButtonElement)
.disabled,
).toBe(true);
fireEvent.click(
screen.getByRole("button", { name: "放弃旧改稿,返回当前版本" }),
);
expect(sessionStorage.getItem(key)).toBeNull();
expect(sent).toHaveLength(0);
fireEvent.click(screen.getByRole("button", { name: "另拟改稿" }));
expect(
screen.getByRole("textbox", { name: "候选改稿" }).textContent,
).toContain("合成原文");
const save = screen.getByRole("button", { name: "保存候选版本" });
expect((save as HTMLButtonElement).disabled).toBe(false);
fireEvent.click(save);
await waitFor(() => expect(sent).toHaveLength(1));
expect(sent[0]).toMatchObject({
expected_revision: 2,
document: current.document,
});
});