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");
});