Files
test/frontend/__tests__/components/common/anime/AnimeConditional.spec.tsx
Benjamin Falch 2bc741fb78
Some checks failed
Mark Stale PRs / stale (push) Has been cancelled
adding monkeytype
2026-04-23 13:53:44 +02:00

231 lines
6.4 KiB
TypeScript

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 { AnimeConditional } from "../../../../src/ts/components/common/anime/AnimeConditional";
describe("AnimeConditional", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
cleanup();
});
it("renders `then` content when `if` is truthy", () => {
render(() => (
<AnimeConditional
if={true}
then={<div data-testid="then-content">then</div>}
else={<div data-testid="else-content">else</div>}
/>
));
expect(screen.getByTestId("then-content")).toBeInTheDocument();
expect(screen.queryByTestId("else-content")).not.toBeInTheDocument();
});
it("renders `else` content when `if` is falsy", () => {
render(() => (
<AnimeConditional
if={false}
then={<div data-testid="then-content">then</div>}
else={<div data-testid="else-content">else</div>}
/>
));
expect(screen.queryByTestId("then-content")).not.toBeInTheDocument();
expect(screen.getByTestId("else-content")).toBeInTheDocument();
});
it("renders `else` content when `if` is null", () => {
render(() => (
<AnimeConditional
if={null}
then={<div data-testid="then-content">then</div>}
else={<div data-testid="else-content">else</div>}
/>
));
expect(screen.queryByTestId("then-content")).not.toBeInTheDocument();
expect(screen.getByTestId("else-content")).toBeInTheDocument();
});
it("switches reactively from `then` to `else`", () => {
const [condition, setCondition] = createSignal(true);
render(() => (
<AnimeConditional
if={condition()}
then={<div data-testid="then-content">then</div>}
else={<div data-testid="else-content">else</div>}
/>
));
expect(screen.getByTestId("then-content")).toBeInTheDocument();
setCondition(false);
expect(screen.queryByTestId("then-content")).not.toBeInTheDocument();
expect(screen.getByTestId("else-content")).toBeInTheDocument();
});
it("switches reactively from `else` to `then`", () => {
const [condition, setCondition] = createSignal(false);
render(() => (
<AnimeConditional
if={condition()}
then={<div data-testid="then-content">then</div>}
else={<div data-testid="else-content">else</div>}
/>
));
expect(screen.getByTestId("else-content")).toBeInTheDocument();
setCondition(true);
expect(screen.getByTestId("then-content")).toBeInTheDocument();
expect(screen.queryByTestId("else-content")).not.toBeInTheDocument();
});
it("supports `then` as a function and passes the truthy value", () => {
const obj = { label: "hello" };
render(() => (
<AnimeConditional
if={obj}
then={(value) => <div data-testid="fn-content">{value().label}</div>}
/>
));
expect(screen.getByTestId("fn-content")).toHaveTextContent("hello");
});
it("does not throw without `else` prop", () => {
expect(() => {
render(() => (
<AnimeConditional
if={true}
then={<div data-testid="then-content">then</div>}
/>
));
}).not.toThrow();
expect(screen.getByTestId("then-content")).toBeInTheDocument();
});
it("does not throw on mount/unmount", () => {
const [show, setShow] = createSignal(true);
expect(() => {
render(() => (
<AnimeConditional
if={show()}
then={<div>then</div>}
else={<div>else</div>}
/>
));
}).not.toThrow();
expect(() => setShow(false)).not.toThrow();
expect(() => setShow(true)).not.toThrow();
});
describe("default animations (opacity fade)", () => {
it("applies default opacity animate on `then` branch", () => {
render(() => <AnimeConditional if={true} then={<div>then</div>} />);
expect(mockAnimate).toHaveBeenCalledWith(
expect.any(HTMLElement),
expect.objectContaining({ opacity: 1, duration: 125 }),
);
});
it("applies default opacity initial state on `then` branch", () => {
render(() => <AnimeConditional if={true} then={<div>then</div>} />);
// Initial call: opacity:0 with duration:0
expect(mockAnimate).toHaveBeenCalledWith(
expect.any(HTMLElement),
expect.objectContaining({ opacity: 0, duration: 0 }),
);
});
});
describe("custom animeProps", () => {
it("uses custom animate params when animeProps provided", () => {
render(() => (
<AnimeConditional
if={true}
then={<div>then</div>}
animeProps={{
initial: { opacity: 0, translateY: -10 },
animate: { opacity: 1, translateY: 0, duration: 400 },
exit: { opacity: 0, translateY: -10, duration: 200 },
}}
/>
));
expect(mockAnimate).toHaveBeenCalledWith(
expect.any(HTMLElement),
expect.objectContaining({ opacity: 1, translateY: 0, duration: 400 }),
);
});
it("uses custom initial state when animeProps provided", () => {
render(() => (
<AnimeConditional
if={true}
then={<div>then</div>}
animeProps={{
initial: { opacity: 0, translateY: -10 },
animate: { opacity: 1, translateY: 0, duration: 400 },
}}
/>
));
// Initial state applied with duration:0
expect(mockAnimate).toHaveBeenCalledWith(
expect.any(HTMLElement),
expect.objectContaining({ opacity: 0, translateY: -10, duration: 0 }),
);
});
});
it("exitBeforeEnter prop does not throw on condition change", () => {
const [cond, setCond] = createSignal(true);
expect(() => {
render(() => (
<AnimeConditional
exitBeforeEnter
if={cond()}
then={<div>then</div>}
else={<div>else</div>}
/>
));
}).not.toThrow();
expect(() => setCond(false)).not.toThrow();
});
});