diff --git a/src/components/pins/exam-centre-countdown.test.tsx b/src/components/pins/exam-centre-countdown.test.tsx new file mode 100644 index 0000000..9ad96e5 --- /dev/null +++ b/src/components/pins/exam-centre-countdown.test.tsx @@ -0,0 +1,88 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { ExamCentreCountdown } from "@/components/pins/exam-centre-countdown"; +import { PinPopup } from "@/components/pins/pin-popup"; +import type { Place } from "@/lib/types"; + +vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } })); + +afterEach(cleanup); + +const centre: Place = { + id: "12345", + name: "Example International School", + type: "sat_centre", + city: "mumbai", + lat: 19.0176, + lng: 72.8562, + gmaps_link: "https://maps.google.com/?q=19.0176,72.8562", + added_by: "someone", + exam: "SAT", + valid_till: "2027-03-15", +}; + +const now = new Date("2026-10-21T12:00:00"); + +describe("ExamCentreCountdown", () => { + it("shows a calm pill while the date is far away", () => { + render(); + + const pill = screen.getByText(/^Valid until/); + expect(pill.textContent).toContain("2027"); + expect(pill.className).toContain("text-success"); + }); + + it("switches to a warning inside the 60 day window", () => { + render( + , + ); + + const pill = screen.getByText(/^Expires soon/); + expect(pill.className).toContain("text-warning"); + }); + + it("still counts as valid on the last day", () => { + render( + , + ); + + expect(screen.getByText(/^Expires soon/)).toBeTruthy(); + }); + + it("shows Expired in the destructive colour once the date has passed", () => { + render( + , + ); + + const pill = screen.getByText(/^Expired/); + expect(pill.className).toContain("text-destructive"); + }); + + it("renders nothing when the place has no valid_till", () => { + const { valid_till, ...withoutDate } = centre; + void valid_till; + const { container } = render(); + + expect(container.innerHTML).toBe(""); + }); +}); + +describe("PinPopup exam centre validity", () => { + it("shows the validity pill and no duplicate reconfirm text for an exam centre", () => { + render(); + + expect(screen.getByText(/^(Valid until|Expires soon|Expired)/)).toBeTruthy(); + expect(screen.getByText("SAT")).toBeTruthy(); + expect(screen.queryByText(/reconfirm by/)).toBeNull(); + }); + + it("shows no pill for a place that is not an exam centre", () => { + const library: Place = { ...centre, type: "library" }; + delete library.exam; + delete library.valid_till; + render(); + + expect(screen.queryByText(/^(Valid until|Expires soon|Expired)/)).toBeNull(); + }); +}); diff --git a/src/components/pins/exam-centre-countdown.tsx b/src/components/pins/exam-centre-countdown.tsx index 46bd4ff..d24d43c 100644 --- a/src/components/pins/exam-centre-countdown.tsx +++ b/src/components/pins/exam-centre-countdown.tsx @@ -26,6 +26,8 @@ function daysUntil(iso: string, now: Date): number | null { export interface ExamCentreCountdownProps { place: Place; className?: string; + /** The clock to compare against; tests pass a fixed date. Defaults to the current time. */ + now?: Date; } /** @@ -35,10 +37,14 @@ export interface ExamCentreCountdownProps { * pattern as verified-badge.tsx: green while comfortably valid, amber inside * the 60-day window, red once the date has passed. */ -export function ExamCentreCountdown({ place, className }: ExamCentreCountdownProps) { +export function ExamCentreCountdown({ + place, + className, + now = new Date(), +}: ExamCentreCountdownProps) { if (!place.valid_till) return null; - const days = daysUntil(place.valid_till, new Date()); + const days = daysUntil(place.valid_till, now); const expired = days !== null && days < 0; const expiringSoon = days !== null && days >= 0 && days <= EXPIRY_WARNING_DAYS; diff --git a/src/components/pins/pin-popup.tsx b/src/components/pins/pin-popup.tsx index ccfa36e..486ebf2 100644 --- a/src/components/pins/pin-popup.tsx +++ b/src/components/pins/pin-popup.tsx @@ -7,21 +7,13 @@ import type { Place } from "@/lib/types"; import { humanizeCity, PLACE_TYPE_LABELS } from "@/lib/types"; import { directionsUrl, PLACE_TYPE_COLORS } from "@/lib/map"; import { buildShareUrl } from "@/lib/share"; +import { ExamCentreCountdown } from "@/components/pins/exam-centre-countdown"; import { VerifiedBadge } from "@/components/pins/verified-badge"; interface PinPopupProps { place: Place; } -function formatValidTill(iso: string): string { - if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return iso; - return new Date(iso + "T00:00:00").toLocaleDateString("en-IN", { - day: "numeric", - month: "short", - year: "numeric", - }); -} - export function PinPopup({ place }: PinPopupProps) { function copyLink() { const url = buildShareUrl({ @@ -61,6 +53,7 @@ export function PinPopup({ place }: PinPopupProps) { {humanizeCity(place.city)} + @@ -69,13 +62,10 @@ export function PinPopup({ place }: PinPopupProps) {

{place.address}

)} - {/* Exam centre validity */} + {/* Exam centre: the validity pill in the header carries the reconfirm date */} {place.exam && (
{place.exam} centre - {place.valid_till && ( - <> · reconfirm by {formatValidTill(place.valid_till)} - )}
)}