// @vitest-environment jsdom // // A clamped text offers More only when it is actually cut (built for the person page, shared with // the collection page, where a one-line summary carried a More that did nothing). import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; import { ClampedText } from "./ClampedText"; beforeAll(() => { class Stub { observe() {} unobserve() {} disconnect() {} takeRecords() { return []; } } globalThis.ResizeObserver ??= Stub as unknown as typeof ResizeObserver; }); afterEach(cleanup); const view = (text: string) => ( ); describe("ClampedText", () => { it("offers no toggle for a text that fits", () => { render(view("Short.")); expect(screen.queryByText("More")).toBeNull(); // jsdom: nothing is cut }); it("offers More for a cut text, and Less once open", () => { const tall = vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(400); const box = vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(60); try { render(view("A long text.\n\nOn and on.")); const p = screen.getByText(/A long text/); expect(p.className).toContain("line-clamp-3"); fireEvent.click(screen.getByText("More")); expect(screen.getByText("Less").getAttribute("aria-expanded")).toBe("true"); expect(p.className).not.toContain("line-clamp-3"); } finally { tall.mockRestore(); box.mockRestore(); } }); it("opens when focus reaches a link the cut hides, and not for one in sight", () => { const tall = vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(400); const box = vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(60); const rect = (top: number, bottom: number) => ({ top, bottom }) as DOMRect; try { render( seen cut , ); const clamp = screen.getByText("seen").parentElement!; vi.spyOn(clamp, "getBoundingClientRect").mockReturnValue(rect(100, 160)); vi.spyOn(screen.getByText("seen"), "getBoundingClientRect").mockReturnValue(rect(100, 120)); act(() => screen.getByText("seen").focus()); expect(screen.getByText("More")).toBeTruthy(); // A Tab onto a link below the cut. The browser scrolls the clamped box to show it (measured in // Chrome: 45 px), so on screen the link sits inside the box: only the text's own top tells. clamp.scrollTop = 240; vi.spyOn(screen.getByText("cut"), "getBoundingClientRect").mockReturnValue(rect(130, 150)); // Opened, the link drops back to its place in the text, as far as the box had scrolled: in a // scrolling panel that is out of sight, so it is brought back into view. const reveal = vi.fn(); screen.getByText("cut").scrollIntoView = reveal; act(() => screen.getByText("cut").focus()); expect(screen.getByText("Less").getAttribute("aria-expanded")).toBe("true"); expect(reveal).toHaveBeenCalledWith({ block: "nearest" }); } finally { tall.mockRestore(); box.mockRestore(); } }); it("starts another text collapsed: a reused view opening the next item", () => { const tall = vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockReturnValue(400); const box = vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(60); try { const { rerender } = render(view("The first show's long summary.")); fireEvent.click(screen.getByText("More")); rerender(view("The next show's long summary.")); expect(screen.getByText(/next show/).className).toContain("line-clamp-3"); expect(screen.getByText("More").getAttribute("aria-expanded")).toBe("false"); } finally { tall.mockRestore(); box.mockRestore(); } }); it("measures again when its box changes: the window or the text size moved the cut", () => { const seen: ResizeObserverCallback[] = []; // the callbacks of observers watching something const real = globalThis.ResizeObserver; globalThis.ResizeObserver = class { constructor(private cb: ResizeObserverCallback) {} observe() { seen.push(this.cb); } unobserve() {} disconnect() {} } as unknown as typeof ResizeObserver; let height = 60; const tall = vi .spyOn(HTMLElement.prototype, "scrollHeight", "get") .mockImplementation(() => height); const box = vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(60); try { render(view("A text that fits, until the reader raises the text size.")); expect(screen.queryByText("More")).toBeNull(); height = 400; // the text size went up: the same text now runs past three lines act(() => seen[seen.length - 1]!([], {} as ResizeObserver)); expect(screen.getByText("More")).toBeTruthy(); } finally { tall.mockRestore(); box.mockRestore(); globalThis.ResizeObserver = real; } }); });