Files
next-tjwater-frontend/src/features/workbench/hooks/use-report-update-motion.test.tsx
T
jiang ec23d44b02 feat(agent): support Responses steering and project-aware tools
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.
2026-10-19 18:46:58 +08:00

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));
});
});