Files
paseo/packages/app/src/utils/time.test.ts
T
paseo-bot[bot]andMohamed Boudra 05874e289a Keep relative timestamps current in agent, schedule, and import-session rows (#5340)
* Keep relative timestamps current in agent, schedule, and import-session rows

Agent list rows, schedule rows, and import-session rows formatted their
"5m ago" labels once at render, so an idle row kept its first label until
something else re-rendered it. They now use useTimeAgo, which rides the
same shared ticker the sidebar and desktop tabs already use.

Closes #4625

* Say "just now" for the whole first minute so fresh labels stay honest

The shared ticker wakes once a minute, so a seconds label could only go
stale. The prose and compact formatters now share one age description,
and the schedule spec anchors its clock to the schedule's creation time.

* Run every cleanup in the schedule relative-time spec and name its steps

* Keep relative labels current with focused subscriptions

* Update usage card after relative-time hook rename

* Anchor import-session age spec to fixture time

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-09-28 18:00:54 +01:00

156 lines
5.9 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
describeCompactTimeAgo,
describeTimeAgo,
formatCompactTimeAgo,
formatCompactTimeAgoAsProse,
formatDuration,
formatMessageTimestamp,
formatTimeAgo,
} from "./time";
describe("formatTimeAgo", () => {
const now = new Date("2026-07-16T12:00:00.000Z");
it.each([
["2026-07-16T11:59:55.000Z", "just now"],
["2026-07-16T11:59:30.000Z", "just now"],
["2026-07-16T11:59:00.000Z", "1m ago"],
["2026-07-16T11:55:00.000Z", "5m ago"],
["2026-07-16T10:00:00.000Z", "2h ago"],
["2026-07-13T12:00:00.000Z", "3d ago"],
["2026-01-15T12:00:00.000Z", "Jan 15"],
])("formats %s as %s", (date, expected) => {
expect(formatTimeAgo(new Date(date), now)).toBe(expected);
});
});
describe("describeTimeAgo", () => {
const now = new Date("2026-07-16T12:00:00.000Z");
it.each([
["2026-07-16T11:59:30.000Z", "just now", "minute"],
["2026-07-16T11:55:00.000Z", "5m ago", "minute"],
["2026-07-16T10:00:00.000Z", "2h ago", "hour"],
["2026-07-13T12:00:00.000Z", "3d ago", "day"],
["2026-01-15T12:00:00.000Z", "Jan 15", "static"],
] as const)("formats %s as %s at %s resolution", (date, label, resolution) => {
expect(describeTimeAgo(new Date(date), now)).toEqual({ label, resolution });
});
});
describe("describeCompactTimeAgo", () => {
const now = new Date("2026-07-16T12:00:00.000Z");
it.each([
["2026-07-16T11:59:59.000Z", "now", "minute"],
["2026-07-16T11:59:30.000Z", "now", "minute"],
["2026-07-16T11:59:00.000Z", "1m", "minute"],
["2026-07-16T11:55:00.000Z", "5m", "minute"],
["2026-07-16T11:00:00.000Z", "1h", "hour"],
["2026-07-16T10:00:00.000Z", "2h", "hour"],
["2026-07-15T12:00:00.000Z", "1d", "day"],
["2026-07-13T12:00:00.000Z", "3d", "day"],
["2026-07-10T12:00:00.000Z", "6d", "day"],
["2026-07-09T12:00:00.000Z", "Jul 9", "static"],
["2026-01-15T12:00:00.000Z", "Jan 15", "static"],
] as const)("formats %s as %s at %s resolution", (date, label, resolution) => {
expect(describeCompactTimeAgo(new Date(date), now)).toEqual({ label, resolution });
});
it("never shows a sub-minute count", () => {
// A seconds label is only true for the second it rendered. Everything under a minute is
// "now", which stays true and lets the row sit still.
for (let seconds = 0; seconds < 60; seconds += 1) {
const date = new Date(now.getTime() - seconds * 1000);
expect(describeCompactTimeAgo(date, now).label).toBe("now");
}
});
it("switches to an absolute date exactly at seven days", () => {
const almost = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000 - 1));
const exactly = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
expect(describeCompactTimeAgo(almost, now).resolution).toBe("day");
expect(describeCompactTimeAgo(exactly, now).resolution).toBe("static");
});
it("keeps formatCompactTimeAgo as the label alone", () => {
const date = new Date("2026-07-16T10:00:00.000Z");
expect(formatCompactTimeAgo(date, now)).toBe(describeCompactTimeAgo(date, now).label);
});
});
describe("formatCompactTimeAgoAsProse", () => {
it("puts elapsed labels back into prose and leaves dates alone", () => {
expect(formatCompactTimeAgoAsProse("now")).toBe("just now");
expect(formatCompactTimeAgoAsProse("3m")).toBe("3m ago");
expect(formatCompactTimeAgoAsProse("2h")).toBe("2h ago");
expect(formatCompactTimeAgoAsProse("Jan 15")).toBe("Jan 15");
});
});
describe("formatDuration", () => {
it("renders sub-minute durations as whole seconds", () => {
expect(formatDuration(0)).toBe("0s");
expect(formatDuration(5_600)).toBe("5s");
expect(formatDuration(9_900)).toBe("9s");
expect(formatDuration(10_400)).toBe("10s");
expect(formatDuration(12_340)).toBe("12s");
expect(formatDuration(47_000)).toBe("47s");
});
it("renders minutes and remainder seconds without decimals", () => {
expect(formatDuration(75_230)).toBe("1m 15s");
expect(formatDuration(132_000)).toBe("2m 12s");
expect(formatDuration(120_000)).toBe("2m");
});
it("renders hours and remainder minutes without decimals", () => {
expect(formatDuration(3_900_000)).toBe("1h 5m");
expect(formatDuration(3_600_000)).toBe("1h");
});
it("guards against negative and NaN", () => {
expect(formatDuration(-1)).toBe("0s");
expect(formatDuration(Number.NaN)).toBe("0s");
});
});
describe("formatMessageTimestamp", () => {
it("shows only time for same-day timestamps", () => {
const now = new Date(2026, 4, 14, 17, 30);
const date = new Date(2026, 4, 14, 12, 23);
const formatted = formatMessageTimestamp(date, now);
expect(formatted).toMatch(/12:23/);
expect(formatted).not.toMatch(/Thursday|Wednesday/);
});
it("includes weekday for timestamps within the last 6 days", () => {
// 2026-05-14 is a Thursday. 2026-05-11 is a Monday.
const now = new Date(2026, 4, 14, 17, 30);
const date = new Date(2026, 4, 11, 22, 12);
const formatted = formatMessageTimestamp(date, now);
expect(formatted).toMatch(/Monday/);
expect(formatted).toMatch(/10:12 PM|22:12/);
});
it("shows the full date for last week's same weekday, even under seven days ago", () => {
// 2026-09-18 is a Friday. The reply is 6 days 23h45m old and lands on today's weekday,
// so a weekday label would read as today.
const now = new Date(2026, 8, 25, 11, 47);
const date = new Date(2026, 8, 18, 12, 2);
const formatted = formatMessageTimestamp(date, now);
expect(formatted).toMatch(/Sep|September/);
expect(formatted).toMatch(/18/);
expect(formatted).not.toMatch(/Friday/);
});
it("includes full date for older timestamps", () => {
const now = new Date(2026, 4, 14, 17, 30);
const date = new Date(2026, 3, 1, 9, 5);
const formatted = formatMessageTimestamp(date, now);
expect(formatted).toMatch(/Apr|April/);
expect(formatted).toMatch(/2026/);
});
});