diff --git a/frontend/src/__tests__/components/RoomList.test.tsx b/frontend/src/__tests__/components/RoomList.test.tsx index 0d401ad5..16a7b9e5 100644 --- a/frontend/src/__tests__/components/RoomList.test.tsx +++ b/frontend/src/__tests__/components/RoomList.test.tsx @@ -16,6 +16,13 @@ describe("RoomList", () => { equipment: "Projector", capacity: 10, }, + { + id: 2, + name: "Room B", + location: "Building 2", + equipment: "Whiteboard", + capacity: 8, + }, ]; test("renders rooms with correct details", () => { @@ -24,6 +31,10 @@ describe("RoomList", () => { expect(screen.getByText(/Location: Building 1/)).toBeInTheDocument(); expect(screen.getByText(/Capacity: 10/)).toBeInTheDocument(); expect(screen.getByText(/Equipment: Projector/)).toBeInTheDocument(); + expect(screen.getByText("Room B")).toBeInTheDocument(); + expect(screen.getByText(/Location: Building 2/)).toBeInTheDocument(); + expect(screen.getByText(/Capacity: 8/)).toBeInTheDocument(); + expect(screen.getByText(/Equipment: Whiteboard/)).toBeInTheDocument(); expect( screen.getByRole("list", { name: /available conference rooms/i }) ).toBeInTheDocument(); @@ -33,4 +44,29 @@ describe("RoomList", () => { render(); expect(screen.getByText("No rooms available.")).toBeInTheDocument(); }); + + test("calls onSelectRoom when a room is clicked", () => { + const onSelectRoom = jest.fn(); + render( + + ); + const roomA = screen.getByTestId("room-item-1"); + const roomB = screen.getByTestId("room-item-2"); + roomA.click(); + expect(onSelectRoom).toHaveBeenCalledWith(1); + roomB.click(); + expect(onSelectRoom).toHaveBeenCalledWith(2); + }); + + test("highlights the selected room", () => { + render( + {}} /> + ); + const roomB = screen.getByTestId("room-item-2"); + expect(roomB.className).toMatch(/Mui-selected/); + }); }); diff --git a/frontend/src/__tests__/pages/LandingPage.test.tsx b/frontend/src/__tests__/pages/LandingPage.test.tsx index 56defb4a..b45a3fd8 100644 --- a/frontend/src/__tests__/pages/LandingPage.test.tsx +++ b/frontend/src/__tests__/pages/LandingPage.test.tsx @@ -5,7 +5,7 @@ * Tests integration of RoomList and BookingList, API data fetching, loading states, * error handling, and navigation. Mocks backend endpoints: * - GET `/rooms/`: Returns ConferenceRoom[]. - * - GET `/bookings/room/?date={YYYY-MM-DD}`: Returns Booking[]. + * - GET `/bookings/room/{roomId}?date={YYYY-MM-DD}`: Returns Booking[] for the selected room and date. */ import { render, screen, waitFor } from "@testing-library/react"; @@ -40,14 +40,30 @@ describe("LandingPage", () => { equipment: "Projector", capacity: 10, }, + { + id: 2, + name: "Room B", + location: "Building 2", + equipment: "Whiteboard", + capacity: 8, + }, ]; - const mockBookings: Booking[] = [ + const mockBookingsA: Booking[] = [ { id: 1, roomId: 1, startTime: "2025-08-27T10:00:00Z", endTime: "2025-08-27T11:00:00Z", - title: "Meeting", + title: "Meeting A", + }, + ]; + const mockBookingsB: Booking[] = [ + { + id: 2, + roomId: 2, + startTime: "2025-08-27T12:00:00Z", + endTime: "2025-08-27T13:00:00Z", + title: "Meeting B", }, ]; @@ -63,20 +79,30 @@ describe("LandingPage", () => { expect(screen.getAllByTestId("skeleton-loader").length).toBeGreaterThan(0); }); - test("renders rooms and bookings correctly", async () => { + test("renders rooms and bookings correctly, and updates bookings on room selection", async () => { mockedAxios.get - .mockResolvedValueOnce({ data: mockRooms }) - .mockResolvedValueOnce({ data: mockBookings }); + .mockResolvedValueOnce({ data: mockRooms }) // rooms + .mockResolvedValueOnce({ data: mockBookingsA }) // bookings for Room A + .mockResolvedValueOnce({ data: mockBookingsB }); // bookings for Room B render(, { wrapper: ({ children }) => createWrapper({children}), }); - await waitFor(() => { - expect(screen.getByText("Room A")).toBeInTheDocument(); - }); - expect(screen.getByText("Meeting")).toBeInTheDocument(); + // Wait for rooms to load + await screen.findByText("Room A"); + await screen.findByText("Room B"); + // Wait for bookings for Room A + await screen.findByText("Meeting A"); + + // Select Room B + const roomB = screen.getByTestId("room-item-2"); + roomB.click(); + + // Wait for bookings for Room B + await screen.findByText("Meeting B"); + expect( screen.getByRole("button", { name: /navigate to booking page/i }) ).toBeInTheDocument(); diff --git a/frontend/src/components/RoomList.tsx b/frontend/src/components/RoomList.tsx index f3af888f..a923a143 100644 --- a/frontend/src/components/RoomList.tsx +++ b/frontend/src/components/RoomList.tsx @@ -1,10 +1,16 @@ /** * Displays a list of available conference rooms in a Material-UI Card. * + * Rooms are selectable if `onSelectRoom` is provided. The selected room is visually highlighted. + * * @component * @example * ```tsx - * + * setSelectedRoomId(roomId)} + * /> * ``` */ import { @@ -14,6 +20,7 @@ import { ListItem, ListItemText, Typography, + ListItemButton, } from "@mui/material"; import MeetingRoomIcon from "@mui/icons-material/MeetingRoom"; import { FC } from "react"; @@ -35,8 +42,16 @@ export interface ConferenceRoom { capacity: number; } +/** + * Props for RoomList. + * @property {ConferenceRoom[]} rooms - List of rooms to display. + * @property {number} [selectedRoomId] - The currently selected room's id. + * @property {(roomId: number) => void} [onSelectRoom] - Callback when a room is selected. + */ interface RoomListProps { rooms: ConferenceRoom[]; + selectedRoomId?: number; + onSelectRoom?: (roomId: number) => void; } /** @@ -44,7 +59,11 @@ interface RoomListProps { * @param {RoomListProps} props - Component props. * @returns {JSX.Element} A Card containing a list of rooms. */ -const RoomList: FC = ({ rooms }) => { +const RoomList: FC = ({ + rooms, + selectedRoomId, + onSelectRoom, +}) => { return ( @@ -54,14 +73,33 @@ const RoomList: FC = ({ rooms }) => { {rooms.length > 0 ? ( - rooms.map((room) => ( - - - - )) + rooms.map((room) => + onSelectRoom ? ( + onSelectRoom(room.id)} + aria-label={`Select room ${room.name}`} + data-testid={`room-item-${room.id}`} + > + + + ) : ( + + + + ) + ) ) : ( No rooms available. )} diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index f1544457..bf85324e 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -20,13 +20,16 @@ import { import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import axios from "axios"; -import { FC } from "react"; +import { FC, useEffect, useState } from "react"; import RoomList, { ConferenceRoom } from "../components/RoomList"; import BookingList, { Booking } from "../components/BookingList"; /** * Renders the landing page with rooms, bookings, and navigation. * Uses react-query for data fetching and skeleton loaders for loading states. + * + * Rooms are selectable; selecting a room refreshes the bookings list for that room. The first room is selected by default. + * * @returns {JSX.Element} The landing page UI. */ const LandingPage: FC = () => { @@ -45,19 +48,30 @@ const LandingPage: FC = () => { axios.get("/rooms/").then((res) => res.data as ConferenceRoom[]), }); - // Fetch today's bookings for the first room (if any) - const firstRoomId = rooms.length > 0 ? rooms[0].id : undefined; + // Room selection state + const [selectedRoomId, setSelectedRoomId] = useState( + undefined + ); + + // Default to first room when rooms load + useEffect(() => { + if (rooms.length > 0 && selectedRoomId === undefined) { + setSelectedRoomId(rooms[0].id); + } + }, [rooms, selectedRoomId]); + + // Fetch bookings for selected room const { data: bookings = [], isLoading: bookingsLoading, error: bookingsError, refetch: refetchBookings, } = useQuery({ - queryKey: ["bookings", today, firstRoomId], - enabled: !!firstRoomId, + queryKey: ["bookings", today, selectedRoomId], + enabled: !!selectedRoomId, queryFn: () => axios - .get(`/bookings/room/${firstRoomId}?date=${today}`) + .get(`/bookings/room/${selectedRoomId}?date=${today}`) .then((res) => res.data as Booking[]), }); @@ -143,7 +157,11 @@ const LandingPage: FC = () => { - +