Files
cinny/src/app/utils/formatTimestamp.test.ts
T
jaredandClaude Opus 5 4d4a76214a
CI / Build & Quality Checks (push) Successful in 1m30s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 6s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s
refactor(time): one timestamp formatter honouring the clock/date settings (#139)
Audit of every rendered time found five families of ad-hoc formatting:
the shared Time component + copies of its today/yesterday branch
(forwarded header, thread summary, read receipts, device tile, moderation
alerts, edit history), locale-default toLocale*String calls that ignored
the user's 12/24 h and date-format settings (scheduled tray, reminders,
schedule preview, notification snooze, bookmarks, threads list, search
cache line, room insights, media gallery), a hard-coded en-US date in the
activity log, and three relative-age variants.

utils/formatTimestamp.ts now holds the rules — today → time; yesterday /
tomorrow → day word + time; last 6 days → weekday + time; older → date +
time in dateFormatString — plus autoDate / time / date / dateTime styles,
formatDayDivider (full weekday), formatShortAge (room list) and
formatRelativeAge (list rows). useTimestampFormatter binds them to the
settings. 11 unit tests with an injected 'now'.

Visible changes are limited to consistency: 12 h times keep the existing
zero-padded hh:mm A; the a11y label and Created-by line use the user's
date format instead of a fixed long month.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-20 00:28:47 -04:00

97 lines
4.3 KiB
TypeScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
formatDayDivider,
formatRelativeAge,
formatShortAge,
formatTimestamp,
} from './formatTimestamp';
// Fri 18 Sep 2026 14:05 local.
const now = new Date(2026, 8, 18, 14, 5).getTime();
const at = (y: number, m: number, d: number, h = 21, min = 14) =>
new Date(y, m - 1, d, h, min).getTime();
const p12 = { hour24Clock: false, dateFormatString: 'D MMM YYYY' };
const p24 = { hour24Clock: true, dateFormatString: 'YYYY-MM-DD' };
describe('formatTimestamp', () => {
it('today → time only', () => {
assert.equal(formatTimestamp(at(2026, 9, 18, 9, 3), p12, 'auto', now), '09:03 AM');
assert.equal(formatTimestamp(at(2026, 9, 18, 9, 3), p24, 'auto', now), '09:03');
});
it('yesterday / tomorrow → day word + time', () => {
assert.equal(formatTimestamp(at(2026, 9, 17), p24, 'auto', now), 'Yesterday 21:14');
assert.equal(formatTimestamp(at(2026, 9, 19), p24, 'auto', now), 'Tomorrow 21:14');
// Just before midnight yesterday is still yesterday, not "hours ago".
assert.equal(formatTimestamp(at(2026, 9, 17, 23, 59), p24, 'auto', now), 'Yesterday 23:59');
});
it('within the last week → weekday + time', () => {
assert.equal(formatTimestamp(at(2026, 9, 14), p24, 'auto', now), 'Mon 21:14');
assert.equal(formatTimestamp(at(2026, 9, 12), p24, 'auto', now), 'Sat 21:14');
});
it('a week or more → date + time in the user format', () => {
assert.equal(formatTimestamp(at(2026, 9, 11), p24, 'auto', now), '2026-09-11 21:14');
assert.equal(formatTimestamp(at(2025, 1, 2), p12, 'auto', now), '2 Jan 2025 09:14 PM');
assert.equal(formatTimestamp(at(2026, 9, 25), p12, 'auto', now), '25 Sep 2026 09:14 PM');
});
it('autoDate never shows the time', () => {
assert.equal(formatTimestamp(at(2026, 9, 18), p12, 'autoDate', now), 'Today');
assert.equal(formatTimestamp(at(2026, 9, 17), p12, 'autoDate', now), 'Yesterday');
assert.equal(formatTimestamp(at(2026, 9, 15), p12, 'autoDate', now), 'Tue');
assert.equal(formatTimestamp(at(2026, 1, 15), p12, 'autoDate', now), '15 Jan 2026');
});
it('fixed styles ignore the relative day', () => {
assert.equal(formatTimestamp(at(2026, 9, 18), p12, 'time', now), '09:14 PM');
assert.equal(formatTimestamp(at(2026, 9, 18), p12, 'date', now), '18 Sep 2026');
assert.equal(formatTimestamp(at(2026, 9, 18), p24, 'dateTime', now), '2026-09-18 21:14');
});
it('falls back to a default date format when the preference is empty', () => {
assert.equal(
formatTimestamp(at(2026, 1, 15), { hour24Clock: true, dateFormatString: '' }, 'date', now),
'15 Jan 2026',
);
});
});
describe('formatShortAge', () => {
it('minutes and hours today', () => {
assert.equal(formatShortAge(now - 20_000, p12, now), 'now');
assert.equal(formatShortAge(now - 5 * 60_000, p12, now), '5m');
assert.equal(formatShortAge(now - 3 * 3_600_000, p12, now), '3h');
});
it('day words, then a short date (year dropped when current)', () => {
assert.equal(formatShortAge(at(2026, 9, 17, 23, 0), p12, now), 'Yesterday');
assert.equal(formatShortAge(at(2026, 9, 14), p12, now), 'Mon');
assert.equal(formatShortAge(at(2026, 3, 2), p12, now), '2 Mar');
assert.equal(formatShortAge(at(2026, 3, 2), p24, now), '03-02');
assert.equal(formatShortAge(at(2025, 3, 2), p12, now), '2 Mar 2025');
});
});
describe('formatRelativeAge', () => {
it('steps through minutes, hours, days, then the date', () => {
assert.equal(formatRelativeAge(now - 10_000, p12, now), 'just now');
assert.equal(formatRelativeAge(now - 7 * 60_000, p12, now), '7m ago');
assert.equal(formatRelativeAge(now - 5 * 3_600_000, p12, now), '5h ago');
assert.equal(formatRelativeAge(at(2026, 9, 17, 8), p12, now), 'yesterday');
assert.equal(formatRelativeAge(at(2026, 9, 15, 8), p12, now), '3d ago');
assert.equal(formatRelativeAge(at(2026, 9, 1, 8), p24, now), '2026-09-01');
});
});
describe('formatDayDivider', () => {
it('uses the full weekday inside the last week', () => {
assert.equal(formatDayDivider(at(2026, 9, 18), p12, now), 'Today');
assert.equal(formatDayDivider(at(2026, 9, 17), p12, now), 'Yesterday');
assert.equal(formatDayDivider(at(2026, 9, 14), p12, now), 'Monday');
assert.equal(formatDayDivider(at(2026, 9, 11), p12, now), '11 Sep 2026');
});
});