security and accessibility pass

This commit is contained in:
2025-05-07 22:15:29 +01:00
parent 6e01777678
commit 3604e0a889
5 changed files with 386 additions and 4 deletions

View 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();
});
});

View 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"
);
});
});