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
调查报告

{revision}

依据{revision}
; } 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); 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(); const section = screen.getByRole("article", { name: "report" }); attach(section); const details = section.querySelector("details")!; expect(animate).not.toHaveBeenCalled(); rerender(); expect(animate).not.toHaveBeenCalled(); rerender(); 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(); 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(); attach(screen.getByRole("article", { name: "report" })); rerender(); rerender(); rerender(); expect(animate).not.toHaveBeenCalled(); }); it("respects reduced motion", () => { const { animate, attach } = motion(true); const { rerender } = render(); attach(screen.getByRole("article", { name: "report" })); rerender(); expect(animate).not.toHaveBeenCalled(); }); it("keeps report updates readable when matchMedia is unavailable", () => { const { animate, attach } = motion(); vi.stubGlobal("matchMedia", undefined); const { rerender } = render(); const section = screen.getByRole("article", { name: "report" }); attach(section); rerender(); 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(); const reader = screen.getByRole("article", { name: "report" }); attach(reader); rerender(); fireEvent.pointerDown(reader); expect(cancel).toHaveBeenCalledTimes(2); reader.querySelector("summary")!.focus(); vi.mocked(animate).mockClear(); rerender(); expect(animate).toHaveBeenCalledTimes(1); expect(animate).toHaveBeenCalledWith(reader.querySelector("header"), expect.any(Object), expect.any(Object)); }); });