Files
frontend-helper/tests/featured-carousel.test.ts
T

96 lines
4.0 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { FeaturedCarousel } from "@/components/FeaturedCarousel";
import { FEATURED_INTERVAL_MS, featuredSlot, startFeaturedAutoplay } from "@/components/featured-carousel-model";
import type { DiscoverExpert } from "@/lib/discover";
const expert = (i: number): DiscoverExpert => ({
id: i, slug: `e${i}`, displayName: `Expert ${i}`, byline: `Byline ${i}`, pictureUrl: i % 2 ? `https://x/${i}.jpg` : null,
rating: 4.5, reviewsCount: i, available: true, location: { city: null, country: null },
});
const experts = (n: number) => Array.from({ length: n }, (_, i) => expert(i + 1));
describe("featuredSlot", () => {
it("places items by offset from the current index, wrapping round", () => {
const slots = Array.from({ length: 8 }, (_, i) => featuredSlot(i, 0, 8));
expect(slots).toEqual(["c0", "r1", "r2", "r3", "r3", "l3", "l2", "l1"]);
expect(featuredSlot(0, 7, 8)).toBe("r1");
expect(featuredSlot(6, 7, 8)).toBe("l1");
});
it("degrades with one or two experts: no empty neighbors", () => {
expect(featuredSlot(0, 0, 1)).toBe("c0");
expect([featuredSlot(0, 0, 2), featuredSlot(1, 0, 2)]).toEqual(["c0", "r1"]);
expect([featuredSlot(0, 1, 2), featuredSlot(1, 1, 2)]).toEqual(["r1", "c0"]);
expect([0, 1, 2].map((i) => featuredSlot(i, 0, 3))).toEqual(["c0", "r1", "l1"]);
});
});
describe("startFeaturedAutoplay", () => {
beforeEach(() => { vi.useFakeTimers(); });
afterEach(() => { vi.useRealTimers(); });
it("advances every 2.8s and wraps to the start", () => {
const seen: number[] = [];
const ctl = startFeaturedAutoplay({ count: 3, onChange: (i) => seen.push(i) });
expect(FEATURED_INTERVAL_MS).toBe(2800);
vi.advanceTimersByTime(2799);
expect(seen).toEqual([]);
vi.advanceTimersByTime(1);
expect(seen).toEqual([1]);
vi.advanceTimersByTime(2800 * 2);
expect(seen).toEqual([1, 2, 0]);
expect(ctl.index).toBe(0);
ctl.stop();
});
it("does not advance while paused and resumes after", () => {
const onChange = vi.fn();
const ctl = startFeaturedAutoplay({ count: 5, onChange });
ctl.pause();
expect(ctl.paused).toBe(true);
vi.advanceTimersByTime(4000 * 3);
expect(onChange).not.toHaveBeenCalled();
expect(ctl.index).toBe(0);
ctl.resume();
vi.advanceTimersByTime(2800);
expect(onChange).toHaveBeenCalledWith(1);
ctl.stop();
});
it("never rotates a single expert and stops cleanly", () => {
const one = vi.fn();
startFeaturedAutoplay({ count: 1, onChange: one });
vi.advanceTimersByTime(10_000);
expect(one).not.toHaveBeenCalled();
const many = vi.fn();
const ctl = startFeaturedAutoplay({ count: 4, onChange: many });
ctl.stop();
vi.advanceTimersByTime(10_000);
expect(many).not.toHaveBeenCalled();
});
});
describe("FeaturedCarousel markup", () => {
it("has a polite live region, hides non-center items and shows the counter and dots", () => {
const out = renderToStaticMarkup(createElement(FeaturedCarousel, { experts: experts(8) }));
expect(out).toContain('aria-live="polite"');
expect(out.match(/data-testid="featured-item"/g)).toHaveLength(8);
expect(out.match(/aria-hidden="true" data-testid="featured-item"/g)).toHaveLength(7);
expect(out).toContain("featured-c0");
expect(out).toContain(">1 / 8<");
expect(out.match(/class="featured-dot( featured-dot-on)?"/g)).toHaveLength(8);
expect(out.match(/featured-dot-on/g)).toHaveLength(1);
expect(out).toContain("--cfw:96px");
});
it("takes a size prop for mobile", () => {
const out = renderToStaticMarkup(createElement(FeaturedCarousel, { experts: experts(3), size: 156 }));
expect(out).toContain("--cfw:156px");
});
it("shows no dots for a single expert", () => {
const out = renderToStaticMarkup(createElement(FeaturedCarousel, { experts: experts(1) }));
expect(out).not.toContain("featured-dot ");
expect(out).toContain(">1 / 1<");
});
});