import { cleanup, render, screen } from "@solidjs/testing-library";
import { createSignal } from "solid-js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { mockAnimate } = vi.hoisted(() => ({
mockAnimate: vi.fn().mockImplementation(() => ({
pause: vi.fn(),
then: vi.fn((cb: () => void) => {
cb();
return Promise.resolve();
}),
})),
}));
vi.mock("animejs", () => ({
animate: mockAnimate,
}));
vi.mock("../../../../src/ts/utils/misc", () => ({
applyReducedMotion: vi.fn((duration: number) => duration),
}));
import { AnimeMatch } from "../../../../src/ts/components/common/anime/AnimeMatch";
import { AnimeSwitch } from "../../../../src/ts/components/common/anime/AnimeSwitch";
describe("AnimeSwitch", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
cleanup();
});
it("renders the matched child", () => {
render(() => (
A
B
));
expect(screen.getByTestId("match-a")).toBeInTheDocument();
expect(screen.queryByTestId("match-b")).not.toBeInTheDocument();
});
it("switches to the next matched child reactively", () => {
const [tab, setTab] = createSignal<"a" | "b">("a");
render(() => (
View A
View B
));
expect(screen.getByTestId("view-a")).toBeInTheDocument();
expect(screen.queryByTestId("view-b")).not.toBeInTheDocument();
setTab("b");
expect(screen.queryByTestId("view-a")).not.toBeInTheDocument();
expect(screen.getByTestId("view-b")).toBeInTheDocument();
});
it("renders nothing when no match", () => {
const { container } = render(() => (
never
));
expect(screen.queryByTestId("no-match")).not.toBeInTheDocument();
// Only AnimePresence wrapper remains
expect(container.querySelectorAll("[data-testid]").length).toBe(0);
});
it("does not throw when switching between children", () => {
const [view, setView] = createSignal<"a" | "b">("a");
expect(() => {
render(() => (
View A
View B
));
}).not.toThrow();
expect(() => setView("b")).not.toThrow();
});
it("passes animeProps down to all AnimeMatch children", () => {
render(() => (
content
));
// Expect animate call with the shared animeProps
expect(mockAnimate).toHaveBeenCalledWith(
expect.any(HTMLElement),
expect.objectContaining({ opacity: 1, duration: 300 }),
);
});
});
describe("AnimeMatch", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
cleanup();
});
it("renders children when `when` is true", () => {
render(() => (
match
));
expect(screen.getByTestId("match-content")).toBeInTheDocument();
});
it("does not render children when `when` is false", () => {
render(() => (
hidden
));
expect(screen.queryByTestId("hidden")).not.toBeInTheDocument();
});
it("per-match animate overrides the shared animeProps", () => {
render(() => (
override
));
// The per-match duration (500) should be used, not the shared one (200)
expect(mockAnimate).toHaveBeenCalledWith(
expect.any(HTMLElement),
expect.objectContaining({ opacity: 1, duration: 500 }),
);
const callsWithSharedDuration = mockAnimate.mock.calls.filter(
([, params]) => params.duration === 200,
);
expect(callsWithSharedDuration.length).toBe(0);
});
it("falls back to context animeProps when no per-match props provided", () => {
render(() => (
content
));
// Should use context duration 250
expect(mockAnimate).toHaveBeenCalledWith(
expect.any(HTMLElement),
expect.objectContaining({ opacity: 1, duration: 250 }),
);
});
});