mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

timezone.test.ts

Language TypeScript7.0 KB159 lines

apps/dashboard/test/timezone.test.ts

  1. /**
  2. * No reader of this board is in UTC.
  3. *
  4. * Times rendered in UTC read as the wrong hour, and after 7 PM as the wrong
  5. * day; a raw ISO string on the page is worse. The correct helpers live in
  6. * `packages/ninety-client/src/dates.ts`. Using them is not the hard part;
  7. * noticing a site that does not is. So this asserts the property over whole
  8. * rendered documents rather than checking sites one at a time. A new
  9. * surface that slices an ISO string fails here without anyone remembering to
  10. * come back and add a case.
  11. */
  12. import { describe, expect, it } from 'vitest'
  13. import { localClock, localDay, localStamp, MERIDIAN_TZ } from '@meridian/ninety-client'
  14. import { render } from '../src/render.js'
  15. import { headline } from '../src/collect.js'
  16. import { FIXTURE_SNAPSHOT } from '../src/fixtures.js'
  17. import { inboxPage, messagePage, sentPage, type InboxData, type SendRecord } from '../src/inbox-view.js'
  18. import { INBOX_MAILBOXES } from '../../../config/inbox.js'
  19. import { viewerFor } from '../src/viewer.js'
  20. import type { InboxMessage, MessageBody } from '@meridian/gmail-client'
  21. /** An instant deliberately chosen to land on a different calendar day in UTC. */
  22. const EVENING = '2026-09-05T02:30:00.000Z' // 9:30 PM CT on Sep 4
  23. describe('localStamp / localClock', () => {
  24. it('renders an instant in Central, not UTC', () => {
  25. // 02:30 UTC on the 5th is 9:30 PM on the 4th in Chicago. A slice of the ISO
  26. // string gets both the day and the hour wrong.
  27. expect(localStamp(EVENING)).toContain('Sep 4')
  28. expect(localStamp(EVENING)).toContain('9:30')
  29. expect(localClock(EVENING)).toBe('9:30 PM')
  30. expect(localDay(EVENING)).toBe('2026-09-04')
  31. })
  32. it('accepts epoch milliseconds, which is what the replied marks store', () => {
  33. expect(localStamp(Date.parse(EVENING))).toBe(localStamp(EVENING))
  34. })
  35. it('renders an absence rather than "Invalid Date"', () => {
  36. // Callers render whatever the board stored, and stored values go missing.
  37. for (const bad of ['', 'not a date', 'undefined']) {
  38. expect(localStamp(bad)).toBe('–')
  39. expect(localClock(bad)).toBe('–')
  40. }
  41. })
  42. it('follows the DST change without a code edit', () => {
  43. // The offset changes twice a year. Intl handles it; the pages say "CT"
  44. // rather than CDT/CST so nothing needs editing.
  45. const summer = localClock('2026-07-01T17:00:00.000Z') // CDT, UTC-5
  46. const winter = localClock('2026-12-01T17:00:00.000Z') // CST, UTC-6
  47. expect(summer).toBe('12:00 PM')
  48. expect(winter).toBe('11:00 AM')
  49. expect(MERIDIAN_TZ).toBe('America/Chicago')
  50. })
  51. })
  52. /** Every ISO instant that should never survive to a rendered page. */
  53. const RAW_ISO = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/
  54. /**
  55. * Strip everything that is not text a reader sees.
  56. *
  57. * Script and style BODIES go too. A code comment explaining the UTC bug is not
  58. * the UTC bug. That would be a hole if an inlined script formatted a date
  59. * wrongly, so the next test closes it directly rather than leaving it to this
  60. * one.
  61. */
  62. const scrub = (html: string): string =>
  63. html
  64. .replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '')
  65. .replace(/<style\b[^>]*>[\s\S]*?<\/style>/g, '')
  66. // datetime="" and machine-readable attributes are allowed to be ISO.
  67. .replace(/datetime="[^"]*"/g, '')
  68. // URLs may legitimately carry timestamps.
  69. .replace(/href="[^"]*"/g, '')
  70. .replace(/src="[^"]*"/g, '')
  71. /** The script bodies on their own, which scrub() removes. */
  72. const scriptsOf = (html: string): string =>
  73. (html.match(/<script\b[^>]*>[\s\S]*?<\/script>/g) ?? []).join('\n')
  74. describe('no rendered page shows a reader a UTC time', () => {
  75. const viewer = viewerFor('hello@meridiandisplay.example')
  76. it('the board carries no raw ISO timestamp and no "UTC" label', () => {
  77. const html = render(FIXTURE_SNAPSHOT, headline(FIXTURE_SNAPSHOT), [], '2026-08-12', null, viewer)
  78. const body = scrub(html)
  79. expect(body).not.toMatch(RAW_ISO)
  80. // The one that shipped: a meeting-commitment row printing the raw instant.
  81. expect(body).not.toContain('.670Z')
  82. expect(body).not.toMatch(/\bUTC\b/)
  83. })
  84. it('every date an inlined script formats passes a timeZone', () => {
  85. // scrub() drops script bodies, so this is where that hole is closed. The
  86. // notes bootstrap runs in the browser and cannot call localDay(); the
  87. // timeZone option is the same fix, and without it a note typed at 8 PM
  88. // showed tomorrow's date to the person who had just typed it.
  89. const js = scriptsOf(
  90. render(FIXTURE_SNAPSHOT, headline(FIXTURE_SNAPSHOT), [], '2026-08-12', null, viewer),
  91. )
  92. for (const call of js.match(/toLocale\w*\([^)]*\)/g) ?? []) {
  93. expect(call, `${call} formats a date with no timeZone`).toContain('timeZone')
  94. }
  95. // And nothing in a script may slice an ISO string into a displayed date.
  96. expect(js).not.toMatch(/\.at[^\n]*\.slice\(0,\s*10\)/)
  97. })
  98. it('the inbox list carries neither', () => {
  99. const data: InboxData = {
  100. syncedAt: EVENING,
  101. mailboxes: [{
  102. mailbox: 'hello@meridiandisplay.example', ok: true, error: null, canSend: true,
  103. messages: [{
  104. id: 'm1', threadId: 't1', mailbox: 'hello@meridiandisplay.example',
  105. sender: 'client@example.com', senderName: 'A Client',
  106. subject: 'Proof', snippet: 'hi', date: EVENING,
  107. labels: ['INBOX'], unread: true,
  108. } satisfies InboxMessage],
  109. }],
  110. }
  111. const html = scrub(inboxPage(data, [], INBOX_MAILBOXES, ['hello@meridiandisplay.example'], 'Sam Whitaker'))
  112. expect(html).not.toMatch(RAW_ISO)
  113. expect(html).not.toMatch(/\bUTC\b/)
  114. })
  115. it('an open message dates itself in Central', () => {
  116. const body: MessageBody = {
  117. id: 'm1', mailbox: 'hello@meridiandisplay.example', sender: 'client@example.com',
  118. senderName: 'A Client', subject: 'Proof', date: EVENING,
  119. to: 'hello@meridiandisplay.example', text: 'hello', kind: 'plain', truncated: false,
  120. html: null, htmlTruncated: false, unread: true, attachments: [],
  121. threadId: 't1', messageId: null, references: null, replyTo: '', cc: '',
  122. }
  123. const html = scrub(messagePage(body, 'Sam Whitaker'))
  124. expect(html).not.toMatch(RAW_ISO)
  125. expect(html).not.toMatch(/\bUTC\b/)
  126. expect(html).toContain('Sep 4')
  127. expect(html).toContain('CT')
  128. })
  129. it('the sent record dates itself in Central: Sam’s "email delivery times"', () => {
  130. const rec: SendRecord = {
  131. nonce: 'n1', at: EVENING, by: 'Sam Whitaker', byEmail: 'hello@meridiandisplay.example',
  132. from: 'hello@meridiandisplay.example', to: 'client@example.com', subject: 'Re: Proof',
  133. originalMailbox: 'hello@meridiandisplay.example', originalId: 'm1', originalMessageId: null,
  134. threadIdSent: null, signatureApplied: false, outcome: 'sent',
  135. gmailId: 'g1', gmailThreadId: 'gt1', error: null, candidates: [],
  136. }
  137. const html = scrub(sentPage([rec], false, rec, false))
  138. expect(html).not.toMatch(RAW_ISO)
  139. expect(html).not.toMatch(/\bUTC\b/)
  140. // The day column was a UTC calendar day, so an evening send showed tomorrow.
  141. expect(html).toContain('2026-09-04')
  142. expect(html).not.toContain('2026-09-05')
  143. })
  144. })