106 lines
3.8 KiB
TypeScript
106 lines
3.8 KiB
TypeScript
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 (
|
|
<ThemeContext.Provider
|
|
value={{
|
|
style,
|
|
colorScheme: "light",
|
|
setStyle() {},
|
|
setColorScheme() {},
|
|
}}
|
|
>
|
|
<CommunityFeedContext.Provider value={{ feed, watch: () => () => {} }}>
|
|
<Marquee />
|
|
</CommunityFeedContext.Provider>
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
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();
|
|
});
|
|
});
|