Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions js/components/common/CampusMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
181 changes: 181 additions & 0 deletions js/components/common/__tests__/CampusMap.test.js
Original file line number Diff line number Diff line change
@@ -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 }) => <div data-testid="map-container">{children}</div>,
TileLayer: ({ url }) => <div data-testid="tile-layer" data-url={url} />,
FeatureGroup: ({ children }) => <div data-testid="feature-group">{children}</div>,
Marker: ({ position, children }) => (
<div data-testid="marker" data-position={JSON.stringify(position)}>
{children}
</div>
),
Popup: ({ children }) => <div data-testid="popup">{children}</div>,
}))
/* 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(
<CampusMap
lecturesByBuilding={mockLecturesByBuilding}
selectedLecTimeframes={[]}
/>
)

const mapContainer = screen.getByTestId("map-container")
expect(mapContainer).toBeTruthy()
})

it("renders markers for each building", () => {
render(
<CampusMap
lecturesByBuilding={mockLecturesByBuilding}
selectedLecTimeframes={[]}
/>
)

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(
<CampusMap lecturesByBuilding={multipleBuildings} selectedLecTimeframes={[]} />
)

const markers = screen.getAllByTestId("marker")
expect(markers).toHaveLength(2)
})

it("renders Zoom to Fit button", () => {
render(
<CampusMap
lecturesByBuilding={mockLecturesByBuilding}
selectedLecTimeframes={[]}
/>
)

const zoomButton = screen.getByText("Zoom to Fit")
expect(zoomButton).toBeTruthy()
})

it("zoom button is clickable", async () => {
const user = userEvent.setup()
render(
<CampusMap
lecturesByBuilding={mockLecturesByBuilding}
selectedLecTimeframes={[]}
/>
)

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(
<CampusMap
lecturesByBuilding={buildingsWithoutCoords}
selectedLecTimeframes={[]}
/>
)

const markers = screen.queryAllByTestId("marker")
expect(markers).toHaveLength(0)
})

it("renders popup component for markers", () => {
render(
<CampusMap
lecturesByBuilding={mockLecturesByBuilding}
selectedLecTimeframes={[]}
/>
)

// Check that popup component is rendered
const popups = screen.getAllByTestId("popup")
expect(popups.length).toBeGreaterThan(0)
})
})
5 changes: 5 additions & 0 deletions style/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,12 @@ ol {
fill: var(--secondary-color);
}

#campus-map {
height: 500px;
}

#map-sidebar {
margin-top: 3.75rem;
overflow-y: auto;
padding: 0;
}
Expand Down
Loading