// The public event screens' time rendering (EVENTS_PLAN.md Phase 14a). // // One property matters here and it is EVENTS.md §I's: **the time beside an // entry is the EVENT's zone, the day it is filed under is the READER's.** A // helper that quietly rendered both in the reader's zone would pass any test // that only ever looked at one of them, and would put an American shard's 8pm // event at "02:00" for a player in Berlin — true, useless, and looking like the // shard's own announcement was wrong. import { test } from 'node:test' import assert from 'node:assert/strict' import { eventTime, eventDateTime, readerDayLabel, statusWord } from '../src/lib/eventCalendar.js' // 2026-09-12T00:00Z is 2026-09-11 20:00 in New York — deliberately an instant // whose DATE differs between the two zones, which is what makes the split // observable at all. const INSTANT = '2026-09-12T00:00:00.000Z' test('the time is rendered in the EVENT’s zone, not the reader’s', () => { assert.equal(eventTime(INSTANT, 'America/New_York'), '20:00 New York') assert.equal(eventTime(INSTANT, 'UTC'), '00:00 UTC') assert.equal(eventTime(INSTANT, 'Europe/Berlin'), '02:00 Berlin') }) test('the zone is named in a form a reader recognises', () => { // `America/New_York` is a database identifier, not something to show a player. assert.match(eventTime(INSTANT, 'America/Los_Angeles'), /Los Angeles$/) }) test('an unknown zone falls back to UTC rather than throwing', () => { // `Intl` rejects an unknown identifier, and an event whose timezone column // holds a typo must still render. assert.equal(eventTime(INSTANT, 'Not/AZone'), '00:00 UTC') assert.equal(eventDateTime(INSTANT, 'Not/AZone'), '2026-09-12 00:00 UTC') }) test('a bad instant renders as nothing rather than as "Invalid Date"', () => { assert.equal(readerDayLabel('not a date'), '') assert.equal(eventDateTime('not a date', 'UTC'), '') }) test('the day label is the reader’s own day, whatever the event’s zone', () => { // Two entries at the same instant in different event zones are filed under one // heading, which is what makes a chronological list group correctly. assert.equal(readerDayLabel(INSTANT), readerDayLabel(INSTANT)) const label = readerDayLabel(INSTANT) assert.ok(label.length > 0) // The instant's UTC date is the 12th and New York's is the 11th; the label // must not carry a zone at all, because it is neither of theirs. assert.equal(/UTC|New York/.test(label), false) }) test('eventDateTime carries the day and the zone together', () => { const text = eventDateTime(INSTANT, 'America/New_York') assert.match(text, /New York$/) assert.match(text, /20:00/) }) // ── The status word ──────────────────────────────────────────────────────── // // Found by the browser walk: the calendar was saying "DID NOT HAPPEN" about a // run four days out that an operator had cancelled. The server publishes // `failed` and `missed` as `cancelled` too — to a visitor the three are one // event — but they do not share one English sentence, so the tense follows the // clock rather than the status. const NOW = Date.parse('2026-09-08T12:00:00Z') test('a cancelled occurrence in the future reads "Cancelled"', () => { assert.equal(statusWord('cancelled', '2026-09-12T18:00:00Z', NOW), 'Cancelled') }) test('a cancelled occurrence in the past reads "Did not happen"', () => { // Which is also the honest word for the failed and missed runs folded into // `cancelled` on the way out. assert.equal(statusWord('cancelled', '2026-09-04T18:00:00Z', NOW), 'Did not happen') }) test('the other three words do not depend on the clock at all', () => { for (const at of ['2026-09-04T18:00:00Z', '2026-09-12T18:00:00Z']) { assert.equal(statusWord('live', at, NOW), 'Happening now') assert.equal(statusWord('scheduled', at, NOW), 'Scheduled') assert.equal(statusWord('completed', at, NOW), 'Finished') } }) test('an unreadable instant falls to the past-tense word rather than throwing', () => { assert.equal(statusWord('cancelled', 'not a date', NOW), 'Did not happen') })