import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";
import {
RelativeTime,
formatAbsoluteTime,
formatDate,
formatExactTime,
formatRelativeTime,
formatShortTime,
inSentence,
timeZoneLabel,
} from "./relative-time";
/** Saturday 26 September 2026, 13:48 in Oslo (CEST, UTC+2). */
const NOW = new Date("2026-09-26T11:48:00Z");
const OSLO = { now: NOW, timeZone: "Europe/Oslo" } as const;
const MINUTE = 60_000;
const HOUR = 60 * MINUTE;
const DAY = 24 * HOUR;
function before(ms: number) {
return new Date(NOW.getTime() - ms);
}
describe("formatRelativeTime", () => {
test("uses the short past phrases the copy rules ask for", () => {
expect(formatRelativeTime(before(20_000), OSLO)).toBe("Just now");
expect(formatRelativeTime(before(59_999), OSLO)).toBe("Just now");
expect(formatRelativeTime(before(MINUTE), OSLO)).toBe("1 min ago");
expect(formatRelativeTime(before(48 * MINUTE), OSLO)).toBe("48 min ago");
expect(formatRelativeTime(before(HOUR), OSLO)).toBe("1 hour ago");
expect(formatRelativeTime(before(2 * HOUR + 10 * MINUTE), OSLO)).toBe("2 hours ago");
});
test("counts calendar days in the viewer's zone after the first day", () => {
// Fri 25 Sep, 08:00 Oslo is more than 24 hours ago and on the previous day.
expect(formatRelativeTime("2026-09-25T06:00:00Z", OSLO)).toBe("Yesterday");
expect(formatRelativeTime("2026-09-23T09:12:00Z", OSLO)).toBe("3 days ago");
expect(formatRelativeTime("2026-09-18T08:15:00Z", OSLO)).toBe("8 days ago");
expect(formatRelativeTime("2026-09-12T11:05:00Z", OSLO)).toBe("2 weeks ago");
expect(formatRelativeTime("2026-09-02T12:40:00Z", OSLO)).toBe("3 weeks ago");
expect(formatRelativeTime("2026-08-26T07:45:00Z", OSLO)).toBe("1 month ago");
expect(formatRelativeTime(before(400 * DAY), OSLO)).toBe("1 year ago");
});
test("reads future times forward", () => {
expect(formatRelativeTime(new Date(NOW.getTime() + 14 * MINUTE), OSLO)).toBe("In 14 min");
expect(formatRelativeTime(new Date(NOW.getTime() + 3 * HOUR), OSLO)).toBe("In 3 hours");
// Sun 27 Sep, 23:00 Oslo: more than a day away, on the next calendar day.
expect(formatRelativeTime("2026-09-27T21:00:00Z", OSLO)).toBe("Tomorrow");
expect(formatRelativeTime(new Date(NOW.getTime() + 5 * DAY), OSLO)).toBe("In 5 days");
});
test("the calendar day follows the time zone, not UTC", () => {
// 23:30 UTC on 24 Sep is already 25 Sep in Tokyo but still 24 Sep in New York.
const late = "2026-09-24T23:30:00Z";
const now = new Date("2026-09-26T10:00:00Z");
expect(formatRelativeTime(late, { now, timeZone: "Asia/Tokyo" })).toBe("Yesterday");
expect(formatRelativeTime(late, { now, timeZone: "America/New_York" })).toBe("2 days ago");
});
});
describe("formatAbsoluteTime", () => {
test("one format: weekday, day, month and 24-hour time, never 'Sept' or seconds", () => {
expect(formatAbsoluteTime("2026-09-28T06:00:00Z", OSLO)).toBe("Mon 28 Sep, 08:00");
expect(formatAbsoluteTime("2026-10-01T07:00:00Z", OSLO)).toBe("Thu 1 Oct, 09:00");
});
test("names today, tomorrow and yesterday", () => {
expect(formatAbsoluteTime("2026-09-26T12:00:00Z", OSLO)).toBe("Today, 14:00");
expect(formatAbsoluteTime("2026-09-27T07:30:00Z", OSLO)).toBe("Tomorrow, 09:30");
expect(formatAbsoluteTime("2026-09-25T15:30:00Z", OSLO)).toBe("Yesterday, 17:30");
});
test("adds the year only outside the current one", () => {
expect(formatAbsoluteTime("2027-03-30T07:00:00Z", OSLO)).toBe("Tue 30 Mar 2027, 09:00");
});
test("the UTC variant computes and says UTC", () => {
expect(formatAbsoluteTime("2026-09-23T09:12:00Z", { now: NOW, utc: true })).toBe(
"Wed 23 Sep, 09:12 UTC",
);
});
});
describe("formatDate and formatExactTime", () => {
test("dates drop the time and the current year", () => {
expect(formatDate("2026-08-14T10:02:00Z", OSLO)).toBe("14 Aug");
expect(formatDate("2027-03-31T10:00:00Z", OSLO)).toBe("31 Mar 2027");
});
test("the exact time always has the year and names the zone in words", () => {
expect(formatExactTime("2026-09-23T09:12:00Z", OSLO)).toBe("Wed 23 Sep 2026, 11:12 Oslo time");
expect(formatExactTime("2026-09-23T09:12:00Z", { utc: true })).toBe(
"Wed 23 Sep 2026, 09:12 UTC",
);
expect(timeZoneLabel("America/Los_Angeles")).toBe("Los Angeles time");
expect(timeZoneLabel("Etc/UTC")).toBe("UTC");
});
});
describe("formatShortTime", () => {
test("later today is the clock alone, this week the weekday, then the date", () => {
expect(formatShortTime("2026-09-26T15:10:00Z", OSLO)).toBe("17:10");
expect(formatShortTime("2026-09-27T07:00:00Z", OSLO)).toBe("Sun 09:00");
expect(formatShortTime("2026-10-02T07:00:00Z", OSLO)).toBe("Fri 09:00");
expect(formatShortTime("2026-10-03T07:00:00Z", OSLO)).toBe("3 Oct");
expect(formatShortTime("2027-01-03T07:00:00Z", OSLO)).toBe("3 Jan 2027");
expect(formatShortTime("2026-09-26T15:10:00Z", { now: NOW, utc: true })).toBe("15:10 UTC");
});
});
describe("inSentence", () => {
test("lowercases only the leading relative word", () => {
expect(inSentence("Yesterday")).toBe("yesterday");
expect(inSentence("Just now")).toBe("just now");
expect(inSentence("In 5 days")).toBe("in 5 days");
expect(inSentence("Today, 14:00")).toBe("today, 14:00");
expect(inSentence("Mon 28 Sep, 08:00")).toBe("Mon 28 Sep, 08:00");
expect(inSentence("3 days ago")).toBe("3 days ago");
});
});
describe("RelativeTime", () => {
test("renders a machine-readable time and a prefixed, sentence-cased label", () => {
const html = renderToStaticMarkup(
,
);
expect(html).toContain('dateTime="2026-09-25T06:00:00.000Z"');
expect(html).toContain("Updated yesterday");
expect(html).toContain('tabindex="0"');
});
test("renders nothing for an invalid date", () => {
expect(renderToStaticMarkup()).toBe("");
});
test("without a tab stop, screen readers still get the exact time", () => {
const html = renderToStaticMarkup(
,
);
expect(html).not.toContain("tabindex");
expect(html).toContain("Wed 23 Sep 2026, 11:12 Oslo time");
});
});