vitest setup + component tests
This commit is contained in:
34
frontend/src/components/ui/badge.test.tsx
Normal file
34
frontend/src/components/ui/badge.test.tsx
Normal file
@@ -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(<Badge>Easy</Badge>);
|
||||||
|
expect(screen.getByText("Easy")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies default variant styles", () => {
|
||||||
|
render(<Badge>Test</Badge>);
|
||||||
|
const badge = screen.getByText("Test");
|
||||||
|
expect(badge).toHaveClass("rounded-full", "text-xs", "font-medium");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies outline variant styles", () => {
|
||||||
|
render(<Badge variant="outline">Test</Badge>);
|
||||||
|
const badge = screen.getByText("Test");
|
||||||
|
expect(badge).toHaveClass("border");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies custom className", () => {
|
||||||
|
render(<Badge className="custom-class">Test</Badge>);
|
||||||
|
const badge = screen.getByText("Test");
|
||||||
|
expect(badge).toHaveClass("custom-class");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports aria-label for accessibility", () => {
|
||||||
|
render(<Badge aria-label="Easy difficulty">Easy</Badge>);
|
||||||
|
const badge = screen.getByText("Easy");
|
||||||
|
expect(badge).toHaveAttribute("aria-label", "Easy difficulty");
|
||||||
|
});
|
||||||
|
});
|
||||||
92
frontend/src/lib/api.test.ts
Normal file
92
frontend/src/lib/api.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
21
frontend/src/test/setup.ts
Normal file
21
frontend/src/test/setup.ts
Normal file
@@ -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(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
24
frontend/vitest.config.ts
Normal file
24
frontend/vitest.config.ts
Normal file
@@ -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"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user