diff --git a/frontend/src/components/ArticleSelector/ArticleSelector.test.tsx b/frontend/src/components/ArticleSelector/ArticleSelector.test.tsx new file mode 100644 index 0000000..11f7885 --- /dev/null +++ b/frontend/src/components/ArticleSelector/ArticleSelector.test.tsx @@ -0,0 +1,64 @@ +import { render } from "@testing-library/react"; +import type { Division } from "../../lib/types"; +import ArticleSelector from "./ArticleSelector"; + +describe("ArticleSelector", () => { + const mockDivision: Division = { + type: "division", + title: "Chapter 1", + subtitle: "Introduction", + level: 0, + content: [ + { + type: "division", + title: "Section 1.1", + subtitle: "Overview", + level: 2, + content: [ + { + type: "article", + id: 1, + title: "Article 1", + subtitle: "Details", + }, + { + type: "article", + id: 2, + title: "Article 2", + subtitle: "Summary", + }, + ], + }, + ], + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("renders a top-level division as an optgroup", () => { + const { getByRole } = render(); + + const optgroup = getByRole("group", { + name: "Chapter 1: Introduction", + }); + expect(optgroup).toBeInTheDocument(); + }); + + test("renders nested divisions as options", () => { + const { getByText } = render(); + + const option1 = getByText("Article 1: Details"); + const option2 = getByText("Article 2: Summary"); + + expect(option1).toBeInTheDocument(); + expect(option2).toBeInTheDocument(); + }); + + test("flattens nested divisions correctly", () => { + const { container } = render(); + + const options = container.querySelectorAll("option"); + expect(options).toHaveLength(2); // Two options rendered + }); +}); diff --git a/frontend/src/components/ArticleSelector/ArticleSelector.tsx b/frontend/src/components/ArticleSelector/ArticleSelector.tsx index 4388915..9f3d390 100644 --- a/frontend/src/components/ArticleSelector/ArticleSelector.tsx +++ b/frontend/src/components/ArticleSelector/ArticleSelector.tsx @@ -1,4 +1,4 @@ -import { JSX } from "react"; +import { Fragment, JSX } from "react"; import type { Division } from "../../lib/types"; import useNavState from "../../store/navStore"; import styles from "./ArticleSelector.module.css"; @@ -7,47 +7,49 @@ type ArticleSelectorProps = { toc: Division[]; }; +/** + * Renders a division and its contents as a nested structure of optgroups and options. + * @param {Division} div - The division to render. + * @returns {JSX.Element} The rendered division, suitable for use inside a `select` tag. + */ +function renderDivision(div: Division): JSX.Element { + const contents = div.content.map((c) => { + if (c.type === "division") { + return renderDivision(c); + } else { + const title = `${c.title}: ${c.subtitle}`; + return ( + + ); + } + }); + + const title = `${div.title}: ${div.subtitle}`; + if (div.level === 0) { + return ( + // For top-level divisions, we can use optgroup + + {contents} + + ); + } else { + // HTML does not support nested optgroups, so we need to flatten the structure + return {contents}; + } +} + function ArticleSelector({ toc }: ArticleSelectorProps) { const { articleId, setArticleId } = useNavState(); - function renderDivision(div: Division): JSX.Element { - const contents = div.content.map((c) => { - if (c.type === "division") { - return renderDivision(c); - } else { - return ( - - ); - } - }); - - if (div.level === 0) { - const title = `${div.title}: ${div.subtitle}`; - return ( - // For top-level divisions, we can use optgroup - - {contents} - - ); - } else { - // HTML does not support nested optgroups, so we need to flatten the structure - return <>{contents}; - } - } - return ( ); } diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 04f08f8..8fb0175 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -16,7 +16,7 @@ type Division = { title: string; subtitle: string; level: number; - content: Article[] | Division[]; + content: (Article | Division)[]; }; export { Language };