Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
96 lines
4.1 KiB
TypeScript
96 lines
4.1 KiB
TypeScript
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { animate } from "motion/mini";
|
|
import { useReportUpdateMotion } from "./use-report-update-motion";
|
|
|
|
vi.mock("motion/mini", () => ({ animate: vi.fn() }));
|
|
|
|
function Reader({ revision = "1", scope = "task-1", live = true }) {
|
|
const ref = useReportUpdateMotion(scope, revision, live);
|
|
return <article ref={ref} aria-label="report"><header>调查报告</header><section><p>{revision}</p><details open><summary>依据</summary>{revision}</details></section></article>;
|
|
}
|
|
|
|
afterEach(() => { cleanup(); vi.unstubAllGlobals(); vi.restoreAllMocks(); });
|
|
|
|
function motion(reduced = false) {
|
|
const cancel = vi.fn();
|
|
vi.mocked(animate).mockReset();
|
|
vi.mocked(animate).mockReturnValue({ cancel } as unknown as ReturnType<typeof animate>);
|
|
vi.stubGlobal("matchMedia", vi.fn(() => ({ matches: reduced, addEventListener: vi.fn(), removeEventListener: vi.fn() })));
|
|
// jsdom does not provide Web Animations.
|
|
const attach = (element: HTMLElement) => Array.from(element.children)
|
|
.forEach((part) => Object.defineProperty(part, "animate", { value: vi.fn() }));
|
|
return { animate, cancel, attach };
|
|
}
|
|
|
|
describe("report update motion", () => {
|
|
it("animates only a new revision and keeps the reading DOM", () => {
|
|
const { animate, cancel, attach } = motion();
|
|
const { rerender, unmount } = render(<Reader />);
|
|
const section = screen.getByRole("article", { name: "report" });
|
|
attach(section);
|
|
const details = section.querySelector("details")!;
|
|
expect(animate).not.toHaveBeenCalled();
|
|
rerender(<Reader />);
|
|
expect(animate).not.toHaveBeenCalled();
|
|
rerender(<Reader revision="2" />);
|
|
expect(animate).toHaveBeenCalledTimes(2);
|
|
expect(animate).toHaveBeenNthCalledWith(1,
|
|
section.querySelector("header"),
|
|
{ opacity: [0, 1], translate: ["0 18px", "0 0"], scale: [0.98, 1] },
|
|
{ duration: 0.4, delay: 0, ease: [0.22, 1, 0.36, 1] }
|
|
);
|
|
expect(animate).toHaveBeenNthCalledWith(2, section.querySelector("section"), expect.any(Object), expect.objectContaining({ delay: 0.045 }));
|
|
expect(section.querySelector("details")).toBe(details);
|
|
expect(details.open).toBe(true);
|
|
rerender(<Reader revision="3" />);
|
|
expect(cancel).toHaveBeenCalledTimes(2);
|
|
unmount();
|
|
expect(cancel).toHaveBeenCalledTimes(4);
|
|
});
|
|
|
|
it("skips history, returning to live content, and task switches", () => {
|
|
const { animate, attach } = motion();
|
|
const { rerender } = render(<Reader />);
|
|
attach(screen.getByRole("article", { name: "report" }));
|
|
rerender(<Reader revision="2" live={false} />);
|
|
rerender(<Reader revision="3" />);
|
|
rerender(<Reader scope="task-2" revision="4" />);
|
|
expect(animate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("respects reduced motion", () => {
|
|
const { animate, attach } = motion(true);
|
|
const { rerender } = render(<Reader />);
|
|
attach(screen.getByRole("article", { name: "report" }));
|
|
rerender(<Reader revision="2" />);
|
|
expect(animate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps report updates readable when matchMedia is unavailable", () => {
|
|
const { animate, attach } = motion();
|
|
vi.stubGlobal("matchMedia", undefined);
|
|
const { rerender } = render(<Reader />);
|
|
const section = screen.getByRole("article", { name: "report" });
|
|
attach(section);
|
|
rerender(<Reader revision="2" />);
|
|
expect(section).toHaveTextContent("2");
|
|
expect(animate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("lets interaction interrupt the group and leaves focused content still", () => {
|
|
const { animate, cancel, attach } = motion();
|
|
const { rerender } = render(<Reader />);
|
|
const reader = screen.getByRole("article", { name: "report" });
|
|
attach(reader);
|
|
rerender(<Reader revision="2" />);
|
|
fireEvent.pointerDown(reader);
|
|
expect(cancel).toHaveBeenCalledTimes(2);
|
|
reader.querySelector("summary")!.focus();
|
|
vi.mocked(animate).mockClear();
|
|
rerender(<Reader revision="3" />);
|
|
expect(animate).toHaveBeenCalledTimes(1);
|
|
expect(animate).toHaveBeenCalledWith(reader.querySelector("header"), expect.any(Object), expect.any(Object));
|
|
});
|
|
});
|