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