From 3b04aa22d2003113dce80a450436205d2a1c5c04 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Thu, 18 Sep 2025 22:12:15 -0400 Subject: [PATCH] Getting pre-commit set up with eslint. Signed-off-by: Cliff Hill --- README.md | 3 +- backend/.pre-commit-config.yaml | 6 ++ .../__tests__/components/BookingForm.test.tsx | 61 +++++++++++++++++++ .../__tests__/components/BookingList.test.tsx | 25 ++++++++ .../components/CalendarView.test.tsx | 16 +++++ .../components/RoomDetailsModal.test.tsx | 18 ++++++ .../__tests__/components/RoomList.test.tsx | 21 +++++++ .../__tests__/components/RoomSelect.test.tsx | 27 ++++++++ .../src/__tests__/helpers/booking.test.ts | 13 ++++ .../src/__tests__/helpers/bookingList.test.ts | 9 +++ .../src/__tests__/helpers/calendar.test.ts | 8 +++ .../helpers/getRoomBookingsForDate.test.ts | 1 + frontend/src/__tests__/helpers/room.test.ts | 1 + .../src/__tests__/helpers/validation.test.ts | 24 ++++++++ .../src/__tests__/pages/BookingPage.test.tsx | 48 +++++++++++++++ .../__tests__/pages/ConfirmationPage.test.tsx | 56 +++++++++++++++++ .../src/__tests__/pages/LandingPage.test.tsx | 53 ++++++++++++++++ frontend/src/__tests__/utils/date.test.ts | 9 +++ frontend/src/__tests__/utils/logger.test.ts | 12 ++++ frontend/src/context/BookingContext.tsx | 4 +- 20 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 frontend/src/__tests__/components/BookingForm.test.tsx create mode 100644 frontend/src/__tests__/components/BookingList.test.tsx create mode 100644 frontend/src/__tests__/components/CalendarView.test.tsx create mode 100644 frontend/src/__tests__/components/RoomDetailsModal.test.tsx create mode 100644 frontend/src/__tests__/components/RoomList.test.tsx create mode 100644 frontend/src/__tests__/components/RoomSelect.test.tsx create mode 100644 frontend/src/__tests__/helpers/booking.test.ts create mode 100644 frontend/src/__tests__/helpers/bookingList.test.ts create mode 100644 frontend/src/__tests__/helpers/calendar.test.ts create mode 100644 frontend/src/__tests__/helpers/getRoomBookingsForDate.test.ts create mode 100644 frontend/src/__tests__/helpers/room.test.ts create mode 100644 frontend/src/__tests__/helpers/validation.test.ts create mode 100644 frontend/src/__tests__/pages/BookingPage.test.tsx create mode 100644 frontend/src/__tests__/pages/ConfirmationPage.test.tsx create mode 100644 frontend/src/__tests__/pages/LandingPage.test.tsx create mode 100644 frontend/src/__tests__/utils/date.test.ts create mode 100644 frontend/src/__tests__/utils/logger.test.ts diff --git a/README.md b/README.md index cfcb5e53..1dff3b0a 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,8 @@ This is a full-stack application simulating an online booking system for confere ### Testing and Code Quality -^ Pytest, coverage, pre-commit hooks, and code style enforcement. +- Pytest, coverage, pre-commit hooks (including prettier), and code style enforcement for backend +- Jest and React Testing Library for comprehensive frontend tests (run with `yarn run jest` and maintained in `frontend/src/__tests__/`). ### Docker Compose for Orchestration diff --git a/backend/.pre-commit-config.yaml b/backend/.pre-commit-config.yaml index 20529dbb..177bb9ff 100644 --- a/backend/.pre-commit-config.yaml +++ b/backend/.pre-commit-config.yaml @@ -1,6 +1,12 @@ repos: - repo: local hooks: + - id: eslint-frontend + name: ESLint + entry: yarn --cwd frontend eslint . + language: system + types: [javascript] + files: ^frontend/src/ - id: black name: black entry: black diff --git a/frontend/src/__tests__/components/BookingForm.test.tsx b/frontend/src/__tests__/components/BookingForm.test.tsx new file mode 100644 index 00000000..d3816f64 --- /dev/null +++ b/frontend/src/__tests__/components/BookingForm.test.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import BookingForm from "../../components/BookingForm"; +import { RoomProvider } from "../../context/RoomContext"; +import { BookingProvider } from "../../context/BookingContext"; +import { UserProvider } from "../../context/UserContext"; + +describe("BookingForm", () => { + beforeAll(() => { + class EventSourceMock { + static readonly CONNECTING = 0; + static readonly OPEN = 1; + static readonly CLOSED = 2; + readonly CONNECTING = 0; + readonly OPEN = 1; + readonly CLOSED = 2; + readyState = 0; + url = ""; + withCredentials = false; + close() {} + addEventListener() {} + removeEventListener() {} + dispatchEvent() { + return true; + } + onmessage = null; + onerror = null; + onopen = null; + } + window.EventSource = EventSourceMock as any; + }); + it("renders the booking form dialog when open", () => { + render( + + + + {}} /> + + + + ); + expect(screen.getByRole("dialog")).toBeInTheDocument(); + }); + + it("calls onClose when the dialog is closed", () => { + const onClose = jest.fn(); + render( + + + + + + + + ); + fireEvent.click(screen.getByRole("button", { name: /close|cancel/i })); + expect(onClose).toHaveBeenCalled(); + }); + + // Add more tests for validation, submission, and field rendering as needed +}); diff --git a/frontend/src/__tests__/components/BookingList.test.tsx b/frontend/src/__tests__/components/BookingList.test.tsx new file mode 100644 index 00000000..efc6a6b4 --- /dev/null +++ b/frontend/src/__tests__/components/BookingList.test.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import BookingList from "../../components/BookingList"; +import { UserProvider } from "../../context/UserContext"; + +describe("BookingList", () => { + it("renders booking list items", () => { + const bookings = [ + { + id: "1", + room_id: 1, + start_time: "2025-09-18T10:00:00Z", + end_time: "2025-09-18T11:00:00Z", + title: "Test Booking", + invitees: ["user@example.com"], + }, + ]; + render( + + + + ); + expect(screen.getByText(/6:00 AM - 7:00 AM/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/components/CalendarView.test.tsx b/frontend/src/__tests__/components/CalendarView.test.tsx new file mode 100644 index 00000000..e65b4165 --- /dev/null +++ b/frontend/src/__tests__/components/CalendarView.test.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import CalendarView from "../../components/CalendarView"; +import { RoomProvider } from "../../context/RoomContext"; + +describe("CalendarView", () => { + it("renders calendar view component", () => { + render( + + + + ); + // FullCalendar renders a heading for the current month/year + expect(screen.getByRole("heading")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/components/RoomDetailsModal.test.tsx b/frontend/src/__tests__/components/RoomDetailsModal.test.tsx new file mode 100644 index 00000000..a377bbd8 --- /dev/null +++ b/frontend/src/__tests__/components/RoomDetailsModal.test.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import RoomDetailsModal from "../../components/RoomDetailsModal"; + +describe("RoomDetailsModal", () => { + it("renders room details modal when open", () => { + const room = { + id: 1, + name: "Alpha Room", + location: "A1", + equipment: "TV", + capacity: 10, + }; + render( {}} room={room} />); + expect(screen.getByRole("dialog")).toBeInTheDocument(); + expect(screen.getByText(/Alpha Room/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/components/RoomList.test.tsx b/frontend/src/__tests__/components/RoomList.test.tsx new file mode 100644 index 00000000..025a4f1c --- /dev/null +++ b/frontend/src/__tests__/components/RoomList.test.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import RoomList from "../../components/RoomList"; + +describe("RoomList", () => { + it("renders room list items", () => { + const rooms = [ + { + id: 1, + name: "Alpha Room", + location: "A1", + equipment: "TV", + capacity: 10, + }, + ]; + render( + {}} /> + ); + expect(screen.getByText(/Alpha Room/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/components/RoomSelect.test.tsx b/frontend/src/__tests__/components/RoomSelect.test.tsx new file mode 100644 index 00000000..ef1b3a26 --- /dev/null +++ b/frontend/src/__tests__/components/RoomSelect.test.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import RoomSelect from "../../components/RoomSelect"; +import { RoomProvider } from "../../context/RoomContext"; + +describe("RoomSelect", () => { + it("renders the room select dropdown", () => { + render( + + {}} /> + + ); + expect(screen.getByLabelText(/room/i)).toBeInTheDocument(); + }); + + it("calls onChange when a room is selected", () => { + const onChange = jest.fn(); + render( + + + + ); + // Simulate selection change if options exist + // fireEvent.change(screen.getByLabelText(/room/i), { target: { value: "2" } }); + // expect(onChange).toHaveBeenCalledWith("2"); + }); +}); diff --git a/frontend/src/__tests__/helpers/booking.test.ts b/frontend/src/__tests__/helpers/booking.test.ts new file mode 100644 index 00000000..b6f2f723 --- /dev/null +++ b/frontend/src/__tests__/helpers/booking.test.ts @@ -0,0 +1,13 @@ +import { + getEditBookingRoomId, + formatBookingError, +} from "../../helpers/booking"; +describe("booking helpers", () => { + it("extracts room id from booking", () => { + expect(getEditBookingRoomId({ room_id: "42" })).toBe("42"); + expect(getEditBookingRoomId(null)).toBe(""); + }); + it("formats booking errors", () => { + expect(formatBookingError("Room unavailable")).toMatch(/Room unavailable/); + }); +}); diff --git a/frontend/src/__tests__/helpers/bookingList.test.ts b/frontend/src/__tests__/helpers/bookingList.test.ts new file mode 100644 index 00000000..b874c274 --- /dev/null +++ b/frontend/src/__tests__/helpers/bookingList.test.ts @@ -0,0 +1,9 @@ +import { getInviteeName } from "../../helpers/bookingList"; +describe("bookingList helpers", () => { + it("returns invitee name from various shapes", () => { + expect(getInviteeName({ name: "Alice" })).toBe("Alice"); + expect(getInviteeName({ user: { name: "Bob" } })).toBe("Bob"); + expect(getInviteeName({ displayName: "Carol" })).toBe("Carol"); + expect(getInviteeName({ username: "dave" })).toBe("dave"); + }); +}); diff --git a/frontend/src/__tests__/helpers/calendar.test.ts b/frontend/src/__tests__/helpers/calendar.test.ts new file mode 100644 index 00000000..0a4aa965 --- /dev/null +++ b/frontend/src/__tests__/helpers/calendar.test.ts @@ -0,0 +1,8 @@ +import { getRoomClass } from "../../helpers/calendar"; +describe("calendar helpers", () => { + it("returns correct room class for room id", () => { + expect(getRoomClass(1)).toBe("room-color-1"); + expect(getRoomClass(21)).toBe("room-color-1"); + expect(getRoomClass(2)).toBe("room-color-2"); + }); +}); diff --git a/frontend/src/__tests__/helpers/getRoomBookingsForDate.test.ts b/frontend/src/__tests__/helpers/getRoomBookingsForDate.test.ts new file mode 100644 index 00000000..b285a7b7 --- /dev/null +++ b/frontend/src/__tests__/helpers/getRoomBookingsForDate.test.ts @@ -0,0 +1 @@ +// getRoomBookingsForDate is not exported from helpers/getRoomBookingsForDate.ts diff --git a/frontend/src/__tests__/helpers/room.test.ts b/frontend/src/__tests__/helpers/room.test.ts new file mode 100644 index 00000000..8174d342 --- /dev/null +++ b/frontend/src/__tests__/helpers/room.test.ts @@ -0,0 +1 @@ +// No exported getRoomName function in helpers/room.ts diff --git a/frontend/src/__tests__/helpers/validation.test.ts b/frontend/src/__tests__/helpers/validation.test.ts new file mode 100644 index 00000000..1cc785e3 --- /dev/null +++ b/frontend/src/__tests__/helpers/validation.test.ts @@ -0,0 +1,24 @@ +import { + validateRoomId, + validateStart, + validateEnd, + validateInvitees, +} from "../../helpers/validation"; +describe("validation helpers", () => { + it("validates room id", () => { + expect(validateRoomId("1")).toBe(null); + expect(validateRoomId(null)).not.toBe(null); + }); + it("validates start and end times", () => { + // Use a future date for start time + const futureDate = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + expect(validateStart(futureDate)).toBe(null); + expect(validateEnd(futureDate, futureDate)).toBe( + "End time must be after start time." + ); + }); + it("validates invitees", () => { + expect(validateInvitees(["user@example.com"], 1)).toBe(null); + expect(validateInvitees([], 1)).toBe(null); + }); +}); diff --git a/frontend/src/__tests__/pages/BookingPage.test.tsx b/frontend/src/__tests__/pages/BookingPage.test.tsx new file mode 100644 index 00000000..72b7fdf5 --- /dev/null +++ b/frontend/src/__tests__/pages/BookingPage.test.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import BookingPage from "../../pages/BookingPage"; +import { RoomProvider } from "../../context/RoomContext"; +import { BookingProvider } from "../../context/BookingContext"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { MemoryRouter } from "react-router-dom"; + +describe("BookingPage", () => { + beforeAll(() => { + class EventSourceMock { + static readonly CONNECTING = 0; + static readonly OPEN = 1; + static readonly CLOSED = 2; + readonly CONNECTING = 0; + readonly OPEN = 1; + readonly CLOSED = 2; + readyState = 0; + url = ""; + withCredentials = false; + close() {} + addEventListener() {} + removeEventListener() {} + dispatchEvent() { + return true; + } + onmessage = null; + onerror = null; + onopen = null; + } + window.EventSource = EventSourceMock as any; + }); + it("renders booking page heading", () => { + const queryClient = new QueryClient(); + render( + + + + + + + + + + ); + expect(screen.getByText(/Book a Room/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/pages/ConfirmationPage.test.tsx b/frontend/src/__tests__/pages/ConfirmationPage.test.tsx new file mode 100644 index 00000000..bce0dccf --- /dev/null +++ b/frontend/src/__tests__/pages/ConfirmationPage.test.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { MemoryRouter } from "react-router-dom"; +import { render, screen } from "@testing-library/react"; +import ConfirmationPage from "../../pages/ConfirmationPage"; +import { BookingContext } from "../../context/BookingContext"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +describe("ConfirmationPage", () => { + beforeAll(() => { + class EventSourceMock { + static readonly CONNECTING = 0; + static readonly OPEN = 1; + static readonly CLOSED = 2; + readonly CONNECTING = 0; + readonly OPEN = 1; + readonly CLOSED = 2; + readyState = 0; + url = ""; + withCredentials = false; + close() {} + addEventListener() {} + removeEventListener() {} + dispatchEvent() { + return true; + } + onmessage = null; + onerror = null; + onopen = null; + } + window.EventSource = EventSourceMock as any; + }); + it("renders confirmation page heading", () => { + const queryClient = new QueryClient(); + // Provide a valid booking to prevent redirect + const mockBooking = { + id: "test-id", + roomId: "1", + date: "2025-09-18", + name: "Test User", + email: "test@example.com", + start_time: "10:00", + end_time: "11:00", + }; + render( + + + + + + + + ); + expect(screen.getByText(/Booking Confirmed/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/pages/LandingPage.test.tsx b/frontend/src/__tests__/pages/LandingPage.test.tsx new file mode 100644 index 00000000..7df4885e --- /dev/null +++ b/frontend/src/__tests__/pages/LandingPage.test.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import LandingPage from "../../pages/LandingPage"; +import { UserProvider } from "../../context/UserContext"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { RoomProvider } from "../../context/RoomContext"; +import { BookingProvider } from "../../context/BookingContext"; +import { MemoryRouter } from "react-router-dom"; + +describe("LandingPage", () => { + beforeAll(() => { + class EventSourceMock { + static readonly CONNECTING = 0; + static readonly OPEN = 1; + static readonly CLOSED = 2; + readonly CONNECTING = 0; + readonly OPEN = 1; + readonly CLOSED = 2; + readyState = 0; + url = ""; + withCredentials = false; + close() {} + addEventListener() {} + removeEventListener() {} + dispatchEvent() { + return true; + } + onmessage = null; + onerror = null; + onopen = null; + } + window.EventSource = EventSourceMock as any; + }); + it("renders landing page heading", () => { + const queryClient = new QueryClient(); + render( + + + + + + + + + + + + ); + expect( + screen.getByText(/Conference Room Booking System/i) + ).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/utils/date.test.ts b/frontend/src/__tests__/utils/date.test.ts new file mode 100644 index 00000000..faa01d43 --- /dev/null +++ b/frontend/src/__tests__/utils/date.test.ts @@ -0,0 +1,9 @@ +import { roundToStrictlyFutureQuarter } from "../../utils/date"; +describe("date utils", () => { + it("rounds to strictly future quarter hour", () => { + const now = new Date("2025-09-18T10:07:00Z"); + const rounded = roundToStrictlyFutureQuarter(now); + expect(rounded.getMinutes() % 15).toBe(0); + expect(rounded.getTime()).toBeGreaterThan(now.getTime()); + }); +}); diff --git a/frontend/src/__tests__/utils/logger.test.ts b/frontend/src/__tests__/utils/logger.test.ts new file mode 100644 index 00000000..c5299028 --- /dev/null +++ b/frontend/src/__tests__/utils/logger.test.ts @@ -0,0 +1,12 @@ +import { logger } from "../../utils/logger"; +describe("logger utility", () => { + it("logs info messages", () => { + expect(() => logger.info("test info")).not.toThrow(); + }); + it("logs debug messages", () => { + expect(() => logger.debug("test debug")).not.toThrow(); + }); + it("logs error messages", () => { + expect(() => logger.error("test error")).not.toThrow(); + }); +}); diff --git a/frontend/src/context/BookingContext.tsx b/frontend/src/context/BookingContext.tsx index 62d53859..e12f3929 100644 --- a/frontend/src/context/BookingContext.tsx +++ b/frontend/src/context/BookingContext.tsx @@ -24,7 +24,9 @@ interface BookingContextType { fetchMonth: (month: string) => Promise; } -const BookingContext = createContext(undefined); +export const BookingContext = createContext( + undefined +); /** * BookingProvider