import { up, down, toggle } from "./index"; import { screen } from "@testing-library/dom"; import { describe, it, expect, beforeEach, vi } from "vitest"; const addMockAnimation = (element, id = "") => { const mockAnimation = { finish: vi.fn(), id, }; element.getAnimations = () => [mockAnimation]; return mockAnimation; }; const withMockAnimation = (element, duration = 0) => { const finish = vi.fn(); const reverse = vi.fn(); let timeCalled = null; element.getAnimations = () => []; element.animate = vi.fn(() => { timeCalled = new Date().getTime(); return { finished: new Promise((resolve) => { setTimeout(resolve, duration); }), finish, }; }); return { element, finish, reverse, getTimeCalled: () => timeCalled }; }; const mockHeightOnce = (element, values) => { const mock = vi.spyOn(element, "clientHeight", "get"); return values.reduce((m, val) => m.mockImplementationOnce(() => val), mock); }; const mockOffsetHeight = (element, height = null) => { vi.spyOn(element, "offsetHeight", "get").mockImplementation(() => height); }; const mockHeight = (element, value) => { return vi .spyOn(element, "clientHeight", "get") .mockImplementation(() => value); }; beforeEach(() => { document.body.innerHTML = `
Content!
`; window.requestAnimationFrame = (cb) => { cb(0); return 0; }; // Does NOT prefer reduced motion. window.matchMedia = () => { return { matches: false, } as MediaQueryList; }; }); it("opens element", async () => { document.body.innerHTML = `
Content!
`; const { element } = withMockAnimation(screen.getByTestId("content")); mockHeightOnce(element, [0, 100]); const opened = await down(element); expect(opened).toBe(true); expect(element.animate).toBeCalledTimes(1); expect(element.style.display).toEqual("block"); expect(element.animate).toHaveBeenCalledWith( [ expect.objectContaining({ height: "0px", paddingBottom: "0px", paddingTop: "0px", }), expect.objectContaining({ height: "100px", paddingBottom: "", paddingTop: "", }), ], { easing: "ease", duration: 250, fill: "backwards" } ); }); it("closes element", async () => { document.body.innerHTML = `
Content!
`; const { element } = withMockAnimation(screen.getByTestId("content")); mockHeight(element, 100); const opened = await up(element); expect(opened).toBe(false); expect(element.animate).toBeCalledTimes(1); expect(element.style.display).toEqual("none"); expect(element.animate).toHaveBeenCalledWith( [ expect.objectContaining({ height: "100px", paddingBottom: "", paddingTop: "", }), expect.objectContaining({ height: "0px", paddingBottom: "0px", paddingTop: "0px", }), ], { easing: "ease", duration: 250, fill: "backwards" } ); }); describe("toggle()", () => { beforeEach(() => { document.body.innerHTML = `
Content!
`; }); describe("animation is allowed to complete fully", () => { it("toggles element open", async () => { const { element } = withMockAnimation(screen.getByTestId("content")); const opened = await toggle(element); expect(opened).toBe(true); expect(element.animate).toBeCalledTimes(1); }); it("toggles element closed", async () => { const { element } = withMockAnimation(screen.getByTestId("content")); // Give it an arbitrary height to mock it being "open." mockOffsetHeight(element, 100); const opened = await toggle(element); expect(opened).toBe(false); expect(element.animate).toBeCalledTimes(1); }); }); describe("animation is rapidly clicked", () => { it("opens down() even though the element is partially expanded due to double click on up()", async () => { // Visible and with explicit height. document.body.innerHTML = `
Content!
`; const { element } = withMockAnimation(screen.getByTestId("content")); const { finish } = addMockAnimation(element, "0"); const opened = await toggle(element); expect(opened).toBe(null); expect(finish).toHaveBeenCalledTimes(1); expect(element.style.display).toEqual("block"); }); it("closes up() even though the element is partially expanded due to double click on down()", async () => { // Visible and with explicit height. document.body.innerHTML = `
Content!
`; const { element } = withMockAnimation(screen.getByTestId("content")); const { finish } = addMockAnimation(element, "1"); const opened = await toggle(element); // Will toggle down(): expect(opened).toBe(null); expect(finish).toHaveBeenCalledTimes(1); expect(element.style.display).toEqual("none"); }); }); }); describe("custom options", () => { beforeEach(() => { document.body.innerHTML = `
Content!
`; }); it("uses default display value", async () => { const { element } = withMockAnimation(screen.getByTestId("content")); expect(element.style.display).toEqual("none"); await down(element); expect(element.style.display).toEqual("block"); }); it("uses custom display property", async () => { const { element } = withMockAnimation(screen.getByTestId("content")); expect(element.style.display).toEqual("none"); await down(element, { display: "flex" }); expect(element.style.display).toEqual("flex"); }); it("uses default overflow property", () => { const { element } = withMockAnimation(screen.getByTestId("content")); expect(element.style.overflow).toEqual(""); down(element); expect(element.style.overflow).toEqual("hidden"); }); it("uses custom overflow property", () => { const { element } = withMockAnimation(screen.getByTestId("content")); expect(element.style.overflow).toEqual(""); down(element, { overflow: "visible" }); expect(element.style.overflow).toEqual("visible"); }); }); describe("accessibility settings", () => { it("disables animation when user prefers reduced motion", async () => { const { element } = withMockAnimation(screen.getByTestId("content")); window.matchMedia = () => { return { matches: true, } as MediaQueryList; }; await up(element); expect(element.animate).toHaveBeenCalledWith(expect.anything(), { duration: 0, easing: "ease", fill: "backwards", }); }); }); describe("overflow handling", () => { it("temporarily sets overflow to hidden", async () => { document.body.innerHTML = `
Content!
`; const { element } = withMockAnimation(screen.getByTestId("content")); expect(element.style.overflow).toEqual(""); element.animate = () => { return { finished: new Promise((resolve) => { expect(element.style.overflow).toEqual("hidden"); resolve(); }), }; }; await down(element); expect(element.style.overflow).toEqual(""); }); }); describe("callback timing", () => { it("should fire callback after animation is complete", async () => { document.body.innerHTML = `
Content!
`; const { element, getTimeCalled } = withMockAnimation( screen.getByTestId("content"), 250 ); await up(element); const difference = new Date().getTime() - getTimeCalled(); expect(difference).toBeGreaterThanOrEqual(250); }); });