- 上下文清单、任务列表与详情、恢复控制、运行观察、创作对话与消息流、正式变更决策面板、正文写作与候选审阅、资料研究等页面同步本轮后端能力。 - 事件标签表补齐后端事件类型;查询键与生成客户端随 HTTP 契约重新生成。 - 依赖调整后 lockfile 同步;交互用例与浏览器旅程 spec 一并更新。 - 证据:make 前端检查(lint + typecheck + Pi 工具桥 tsc)exit 0;make 前端测试 37 文件 / 120 用例通过。
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
/** DOM组合事件与真实编辑器组件;不冒称操作系统原生输入法验收。 */
|
||
import { StrictMode } from "react";
|
||
import { afterEach, expect, it, vi } from "vitest";
|
||
import {
|
||
cleanup,
|
||
fireEvent,
|
||
render,
|
||
screen,
|
||
waitFor,
|
||
} from "@testing-library/react";
|
||
import { MemoryRouter, Route, Routes } from "react-router-dom";
|
||
import { Ui依赖装配 } from "../../src/应用/依赖装配";
|
||
import { Ui正文页 } from "../../src/功能/正文写作";
|
||
|
||
afterEach(() => {
|
||
cleanup();
|
||
vi.unstubAllGlobals();
|
||
localStorage.clear();
|
||
});
|
||
// @muse-case {"case_id":"NC-editor-ime-save","environment":"离线组件/实际编辑器;HTTP响应替身不计真实链路","given":"StrictMode和真实编辑器组件","when":"触发DOM组合输入并结束组合","then":["组合期间无提交,结束后提交完整中文;不计原生OS输入法验收"],"contract":"docs/系统架构/新版设计/接口契约/正文结构与中文选区.md"}
|
||
it("NC-editor-ime-save:StrictMode编辑会话在组合结束后保存完整中文", async () => {
|
||
const writes: unknown[] = [];
|
||
vi.stubGlobal(
|
||
"fetch",
|
||
vi.fn(async (request: Request) => {
|
||
const url = new URL(request.url);
|
||
let data: unknown;
|
||
if (url.pathname.endsWith("session")) data = { author_id: "author" };
|
||
else if (url.pathname.endsWith("chapters"))
|
||
data = {
|
||
work_id: "work",
|
||
revision: 1,
|
||
chapters: [{ chapter_id: "chapter", title: "第一章", position: 1 }],
|
||
};
|
||
else if (request.method === "POST") {
|
||
const body = await request.json();
|
||
writes.push(body);
|
||
data = {
|
||
receipt_id: "receipt",
|
||
results: [
|
||
{
|
||
chapter_id: "chapter",
|
||
branch_id: "main",
|
||
revision: 1,
|
||
document_hash: "hash1",
|
||
},
|
||
],
|
||
};
|
||
} else
|
||
data = {
|
||
work_id: "work",
|
||
document_id: "doc",
|
||
chapter_id: "chapter",
|
||
branch_id: "main",
|
||
revision: 0,
|
||
state: "unsaved",
|
||
document_hash: "hash0",
|
||
visible_text_hash: "text0",
|
||
visible_text: "",
|
||
document: {
|
||
format_version: 1,
|
||
paragraphs: [{ paragraph_id: "p0", content: [] }],
|
||
},
|
||
};
|
||
return new Response(JSON.stringify(data), {
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
}),
|
||
);
|
||
render(
|
||
<StrictMode>
|
||
<Ui依赖装配>
|
||
<MemoryRouter initialEntries={["/works/work/write/chapter"]}>
|
||
<Routes>
|
||
<Route
|
||
path="/works/:work_id/write/:chapter_id"
|
||
element={<Ui正文页 />}
|
||
/>
|
||
</Routes>
|
||
</MemoryRouter>
|
||
</Ui依赖装配>
|
||
</StrictMode>,
|
||
);
|
||
const editor = await screen.findByRole("textbox", { name: "章节正文" });
|
||
fireEvent.compositionStart(editor);
|
||
editor.querySelector("p")!.textContent = " “中文”e\u0301";
|
||
fireEvent.input(editor, {
|
||
inputType: "insertCompositionText",
|
||
data: " “中文”e\u0301",
|
||
isComposing: true,
|
||
});
|
||
await waitFor(() => expect(screen.getByText("正在输入…")).toBeTruthy());
|
||
fireEvent.click(screen.getByRole("button", { name: "立即保存" }));
|
||
expect(writes).toHaveLength(0);
|
||
fireEvent.compositionEnd(editor);
|
||
await waitFor(() => expect(writes).toHaveLength(1), { timeout: 3000 });
|
||
expect(writes[0]).toMatchObject({
|
||
chapter_id: "chapter",
|
||
expected_revision: 0,
|
||
document: { paragraphs: [{ content: [{ text: " “中文”e\u0301" }] }] },
|
||
});
|
||
expect(await screen.findByText("服务器已保存")).toBeTruthy();
|
||
});
|