test(frontend): add component tests — TimeEntryForm, WeeklyView, App smoke
- TimeEntryForm.test.tsx: fills date/project/h:mm, clicks submit, asserts fetch called with correct JSON body (mocked fetch) - WeeklyView.test.tsx: renders with stubbed WeeklyReport fixture, asserts each day's h:mm and the week total display correctly - App.test.tsx: smoke test — renders layout with TopBar and main content Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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(<App />);
|
||||
it("renders the top bar and main content area", () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/weekly"]}>
|
||||
<App />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
expect(screen.getByText("TimeTracker")).toBeDefined();
|
||||
expect(screen.getByRole("main")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
<MemoryRouter>
|
||||
<TimeEntryForm userId={1} onSaved={onSaved} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -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(
|
||||
<MemoryRouter>
|
||||
<WeeklyView />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
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(
|
||||
<MemoryRouter>
|
||||
<WeeklyView />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
// 720 min = 12:00, rendered as "Week total: 12:00"
|
||||
expect(screen.getByText(/Week total.*12:00/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user