security and accessibility pass
This commit is contained in:
92
frontend/src/components/questions/question-card.test.tsx
Normal file
92
frontend/src/components/questions/question-card.test.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { QuestionCard } from "./question-card";
|
||||
import type { QuestionListItem } from "@/types";
|
||||
|
||||
const mockQuestion: QuestionListItem = {
|
||||
id: "123",
|
||||
title: "Two Sum",
|
||||
slug: "two-sum",
|
||||
difficulty: "easy",
|
||||
leetcode_id: 1,
|
||||
leetcode_url: "https://leetcode.com/problems/two-sum",
|
||||
categories: [
|
||||
{ id: "cat1", name: "Arrays", slug: "arrays" },
|
||||
{ id: "cat2", name: "Hash Table", slug: "hash-table" },
|
||||
],
|
||||
patterns: [{ id: "pat1", name: "Hash Map", slug: "hash-map" }],
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
describe("QuestionCard", () => {
|
||||
it("renders question title", () => {
|
||||
render(<QuestionCard question={mockQuestion} />);
|
||||
expect(screen.getByText("Two Sum")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders difficulty badge with correct label", () => {
|
||||
render(<QuestionCard question={mockQuestion} />);
|
||||
const badge = screen.getByText("Easy");
|
||||
expect(badge).toBeInTheDocument();
|
||||
expect(badge).toHaveAttribute("aria-label", "Easy difficulty");
|
||||
});
|
||||
|
||||
it("links to question detail page", () => {
|
||||
render(<QuestionCard question={mockQuestion} />);
|
||||
const link = screen.getByRole("link");
|
||||
expect(link).toHaveAttribute("href", "/questions/two-sum");
|
||||
});
|
||||
|
||||
it("renders all categories", () => {
|
||||
render(<QuestionCard question={mockQuestion} />);
|
||||
expect(screen.getByText("Arrays")).toBeInTheDocument();
|
||||
expect(screen.getByText("Hash Table")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders patterns", () => {
|
||||
render(<QuestionCard question={mockQuestion} />);
|
||||
expect(screen.getByText("Hash Map")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders LeetCode ID when present", () => {
|
||||
render(<QuestionCard question={mockQuestion} />);
|
||||
expect(screen.getByText("LeetCode #1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render LeetCode ID when absent", () => {
|
||||
const questionWithoutLeetcode = {
|
||||
...mockQuestion,
|
||||
leetcode_id: null,
|
||||
};
|
||||
render(<QuestionCard question={questionWithoutLeetcode} />);
|
||||
expect(screen.queryByText(/LeetCode/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders medium difficulty with correct styling", () => {
|
||||
const mediumQuestion = { ...mockQuestion, difficulty: "medium" as const };
|
||||
render(<QuestionCard question={mediumQuestion} />);
|
||||
const badge = screen.getByText("Medium");
|
||||
expect(badge).toHaveAttribute("aria-label", "Medium difficulty");
|
||||
});
|
||||
|
||||
it("renders hard difficulty with correct styling", () => {
|
||||
const hardQuestion = { ...mockQuestion, difficulty: "hard" as const };
|
||||
render(<QuestionCard question={hardQuestion} />);
|
||||
const badge = screen.getByText("Hard");
|
||||
expect(badge).toHaveAttribute("aria-label", "Hard difficulty");
|
||||
});
|
||||
|
||||
it("handles empty patterns array", () => {
|
||||
const questionNoPatterns = { ...mockQuestion, patterns: [] };
|
||||
render(<QuestionCard question={questionNoPatterns} />);
|
||||
expect(screen.getByText("Two Sum")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Hash Map")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("handles empty categories array", () => {
|
||||
const questionNoCategories = { ...mockQuestion, categories: [] };
|
||||
render(<QuestionCard question={questionNoCategories} />);
|
||||
expect(screen.getByText("Two Sum")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Arrays")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
198
frontend/src/components/questions/question-filters.test.tsx
Normal file
198
frontend/src/components/questions/question-filters.test.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { QuestionFilters } from "./question-filters";
|
||||
import type { Category, Pattern } from "@/types";
|
||||
|
||||
const mockPush = vi.fn();
|
||||
const mockSearchParams = new URLSearchParams();
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
useSearchParams: () => mockSearchParams,
|
||||
}));
|
||||
|
||||
const mockCategories: Category[] = [
|
||||
{ id: "1", name: "Arrays", slug: "arrays", description: null, question_count: 5 },
|
||||
{ id: "2", name: "Strings", slug: "strings", description: null, question_count: 3 },
|
||||
];
|
||||
|
||||
const mockPatterns: Pattern[] = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Two Pointers",
|
||||
slug: "two-pointers",
|
||||
description: null,
|
||||
when_to_use: null,
|
||||
question_count: 4,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Sliding Window",
|
||||
slug: "sliding-window",
|
||||
description: null,
|
||||
when_to_use: null,
|
||||
question_count: 2,
|
||||
},
|
||||
];
|
||||
|
||||
describe("QuestionFilters", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSearchParams.delete("category");
|
||||
mockSearchParams.delete("pattern");
|
||||
mockSearchParams.delete("page");
|
||||
});
|
||||
|
||||
it("renders category filter with label", () => {
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByLabelText("Category")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders pattern filter with label", () => {
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByLabelText("Pattern")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all category options with counts", () => {
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("All Categories")).toBeInTheDocument();
|
||||
expect(screen.getByText("Arrays (5)")).toBeInTheDocument();
|
||||
expect(screen.getByText("Strings (3)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all pattern options with counts", () => {
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("All Patterns")).toBeInTheDocument();
|
||||
expect(screen.getByText("Two Pointers (4)")).toBeInTheDocument();
|
||||
expect(screen.getByText("Sliding Window (2)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("navigates when category is selected", () => {
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
/>
|
||||
);
|
||||
const categorySelect = screen.getByLabelText("Category");
|
||||
fireEvent.change(categorySelect, { target: { value: "arrays" } });
|
||||
expect(mockPush).toHaveBeenCalledWith("/questions?category=arrays");
|
||||
});
|
||||
|
||||
it("navigates when pattern is selected", () => {
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
/>
|
||||
);
|
||||
const patternSelect = screen.getByLabelText("Pattern");
|
||||
fireEvent.change(patternSelect, { target: { value: "two-pointers" } });
|
||||
expect(mockPush).toHaveBeenCalledWith("/questions?pattern=two-pointers");
|
||||
});
|
||||
|
||||
it("removes category param when All Categories is selected", () => {
|
||||
mockSearchParams.set("category", "arrays");
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
currentCategory="arrays"
|
||||
/>
|
||||
);
|
||||
const categorySelect = screen.getByLabelText("Category");
|
||||
fireEvent.change(categorySelect, { target: { value: "" } });
|
||||
expect(mockPush).toHaveBeenCalledWith("/questions?");
|
||||
});
|
||||
|
||||
it("removes pattern param when All Patterns is selected", () => {
|
||||
mockSearchParams.set("pattern", "two-pointers");
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
currentPattern="two-pointers"
|
||||
/>
|
||||
);
|
||||
const patternSelect = screen.getByLabelText("Pattern");
|
||||
fireEvent.change(patternSelect, { target: { value: "" } });
|
||||
expect(mockPush).toHaveBeenCalledWith("/questions?");
|
||||
});
|
||||
|
||||
it("shows current category as selected", () => {
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
currentCategory="arrays"
|
||||
/>
|
||||
);
|
||||
const categorySelect = screen.getByLabelText("Category") as HTMLSelectElement;
|
||||
expect(categorySelect.value).toBe("arrays");
|
||||
});
|
||||
|
||||
it("shows current pattern as selected", () => {
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
currentPattern="two-pointers"
|
||||
/>
|
||||
);
|
||||
const patternSelect = screen.getByLabelText("Pattern") as HTMLSelectElement;
|
||||
expect(patternSelect.value).toBe("two-pointers");
|
||||
});
|
||||
|
||||
it("resets page when filter changes", () => {
|
||||
mockSearchParams.set("page", "3");
|
||||
mockSearchParams.set("category", "strings");
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
currentCategory="strings"
|
||||
/>
|
||||
);
|
||||
const categorySelect = screen.getByLabelText("Category");
|
||||
fireEvent.change(categorySelect, { target: { value: "arrays" } });
|
||||
expect(mockPush).toHaveBeenCalledWith("/questions?category=arrays");
|
||||
});
|
||||
|
||||
it("preserves other params when changing category", () => {
|
||||
mockSearchParams.set("pattern", "two-pointers");
|
||||
render(
|
||||
<QuestionFilters
|
||||
categories={mockCategories}
|
||||
patterns={mockPatterns}
|
||||
currentPattern="two-pointers"
|
||||
/>
|
||||
);
|
||||
const categorySelect = screen.getByLabelText("Category");
|
||||
fireEvent.change(categorySelect, { target: { value: "arrays" } });
|
||||
expect(mockPush).toHaveBeenCalledWith(
|
||||
"/questions?pattern=two-pointers&category=arrays"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user