src/components/dives/dive-form-sections.render.test.tsx
Rama main · réplica de lectura del repositorio original.
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 (
<SectionIndexProvider index={sections.index}>
<SectionIndexNav />
{groups.map((group) =>
group === "Import" ? (
<DiveFormSection
key={group}
title={group}
action={<button type="button">Upload</button>}
>
<p>Files</p>
</DiveFormSection>
) : (
<DiveFormSection
key={group}
title={group}
open={!sections.collapsedGroups.has(group)}
onOpenChange={(open) => sections.setGroupOpen(group, open)}
>
<p>{group} fields</p>
</DiveFormSection>
),
)}
</SectionIndexProvider>
);
}
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<HTMLElement>('[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(
<Harness groups={["Notes", "Import", "Dive info"]} />,
);
expect(entries()).toEqual(["Import", "Dive info", "Notes"]);
rerender(<Harness groups={["Notes", "Import"]} />);
expect(entries()).toEqual(["Import", "Notes"]);
});
it("marks the last section whose heading has stuck, else the first", () => {
render(<Harness groups={["Import", "Dive info", "Notes"]} />);
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(<Harness groups={["Import", "Notes"]} />);
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(<Harness groups={["Import", "Notes"]} />);
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);
});
});