Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { hydrateRoot } from "react-dom/client";
import { renderToString } from "react-dom/server";
import { act } from "react-dom/test-utils";
import { afterEach, describe, expect, it } from "vitest";
import { DateTime, Settings } from "luxon";
import { HoursStatus } from "./hoursStatus.js";
import { HoursData } from "./hoursSampleData.js";

const originalNow = Settings.now;
const originalDefaultZone = Settings.defaultZone;
const originalActEnvironment = (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
.IS_REACT_ACT_ENVIRONMENT;

describe("HoursStatus hydration", () => {
afterEach(async () => {
Settings.now = originalNow;
Settings.defaultZone = originalDefaultZone;
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
originalActEnvironment;
vi.restoreAllMocks();
document.body.innerHTML = "";
});

it("hydrates from the placeholder to the current status without warnings", async () => {
const mockedNow = DateTime.fromObject(
{ year: 2025, month: 1, day: 7, hour: 10 },
{ zone: "America/New_York" }
);

Settings.now = () => mockedNow.toMillis();
Settings.defaultZone = "America/New_York";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

vi.spyOn(globalThis, "setTimeout").mockImplementation((() => 0) as typeof setTimeout);
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});

const container = document.createElement("div");
container.innerHTML = renderToString(
<div style={{ minHeight: "1.5em" }} className="HoursStatus" />
);
document.body.appendChild(container);

expect(container.textContent).toBe("");

let root: ReturnType<typeof hydrateRoot> | undefined;

await act(async () => {
root = hydrateRoot(container, <HoursStatus hours={HoursData} timezone="America/New_York" />);
await Promise.resolve();
});

expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(container.textContent).toContain("Open Now");
expect(container.textContent).toContain("Closes at");
expect(container.textContent).toContain("6:02 PM");
expect(container.textContent).toContain("Tuesday");

await act(async () => {
root?.unmount();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// @vitest-environment node

import { afterEach, describe, expect, it } from "vitest";
import { DateTime, Settings } from "luxon";
import { renderToString } from "react-dom/server";
import { HoursStatus } from "./hoursStatus.js";
import { HoursData } from "./hoursSampleData.js";

const originalNow = Settings.now;
const originalDefaultZone = Settings.defaultZone;

describe("HoursStatus SSR", () => {
afterEach(() => {
Settings.now = originalNow;
Settings.defaultZone = originalDefaultZone;
vi.restoreAllMocks();
});

it("renders the placeholder without SSR warnings", () => {
const mockedNow = DateTime.fromObject(
{ year: 2025, month: 1, day: 7, hour: 10 },
{ zone: "America/New_York" }
);

Settings.now = () => mockedNow.toMillis();
Settings.defaultZone = "America/New_York";

vi.spyOn(globalThis, "setTimeout").mockImplementation((() => 0) as typeof setTimeout);
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});

const html = renderToString(<HoursStatus hours={HoursData} timezone="America/New_York" />);

expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(html).toContain('class="HoursStatus"');
expect(html).toContain("min-height:1.5em");
expect(html).not.toContain("HoursStatus-current");
expect(html).not.toContain("Open Now");
expect(html).not.toContain("Closed");
expect(html).not.toContain("Closes at");
expect(html).not.toContain("Opens at");
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useLayoutEffect, useState } from "react";
import c from "classnames";
import { Hours } from "./hours.js";
import { DateTime } from "luxon";
import { HoursStatusProps, StatusParams, StatusTemplateParams } from "./types.js";

const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;

function isOpen24h(params: StatusParams): boolean {
return params?.currentInterval?.is24h?.() || false;
}
Expand Down Expand Up @@ -115,7 +117,7 @@ const HoursStatus: React.FC<HoursStatusProps> = (props) => {
// https://reactjs.org/docs/react-dom.html#hydrate
const [isClient, setIsClient] = useState(false);

useEffect(() => {
useIsomorphicLayoutEffect(() => {
setIsClient(true);
}, []);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { hydrateRoot } from "react-dom/client";
import { renderToString } from "react-dom/server";
import { act } from "react-dom/test-utils";
import { afterEach, describe, expect, it } from "vitest";
import { DateTime, Settings } from "luxon";
import { HoursTable, ServerSideHoursTable } from "./hoursTable.js";
import { HoursData } from "./hoursSampleData.js";

const originalNow = Settings.now;
const originalDefaultZone = Settings.defaultZone;
const originalResolvedOptions = Intl.DateTimeFormat.prototype.resolvedOptions;
const originalActEnvironment = (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean })
.IS_REACT_ACT_ENVIRONMENT;

describe("HoursTable hydration", () => {
afterEach(() => {
Settings.now = originalNow;
Settings.defaultZone = originalDefaultZone;
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
originalActEnvironment;
vi.restoreAllMocks();
document.body.innerHTML = "";
});

it("hydrates from the server-safe table to the client-aware table without warnings", async () => {
const mockedNow = DateTime.fromObject(
{ year: 2025, month: 1, day: 9, hour: 12 },
{ zone: "America/New_York" }
);

Settings.now = () => mockedNow.toMillis();
Settings.defaultZone = "America/New_York";
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

vi.spyOn(Intl.DateTimeFormat.prototype, "resolvedOptions").mockImplementation(
function (this: Intl.DateTimeFormat) {
return {
...originalResolvedOptions.call(this),
timeZone: "America/New_York",
};
}
);

const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const serverHtml = renderToString(
<ServerSideHoursTable hours={HoursData} startOfWeek="today" />
);
const container = document.createElement("div");
container.innerHTML = serverHtml;
document.body.appendChild(container);

const getDayLabels = () =>
Array.from(container.querySelectorAll(".HoursTable-day"), (element) => element.textContent);

expect(getDayLabels()[0]).toBe("Sunday");
expect(container.querySelector(".HoursTable-row.is-today")).toBeNull();

let root: ReturnType<typeof hydrateRoot> | undefined;

await act(async () => {
root = hydrateRoot(container, <HoursTable hours={HoursData} startOfWeek="today" />);
await Promise.resolve();
});

const todayRow = container.querySelector(".HoursTable-row.is-today");

expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(getDayLabels()[0]).toBe("Thursday");
expect(todayRow?.querySelector(".HoursTable-day")?.textContent).toBe("Thursday");
expect(todayRow?.querySelector(".HoursTable-intervals")?.textContent).toContain(
"9:04 AM - 6:04 PM"
);

await act(async () => {
root?.unmount();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// @vitest-environment node

import { afterEach, describe, expect, it } from "vitest";
import { DateTime, Settings } from "luxon";
import { renderToString } from "react-dom/server";
import { HoursTable } from "./hoursTable.js";
import { HoursData } from "./hoursSampleData.js";

const originalNow = Settings.now;
const originalDefaultZone = Settings.defaultZone;

describe("HoursTable SSR", () => {
afterEach(() => {
Settings.now = originalNow;
Settings.defaultZone = originalDefaultZone;
vi.restoreAllMocks();
});

it("renders the server-safe table without SSR warnings", () => {
const mockedNow = DateTime.fromObject(
{ year: 2025, month: 1, day: 9, hour: 12 },
{ zone: "America/New_York" }
);

Settings.now = () => mockedNow.toMillis();
Settings.defaultZone = "America/New_York";

const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const html = renderToString(<HoursTable hours={HoursData} startOfWeek="today" />);
const dayLabels = [...html.matchAll(/<span class="HoursTable-day">([^<]+)<\/span>/g)].map(
(match) => match[1]
);

expect(consoleErrorSpy).not.toHaveBeenCalled();
expect(html).toContain("HoursTable-row");
expect(html).not.toContain("is-today");
expect(dayLabels[0]).toBe("Sunday");
});
});
6 changes: 4 additions & 2 deletions packages/pages-components/src/components/hours/hoursTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useLayoutEffect, useState } from "react";
import c from "classnames";
import "./hoursTable.css";
import {
Expand All @@ -20,6 +20,8 @@ import {
} from "./hours.js";
import { DateTime, WeekdayNumbers } from "luxon";

const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;

/**
*
* @param hoursDays - HoursTableDayData[]
Expand Down Expand Up @@ -140,7 +142,7 @@ const HoursTable: React.FC<HoursTableProps> = (props) => {
// On the second pass (After the page has been loaded), render the content
// https://reactjs.org/docs/react-dom.html#hydrate
const [isClient, setIsClient] = useState(false);
useEffect(() => {
useIsomorphicLayoutEffect(() => {
setIsClient(true);
}, []);

Expand Down
Loading