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