import { afterEach, describe, expect, test, vi } from "vitest"; import { fireEvent, screen } from "@testing-library/react"; import type { CommunityFeed } from "../../../../shared/community.ts"; import { Marquee } from "../../../components/layout/Marquee.tsx"; import { CommunityFeedContext } from "../../../contexts/social/CommunityFeedContext.ts"; import { ThemeContext } from "../../../contexts/theme/ThemeContext.ts"; import type { StyleName } from "../../../../shared/theme.ts"; import { makeCommunityFeed, makeSession } from "../../fixtures.ts"; import { renderWithContexts } from "../../render.tsx"; const joined = (username: string) => makeCommunityFeed({ events: [{ id: `user_joined:${username}`, at: new Date(), type: "user_joined", username, }], }); function marquee(feed: CommunityFeed | null, style: StyleName = "geocities") { return ( () => {} }}> ); } const session = makeSession({ username: "me" }); const track = () => document.querySelector(".site-marquee-track"); afterEach(() => vi.restoreAllMocks()); describe("Marquee", () => { test("draws nothing in another style, nor for a visitor", () => { const band = () => document.querySelector(".site-marquee"); const { rerender, unmount } = renderWithContexts( marquee(joined("ann"), "smooth"), { session }, ); expect(band()).toBeNull(); rerender(marquee(joined("ann"))); expect(band()).toBeInTheDocument(); unmount(); renderWithContexts(marquee(joined("ann"))); expect(band()).toBeNull(); }); test("an empty band until the feed is loaded, then the feed twice over", () => { const { rerender } = renderWithContexts(marquee(null), { session }); expect(document.querySelector(".site-marquee")).toBeEmptyDOMElement(); rerender(marquee(joined("ann"))); // The second copy is the loop's: neither read out nor tabbed to. expect(screen.getAllByRole("link", { name: "ann" })).toHaveLength(1); const [first, second] = [...document.querySelectorAll("a[href='/@ann']")]; expect(first).not.toHaveAttribute("tabindex"); expect(second).toHaveAttribute("tabindex", "-1"); expect(second.closest("[aria-hidden='true']")).toBeInTheDocument(); }); test("what changes mid-crawl waits for the next loop", () => { const { rerender } = renderWithContexts(marquee(joined("ann")), { session, ws: { onlineUsers: [] }, }); rerender(marquee(joined("bob"))); expect(screen.getByRole("link", { name: "ann" })).toBeInTheDocument(); expect(screen.queryByRole("link", { name: "bob" })).toBeNull(); const before = track()!; fireEvent.animationIteration(before); expect(screen.getByRole("link", { name: "bob" })).toBeInTheDocument(); expect(screen.queryByRole("link", { name: "ann" })).toBeNull(); // A new loop is a new track, whose animation starts over. expect(track()).not.toBe(before); }); test("with reduced motion, the line shows once and follows the feed", () => { vi.spyOn(globalThis, "matchMedia").mockImplementation((query) => ({ matches: query.includes("reduce"), media: query, addEventListener() {}, removeEventListener() {}, }) as unknown as MediaQueryList ); const { rerender } = renderWithContexts(marquee(joined("ann")), { session, }); expect(document.querySelectorAll("a[href='/@ann']")).toHaveLength(1); rerender(marquee(joined("bob"))); expect(screen.getByRole("link", { name: "bob" })).toBeInTheDocument(); }); });