From f0ab563f962d2a7ba671e041a40fb7cc779909bc Mon Sep 17 00:00:00 2001
From: bntvllnt
Date: Wed, 13 May 2026 07:45:26 +0200
Subject: [PATCH] test(components): backfill learning content coverage
---
.../lang-provider/lang-provider.test.tsx | 47 ++++++
.../overview-board/overview-board.test.tsx | 72 ++++++++
packages/ui/src/components/quiz/quiz.test.tsx | 96 +++++++++++
.../step-by-step/step-by-step.test.tsx | 48 ++++++
.../tutorial-complete.test.tsx | 157 ++++++++++++++++++
5 files changed, 420 insertions(+)
create mode 100644 packages/ui/src/components/lang-provider/lang-provider.test.tsx
create mode 100644 packages/ui/src/components/overview-board/overview-board.test.tsx
create mode 100644 packages/ui/src/components/quiz/quiz.test.tsx
create mode 100644 packages/ui/src/components/step-by-step/step-by-step.test.tsx
create mode 100644 packages/ui/src/components/tutorial-complete/tutorial-complete.test.tsx
diff --git a/packages/ui/src/components/lang-provider/lang-provider.test.tsx b/packages/ui/src/components/lang-provider/lang-provider.test.tsx
new file mode 100644
index 00000000..18c6d152
--- /dev/null
+++ b/packages/ui/src/components/lang-provider/lang-provider.test.tsx
@@ -0,0 +1,47 @@
+import { render, waitFor } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { LangProvider } from "./lang-provider";
+
+let mockPathname = "/en/docs";
+
+vi.mock("next/navigation", () => ({
+ usePathname: () => mockPathname,
+}));
+
+describe("LangProvider", () => {
+ afterEach(() => {
+ mockPathname = "/en/docs";
+ document.documentElement.removeAttribute("lang");
+ });
+
+ it("sets the document language from a supported pathname prefix", async () => {
+ mockPathname = "/fr/components/button";
+
+ render();
+
+ await waitFor(() => {
+ expect(document.documentElement).toHaveAttribute("lang", "fr");
+ });
+ });
+
+ it("uses the default language when the pathname has no locale prefix", async () => {
+ mockPathname = "/components/button";
+
+ render();
+
+ await waitFor(() => {
+ expect(document.documentElement).toHaveAttribute("lang", "fr");
+ });
+ });
+
+ it("ignores unsupported locale prefixes", async () => {
+ mockPathname = "/de/components/button";
+
+ render();
+
+ await waitFor(() => {
+ expect(document.documentElement).toHaveAttribute("lang", "en");
+ });
+ });
+});
diff --git a/packages/ui/src/components/overview-board/overview-board.test.tsx b/packages/ui/src/components/overview-board/overview-board.test.tsx
new file mode 100644
index 00000000..62e9295c
--- /dev/null
+++ b/packages/ui/src/components/overview-board/overview-board.test.tsx
@@ -0,0 +1,72 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { OverviewBoard, type OverviewBoardItem } from "./overview-board";
+
+const errorItem: OverviewBoardItem = {
+ ctaLabel: "Review errors",
+ description: "Two checks need attention.",
+ handleCtaClick: vi.fn(),
+ heading: "Error budget",
+ id: "errors",
+ metric: "2",
+ tone: "danger",
+};
+
+const actionItem: OverviewBoardItem = {
+ description: "One action is waiting.",
+ heading: "Action queue",
+ id: "actions",
+ metric: "1",
+ tone: "warning",
+};
+
+const items: OverviewBoardItem[] = [errorItem, actionItem];
+
+describe("OverviewBoard", () => {
+ it("renders board copy and item metrics", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByText("Operations")).toBeInTheDocument();
+ expect(
+ screen.getByRole("heading", { name: "Run overview" }),
+ ).toBeInTheDocument();
+ expect(screen.getByText("Critical work at a glance.")).toBeInTheDocument();
+ expect(screen.getByText("Error budget")).toBeInTheDocument();
+ expect(screen.getByText("Two checks need attention.")).toBeInTheDocument();
+ expect(screen.getByText("2")).toBeInTheDocument();
+ });
+
+ it("renders default icons based on heading text", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelector(".lucide-circle-alert")).toBeInTheDocument();
+ expect(container.querySelector(".lucide-list-todo")).toBeInTheDocument();
+ });
+
+ it("renders CTA buttons only when configured and calls the item handler", () => {
+ const handleCtaClick = vi.fn();
+ render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: /review errors/i }));
+
+ expect(handleCtaClick).toHaveBeenCalledTimes(1);
+ expect(
+ screen.queryByRole("button", { name: /action queue/i }),
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/ui/src/components/quiz/quiz.test.tsx b/packages/ui/src/components/quiz/quiz.test.tsx
new file mode 100644
index 00000000..a1d95d81
--- /dev/null
+++ b/packages/ui/src/components/quiz/quiz.test.tsx
@@ -0,0 +1,96 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { Quiz, type QuizOption } from "./quiz";
+
+function keyedOption(option: QuizOption, key: string): QuizOption {
+ return Object.assign(option, {
+ toString: () => key,
+ });
+}
+
+const options: QuizOption[] = [
+ keyedOption(
+ {
+ correct: true,
+ explanation: "Paris is the capital of France.",
+ label: "Paris",
+ },
+ "paris",
+ ),
+ keyedOption(
+ {
+ explanation: "London is the capital of the United Kingdom.",
+ label: "London",
+ },
+ "london",
+ ),
+];
+
+function renderQuiz(onAnswer = vi.fn()) {
+ render(
+ Paris is correct.
}
+ hint="Think of the Eiffel Tower."
+ onAnswer={onAnswer}
+ options={options}
+ question="What is the capital of France?"
+ />,
+ );
+
+ return { onAnswer };
+}
+
+describe("Quiz", () => {
+ it("renders the question and disables submission until an option is selected", () => {
+ renderQuiz();
+
+ expect(
+ screen.getByText("What is the capital of France?"),
+ ).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Check Answer" })).toBeDisabled();
+
+ fireEvent.click(screen.getByRole("button", { name: "Paris" }));
+
+ expect(
+ screen.getByRole("button", { name: "Check Answer" }),
+ ).not.toBeDisabled();
+ });
+
+ it("reveals the hint before submission", () => {
+ renderQuiz();
+
+ fireEvent.click(screen.getByRole("button", { name: "Show hint" }));
+
+ expect(screen.getByText("Think of the Eiffel Tower.")).toBeInTheDocument();
+ });
+
+ it("submits a correct answer and resets state", () => {
+ const { onAnswer } = renderQuiz();
+
+ fireEvent.click(screen.getByRole("button", { name: "Paris" }));
+ fireEvent.click(screen.getByRole("button", { name: "Check Answer" }));
+
+ expect(onAnswer).toHaveBeenCalledWith(true);
+ expect(screen.getByText("Correct!")).toBeInTheDocument();
+ expect(screen.getByText("Paris is correct.")).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole("button", { name: "Try Again" }));
+
+ expect(screen.getByRole("button", { name: "Check Answer" })).toBeDisabled();
+ expect(screen.queryByText("Correct!")).not.toBeInTheDocument();
+ });
+
+ it("submits an incorrect answer", () => {
+ const { onAnswer } = renderQuiz();
+
+ fireEvent.click(screen.getByRole("button", { name: "London" }));
+ fireEvent.click(screen.getByRole("button", { name: "Check Answer" }));
+
+ expect(onAnswer).toHaveBeenCalledWith(false);
+ expect(screen.getByText("Not quite right.")).toBeInTheDocument();
+ expect(
+ screen.getByText("London is the capital of the United Kingdom."),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/packages/ui/src/components/step-by-step/step-by-step.test.tsx b/packages/ui/src/components/step-by-step/step-by-step.test.tsx
new file mode 100644
index 00000000..a5ef31f2
--- /dev/null
+++ b/packages/ui/src/components/step-by-step/step-by-step.test.tsx
@@ -0,0 +1,48 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { Step, StepByStep } from "./step-by-step";
+
+function renderSteps(interactive = false) {
+ render(
+
+
+ Install dependencies.
+
+
+ Build the package.
+
+ ,
+ );
+}
+
+describe("StepByStep", () => {
+ it("renders non-interactive numbered steps", () => {
+ renderSteps();
+
+ expect(
+ screen.getByRole("heading", { name: "Setup guide" }),
+ ).toBeInTheDocument();
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.getByText("2")).toBeInTheDocument();
+ expect(screen.getByText("Install")).toBeInTheDocument();
+ expect(screen.getByText("Build the package.")).toBeInTheDocument();
+ });
+
+ it("tracks completed steps in interactive mode", () => {
+ renderSteps(true);
+
+ expect(screen.getByText("0/2 completed")).toBeInTheDocument();
+
+ const firstStepButton = screen.getByRole("button", { name: "1" });
+ fireEvent.click(firstStepButton);
+
+ expect(screen.getByText("1/2 completed")).toBeInTheDocument();
+ expect(screen.getByText("Install")).toHaveClass("line-through");
+
+ fireEvent.click(firstStepButton);
+
+ expect(screen.getByText("0/2 completed")).toBeInTheDocument();
+ expect(screen.getByText("Install")).not.toHaveClass("line-through");
+ });
+});
diff --git a/packages/ui/src/components/tutorial-complete/tutorial-complete.test.tsx b/packages/ui/src/components/tutorial-complete/tutorial-complete.test.tsx
new file mode 100644
index 00000000..0d7cea70
--- /dev/null
+++ b/packages/ui/src/components/tutorial-complete/tutorial-complete.test.tsx
@@ -0,0 +1,157 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import type { ReactNode } from "react";
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock("../profile-section", () => ({
+ ProfileSection: ({
+ dict,
+ }: {
+ dict: { profile: { name: string; tagline: string } };
+ }) => (
+
+ {dict.profile.name}
+ {dict.profile.tagline}
+
+ ),
+}));
+
+vi.mock("../share-section", () => ({
+ ShareSection: ({
+ shareTitle,
+ title,
+ url,
+ }: {
+ shareTitle: string;
+ title: string;
+ url: string;
+ }) => (
+
+ {shareTitle}: {title} ({url})
+
+ ),
+}));
+
+import {
+ TutorialComplete,
+ type TutorialCompleteProps,
+} from "./tutorial-complete";
+
+const labels = {
+ backToTutorials: "Back to Tutorials",
+ profileName: "Jane Doe",
+ profileTagline: "React Developer",
+ relatedContent: "Continue Learning",
+ reviewSections: "Review Sections",
+ shareOn: "Share on",
+ shareTitle: "Share this tutorial",
+ startOver: "Start Over",
+ tutorialComplete: "Tutorial Complete!",
+ tutorialFinished: "Tutorial Finished",
+ youveCompletedAll: "You've completed all sections of",
+ youveFinishedWith: "You've finished with",
+};
+
+const sections = [
+ { id: "intro", title: "Introduction" },
+ { id: "advanced", title: "Advanced Concepts" },
+];
+
+function TestLink({
+ children,
+ className,
+ href,
+}: {
+ children: ReactNode;
+ className?: string;
+ href: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+function renderTutorialComplete(props: Partial = {}) {
+ const onGoToSection = vi.fn();
+ const onRestart = vi.fn();
+ render(
+ ,
+ );
+
+ return { onGoToSection, onRestart };
+}
+
+describe("TutorialComplete", () => {
+ it("renders the full completion state and restarts", () => {
+ const { onRestart } = renderTutorialComplete();
+
+ expect(
+ screen.getByRole("heading", { name: "Tutorial Complete!" }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('You\'ve completed all sections of "React Basics"'),
+ ).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole("button", { name: "Start Over" }));
+
+ expect(onRestart).toHaveBeenCalledTimes(1);
+ });
+
+ it("renders partial completion copy", () => {
+ renderTutorialComplete({
+ completedSections: new Set(["intro"]),
+ completionPercent: 50,
+ });
+
+ expect(
+ screen.getByRole("heading", { name: "Tutorial Finished" }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText('You\'ve finished with "React Basics" (50%)'),
+ ).toBeInTheDocument();
+ });
+
+ it("navigates to review sections by index", () => {
+ const { onGoToSection } = renderTutorialComplete();
+
+ fireEvent.click(screen.getByRole("button", { name: /advanced concepts/i }));
+
+ expect(onGoToSection).toHaveBeenCalledWith(1);
+ });
+
+ it("renders related links, share content, profile, and back link", () => {
+ renderTutorialComplete({
+ profile: {
+ imageSource: "/profile.png",
+ socialLinks: [{ href: "https://example.com", label: "Website" }],
+ },
+ });
+
+ expect(
+ screen.getByRole("link", { name: /tutorial next tutorial/i }),
+ ).toHaveAttribute("href", "/next");
+ expect(screen.getByLabelText("share")).toHaveTextContent(
+ "Share this tutorial: React Basics (https://example.com/tutorial)",
+ );
+ expect(screen.getByLabelText("profile")).toHaveTextContent("Jane Doe");
+ expect(
+ screen.getByRole("link", { name: "← Back to Tutorials" }),
+ ).toHaveAttribute("href", "/tutorials");
+ });
+});