Focus had scrolled the clamped box itself; opened, the link dropped by that much, out of a scrolling panel's sight. DESIGN states where Close sits in the stacked layout; a stale test comment goes.
125 lines
5.2 KiB
TypeScript
125 lines
5.2 KiB
TypeScript
// @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) => (
|
|
<ClampedText text={text} lines={3} moreLabel="More" lessLabel="Less" />
|
|
);
|
|
|
|
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(
|
|
<ClampedText text="t" lines={3} moreLabel="More" lessLabel="Less">
|
|
<a href="#a">seen</a>
|
|
<a href="#b">cut</a>
|
|
</ClampedText>,
|
|
);
|
|
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;
|
|
}
|
|
});
|
|
});
|