110 lines
4.7 KiB
TypeScript
110 lines
4.7 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, featuredLiveMode, 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("starts with the live region off while it rotates, and hides non-center items and shows the counter and dots", () => {
|
|
const out = renderToStaticMarkup(createElement(FeaturedCarousel, { experts: experts(8) }));
|
|
expect(out).toContain('aria-live="off"');
|
|
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<");
|
|
expect(out).toContain('aria-live="polite"');
|
|
});
|
|
});
|
|
|
|
describe("featuredLiveMode (WAI-ARIA carousel live region)", () => {
|
|
it("is off while autoplay rotates", () => {
|
|
expect(featuredLiveMode({ rotating: true, paused: false })).toBe("off");
|
|
});
|
|
it("is polite while paused by hover or focus", () => {
|
|
expect(featuredLiveMode({ rotating: true, paused: true })).toBe("polite");
|
|
});
|
|
it("is polite when rotation is stopped (reduced motion or one expert)", () => {
|
|
expect(featuredLiveMode({ rotating: false, paused: false })).toBe("polite");
|
|
expect(featuredLiveMode({ rotating: false, paused: true })).toBe("polite");
|
|
});
|
|
});
|