import { readFileSync, readdirSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { render } from "@testing-library/react"; import { CertificationsPageFrame } from "@/components/certifications/certifications-page-frame"; import { ContactsPageFrame } from "@/components/contacts/contacts-page-frame"; import { CoursesPageFrame } from "@/components/courses/courses-page-frame"; import { DivesPageFrame } from "@/components/dives/dives-page-frame"; import { PeoplePageFrame } from "@/components/people/people-page-frame"; import { SpeciesPageFrame } from "@/components/species/species-page-frame"; import { TripsPageFrame } from "@/components/trips/trips-page-frame"; // A one-list page's card shows no title, but it is still a section of its page, so it // keeps an `sr-only` `h2`. Nothing on screen would miss that heading if it went - the // only thing that notices is an empty page's outline, where the empty state's `h3` would // then sit straight under the page's `h1`. So this asserts the outline of the *empty* // render rather than the presence of the element: what the heading is for, rather than // the fact of it. const FRAMES = { "dives-page-frame": DivesPageFrame, "trips-page-frame": TripsPageFrame, "species-page-frame": SpeciesPageFrame, "courses-page-frame": CoursesPageFrame, "certifications-page-frame": CertificationsPageFrame, "contacts-page-frame": ContactsPageFrame, "people-page-frame": PeoplePageFrame, }; // The admin frames are checked in their own directories, since nothing out here may // import that section (`lib/admin-isolation.test.ts`). const CHECKED_INSIDE_ADMIN = [ "invite-queue-frame", "proposal-queue-frame", "species-catalog-frame", ]; const COMPONENTS = path.resolve( path.dirname(fileURLToPath(import.meta.url)), "../components", ); // Derived rather than listed: a one-list frame is one that draws a `CountBadge`, itself // or through `TableListFrame`, so a new one fails here rather than going unchecked. // `TableListFrame` is checked through the pages that draw it. The gear cards are not // frames and hold two lists between them, which is why their titles stay on screen // while there is anything under them (`app/gear/page.render.test.tsx`). const countingFrames = () => readdirSync(COMPONENTS, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && entry.name !== "ui") .flatMap((entry) => readdirSync(path.join(COMPONENTS, entry.name)) .filter((file) => file.endsWith("-frame.tsx")) .filter((file) => /\b(CountBadge|TableListFrame)\b/.test( readFileSync(path.join(COMPONENTS, entry.name, file), "utf8"), ), ) .map((file) => file.replace(/\.tsx$/, "")), ); describe("a one-list page's card keeps a heading nobody sees", () => { it("covers every frame that counts a list", () => { expect([...countingFrames()].sort()).toEqual( [...Object.keys(FRAMES), ...CHECKED_INSIDE_ADMIN].sort(), ); }); it.each(Object.entries(FRAMES))("%s", (_name, Frame) => { const { container } = render( , ); const headings = [...container.querySelectorAll("h1, h2, h3, h4, h5, h6")]; expect(headings.map((heading) => Number(heading.tagName[1]))).toEqual([ 1, 2, 3, ]); expect(headings[1]).toHaveClass("sr-only"); }); // And that heading is all it keeps. "0 total dives" over "No dives logged // yet" counts the thing the sentence below has just said there is none of, // and a search box that would search nothing sits beside it. Each frame's own // tests cover the list *narrowed* to nothing, which keeps both. it.each(Object.entries(FRAMES))("%s, and nothing else", (_name, Frame) => { const { container } = render( , ); const heading = container.querySelector("h2")!; expect([...heading.parentElement!.children]).toEqual([heading]); }); });