diff --git a/frontend/src/components/ui/badge.test.tsx b/frontend/src/components/ui/badge.test.tsx new file mode 100644 index 0000000..9398eb9 --- /dev/null +++ b/frontend/src/components/ui/badge.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { Badge } from "./badge"; + +describe("Badge", () => { + it("renders children content", () => { + render(Easy); + expect(screen.getByText("Easy")).toBeInTheDocument(); + }); + + it("applies default variant styles", () => { + render(Test); + const badge = screen.getByText("Test"); + expect(badge).toHaveClass("rounded-full", "text-xs", "font-medium"); + }); + + it("applies outline variant styles", () => { + render(Test); + const badge = screen.getByText("Test"); + expect(badge).toHaveClass("border"); + }); + + it("applies custom className", () => { + render(Test); + const badge = screen.getByText("Test"); + expect(badge).toHaveClass("custom-class"); + }); + + it("supports aria-label for accessibility", () => { + render(Easy); + const badge = screen.getByText("Easy"); + expect(badge).toHaveAttribute("aria-label", "Easy difficulty"); + }); +}); diff --git a/frontend/src/lib/api.test.ts b/frontend/src/lib/api.test.ts new file mode 100644 index 0000000..c21dc62 --- /dev/null +++ b/frontend/src/lib/api.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +describe("API client", () => { + const originalFetch = global.fetch; + + beforeEach(() => { + vi.resetModules(); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it("getQuestions builds correct query string with filters", async () => { + let capturedUrl = ""; + global.fetch = vi.fn().mockImplementation((url: string) => { + capturedUrl = url; + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + items: [], + total: 0, + page: 1, + limit: 20, + pages: 0, + }), + }); + }); + + const { getQuestions } = await import("./api"); + await getQuestions({ page: 2, difficulty: "easy", category: "arrays" }); + + expect(capturedUrl).toContain("page=2"); + expect(capturedUrl).toContain("difficulty=easy"); + expect(capturedUrl).toContain("category=arrays"); + }); + + it("getQuestions omits empty filters", async () => { + let capturedUrl = ""; + global.fetch = vi.fn().mockImplementation((url: string) => { + capturedUrl = url; + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + items: [], + total: 0, + page: 1, + limit: 20, + pages: 0, + }), + }); + }); + + const { getQuestions } = await import("./api"); + await getQuestions({}); + + expect(capturedUrl).not.toContain("?"); + }); + + it("throws error on non-ok response", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: "Not Found", + }); + + const { getQuestion } = await import("./api"); + + await expect(getQuestion("nonexistent")).rejects.toThrow("API error: 404"); + }); + + it("getStats returns stats data", async () => { + const mockStats = { + total_questions: 10, + by_difficulty: { easy: 3, medium: 5, hard: 2 }, + by_category: [], + by_pattern: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockStats), + }); + + const { getStats } = await import("./api"); + const result = await getStats(); + + expect(result).toEqual(mockStats); + }); +}); diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts new file mode 100644 index 0000000..d4e9dca --- /dev/null +++ b/frontend/src/test/setup.ts @@ -0,0 +1,21 @@ +import "@testing-library/jest-dom/vitest"; +import { cleanup } from "@testing-library/react"; +import { afterEach, vi } from "vitest"; + +afterEach(() => { + cleanup(); +}); + +Object.defineProperty(window, "matchMedia", { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..f9b69fd --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; +import path from "path"; + +export default defineConfig({ + plugins: [react()], + test: { + environment: "jsdom", + globals: true, + setupFiles: ["./src/test/setup.ts"], + include: ["src/**/*.test.{ts,tsx}"], + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.{ts,tsx}"], + exclude: ["src/test/**", "src/**/*.test.{ts,tsx}", "src/types/**"], + }, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +});