| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | import { |
| 3 | formatNextScheduledTaskRun, |
| 4 | formatScheduledTaskInterval, |
| 5 | } from "./scheduledTaskPresentation"; |
| 6 | |
| 7 | const MINUTE = 60_000; |
| 8 | const now = new Date(2026, 8, 17, 9).getTime(); |
| 9 | const timeLabel = (date: Date) => |
| 10 | date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); |
| 11 | |
| 12 | describe("formatScheduledTaskInterval", () => { |
| 13 | it.each([ |
| 14 | [1, "Every minute"], |
| 15 | [15, "Every 15 minutes"], |
| 16 | [60, "Every hour"], |
| 17 | [90, "Every 1 hour 30 minutes"], |
| 18 | [120, "Every 2 hours"], |
| 19 | [1440, "Every day"], |
| 20 | [1500, "Every 1 day 1 hour"], |
| 21 | [2880, "Every 2 days"], |
| 22 | [10080, "Every week"], |
| 23 | ])("formats a %i-minute interval as %s", (minutes, expected) => { |
| 24 | expect(formatScheduledTaskInterval(minutes * MINUTE)).toBe(expected); |
| 25 | }); |
| 26 | |
| 27 | it("retains precision for legacy sub-minute schedules", () => { |
| 28 | expect(formatScheduledTaskInterval(30_000)).toBe("Every 30 seconds"); |
| 29 | expect(formatScheduledTaskInterval(90_000)).toBe("Every 1 minute 30 seconds"); |
| 30 | }); |
| 31 | }); |
| 32 | |
| 33 | describe("formatNextScheduledTaskRun", () => { |
| 34 | it.each([ |
| 35 | [30_000, "Next run in less than a minute"], |
| 36 | [MINUTE, "Next run in 1 minute"], |
| 37 | [15 * MINUTE, "Next run in 15 minutes"], |
| 38 | [60 * MINUTE, "Next run in 1 hour"], |
| 39 | [120 * MINUTE, "Next run in 2 hours"], |
| 40 | [0, "Next run due"], |
| 41 | [-MINUTE, "Next run due"], |
| 42 | ])("formats a nearby run %i ms away", (offset, expected) => { |
| 43 | expect(formatNextScheduledTaskRun(new Date(now + offset).toISOString(), now)).toBe(expected); |
| 44 | }); |
| 45 | |
| 46 | it("uses tomorrow by calendar date, including runs just across midnight", () => { |
| 47 | const current = new Date(2026, 8, 17, 23, 50); |
| 48 | const next = new Date(2026, 8, 18, 0, 10); |
| 49 | expect(formatNextScheduledTaskRun(next.toISOString(), current.getTime())).toBe( |
| 50 | `Next run tomorrow at ${timeLabel(next)}`, |
| 51 | ); |
| 52 | }); |
| 53 | |
| 54 | it("uses the next weekday within a week", () => { |
| 55 | const next = new Date(2026, 8, 21, 14, 30); |
| 56 | expect(formatNextScheduledTaskRun(next.toISOString(), now)).toBe( |
| 57 | `Next run next ${next.toLocaleDateString([], { weekday: "long" })} at ${timeLabel(next)}`, |
| 58 | ); |
| 59 | }); |
| 60 | |
| 61 | it("includes the date for more distant runs and the year only when necessary", () => { |
| 62 | for (const next of [new Date(2026, 9, 5, 9), new Date(2027, 0, 1, 9)]) { |
| 63 | const date = next.toLocaleDateString([], { |
| 64 | month: "short", |
| 65 | day: "numeric", |
| 66 | ...(next.getFullYear() === 2026 ? {} : { year: "numeric" as const }), |
| 67 | }); |
| 68 | expect(formatNextScheduledTaskRun(next.toISOString(), now)).toBe( |
| 69 | `Next run ${date} at ${timeLabel(next)}`, |
| 70 | ); |
| 71 | } |
| 72 | }); |
| 73 | |
| 74 | it("does not show an invalid date", () => { |
| 75 | expect(formatNextScheduledTaskRun("invalid", now)).toBe("Next run unavailable"); |
| 76 | }); |
| 77 | }); |