diff --git a/packages/ui/src/components/category-filter/category-filter.test.tsx b/packages/ui/src/components/category-filter/category-filter.test.tsx
new file mode 100644
index 00000000..a05c3768
--- /dev/null
+++ b/packages/ui/src/components/category-filter/category-filter.test.tsx
@@ -0,0 +1,66 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+let mockPathname = "/en/design-systems";
+
+vi.mock("next/navigation", () => ({
+ usePathname: () => mockPathname,
+}));
+
+import { CategoryFilter } from "./category-filter";
+
+describe("CategoryFilter", () => {
+ it("renders nothing when there are no categories", () => {
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("deduplicates and sorts category labels", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getAllByText(/Alpha|Design systems|Zeta/)).toHaveLength(3);
+ expect(
+ screen.getAllByText(/Alpha|Design systems|Zeta/).map((node) => {
+ return node.textContent;
+ }),
+ ).toEqual(["Alpha", "Design systems", "Zeta"]);
+ });
+
+ it("slugifies category links with the active language", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Résumé Tips").closest("a")).toHaveAttribute(
+ "href",
+ "/fr/resume-tips",
+ );
+ expect(screen.getByText("Data & AI").closest("a")).toHaveAttribute(
+ "href",
+ "/fr/data-ai",
+ );
+ });
+
+ it("renders the selected category as a non-link badge", () => {
+ mockPathname = "/en/design-systems";
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Design systems").closest("a")).toBeNull();
+ expect(screen.getByText("Components").closest("a")).toHaveAttribute(
+ "href",
+ "/en/components",
+ );
+ });
+});
diff --git a/packages/ui/src/components/content-intro/content-intro.test.tsx b/packages/ui/src/components/content-intro/content-intro.test.tsx
new file mode 100644
index 00000000..74be5649
--- /dev/null
+++ b/packages/ui/src/components/content-intro/content-intro.test.tsx
@@ -0,0 +1,76 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { ContentIntro, type ContentIntroSection } from "./content-intro";
+
+const sections: ContentIntroSection[] = [
+ { id: "setup", title: "Set up the workspace" },
+ { id: "ship", title: "Ship the first flow" },
+];
+
+const baseProps = {
+ completedSections: new Set(),
+ estimatedTime: "12 min",
+ onGoToSection: vi.fn(),
+ onStart: vi.fn(),
+ renderIntroContent: () => Read the framing first.
,
+ sections,
+ title: "Build a tutorial",
+};
+
+describe("ContentIntro", () => {
+ it("renders the intro content and table of contents", () => {
+ render();
+
+ expect(screen.getByText("Build a tutorial")).toBeInTheDocument();
+ expect(screen.getByText("Read the framing first.")).toBeInTheDocument();
+ expect(screen.getByText("Set up the workspace")).toBeInTheDocument();
+ expect(screen.getByText("Ship the first flow")).toBeInTheDocument();
+ });
+
+ it("calls onGoToSection with the clicked section index", () => {
+ const handleGoToSection = vi.fn();
+ render();
+
+ fireEvent.click(screen.getByText("Ship the first flow"));
+
+ expect(handleGoToSection).toHaveBeenCalledWith(1);
+ });
+
+ it("shows progress and continue copy when sections are completed", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("1/2 completed")).toBeInTheDocument();
+ expect(screen.getByText("Keep going")).toBeInTheDocument();
+ expect(screen.getByText("Set up the workspace")).toHaveClass(
+ "line-through",
+ );
+ });
+
+ it("calls onStart from the primary button and Enter shortcut", () => {
+ const handleStart = vi.fn();
+ render();
+
+ fireEvent.click(screen.getByText("Start Tutorial"));
+ fireEvent.keyDown(document, { key: "Enter" });
+
+ expect(handleStart).toHaveBeenCalledTimes(2);
+ });
+
+ it("renders additional content when supplied", () => {
+ render(
+ Author notes}
+ />,
+ );
+
+ expect(screen.getByText("Author notes")).toBeInTheDocument();
+ });
+});
diff --git a/packages/ui/src/components/table-of-contents-panel/table-of-contents-panel.test.tsx b/packages/ui/src/components/table-of-contents-panel/table-of-contents-panel.test.tsx
new file mode 100644
index 00000000..8ef6aeeb
--- /dev/null
+++ b/packages/ui/src/components/table-of-contents-panel/table-of-contents-panel.test.tsx
@@ -0,0 +1,93 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import {
+ TableOfContentsPanel,
+ type TOCSection,
+} from "./table-of-contents-panel";
+
+const sections: TOCSection[] = [
+ { id: "intro", title: "Introduction" },
+ { id: "deep-dive", title: "Deep dive" },
+];
+
+const baseProps = {
+ completedSections: new Set(["intro"]),
+ completionCount: 1,
+ currentSectionIndex: 0,
+ isOpen: true,
+ onClose: vi.fn(),
+ onSelectSection: vi.fn(),
+ sections,
+ totalSections: 2,
+};
+
+describe("TableOfContentsPanel", () => {
+ it("renders nothing when closed", () => {
+ render();
+
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+
+ it("renders the dialog with progress and sections when open", () => {
+ render();
+
+ expect(screen.getByRole("dialog")).toHaveAttribute("aria-modal", "true");
+ expect(screen.getByText("Table of Contents")).toBeInTheDocument();
+ expect(screen.getByText("1 / 2 (50%)")).toBeInTheDocument();
+ expect(screen.getByText("Introduction")).toBeInTheDocument();
+ expect(screen.getByText("Deep dive")).toBeInTheDocument();
+ });
+
+ it("selects a section and closes the panel", () => {
+ const handleClose = vi.fn();
+ const handleSelectSection = vi.fn();
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByText("Deep dive"));
+
+ expect(handleSelectSection).toHaveBeenCalledWith(1);
+ expect(handleClose).toHaveBeenCalledTimes(1);
+ });
+
+ it("closes on Escape and backdrop click", () => {
+ const handleClose = vi.fn();
+ const { container } = render(
+ ,
+ );
+ const backdrop = container.querySelector(
+ "[aria-hidden='true']",
+ );
+ if (!backdrop) throw new Error("Expected backdrop to render");
+
+ fireEvent.keyDown(window, { key: "Escape" });
+ fireEvent.click(backdrop);
+
+ expect(handleClose).toHaveBeenCalledTimes(2);
+ });
+
+ it("renders the reset action only when progress exists", () => {
+ const handleReset = vi.fn();
+ const { rerender } = render(
+ ,
+ );
+
+ fireEvent.click(screen.getByText("Reset Progress"));
+ expect(handleReset).toHaveBeenCalledTimes(1);
+
+ rerender(
+ ,
+ );
+ expect(screen.queryByText("Reset Progress")).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/ui/src/components/tutorial-filters/tutorial-filters.test.tsx b/packages/ui/src/components/tutorial-filters/tutorial-filters.test.tsx
new file mode 100644
index 00000000..8b219185
--- /dev/null
+++ b/packages/ui/src/components/tutorial-filters/tutorial-filters.test.tsx
@@ -0,0 +1,119 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import {
+ TutorialFilters,
+ type TutorialFiltersLabels,
+} from "./tutorial-filters";
+
+const labels: TutorialFiltersLabels = {
+ activeFilters: "Active filters:",
+ clear: "Clear",
+ clearAll: "Clear all",
+ difficulty: {
+ advanced: "Advanced",
+ all: "All",
+ beginner: "Beginner",
+ intermediate: "Intermediate",
+ },
+ difficultyLabel: "Difficulty",
+ searchFilter: "Search",
+ searchLabel: "Search tutorials",
+ searchPlaceholder: "Search by topic",
+ tagsLabel: "Tags",
+};
+
+const baseProps = {
+ currentDifficulty: "",
+ currentTags: [] as string[],
+ labels,
+ onFilterChange: vi.fn(),
+ searchQuery: "",
+ tags: ["React", "Design"],
+};
+
+describe("TutorialFilters", () => {
+ it("renders search, difficulty options, and tags", () => {
+ render();
+
+ expect(screen.getByLabelText("Search tutorials")).toHaveAttribute(
+ "placeholder",
+ "Search by topic",
+ );
+ expect(screen.getByText("Beginner")).toBeInTheDocument();
+ expect(screen.getByText("Advanced")).toBeInTheDocument();
+ expect(screen.getByText("React")).toBeInTheDocument();
+ expect(screen.getByText("Design")).toBeInTheDocument();
+ });
+
+ it("emits search and difficulty updates", () => {
+ const handleFilterChange = vi.fn();
+ render(
+ ,
+ );
+
+ fireEvent.change(screen.getByLabelText("Search tutorials"), {
+ target: { value: "state" },
+ });
+ fireEvent.click(screen.getByText("Intermediate"));
+
+ expect(handleFilterChange).toHaveBeenCalledWith({ search: "state" });
+ expect(handleFilterChange).toHaveBeenCalledWith({
+ difficulty: "intermediate",
+ });
+ });
+
+ it("toggles tags from the current selection", () => {
+ const handleFilterChange = vi.fn();
+ render(
+ ,
+ );
+ const [reactTag] = screen.getAllByText("React");
+ if (!reactTag) throw new Error("Expected React tag to render");
+
+ fireEvent.click(reactTag);
+ fireEvent.click(screen.getByText("Design"));
+
+ expect(handleFilterChange).toHaveBeenCalledWith({ tags: [] });
+ expect(handleFilterChange).toHaveBeenCalledWith({
+ tags: ["React", "Design"],
+ });
+ });
+
+ it("renders active filters and clears them", () => {
+ const handleFilterChange = vi.fn();
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Active filters:")).toBeInTheDocument();
+ expect(screen.getByText('Search "routing"')).toBeInTheDocument();
+
+ fireEvent.click(screen.getByText("Clear"));
+ fireEvent.click(screen.getByText("Clear all"));
+
+ expect(handleFilterChange).toHaveBeenCalledWith({ tags: [] });
+ expect(handleFilterChange).toHaveBeenCalledWith({
+ difficulty: "all",
+ search: "",
+ tags: [],
+ });
+ });
+
+ it("disables search and difficulty controls while pending", () => {
+ render();
+
+ expect(screen.getByLabelText("Search tutorials")).toBeDisabled();
+ expect(screen.getByText("Beginner").closest("button")).toBeDisabled();
+ });
+});
diff --git a/packages/ui/src/components/tutorial-intro-content/tutorial-intro-content.test.tsx b/packages/ui/src/components/tutorial-intro-content/tutorial-intro-content.test.tsx
new file mode 100644
index 00000000..697f5baf
--- /dev/null
+++ b/packages/ui/src/components/tutorial-intro-content/tutorial-intro-content.test.tsx
@@ -0,0 +1,64 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { TutorialIntroContent } from "./tutorial-intro-content";
+
+describe("TutorialIntroContent", () => {
+ it("renders the supplied title and markdown content", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByRole("heading", { name: "Start here" })).toBeVisible();
+ expect(screen.getByText(/Intro paragraph with/)).toBeInTheDocument();
+ expect(screen.getByText("strong text")).toHaveClass("font-semibold");
+ });
+
+ it("renders links and inline code with the expected semantics", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("pnpm").tagName).toBe("CODE");
+ expect(screen.getByText("pnpm").closest("a")).toHaveAttribute(
+ "href",
+ "https://pnpm.io",
+ );
+ });
+
+ it("strips MDX component tags before markdown rendering", () => {
+ render(
+ Hidden client-only content",
+ "",
+ ].join("\n\n")}
+ title="Hybrid content"
+ />,
+ );
+
+ expect(screen.getByText("Visible copy.")).toBeInTheDocument();
+ expect(
+ screen.queryByText("Hidden client-only content"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("applies a custom class name to the section", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelector("section")).toHaveClass("custom-intro");
+ });
+});