diff --git a/__tests__/components/Countdown.test.tsx b/__tests__/components/Countdown.test.tsx new file mode 100644 index 00000000..8a47b618 --- /dev/null +++ b/__tests__/components/Countdown.test.tsx @@ -0,0 +1,55 @@ +import { render, act } from "@testing-library/react"; +import Countdown from "@/components/elements/Countdown"; +import "@testing-library/jest-dom"; + +describe("Countdown", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); + }); + + it("renders initial state as zero (hydration match)", async () => { + /* + * To verify the hydration match (server render = 0), we can't easily prevent + * useEffect from running immediately in JSDOM. However, passing the current + * time effectively simulates the "0" state before any tick happens. + */ + const now = new Date().toISOString(); + const { container: containerNow, unmount } = render(); + + expect(containerNow.querySelector("#days1")).toHaveTextContent("0"); + expect(containerNow.querySelector("#hours1")).toHaveTextContent("0"); + expect(containerNow.querySelector("#minutes1")).toHaveTextContent("0"); + expect(containerNow.querySelector("#seconds1")).toHaveTextContent("0"); + + unmount(); + }); + + it("updates countdown after mount", () => { + // Use a specific time offset: 1 hour 30 mins + const futureDate = new Date(Date.now() + 1000 * 60 * 60 * 1 + 1000 * 60 * 30); + const { container } = render(); + + /* + * Fast-forward useEffect to trigger the updateTime call. + * The useEffect runs immediately on mount in tests, but the updateTime inside it + * might need a tick. We advance 1000ms to ensure the interval fires. + */ + act(() => { + jest.advanceTimersByTime(1000); + }); + + // 90 mins = 0 days, 1 hour, 30 mins + const dayElement = container.querySelector("#days1"); + const hourElement = container.querySelector("#hours1"); + const minuteElement = container.querySelector("#minutes1"); + + expect(dayElement).toHaveTextContent("0"); + expect(hourElement).toHaveTextContent("1"); + expect(minuteElement).toHaveTextContent("29"); + }); +}); diff --git a/components/elements/Countdown.tsx b/components/elements/Countdown.tsx index ffd323c1..cab03beb 100644 --- a/components/elements/Countdown.tsx +++ b/components/elements/Countdown.tsx @@ -22,13 +22,18 @@ interface CountdownProps { } export default function Countdown({ style, eventDate }: Readonly) { - const [timeDif, setTimeDif] = useState(() => { - const now = Date.now(); - const targetDate = new Date(eventDate); - return targetDate.getTime() - now; - }); + const [timeDif, setTimeDif] = useState(0); useEffect(() => { + // Initialize time difference on client side to avoid hydration mismatch + const updateTime = () => { + const now = Date.now(); + const targetDate = new Date(eventDate); + setTimeDif(Math.max(0, targetDate.getTime() - now)); + }; + + updateTime(); + const interval = setInterval(() => { setTimeDif((prev) => { const updatedTime = prev - 1000; @@ -41,7 +46,7 @@ export default function Countdown({ style, eventDate }: Readonly }, 1000); return () => clearInterval(interval); - }, []); + }, [eventDate]); const timeParts = getPartsOfTimeDuration(timeDif); diff --git a/cypress/e2e/home/home-editions.cy.ts b/cypress/e2e/home/home-editions.cy.ts index 4d8557a8..1b2bf130 100644 --- a/cypress/e2e/home/home-editions.cy.ts +++ b/cypress/e2e/home/home-editions.cy.ts @@ -11,7 +11,7 @@ describe("Home Pages (2023-2026)", () => { cy.visit(edition.path, { timeout: 120000 }); cy.get(".hero8-header", { timeout: 30000 }).within(() => { - cy.get("h5").should("have.length.at.least", 2); + cy.get(".hero8-header__event-line").should("have.length.at.least", 2); cy.contains(edition.venue, { matchCase: false }).should("be.visible"); cy.contains(edition.date, { matchCase: false }).should("be.visible"); });