Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 55 additions & 0 deletions __tests__/components/Countdown.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Countdown eventDate={now} />);

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(<Countdown eventDate={futureDate.toISOString()} />);

/*
* 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");
});
});
17 changes: 11 additions & 6 deletions components/elements/Countdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ interface CountdownProps {
}

export default function Countdown({ style, eventDate }: Readonly<CountdownProps>) {
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;
Expand All @@ -41,7 +46,7 @@ export default function Countdown({ style, eventDate }: Readonly<CountdownProps>
}, 1000);

return () => clearInterval(interval);
}, []);
}, [eventDate]);

const timeParts = getPartsOfTimeDuration(timeDif);

Expand Down
2 changes: 1 addition & 1 deletion cypress/e2e/home/home-editions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down