Files
suaveplan/packages/localization/i18n/react-i18n/src/hooks/use-relative-time/use-relative-time.test.tsx
2026-05-27 23:47:53 +01:00

167 lines
5.5 KiB
TypeScript

import { createI18n } from "@suaveplan/i18n/plugin";
import { afterEach, describe, expect, it } from "@suaveplan/testing/runner";
import { renderComponent } from "@suaveplan/testing-dom/render";
import { act } from "@testing-library/react";
import { TranslationProvider } from "../../provider/provider.js";
import { resetWarnOncePerProcess } from "../../utils/warn-once-per-process.js";
import {
clearUseRelativeTimeCache,
useRelativeTime,
} from "./use-relative-time.js";
interface ProcessShape {
env?: { NODE_ENV?: string };
}
function withEnv<T>(value: string, fn: () => T): T {
const proc = (globalThis as unknown as { process?: ProcessShape }).process;
const previous = proc?.env?.NODE_ENV;
if (proc?.env !== undefined) proc.env.NODE_ENV = value;
try {
return fn();
} finally {
if (proc?.env !== undefined) {
if (previous === undefined) delete proc.env.NODE_ENV;
else proc.env.NODE_ENV = previous;
}
}
}
function Probe(props: {
readonly deltaMs: number;
readonly options?: Parameters<typeof useRelativeTime>[1];
}) {
const formatted = useRelativeTime(props.deltaMs, props.options);
return <span data-testid="probe">{formatted}</span>;
}
afterEach(() => {
resetWarnOncePerProcess();
clearUseRelativeTimeCache();
});
describe("useRelativeTime", () => {
it("renders a future minutes delta in English", { timeout: 5000 }, () => {
const i18n = createI18n({
defaultLocale: "en-GB",
messages: { "en-GB": {} },
});
const { getByTestId } = renderComponent(
<TranslationProvider i18n={i18n}>
<Probe deltaMs={5 * 60_000} />
</TranslationProvider>
);
expect(getByTestId("probe").textContent).toMatch(/in 5 minutes/);
});
it("renders a past hours delta with negative sign", {
timeout: 5000,
}, () => {
const i18n = createI18n({
defaultLocale: "en-GB",
messages: { "en-GB": {} },
});
const { getByTestId } = renderComponent(
<TranslationProvider i18n={i18n}>
<Probe deltaMs={-3 * 3_600_000} />
</TranslationProvider>
);
expect(getByTestId("probe").textContent).toMatch(/3 hours ago/);
});
it("re-renders with French phrasing on locale change", {
timeout: 5000,
}, async () => {
const i18n = createI18n({
defaultLocale: "en-GB",
messages: { "en-GB": {}, "fr-FR": {} },
});
const { getByTestId } = renderComponent(
<TranslationProvider i18n={i18n}>
<Probe deltaMs={5 * 60_000} />
</TranslationProvider>
);
const en = getByTestId("probe").textContent ?? "";
expect(en).toMatch(/in 5 minutes/);
await act(async () => {
await i18n.changeLanguage("fr-FR");
});
const fr = getByTestId("probe").textContent ?? "";
expect(fr).not.toBe(en);
expect(fr).toMatch(/dans 5 minutes/);
});
it("respects an explicit unit override", { timeout: 5000 }, () => {
const i18n = createI18n({
defaultLocale: "en-GB",
messages: { "en-GB": {} },
});
const { getByTestId } = renderComponent(
<TranslationProvider i18n={i18n}>
<Probe
deltaMs={3_600_000}
options={{ unit: "minutes", numeric: "always" }}
/>
</TranslationProvider>
);
expect(getByTestId("probe").textContent).toMatch(/in 60 minutes/);
});
it("respects style: 'short'", { timeout: 5000 }, () => {
const i18n = createI18n({
defaultLocale: "en-GB",
messages: { "en-GB": {} },
});
const { getByTestId } = renderComponent(
<TranslationProvider i18n={i18n}>
<Probe deltaMs={5 * 60_000} options={{ style: "short" }} />
</TranslationProvider>
);
expect(getByTestId("probe").textContent).toMatch(/min\b/);
});
it("auto-selects seconds, days, weeks, months, years when below thresholds", {
timeout: 5000,
}, () => {
const i18n = createI18n({
defaultLocale: "en-GB",
messages: { "en-GB": {} },
});
const cases: Array<[number, RegExp]> = [
[10_000, /10 seconds|seconds/],
[3 * 86_400_000, /3 days/],
[3 * 604_800_000, /3 weeks/],
[3 * 2_629_800_000, /3 months/],
[3 * 31_557_600_000, /3 years/],
];
for (const [delta, regex] of cases) {
const { getByTestId, unmount } = renderComponent(
<TranslationProvider i18n={i18n}>
<Probe deltaMs={delta} options={{ numeric: "always" }} />
</TranslationProvider>
);
expect(getByTestId("probe").textContent).toMatch(regex);
unmount();
}
});
it("formats via en-GB on the production no-Provider fallback", {
timeout: 5000,
}, () => {
const original = console.warn;
console.warn = (() => {}) as typeof console.warn;
try {
withEnv("production", () => {
const { getByTestId } = renderComponent(
<Probe deltaMs={5 * 60_000} />
);
expect(getByTestId("probe").textContent).toMatch(
/in 5 minutes/
);
});
} finally {
console.warn = original;
}
});
});