// @vitest-environment jsdom
//
// The floating header knows no page: a page fills it through PageHeader (a portal into the header's
// root box), and the shell's PageHeaderDefault shows only while no page does. These pin the slot's
// three promises: the page's pills land IN the header, the default steps aside for them and comes
// back when the next page has none, and a shell without a header (the channel page) renders the
// page's pills nowhere.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { render, cleanup, screen } from "@testing-library/react";
import PageShell, { PageHeader, PageHeaderDefault } from "./PageShell";
vi.mock("./PageScroller", () => ({
default: ({ children }: { children: React.ReactNode }) =>
{children}
,
}));
beforeEach(() => {
vi.stubGlobal(
"ResizeObserver",
class {
observe() {}
disconnect() {}
},
);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
const hdr = ;
const fallback = (
default
);
describe("the header slot", () => {
it("puts a page's pills inside the header box", () => {
render(
search
,
);
expect(screen.getByTestId("hdr").textContent).toBe("search");
});
it("shows the default on a page with no header of its own", () => {
render({fallback});
expect(screen.getByTestId("hdr").textContent).toBe("default");
});
it("hides the default while a page fills the header, and brings it back on the next page", () => {
const page = (own: boolean) => (
{own && (
search
)}
{fallback}
);
const { rerender } = render(page(true));
expect(screen.getByTestId("hdr").textContent).toBe("search");
rerender(page(false));
expect(screen.getByTestId("hdr").textContent).toBe("default");
rerender(page(true));
expect(screen.getByTestId("hdr").textContent).toBe("search");
});
it("keeps the default hidden while one of two PageHeaders on a page remains", () => {
const page = (both: boolean) => (
search
{both && (
toggle
)}
{fallback}
);
const { rerender } = render(page(true));
rerender(page(false));
expect(screen.getByTestId("hdr").textContent).toBe("search");
});
it("pads the content by the filled header before paint, not when the observer catches up", () => {
// jsdom does no layout: the header's height here is one 40px row per pill in it. The observer
// never fires in this test, so only a measurement made as the pills arrive can see them.
const height = (el: HTMLElement) => 40 * el.childElementCount;
const page = (own: boolean) => (
{own && (
search
scope
)}
{fallback}
);
const { rerender, container } = render({null});
Object.defineProperty(screen.getByTestId("hdr"), "offsetHeight", {
get() {
return height(this as HTMLElement);
},
configurable: true,
});
const pad = () =>
(container.querySelector('[class*="pt-(--hdr-h)"]') as HTMLElement).style.getPropertyValue(
"--hdr-h",
);
rerender(page(false));
expect(pad()).toBe("44px"); // the default alone: one row
rerender(page(true));
expect(pad()).toBe("84px"); // the page's two pills, the default gone
});
it("re-measures when one page's pills replace another's in the same commit", () => {
// Feed → Playlists: the leaving page's claim and the arriving page's land in one commit, so the
// number of claims never changes — the pad must follow the new pills all the same.
const height = (el: HTMLElement) => 40 * el.childElementCount;
const Feed = () => (
search
scope
);
const Playlists = () => (
search
);
const page = (which: "feed" | "playlists") => (
{which === "feed" ? : }
{fallback}
);
const { rerender, container } = render({null});
Object.defineProperty(screen.getByTestId("hdr"), "offsetHeight", {
get() {
return height(this as HTMLElement);
},
configurable: true,
});
const pad = () =>
(container.querySelector('[class*="pt-(--hdr-h)"]') as HTMLElement).style.getPropertyValue(
"--hdr-h",
);
rerender(page("feed"));
expect(pad()).toBe("84px");
rerender(page("playlists"));
expect(pad()).toBe("44px");
rerender(page("feed"));
expect(pad()).toBe("84px");
});
it("renders a page's pills nowhere in a shell without a header", () => {
render(
search
{fallback}
,
);
expect(screen.queryByText("search")).toBeNull();
expect(screen.queryByText("default")).toBeNull();
});
});