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 = () => {
-
+