Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f5a0dcd
vitest jsdom project with testing-library
theobong Sep 23, 2026
2d0b353
name the profile navigation test after its module
theobong Sep 23, 2026
ad5c436
shared api mock and query render helpers for dom tests
theobong Sep 23, 2026
8155e47
reset api mocks after unmount so in-flight queries cannot leak
theobong Sep 23, 2026
3f5814b
test: assert modal accessibility behaviorally
theobong Sep 23, 2026
a762d11
test: assert home tile accessibility behaviorally
theobong Sep 23, 2026
56f6e83
characterize hash routing and toast store
theobong Sep 23, 2026
860d192
characterize operator bootstrap and logout
theobong Sep 23, 2026
e8fd72a
characterize query client, key factories, error copy and dates
theobong Sep 23, 2026
c9e2f35
characterize leaflet map pins, tooltips and teardown
theobong Sep 23, 2026
6cbe1cd
characterize section page list, detail and deep-link states
theobong Sep 23, 2026
433e553
pin test locale and timezone; pane locators and fake-timer helper
theobong Sep 24, 2026
4c261a1
characterize list and infinite cache collisions by rendering the real…
theobong Sep 24, 2026
1b84906
test: assert home tile keyboard behavior instead of react internals
theobong Sep 24, 2026
6c6e41f
flippable current-behavior names, deterministic debounce tests, resto…
theobong Sep 24, 2026
dc6af84
merge origin/main
theobong Sep 24, 2026
3972c71
re-pin mail inbox characterization to the unified feed
theobong Sep 24, 2026
8d105bc
page tests wait on the right pane and use fake timers for debounce
theobong Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"start": "next start",
"lint": "next lint --max-warnings=0",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"test": "vitest run",
"clean": "rimraf .next out *.tsbuildinfo"
},
"dependencies": {
Expand All @@ -27,13 +27,18 @@
"zustand": "^5.0.2"
},
"devDependencies": {
"@testing-library/dom": "^10.4.2",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@testing-library/user-event": "^14.6.7",
"@types/leaflet": "^1.9.15",
"@types/node": "^22.10.2",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-config-next": "15.5.25",
"jsdom": "^26.1.0",
"postcss": "^8.5.28",
"rimraf": "^6.0.1",
"tailwindcss": "^3.4.17",
Expand Down
157 changes: 157 additions & 0 deletions apps/admin/src/components/map/leaflet-map.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { fireEvent, render } from "@testing-library/react"
import L from "leaflet"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"

import { BoundaryMap } from "@/components/map/boundary-map"
import { LeafletMap, type MapPin } from "@/components/map/leaflet-map"

// jsdom has no layout engine: Leaflet sizes the map from clientWidth/clientHeight (0 in jsdom, which
// makes fitBounds produce NaN coordinates) and the wrappers observe their container for resizes.
beforeEach(() => {
vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(800)
vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(600)
vi.stubGlobal(
"ResizeObserver",
class {
observe() {}
unobserve() {}
disconnect() {}
},
)
})

afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})

const PINS: MapPin[] = [
{ id: "r1", lat: 34.05, lng: -118.25, category: "pothole", tip: "Deep pothole", place: "Los Angeles" },
{ id: "r2", lat: 34.06, lng: -118.24, category: "graffiti", draft: true, label: "Tagged wall" },
{ id: "e1", lat: 34.07, lng: -118.23, kind: "event", tip: "Park cleanup" },
]

function markerIcons(container: HTMLElement): HTMLElement[] {
return [...container.querySelectorAll<HTMLElement>(".leaflet-marker-icon")]
}

function hoverAndReadTooltip(container: HTMLElement, icon: HTMLElement): HTMLElement | null {
fireEvent.mouseOver(icon)
return container.querySelector<HTMLElement>(".leaflet-tooltip")
}

describe("LeafletMap", () => {
it("renders one marker per pin with the state-specific pin markup", () => {
const { container } = render(<LeafletMap pins={PINS} />)

const icons = markerIcons(container)
expect(icons).toHaveLength(3)
expect(icons.map((el) => el.querySelector(".pi-pin2")?.className)).toEqual([
"pi-pin2 routed",
"pi-pin2 needs",
"pi-pin2 event",
])
for (const el of icons) expect(el).toHaveClass("pi-pin2-wrap")
})

it("renders the active pin with the active treatment", () => {
const { container } = render(<LeafletMap pins={PINS} activeId="r2" />)

const active = container.querySelectorAll(".pi-pin2.is-active")
expect(active).toHaveLength(1)
expect(active[0]).toHaveClass("needs")
})

it("reconciles markers when the pin list changes", () => {
const { container, rerender } = render(<LeafletMap pins={PINS} />)
expect(markerIcons(container)).toHaveLength(3)

rerender(<LeafletMap pins={PINS.slice(0, 1)} />)
expect(markerIcons(container)).toHaveLength(1)
})

it("shows the title and place in the tooltip, joined by a middle dot", () => {
const { container } = render(<LeafletMap pins={PINS} />)

const tip = hoverAndReadTooltip(container, markerIcons(container)[0]!)
expect(tip).not.toBeNull()
expect(tip).toHaveClass("pi-map-tip")
expect(tip).toHaveTextContent("Deep pothole · Los Angeles")
})

it("falls back to the label when a pin has no tip and omits the place suffix", () => {
const { container } = render(<LeafletMap pins={PINS} />)

const tip = hoverAndReadTooltip(container, markerIcons(container)[1]!)
expect(tip?.textContent).toBe("Tagged wall")
})

it("binds no tooltip for a pin with neither tip nor label", () => {
const { container } = render(<LeafletMap pins={[{ id: "x", lat: 34, lng: -118 }]} />)

const tip = hoverAndReadTooltip(container, markerIcons(container)[0]!)
expect(tip).toBeNull()
})

it("parses a pin title in the tooltip as HTML markup (current behavior)", () => {
const pin: MapPin = { id: "h", lat: 34, lng: -118, tip: "<b>x</b>", place: "<i>y</i>" }
const { container } = render(<LeafletMap pins={[pin]} />)

const tip = hoverAndReadTooltip(container, markerIcons(container)[0]!)
expect(tip?.querySelector("b")?.textContent).toBe("x")
expect(tip?.querySelector("i")?.textContent).toBe("y")
expect(tip?.textContent).toBe("x · y")
expect(tip?.textContent).not.toContain("<b>")
})

it("calls onPinTap with the tapped pin", () => {
const onPinTap = vi.fn()
const { container } = render(<LeafletMap pins={PINS} onPinTap={onPinTap} />)

fireEvent.click(markerIcons(container)[2]!)
expect(onPinTap).toHaveBeenCalledTimes(1)
expect(onPinTap).toHaveBeenCalledWith(PINS[2])
})

it("removes the Leaflet map on unmount", () => {
const remove = vi.spyOn(L.Map.prototype, "remove")
const { unmount } = render(<LeafletMap pins={PINS} />)
expect(remove).not.toHaveBeenCalled()

unmount()
expect(remove).toHaveBeenCalledTimes(1)
})
})

describe("BoundaryMap", () => {
const square = {
type: "Polygon",
coordinates: [
[
[-118.3, 34.0],
[-118.2, 34.0],
[-118.2, 34.1],
[-118.3, 34.1],
[-118.3, 34.0],
],
],
}
const bbox: [number, number, number, number] = [-118.3, 34.0, -118.2, 34.1]

it("draws the boundary polygon", () => {
const { container } = render(<BoundaryMap geometry={square} bbox={bbox} layer="county" />)

const path = container.querySelector("path.leaflet-interactive")
expect(path).not.toBeNull()
expect(path).toHaveAttribute("stroke", "#3F7CAC")
})

it("removes the Leaflet map on unmount", () => {
const remove = vi.spyOn(L.Map.prototype, "remove")
const { unmount } = render(<BoundaryMap geometry={square} bbox={bbox} />)
expect(remove).not.toHaveBeenCalled()

unmount()
expect(remove).toHaveBeenCalledTimes(1)
})
})
33 changes: 0 additions & 33 deletions apps/admin/src/components/shared/modal-accessibility.test.ts

This file was deleted.

Loading
Loading