diff --git a/frontend/tests/App.test.tsx b/frontend/tests/App.test.tsx index 7d65089..b153b98 100644 --- a/frontend/tests/App.test.tsx +++ b/frontend/tests/App.test.tsx @@ -1,10 +1,31 @@ import { render, screen } from "@testing-library/react"; -import { describe, expect, it } from "vitest"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it, vi } from "vitest"; import App from "../src/App"; +// minimal stub so App renders without network calls +vi.mock("../src/lib/api", () => ({ + api: { + users: { list: vi.fn().mockResolvedValue([]) }, + reports: { + weekly: vi.fn().mockResolvedValue({ + days: [], + weekTotalMinutes: 0, + weekTotalCostCents: 0, + }), + }, + timeEntries: { list: vi.fn().mockResolvedValue([]) }, + }, +})); + describe("App", () => { - it("renders a main element", () => { - render(); + it("renders the top bar and main content area", () => { + render( + + + , + ); + expect(screen.getByText("TimeTracker")).toBeDefined(); expect(screen.getByRole("main")).toBeDefined(); }); }); diff --git a/frontend/tests/TimeEntryForm.test.tsx b/frontend/tests/TimeEntryForm.test.tsx new file mode 100644 index 0000000..e676521 --- /dev/null +++ b/frontend/tests/TimeEntryForm.test.tsx @@ -0,0 +1,81 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it, vi } from "vitest"; +import { TimeEntryForm } from "../src/components/TimeEntryForm"; + +// stub api module +vi.mock("../src/lib/api", () => ({ + api: { + projects: { + list: () => + Promise.resolve([ + { + id: 1, + name: "Test Project", + timeBudgetMinutes: 6000, + costBudgetCents: 100000, + hourlyRateCents: 10000, + minutesLogged: 0, + costLoggedCents: 0, + timeExceeded: false, + costExceeded: false, + }, + ]), + }, + timeEntries: { + create: vi.fn().mockResolvedValue({ + id: 42, + userId: 1, + userName: "Alice", + projectId: 1, + projectName: "Test Project", + entryDate: "2026-05-16", + durationMinutes: 90, + }), + }, + }, +})); + +function renderForm(onSaved = vi.fn()) { + return render( + + + , + ); +} + +describe("TimeEntryForm", () => { + it("submits with correct durationMinutes", async () => { + const onSaved = vi.fn(); + renderForm(onSaved); + + // wait for project list to load + await waitFor(() => + expect(screen.getByRole("combobox")).toBeInTheDocument(), + ); + + const durationInput = screen.getByPlaceholderText(/e\.g\. 2:30/i); + fireEvent.change(durationInput, { target: { value: "1:30" } }); + + fireEvent.click(screen.getByRole("button", { name: /log/i })); + + const { api } = await import("../src/lib/api"); + await waitFor(() => { + expect(api.timeEntries.create).toHaveBeenCalledWith( + expect.objectContaining({ durationMinutes: 90 }), + ); + }); + await waitFor(() => expect(onSaved).toHaveBeenCalled()); + }); + + it("shows error for invalid duration format", async () => { + renderForm(); + await waitFor(() => screen.getByRole("combobox")); + + const durationInput = screen.getByPlaceholderText(/e\.g\. 2:30/i); + fireEvent.change(durationInput, { target: { value: "abc" } }); + fireEvent.click(screen.getByRole("button", { name: /log/i })); + + expect(await screen.findByText(/h:mm format/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/WeeklyView.test.tsx b/frontend/tests/WeeklyView.test.tsx new file mode 100644 index 0000000..bd1e66b --- /dev/null +++ b/frontend/tests/WeeklyView.test.tsx @@ -0,0 +1,62 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import { describe, expect, it, vi } from "vitest"; +import { WeeklyView } from "../src/components/WeeklyView"; + +vi.mock("../src/lib/api", () => ({ + api: { + reports: { + weekly: vi.fn().mockResolvedValue({ + days: [ + { date: "2026-05-11", totalMinutes: 480, totalCostCents: 38400 }, + { date: "2026-05-12", totalMinutes: 0, totalCostCents: 0 }, + { date: "2026-05-13", totalMinutes: 0, totalCostCents: 0 }, + { date: "2026-05-14", totalMinutes: 0, totalCostCents: 0 }, + { date: "2026-05-15", totalMinutes: 240, totalCostCents: 19200 }, + { date: "2026-05-16", totalMinutes: 0, totalCostCents: 0 }, + { date: "2026-05-17", totalMinutes: 0, totalCostCents: 0 }, + ], + weekTotalMinutes: 720, + weekTotalCostCents: 57600, + }), + }, + timeEntries: { list: vi.fn().mockResolvedValue([]) }, + }, +})); + +vi.mock("../src/state/UserContext", () => ({ + useUser: () => ({ + selectedUserId: 1, + selectedUser: null, + users: [], + setSelectedUserId: vi.fn(), + }), +})); + +describe("WeeklyView", () => { + it("renders daily totals from report", async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText("8:00")).toBeInTheDocument(); // 480 min + expect(screen.getByText("4:00")).toBeInTheDocument(); // 240 min + }); + }); + + it("renders the week total", async () => { + render( + + + , + ); + + await waitFor(() => { + // 720 min = 12:00, rendered as "Week total: 12:00" + expect(screen.getByText(/Week total.*12:00/)).toBeInTheDocument(); + }); + }); +});