From e2415cda2bf601a22c2cd959a7f180cfd07f8175 Mon Sep 17 00:00:00 2001 From: Revopush Date: Thu, 15 Oct 2026 16:27:46 +0300 Subject: [PATCH 1/2] Replace moment with a small date helper The CLI used moment for table dates, the access key expiry date and debug log timestamps. date-utils reproduces moment 2.31.0's English output exactly, including its relative-time cutoffs, its daylight saving aware day count, and "Invalid date" for missing or invalid times. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 10 ---- package.json | 1 - script/command-executor.ts | 16 +----- script/commands/debug.ts | 4 +- script/utils/date-utils.ts | 113 +++++++++++++++++++++++++++++++++++++ test/date-utils.ts | 80 ++++++++++++++++++++++++++ 6 files changed, 197 insertions(+), 27 deletions(-) create mode 100644 script/utils/date-utils.ts create mode 100644 test/date-utils.ts diff --git a/package-lock.json b/package-lock.json index 2a43d42..096aaf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "email-validator": "^2.0.4", "gradle-to-js": "2.0.1", "jsonwebtoken": "^9.0.2", - "moment": "^2.29.4", "opener": "^1.5.2", "parse-duration": "^2.1.6", "plist": "^3.1.0", @@ -2569,15 +2568,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/moment": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.31.0.tgz", - "integrity": "sha512-0acOTfMiWOheYS4eoWb80yYMb/JLvVv9SHbs2PehaDzfUG0Bw855SKyk0IKTnPGa5+U2bmi3W68l1+sGLX/pvw==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", diff --git a/package.json b/package.json index 536b272..24922e1 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "email-validator": "^2.0.4", "gradle-to-js": "2.0.1", "jsonwebtoken": "^9.0.2", - "moment": "^2.29.4", "opener": "^1.5.2", "parse-duration": "^2.1.6", "plist": "^3.1.0", diff --git a/script/command-executor.ts b/script/command-executor.ts index 584185d..79a1c8e 100644 --- a/script/command-executor.ts +++ b/script/command-executor.ts @@ -7,7 +7,6 @@ const childProcess = require("child_process"); import debugCommand from "./commands/debug"; import * as fs from "fs"; import * as chalk from "chalk"; -import * as moment from "moment"; import * as os from "os"; import * as path from "path"; import * as semver from "semver"; @@ -40,6 +39,7 @@ import { } from "./react-native-utils"; import { isHermesEnabled } from "./utils/hermes-utils"; import { fileDoesNotExistOrIsDirectory, fileExists, isBinaryOrZip, extractArchive } from "./utils/file-utils"; +import { formatDate, formatLongDateTime } from "./utils/date-utils"; import { getAndroidVersionInfo } from "./utils/gradle-utils"; import { analyzeReleaseContents, @@ -153,7 +153,7 @@ async function accessKeyPatch(command: cli.IAccessKeyPatchCommand): Promise { deleteConnectionInfoCache(); } -function formatDate(unixOffset: number): string { - const date: moment.Moment = moment(unixOffset); - const now: moment.Moment = moment(); - if (Math.abs(now.diff(date, "days")) < 30) { - return date.fromNow(); // "2 hours ago" - } else if (now.year() === date.year()) { - return date.format("MMM D"); // "Nov 6" - } else { - return date.format("MMM D, YYYY"); // "Nov 6, 2014" - } -} - function printAppList(format: string, apps: App[]): void { if (format === "json") { printJson(apps); diff --git a/script/commands/debug.ts b/script/commands/debug.ts index 4c556c1..b78829a 100644 --- a/script/commands/debug.ts +++ b/script/commands/debug.ts @@ -3,8 +3,8 @@ import * as childProcess from "child_process"; import * as cli from "../../script/types/cli"; -import * as moment from "moment"; import * as path from "path"; +import { formatClockTime } from "../utils/date-utils"; const simctl = require("simctl"); const which = require("which"); @@ -111,7 +111,7 @@ function processLogData(logData: Buffer) { // log message prefix that is added to each entry. const message = line.substring(line.indexOf(logMessagePrefix) + logMessagePrefix.length); - const timeStamp = moment().format("hh:mm:ss"); + const timeStamp = formatClockTime(new Date()); return `[${timeStamp}] ${message}`; }) .forEach((line: string) => console.log(line)); diff --git a/script/utils/date-utils.ts b/script/utils/date-utils.ts new file mode 100644 index 0000000..0702bcb --- /dev/null +++ b/script/utils/date-utils.ts @@ -0,0 +1,113 @@ +// English date formatting with the exact output the CLI printed with moment.js + +const MONTHS = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; +const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + +const INVALID_DATE = "Invalid date"; + +const MS_PER_SECOND = 1000; +const MS_PER_MINUTE = 60 * MS_PER_SECOND; +const MS_PER_HOUR = 60 * MS_PER_MINUTE; +const MS_PER_DAY = 24 * MS_PER_HOUR; + +function pad2(value: number): string { + return String(value).padStart(2, "0"); +} + +function hours12(date: Date): number { + return date.getHours() % 12 || 12; +} + +function shortMonth(date: Date): string { + return MONTHS[date.getMonth()].slice(0, 3); +} + +function relativePhrase(ms: number): string { + const seconds: number = Math.round(ms / MS_PER_SECOND); + const minutes: number = Math.round(ms / MS_PER_MINUTE); + const hours: number = Math.round(ms / MS_PER_HOUR); + const days: number = Math.round(ms / MS_PER_DAY); + + if (seconds <= 44) { + return "a few seconds"; + } + if (minutes <= 1) { + return "a minute"; + } + if (minutes < 45) { + return `${minutes} minutes`; + } + if (hours <= 1) { + return "an hour"; + } + if (hours < 22) { + return `${hours} hours`; + } + if (days <= 1) { + return "a day"; + } + if (days < 26) { + return `${days} days`; + } + return "a month"; +} + +// moment's `fromNow()` wording, e.g. "a few seconds ago", "an hour ago", "in 20 days". Only valid within 30 days, +// which is all formatDate uses it for; beyond that moment counts calendar months. +function formatRelativeTime(date: Date, now: Date): string { + const msFromNow: number = date.getTime() - now.getTime(); + const phrase: string = relativePhrase(Math.abs(msFromNow)); + return msFromNow > 0 ? `in ${phrase}` : `${phrase} ago`; +} + +/** Relative time within 30 days, otherwise "Nov 6" in the current year and "Nov 6, 2014" before it */ +export function formatDate(unixOffset: number | undefined, now: Date = new Date()): string { + // Like moment: a missing time means now, while null and NaN are invalid + const date: Date = unixOffset === undefined ? now : new Date(unixOffset ?? NaN); + if (isNaN(date.getTime())) { + return INVALID_DATE; + } + + // Whole days between the dates, ignoring daylight saving time shifts (moment's `diff(date, "days")`) + const zoneDelta: number = (now.getTimezoneOffset() - date.getTimezoneOffset()) * MS_PER_MINUTE; + const days: number = Math.trunc((now.getTime() - date.getTime() - zoneDelta) / MS_PER_DAY); + + if (Math.abs(days) < 30) { + return formatRelativeTime(date, now); + } else if (now.getFullYear() === date.getFullYear()) { + return `${shortMonth(date)} ${date.getDate()}`; + } else { + return `${shortMonth(date)} ${date.getDate()}, ${date.getFullYear()}`; + } +} + +/** e.g. "Thursday, December 3, 2026 2:05 PM" (moment's "LLLL") */ +export function formatLongDateTime(date: Date): string { + if (isNaN(date.getTime())) { + return INVALID_DATE; + } + + const meridiem: string = date.getHours() < 12 ? "AM" : "PM"; + return ( + `${WEEKDAYS[date.getDay()]}, ${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} ` + + `${hours12(date)}:${pad2(date.getMinutes())} ${meridiem}` + ); +} + +/** e.g. "02:05:09", a 12-hour clock without AM/PM (moment's "hh:mm:ss") */ +export function formatClockTime(date: Date): string { + return `${pad2(hours12(date))}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`; +} diff --git a/test/date-utils.ts b/test/date-utils.ts new file mode 100644 index 0000000..2ab6bb7 --- /dev/null +++ b/test/date-utils.ts @@ -0,0 +1,80 @@ +import { strict as assert } from "assert"; +import { formatClockTime, formatDate, formatLongDateTime } from "../script/utils/date-utils"; + +// Expected strings are what moment.js printed for the same dates (test/setup.ts fixes the time zone) +describe("date-utils", () => { + const now = new Date(2026, 9, 15, 12, 0, 0); + const SECOND = 1000; + const MINUTE = 60 * SECOND; + const HOUR = 60 * MINUTE; + const DAY = 24 * HOUR; + + describe("formatDate", () => { + const relativeCases: Array<[offset: number, expected: string]> = [ + [-10 * SECOND, "a few seconds ago"], + [-44 * SECOND, "a few seconds ago"], + [-45 * SECOND, "a minute ago"], + [-89 * SECOND, "a minute ago"], + [-90 * SECOND, "2 minutes ago"], + [-44 * MINUTE, "44 minutes ago"], + [-45 * MINUTE, "an hour ago"], + [-2 * HOUR, "2 hours ago"], + [-21 * HOUR, "21 hours ago"], + [-22 * HOUR, "a day ago"], + [-36 * HOUR, "2 days ago"], + [-25 * DAY, "25 days ago"], + [-26 * DAY, "a month ago"], + [-29 * DAY, "a month ago"], + [20 * DAY, "in 20 days"], + ]; + + for (const [offset, expected] of relativeCases) { + it(`shows "${expected}" within 30 days`, () => { + assert.equal(formatDate(now.getTime() + offset, now), expected); + }); + } + + it("shows the month and day for older dates in the current year", () => { + assert.equal(formatDate(new Date(2026, 7, 31, 9, 30).getTime(), now), "Aug 31"); + }); + + it("adds the year for dates in earlier years", () => { + assert.equal(formatDate(new Date(2014, 10, 6, 9, 30).getTime(), now), "Nov 6, 2014"); + }); + + it("treats a missing time as now and an invalid one like moment", () => { + assert.equal(formatDate(undefined, now), "a few seconds ago"); + assert.equal(formatDate(null, now), "Invalid date"); + assert.equal(formatDate(NaN, now), "Invalid date"); + }); + + it("counts days across a daylight saving time change", () => { + // 30 days back from Nov 15 crosses the Nov 1 clock change, which adds an hour + const later = new Date(2026, 10, 15, 12, 0); + assert.equal(formatDate(new Date(2026, 9, 16, 13, 0).getTime(), later), "a month ago"); + assert.equal(formatDate(new Date(2026, 9, 16, 12, 0).getTime(), later), "Oct 16"); + }); + }); + + describe("formatLongDateTime", () => { + it("formats the weekday, date and 12-hour time", () => { + assert.equal(formatLongDateTime(new Date(2026, 11, 3, 14, 5)), "Thursday, December 3, 2026 2:05 PM"); + }); + + it("shows an invalid date like moment", () => { + assert.equal(formatLongDateTime(new Date(NaN)), "Invalid date"); + }); + + it("shows midnight and noon as 12", () => { + assert.equal(formatLongDateTime(new Date(2026, 0, 1, 0, 0)), "Thursday, January 1, 2026 12:00 AM"); + assert.equal(formatLongDateTime(new Date(2026, 0, 1, 12, 0)), "Thursday, January 1, 2026 12:00 PM"); + }); + }); + + describe("formatClockTime", () => { + it("pads a 12-hour time without AM/PM", () => { + assert.equal(formatClockTime(new Date(2026, 9, 15, 14, 5, 9)), "02:05:09"); + assert.equal(formatClockTime(new Date(2026, 9, 15, 0, 0, 0)), "12:00:00"); + }); + }); +}); From c0be4cf4b5a0da8f7e0bdd96e4e9daf37728071d Mon Sep 17 00:00:00 2001 From: Revopush Date: Thu, 15 Oct 2026 16:32:48 +0300 Subject: [PATCH 2/2] Format dates with Intl instead of a moment clone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exact moment wording is not part of the CLI's contract, so the built-in Intl formatters replace the date-utils helper. Relative times use exact numbers ("50 minutes ago", "yesterday"), the expiry date reads "… at 2:05 PM" and debug timestamps use a 24-hour clock. A missing date shows as an empty cell instead of crashing. Co-Authored-By: Claude Opus 5.5 --- script/command-executor.ts | 28 ++++++++- script/commands/debug.ts | 3 +- script/utils/date-utils.ts | 113 ------------------------------------- test/cli.ts | 4 +- test/date-utils.ts | 80 -------------------------- 5 files changed, 29 insertions(+), 199 deletions(-) delete mode 100644 script/utils/date-utils.ts delete mode 100644 test/date-utils.ts diff --git a/script/command-executor.ts b/script/command-executor.ts index 79a1c8e..9276ec8 100644 --- a/script/command-executor.ts +++ b/script/command-executor.ts @@ -39,7 +39,6 @@ import { } from "./react-native-utils"; import { isHermesEnabled } from "./utils/hermes-utils"; import { fileDoesNotExistOrIsDirectory, fileExists, isBinaryOrZip, extractArchive } from "./utils/file-utils"; -import { formatDate, formatLongDateTime } from "./utils/date-utils"; import { getAndroidVersionInfo } from "./utils/gradle-utils"; import { analyzeReleaseContents, @@ -153,7 +152,7 @@ async function accessKeyPatch(command: cli.IAccessKeyPatchCommand): Promise = [ + ["day", 24 * 60 * 60 * 1000], + ["hour", 60 * 60 * 1000], + ["minute", 60 * 1000], + ["second", 1000], +]; + +// "2 hours ago" within 30 days, otherwise "Nov 6" in the current year and "Nov 6, 2014" before it +function formatDate(unixOffset: number): string { + if (!Number.isFinite(unixOffset)) { + return ""; + } + + const msFromNow: number = unixOffset - Date.now(); + if (Math.abs(msFromNow) < 30 * RELATIVE_TIME_UNITS[0][1]) { + const [unit, ms] = RELATIVE_TIME_UNITS.find(([, ms]) => Math.abs(msFromNow) >= ms) ?? ["second", 1000]; + return relativeTimeFormat.format(Math.round(msFromNow / ms), unit); + } + + const date = new Date(unixOffset); + const year = date.getFullYear() === new Date().getFullYear() ? undefined : "numeric"; + return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year }); +} + function printJson(object: any): void { log(JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2)); } diff --git a/script/commands/debug.ts b/script/commands/debug.ts index b78829a..a8880a5 100644 --- a/script/commands/debug.ts +++ b/script/commands/debug.ts @@ -4,7 +4,6 @@ import * as childProcess from "child_process"; import * as cli from "../../script/types/cli"; import * as path from "path"; -import { formatClockTime } from "../utils/date-utils"; const simctl = require("simctl"); const which = require("which"); @@ -111,7 +110,7 @@ function processLogData(logData: Buffer) { // log message prefix that is added to each entry. const message = line.substring(line.indexOf(logMessagePrefix) + logMessagePrefix.length); - const timeStamp = formatClockTime(new Date()); + const timeStamp = new Date().toLocaleTimeString("en-US", { hourCycle: "h23" }); return `[${timeStamp}] ${message}`; }) .forEach((line: string) => console.log(line)); diff --git a/script/utils/date-utils.ts b/script/utils/date-utils.ts deleted file mode 100644 index 0702bcb..0000000 --- a/script/utils/date-utils.ts +++ /dev/null @@ -1,113 +0,0 @@ -// English date formatting with the exact output the CLI printed with moment.js - -const MONTHS = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", -]; -const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; - -const INVALID_DATE = "Invalid date"; - -const MS_PER_SECOND = 1000; -const MS_PER_MINUTE = 60 * MS_PER_SECOND; -const MS_PER_HOUR = 60 * MS_PER_MINUTE; -const MS_PER_DAY = 24 * MS_PER_HOUR; - -function pad2(value: number): string { - return String(value).padStart(2, "0"); -} - -function hours12(date: Date): number { - return date.getHours() % 12 || 12; -} - -function shortMonth(date: Date): string { - return MONTHS[date.getMonth()].slice(0, 3); -} - -function relativePhrase(ms: number): string { - const seconds: number = Math.round(ms / MS_PER_SECOND); - const minutes: number = Math.round(ms / MS_PER_MINUTE); - const hours: number = Math.round(ms / MS_PER_HOUR); - const days: number = Math.round(ms / MS_PER_DAY); - - if (seconds <= 44) { - return "a few seconds"; - } - if (minutes <= 1) { - return "a minute"; - } - if (minutes < 45) { - return `${minutes} minutes`; - } - if (hours <= 1) { - return "an hour"; - } - if (hours < 22) { - return `${hours} hours`; - } - if (days <= 1) { - return "a day"; - } - if (days < 26) { - return `${days} days`; - } - return "a month"; -} - -// moment's `fromNow()` wording, e.g. "a few seconds ago", "an hour ago", "in 20 days". Only valid within 30 days, -// which is all formatDate uses it for; beyond that moment counts calendar months. -function formatRelativeTime(date: Date, now: Date): string { - const msFromNow: number = date.getTime() - now.getTime(); - const phrase: string = relativePhrase(Math.abs(msFromNow)); - return msFromNow > 0 ? `in ${phrase}` : `${phrase} ago`; -} - -/** Relative time within 30 days, otherwise "Nov 6" in the current year and "Nov 6, 2014" before it */ -export function formatDate(unixOffset: number | undefined, now: Date = new Date()): string { - // Like moment: a missing time means now, while null and NaN are invalid - const date: Date = unixOffset === undefined ? now : new Date(unixOffset ?? NaN); - if (isNaN(date.getTime())) { - return INVALID_DATE; - } - - // Whole days between the dates, ignoring daylight saving time shifts (moment's `diff(date, "days")`) - const zoneDelta: number = (now.getTimezoneOffset() - date.getTimezoneOffset()) * MS_PER_MINUTE; - const days: number = Math.trunc((now.getTime() - date.getTime() - zoneDelta) / MS_PER_DAY); - - if (Math.abs(days) < 30) { - return formatRelativeTime(date, now); - } else if (now.getFullYear() === date.getFullYear()) { - return `${shortMonth(date)} ${date.getDate()}`; - } else { - return `${shortMonth(date)} ${date.getDate()}, ${date.getFullYear()}`; - } -} - -/** e.g. "Thursday, December 3, 2026 2:05 PM" (moment's "LLLL") */ -export function formatLongDateTime(date: Date): string { - if (isNaN(date.getTime())) { - return INVALID_DATE; - } - - const meridiem: string = date.getHours() < 12 ? "AM" : "PM"; - return ( - `${WEEKDAYS[date.getDay()]}, ${MONTHS[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} ` + - `${hours12(date)}:${pad2(date.getMinutes())} ${meridiem}` - ); -} - -/** e.g. "02:05:09", a 12-hour clock without AM/PM (moment's "hh:mm:ss") */ -export function formatClockTime(date: Date): string { - return `${pad2(hours12(date))}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`; -} diff --git a/test/cli.ts b/test/cli.ts index aa8b636..026eed2 100644 --- a/test/cli.ts +++ b/test/cli.ts @@ -156,7 +156,7 @@ describe("CLI", () => { await execute(command); assertLoggedOnce( - 'Successfully changed the expiration date of the "Test name" access key to Wednesday, August 17, 2016 12:07 PM.' + 'Successfully changed the expiration date of the "Test name" access key to Wednesday, August 17, 2016 at 12:07 PM.' ); }); @@ -171,7 +171,7 @@ describe("CLI", () => { await execute(command); assertLoggedOnce( - 'Successfully renamed the access key "Test name" to "Updated name" and changed its expiration date to Wednesday, August 17, 2016 12:07 PM.' + 'Successfully renamed the access key "Test name" to "Updated name" and changed its expiration date to Wednesday, August 17, 2016 at 12:07 PM.' ); }); diff --git a/test/date-utils.ts b/test/date-utils.ts deleted file mode 100644 index 2ab6bb7..0000000 --- a/test/date-utils.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { strict as assert } from "assert"; -import { formatClockTime, formatDate, formatLongDateTime } from "../script/utils/date-utils"; - -// Expected strings are what moment.js printed for the same dates (test/setup.ts fixes the time zone) -describe("date-utils", () => { - const now = new Date(2026, 9, 15, 12, 0, 0); - const SECOND = 1000; - const MINUTE = 60 * SECOND; - const HOUR = 60 * MINUTE; - const DAY = 24 * HOUR; - - describe("formatDate", () => { - const relativeCases: Array<[offset: number, expected: string]> = [ - [-10 * SECOND, "a few seconds ago"], - [-44 * SECOND, "a few seconds ago"], - [-45 * SECOND, "a minute ago"], - [-89 * SECOND, "a minute ago"], - [-90 * SECOND, "2 minutes ago"], - [-44 * MINUTE, "44 minutes ago"], - [-45 * MINUTE, "an hour ago"], - [-2 * HOUR, "2 hours ago"], - [-21 * HOUR, "21 hours ago"], - [-22 * HOUR, "a day ago"], - [-36 * HOUR, "2 days ago"], - [-25 * DAY, "25 days ago"], - [-26 * DAY, "a month ago"], - [-29 * DAY, "a month ago"], - [20 * DAY, "in 20 days"], - ]; - - for (const [offset, expected] of relativeCases) { - it(`shows "${expected}" within 30 days`, () => { - assert.equal(formatDate(now.getTime() + offset, now), expected); - }); - } - - it("shows the month and day for older dates in the current year", () => { - assert.equal(formatDate(new Date(2026, 7, 31, 9, 30).getTime(), now), "Aug 31"); - }); - - it("adds the year for dates in earlier years", () => { - assert.equal(formatDate(new Date(2014, 10, 6, 9, 30).getTime(), now), "Nov 6, 2014"); - }); - - it("treats a missing time as now and an invalid one like moment", () => { - assert.equal(formatDate(undefined, now), "a few seconds ago"); - assert.equal(formatDate(null, now), "Invalid date"); - assert.equal(formatDate(NaN, now), "Invalid date"); - }); - - it("counts days across a daylight saving time change", () => { - // 30 days back from Nov 15 crosses the Nov 1 clock change, which adds an hour - const later = new Date(2026, 10, 15, 12, 0); - assert.equal(formatDate(new Date(2026, 9, 16, 13, 0).getTime(), later), "a month ago"); - assert.equal(formatDate(new Date(2026, 9, 16, 12, 0).getTime(), later), "Oct 16"); - }); - }); - - describe("formatLongDateTime", () => { - it("formats the weekday, date and 12-hour time", () => { - assert.equal(formatLongDateTime(new Date(2026, 11, 3, 14, 5)), "Thursday, December 3, 2026 2:05 PM"); - }); - - it("shows an invalid date like moment", () => { - assert.equal(formatLongDateTime(new Date(NaN)), "Invalid date"); - }); - - it("shows midnight and noon as 12", () => { - assert.equal(formatLongDateTime(new Date(2026, 0, 1, 0, 0)), "Thursday, January 1, 2026 12:00 AM"); - assert.equal(formatLongDateTime(new Date(2026, 0, 1, 12, 0)), "Thursday, January 1, 2026 12:00 PM"); - }); - }); - - describe("formatClockTime", () => { - it("pads a 12-hour time without AM/PM", () => { - assert.equal(formatClockTime(new Date(2026, 9, 15, 14, 5, 9)), "02:05:09"); - assert.equal(formatClockTime(new Date(2026, 9, 15, 0, 0, 0)), "12:00:00"); - }); - }); -});