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