68 lines
4.0 KiB
TypeScript
68 lines
4.0 KiB
TypeScript
import { act, fireEvent, render, screen, cleanup } from "@testing-library/react";
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
import type { AnswerViewStateRecord, AnswerViewStateWrite } from "../api/answer-view-state";
|
|
import { AnswerViewStateScope } from "../session-state/answer-view-state-scope";
|
|
import { AnswerViewPersistenceContext } from "../session-state/answer-view-persistence-context";
|
|
import type { AnswerUiBlock } from "./contract";
|
|
import { AnswerPartsView } from "./answer-parts-view";
|
|
|
|
vi.mock("../components/streaming-token-response", () => ({ StreamingTokenResponse: () => null }));
|
|
afterEach(() => { cleanup(); vi.useRealTimers(); });
|
|
const block: AnswerUiBlock = {
|
|
version: "answer-ui@1", block_id: "block", phase: "final", fallback_text: "资料",
|
|
spec: { root: "form", elements: { form: { type: "FollowUpForm", children: [], props: {
|
|
title: "资料", description: "补充", fields: [{ name: "note", label: "现场记录", kind: "text", required: false }]
|
|
} } } }
|
|
};
|
|
const remote: AnswerViewStateRecord = {
|
|
version: "answer-view-state@1", message_id: "message", block_id: "block", revision: 4,
|
|
schema_digest: "digest", updated_at: null, nodes: { form: { type: "FollowUpForm", values: { note: "远端" } } }, block: { evidenceFilter: "all" }
|
|
};
|
|
function fixture() {
|
|
let restore!: (value: AnswerViewStateRecord) => void;
|
|
const client = { getAnswerViewState: vi.fn(() => new Promise<AnswerViewStateRecord>((resolve) => { restore = resolve; })),
|
|
putAnswerViewState: vi.fn(async (_session: string, write: AnswerViewStateWrite) => ({ ...remote, revision: 5, nodes: write.nodes, block: write.block })) };
|
|
const scope = new AnswerViewStateScope("actor:project", client);
|
|
const view = (persist: boolean, value = block) => <AnswerViewPersistenceContext.Provider value={{ scope, sessionId: "session" }}>
|
|
<AnswerPartsView parts={[{ type: "answer-ui", block: value }]} messageId="message" disabled={false} streaming={!persist} streamDone={persist} persist={persist} />
|
|
</AnswerViewPersistenceContext.Provider>;
|
|
return { scope, client, view, restore: (record: AnswerViewStateRecord) => restore(record) };
|
|
}
|
|
|
|
it("keeps the same form DOM and selection when persistence starts, preserving edits against late restore", async () => {
|
|
vi.useFakeTimers();
|
|
const f = fixture();
|
|
const rendered = render(f.view(false));
|
|
const input = screen.getByLabelText("现场记录") as HTMLInputElement;
|
|
fireEvent.change(input, { target: { value: "本地草稿" } });
|
|
input.focus(); input.setSelectionRange(1, 3);
|
|
rendered.rerender(f.view(false, structuredClone(block)));
|
|
expect(screen.getByLabelText("现场记录")).toBe(input);
|
|
expect(f.client.getAnswerViewState).not.toHaveBeenCalled();
|
|
rendered.rerender(f.view(true, structuredClone(block)));
|
|
expect(screen.getByLabelText("现场记录")).toBe(input);
|
|
expect(input.value).toBe("本地草稿");
|
|
expect(input.selectionStart).toBe(1);
|
|
expect(input.selectionEnd).toBe(3);
|
|
expect(f.client.getAnswerViewState).toHaveBeenCalledTimes(1);
|
|
await act(async () => f.restore(remote));
|
|
expect(input.value).toBe("本地草稿");
|
|
await act(async () => vi.advanceTimersByTimeAsync(500));
|
|
expect(f.client.putAnswerViewState.mock.calls[0][1]).toMatchObject({ expected_revision: 4, nodes: { form: { values: { note: "本地草稿" } } } });
|
|
rendered.unmount(); f.scope.dispose();
|
|
});
|
|
|
|
it("never reads or writes process blocks even when persistence is requested", async () => {
|
|
vi.useFakeTimers();
|
|
const f = fixture();
|
|
const rendered = render(f.view(true, { ...block, phase: "process" }));
|
|
fireEvent.change(screen.getByLabelText("现场记录"), { target: { value: "过程草稿" } });
|
|
expect(screen.queryByText("正在恢复交互状态")).toBeNull();
|
|
await act(async () => vi.advanceTimersByTimeAsync(2000));
|
|
rendered.unmount();
|
|
await act(async () => vi.advanceTimersByTimeAsync(1000));
|
|
expect(f.client.getAnswerViewState).not.toHaveBeenCalled();
|
|
expect(f.client.putAnswerViewState).not.toHaveBeenCalled();
|
|
f.scope.dispose();
|
|
});
|