import { act } from "react"; import { describe, expect, it, vi } from "vitest"; import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { useDiveFormSections } from "./dive-form-sections"; import { SectionIndexNav, SectionIndexProvider, } from "@/components/ui/section-index"; import { DiveFormSection } from "./dive-form-section"; import { DIVE_SECTION_ICONS } from "./dive-section-icon"; import { DIVE_FORM_FIELD_GROUPS, type DiveFormFieldGroup, } from "@/lib/dive-form-fields"; import { scrollPast } from "@/test/intersection"; // The form, reduced to what the index reads: the hook that owns which sections // are collapsed, and a card per section reporting itself to it. Import is the // one with an action in place of a chevron, as the real one is. function Harness({ groups }: { groups: readonly DiveFormFieldGroup[] }) { const sections = useDiveFormSections(); return ( {groups.map((group) => group === "Import" ? ( Upload} >

Files

) : ( sections.setGroupOpen(group, open)} >

{group} fields

), )}
); } function index() { return within(screen.getByRole("navigation", { name: "Sections" })); } function entries() { return index() .getAllByRole("button") .map((entry) => entry.textContent); } // The card a heading belongs to - the focusable ancestor a jump lands on. function cardOf(heading: HTMLElement) { return heading.closest('[tabindex="-1"]')!; } describe("the dive form's section index", () => { it("lists the sections on the page, in form order, and follows them", () => { const { rerender } = render( , ); expect(entries()).toEqual(["Import", "Dive info", "Notes"]); rerender(); expect(entries()).toEqual(["Import", "Notes"]); }); it("marks the last section whose heading has stuck, else the first", () => { render(); const current = () => index().getByRole("button", { current: "location" }).textContent; expect(current()).toBe("Import"); act(() => scrollPast(cardOf(screen.getByRole("heading", { name: "Import" }))), ); expect(current()).toBe("Import"); act(() => scrollPast(cardOf(screen.getByRole("heading", { name: "Dive info" }))), ); expect(current()).toBe("Dive info"); expect( index().getAllByRole("button", { current: "location" }), ).toHaveLength(1); }); it("opens a collapsed section on a jump, scrolls to it and puts focus on it", async () => { render(); const scrollIntoView = vi.mocked(Element.prototype.scrollIntoView); scrollIntoView.mockClear(); const heading = () => within(screen.getByRole("heading", { name: "Notes" })).getByRole( "button", ); await userEvent.click(heading()); expect(heading()).toHaveAttribute("aria-expanded", "false"); await userEvent.click(index().getByRole("button", { name: "Notes" })); expect(heading()).toHaveAttribute("aria-expanded", "true"); expect(screen.getByText("Notes fields")).toBeInTheDocument(); expect(scrollIntoView).toHaveBeenCalledWith( expect.objectContaining({ block: "start" }), ); expect(heading()).toHaveFocus(); }); it("lands a jump to a section without a heading control on its card", async () => { render(); await userEvent.click(index().getByRole("button", { name: "Import" })); expect( cardOf(screen.getByRole("heading", { name: "Import" })), ).toHaveFocus(); }); }); describe("DIVE_SECTION_ICONS", () => { it("gives every section a glyph of its own", () => { const icons = DIVE_FORM_FIELD_GROUPS.map( (group) => DIVE_SECTION_ICONS[group], ); expect(new Set(icons).size).toBe(DIVE_FORM_FIELD_GROUPS.length); }); });