Initial Commit
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user