diff --git a/CHANGELOG.md b/CHANGELOG.md
index 296e63ea9..50c45d787 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,8 +10,12 @@
### 🐛 Bug fixes
+- Fixed campus map not rendering in timetable modal by adding explicit height to `#campus-map` container
+- Fixed "Zoom to Fit" button: resolved sidebar overlap and enabled view reset for single-marker maps
+
### 🔧 Internal changes
+- Added test coverage for `CampusMap` component (7 test cases covering rendering, markers, and zoom functionality)
- Simplified timetable availability check in `Description.tsx` by removing redundant `Object.keys(sessions).length !== 0` condition
- Updated OpenStreetMap tile layer URL from HTTP to HTTPS in `CampusMap.tsx` for improved security
- Extracted read-only selection, prerequisite, and status computations (`isSelected`, `arePrereqsSatisfied*`, `compute*Status`, `totalSelectedCredits`) from `Graph.js` into a typed `graphState.ts` module
diff --git a/js/components/common/CampusMap.tsx b/js/components/common/CampusMap.tsx
index 19fd578ca..f82f56ffb 100644
--- a/js/components/common/CampusMap.tsx
+++ b/js/components/common/CampusMap.tsx
@@ -33,12 +33,16 @@ export function CampusMap({
if (!map || !mapGroup) return
// markerCount is the number of markers on the map.
- // Only adjust the map bounds if there are multiple markers because the zoom will
- // be strange for 1 marker. Note that the map will be centered on the single marker
const markerCount = mapGroup.getLayers().length
if (markerCount > 1) {
+ // Multiple markers: fit bounds to show all markers
map.fitBounds(mapGroup.getBounds(), { padding: [30, 30] })
+ } else if (markerCount === 1) {
+ // Single marker: reset to default zoom level 16 and center on the marker
+ const bounds = mapGroup.getBounds()
+ const center = bounds.getCenter()
+ map.setView(center, 16)
}
}
diff --git a/js/components/common/__tests__/CampusMap.test.js b/js/components/common/__tests__/CampusMap.test.js
new file mode 100644
index 000000000..52fec03fb
--- /dev/null
+++ b/js/components/common/__tests__/CampusMap.test.js
@@ -0,0 +1,181 @@
+import { render, screen } from "@testing-library/react"
+import userEvent from "@testing-library/user-event"
+import { CampusMap } from "../CampusMap"
+
+// Mock Leaflet to avoid DOM rendering issues in tests
+jest.mock("leaflet", () => ({
+ Icon: jest.fn(() => ({})),
+ Map: jest.fn(),
+ latLngBounds: jest.fn(() => ({
+ extend: jest.fn(),
+ getCenter: jest.fn(() => ({ lat: 43.6596426, lng: -79.3976676 })),
+ })),
+ latLng: jest.fn((lat, lng) => ({ lat, lng })),
+}))
+
+/* eslint-disable react/prop-types */
+jest.mock("react-leaflet", () => ({
+ MapContainer: ({ children }) =>
{children}
,
+ TileLayer: ({ url }) => ,
+ FeatureGroup: ({ children }) => {children}
,
+ Marker: ({ position, children }) => (
+
+ {children}
+
+ ),
+ Popup: ({ children }) => {children}
,
+}))
+/* eslint-enable react/prop-types */
+
+describe("CampusMap", () => {
+ const mockLecturesByBuilding = [
+ {
+ buildingName: "Bahen Centre for Information Technology",
+ buildingCode: "BA",
+ address: "40 St George St",
+ lat: 43.6596426,
+ lng: -79.3976676,
+ postalCode: "M5S 2E4",
+ days: [
+ {
+ dayString: "Monday",
+ dayOrder: 0,
+ timeframes: [
+ {
+ courseCode: "CSC309H1",
+ session: "L5101",
+ startTime: 11,
+ endTime: 13,
+ room: "BA3200",
+ },
+ ],
+ },
+ ],
+ },
+ ]
+
+ it("renders the map container", () => {
+ render(
+
+ )
+
+ const mapContainer = screen.getByTestId("map-container")
+ expect(mapContainer).toBeTruthy()
+ })
+
+ it("renders markers for each building", () => {
+ render(
+
+ )
+
+ const markers = screen.getAllByTestId("marker")
+ expect(markers).toHaveLength(1)
+ })
+
+ it("renders multiple markers for multiple buildings", () => {
+ const multipleBuildings = [
+ ...mockLecturesByBuilding,
+ {
+ buildingName: "Myhal Centre",
+ buildingCode: "MY",
+ address: "55 St George St",
+ lat: 43.6597,
+ lng: -79.3975,
+ postalCode: "M5S 2E5",
+ days: [
+ {
+ dayString: "Tuesday",
+ dayOrder: 1,
+ timeframes: [
+ {
+ courseCode: "CSC301H1",
+ session: "L0101",
+ startTime: 14,
+ endTime: 16,
+ room: "MY150",
+ },
+ ],
+ },
+ ],
+ },
+ ]
+
+ render(
+
+ )
+
+ const markers = screen.getAllByTestId("marker")
+ expect(markers).toHaveLength(2)
+ })
+
+ it("renders Zoom to Fit button", () => {
+ render(
+
+ )
+
+ const zoomButton = screen.getByText("Zoom to Fit")
+ expect(zoomButton).toBeTruthy()
+ })
+
+ it("zoom button is clickable", async () => {
+ const user = userEvent.setup()
+ render(
+
+ )
+
+ const zoomButton = screen.getByText("Zoom to Fit")
+ await user.click(zoomButton)
+
+ // Button should not throw errors when clicked
+ expect(zoomButton).toBeTruthy()
+ })
+
+ it("does not render markers for buildings without coordinates", () => {
+ const buildingsWithoutCoords = [
+ {
+ buildingName: "Unknown Building",
+ buildingCode: "UNK",
+ address: "Unknown",
+ lat: undefined,
+ lng: undefined,
+ postalCode: "M5S 2E4",
+ days: [],
+ },
+ ]
+
+ render(
+
+ )
+
+ const markers = screen.queryAllByTestId("marker")
+ expect(markers).toHaveLength(0)
+ })
+
+ it("renders popup component for markers", () => {
+ render(
+
+ )
+
+ // Check that popup component is rendered
+ const popups = screen.getAllByTestId("popup")
+ expect(popups.length).toBeGreaterThan(0)
+ })
+})
diff --git a/style/app.scss b/style/app.scss
index a967783e3..8344cbfc7 100644
--- a/style/app.scss
+++ b/style/app.scss
@@ -337,7 +337,12 @@ ol {
fill: var(--secondary-color);
}
+#campus-map {
+ height: 500px;
+}
+
#map-sidebar {
+ margin-top: 3.75rem;
overflow-y: auto;
padding: 0;
}