Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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(<CategoryFilter categories={[]} lang="en" />);

expect(container).toBeEmptyDOMElement();
});

it("deduplicates and sorts category labels", () => {
render(
<CategoryFilter
categories={["zeta", "alpha", "zeta", "design systems"]}
lang="en"
/>,
);

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(
<CategoryFilter categories={["Résumé Tips", "Data & AI"]} lang="fr" />,
);

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(
<CategoryFilter
categories={["design systems", "components"]}
lang="en"
/>,
);

expect(screen.getByText("Design systems").closest("a")).toBeNull();
expect(screen.getByText("Components").closest("a")).toHaveAttribute(
"href",
"/en/components",
);
});
});
76 changes: 76 additions & 0 deletions packages/ui/src/components/content-intro/content-intro.test.tsx
Original file line number Diff line number Diff line change
@@ -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<string>(),
estimatedTime: "12 min",
onGoToSection: vi.fn(),
onStart: vi.fn(),
renderIntroContent: () => <p>Read the framing first.</p>,
sections,
title: "Build a tutorial",
};

describe("ContentIntro", () => {
it("renders the intro content and table of contents", () => {
render(<ContentIntro {...baseProps} />);

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(<ContentIntro {...baseProps} onGoToSection={handleGoToSection} />);

fireEvent.click(screen.getByText("Ship the first flow"));

expect(handleGoToSection).toHaveBeenCalledWith(1);
});

it("shows progress and continue copy when sections are completed", () => {
render(
<ContentIntro
{...baseProps}
completedSections={new Set(["setup"])}
labels={{ continueLabel: "Keep going" }}
/>,
);

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(<ContentIntro {...baseProps} onStart={handleStart} />);

fireEvent.click(screen.getByText("Start Tutorial"));
fireEvent.keyDown(document, { key: "Enter" });

expect(handleStart).toHaveBeenCalledTimes(2);
});

it("renders additional content when supplied", () => {
render(
<ContentIntro
{...baseProps}
additionalContent={<aside>Author notes</aside>}
/>,
);

expect(screen.getByText("Author notes")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -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<string>(["intro"]),
completionCount: 1,
currentSectionIndex: 0,
isOpen: true,
onClose: vi.fn(),
onSelectSection: vi.fn(),
sections,
totalSections: 2,
};

describe("TableOfContentsPanel", () => {
it("renders nothing when closed", () => {
render(<TableOfContentsPanel {...baseProps} isOpen={false} />);

expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});

it("renders the dialog with progress and sections when open", () => {
render(<TableOfContentsPanel {...baseProps} />);

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(
<TableOfContentsPanel
{...baseProps}
onClose={handleClose}
onSelectSection={handleSelectSection}
/>,
);

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(
<TableOfContentsPanel {...baseProps} onClose={handleClose} />,
);
const backdrop = container.querySelector<HTMLElement>(
"[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(
<TableOfContentsPanel {...baseProps} onReset={handleReset} />,
);

fireEvent.click(screen.getByText("Reset Progress"));
expect(handleReset).toHaveBeenCalledTimes(1);

rerender(
<TableOfContentsPanel
{...baseProps}
completionCount={0}
onReset={handleReset}
/>,
);
expect(screen.queryByText("Reset Progress")).not.toBeInTheDocument();
});
});
119 changes: 119 additions & 0 deletions packages/ui/src/components/tutorial-filters/tutorial-filters.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<TutorialFilters {...baseProps} />);

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(
<TutorialFilters {...baseProps} onFilterChange={handleFilterChange} />,
);

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(
<TutorialFilters
{...baseProps}
currentTags={["React"]}
onFilterChange={handleFilterChange}
/>,
);
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(
<TutorialFilters
{...baseProps}
currentDifficulty="advanced"
currentTags={["React"]}
onFilterChange={handleFilterChange}
searchQuery="routing"
/>,
);

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(<TutorialFilters {...baseProps} isPending />);

expect(screen.getByLabelText("Search tutorials")).toBeDisabled();
expect(screen.getByText("Beginner").closest("button")).toBeDisabled();
});
});
Loading
Loading