checkin.test.ts
Language TypeScript14.8 KB383 lines
/*** The end-of-day note as a reader sees it: who gets the card, which day it is* about, and the four things that must never look like each other.** The access tests are the point of this file. The tab the card sits on is* visible to everyone who reads the Leadership Team's to-dos (which is what* makes the onboarding plan visible to them), and the readers of the note* itself are a separate, hand-written list. Two grants that overlap in most* cases and differ in one are exactly the shape that gets widened by analogy* later (rule 13), so the difference is pinned by name.** What is not pinned: the wording of the three questions. It may be rewritten* at any time, and tests assert properties, not wording.*/import { describe, expect, it } from 'vitest'import {CHECKIN_END,CHECKIN_START,checkinBlock,checkinDay,checkinIsOpen,checkinPlaceholder,checkinWindow,expectedDays,isWeekday,localHour,parseOutcome,previousDay,stampCheckin,type CheckinNote,type CheckinRevision,type CheckinStore,} from '../src/checkin-view.js'import {CHECKIN_ASKED_FROM,CHECKIN_AUTHORS,CHECKIN_READERS,canReadCheckinsOf,canWriteCheckin,hasCheckin,readableCheckinAuthors,} from '../../../config/checkin.js'import { COMPANY_VIEWER, viewerFor } from '../src/viewer.js'import { onboardingSurface } from '../src/surfaces.js'import { render } from '../src/render.js'import { FIXTURE_SNAPSHOT } from '../src/fixtures.js'import { headline } from '../src/collect.js'const ROSA = 'Rosa Galván'const rosa = viewerFor('accounts@meridiandisplay.example')const sam = viewerFor('hello@meridiandisplay.example')const theo = viewerFor('frontdesk@meridiandisplay.example')const nora = viewerFor('nora.l@meridiandisplay.example')const ines = viewerFor('projects@meridiandisplay.example')const marco = viewerFor('studio@meridiandisplay.example')const EVENING = new Date('2026-09-15T23:40:00.000Z') // 6:40 PM Tue, Chicagoconst AFTERNOON = new Date('2026-09-15T18:00:00.000Z') // 1:00 PM Tue, Chicagoconst AFTER_MIDNIGHT = new Date('2026-09-16T05:30:00.000Z') // 12:30 AM Wedconst note = (over: Partial<CheckinNote> = {}): CheckinNote => ({person: ROSA,day: '2026-09-15',did: 'Answered the new leads and sent two proofs.',unsure: 'Whether a rush order needs a second proof.',need: 'A walkthrough of the proof tool.',submittedAt: EVENING.toISOString(),updatedAt: EVENING.toISOString(),revisions: 1,authorEmail: 'accounts@meridiandisplay.example',...over,})const ok = (notes: CheckinNote[] = [], earlier: CheckinRevision[] = []): CheckinStore => ({state: 'ok',notes,earlier,truncated: false,})describe('who gets the card at all', () => {it('Rosa writes one', () => {expect(canWriteCheckin(rosa)).toBe(true)expect(readableCheckinAuthors(rosa)).toEqual([ROSA])})it('Sam and Theo read hers, and write none of their own', () => {for (const v of [sam, theo]) {expect(canReadCheckinsOf(v, ROSA)).toBe(true)expect(canWriteCheckin(v)).toBe(false)}})it('Ines reads hers too, by Sam\u2019s grant of 2026-09-16', () => {// Ines is on the reader list by name, not by way of `seesLeadership`.expect(canReadCheckinsOf(ines, ROSA)).toBe(true)expect(canWriteCheckin(ines)).toBe(false)})it('Nora does NOT, though she sees the same tab and holds the same role flag', () => {// The whole point. Nora and Ines read Leadership to-dos alike, which is what makes Rosa's plan visible to// both; only Ines was granted the note. A reader list derived from// `seesLeadership` would have handed it to Nora without anybody deciding.expect(nora.seesLeadership).toBe(true)expect(ines.seesLeadership).toBe(true)expect(hasCheckin(nora)).toBe(false)expect(canReadCheckinsOf(nora, ROSA)).toBe(false)})it('nobody else on the roster, and no unidentified viewer', () => {for (const v of [marco, COMPANY_VIEWER]) expect(hasCheckin(v)).toBe(false)})it('renders literally nothing for a viewer with no grant', () => {// Not a hidden div. The per-person briefings were `hidden` in one shared// document once, and anybody through the Access door could read all seven// from view-source.for (const v of [nora, marco, COMPANY_VIEWER]) {expect(checkinBlock(v, { [ROSA]: ok([note()]) }, EVENING)).toBe('')}})it('does not put the placeholder in a page it will not fill', () => {const forNora = onboardingSurface(nora)expect(forNora).toContain('Onboarding') // she still sees the planexpect(forNora).not.toContain(CHECKIN_START)expect(onboardingSurface(ines)).toContain(CHECKIN_START)expect(onboardingSurface(sam)).toContain(CHECKIN_START)expect(onboardingSurface(rosa)).toContain(CHECKIN_START)})it('the whole page carries the placeholder for a reader and not for a non-reader', () => {const page = (v: typeof sam): string =>render(FIXTURE_SNAPSHOT, headline(FIXTURE_SNAPSHOT), [], '2026-09-15', null, v)expect(page(sam)).toContain(CHECKIN_START)expect(page(theo)).toContain(CHECKIN_START)expect(page(rosa)).toContain(CHECKIN_START)expect(page(ines)).toContain(CHECKIN_START)expect(page(nora)).not.toContain(CHECKIN_START)expect(page(marco)).not.toContain(CHECKIN_START)})it('config keeps the author list and the reader list apart', () => {// Two lists, both written by hand. Neither derives from the other, and the// author is not on the reader list; she reads her own words through her// own card, which is a different code path.expect(Object.keys(CHECKIN_AUTHORS)).not.toEqual(expect.arrayContaining(CHECKIN_READERS))})})describe('which day a note is about', () => {it('an evening note is today', () => {expect(checkinDay(EVENING)).toBe('2026-09-15')})it('a 12:30 AM note is still yesterday', () => {// She finished at half past midnight. Filing it forward would show a missed// day behind her and a note on a day she has not worked.expect(checkinDay(AFTER_MIDNIGHT)).toBe('2026-09-15')})it('a 10 AM note is today again', () => {expect(checkinDay(new Date('2026-09-16T15:00:00.000Z'))).toBe('2026-09-16')})it('reads the hour in Meridian time, not UTC', () => {// 23:40 UTC is the next day in UTC and 6:40 PM in Chicago. Slicing an ISO// string here would file every evening note against tomorrow.expect(localHour(EVENING)).toBe(18)expect(EVENING.toISOString().slice(0, 10)).toBe('2026-09-15')expect(localHour(new Date('2026-09-16T02:00:00.000Z'))).toBe(21)})it('is open in the evening and through the small hours, folded during the day', () => {expect(checkinIsOpen(EVENING)).toBe(true)expect(checkinIsOpen(AFTER_MIDNIGHT)).toBe(true)expect(checkinIsOpen(AFTERNOON)).toBe(false)})it('crosses a month and a year boundary without arithmetic of its own', () => {expect(previousDay('2026-09-01')).toBe('2026-08-31')expect(previousDay('2027-01-01')).toBe('2026-12-31')expect(previousDay('2028-03-01')).toBe('2028-02-29')})})describe('the days a log accounts for', () => {it('lists weekdays back to the day the habit began, newest first', () => {const days = expectedDays('2026-09-18', '2026-09-15')expect(days).toEqual(['2026-09-18', '2026-09-17', '2026-09-16', '2026-09-15'])})it('leaves weekends out, so they are not reported as missed', () => {expect(expectedDays('2026-09-21', '2026-09-18')).toEqual(['2026-09-21', '2026-09-18'])expect(isWeekday('2026-09-19')).toBe(false)expect(isWeekday('2026-09-21')).toBe(true)})it('never reaches back past the day a note was first asked for', () => {// She started on the 14th; the habit started on the 15th. A log calling the// 14th a missed day would be reporting a failure against a rule that did// not exist yet.const days = expectedDays('2026-09-16', CHECKIN_ASKED_FROM)expect(days).not.toContain('2026-09-14')expect(days[days.length - 1]).toBe(CHECKIN_ASKED_FROM)})it('accounts for nothing when nothing has been asked for', () => {expect(expectedDays('2026-09-16', null)).toEqual([])})it('queries the same window it renders', () => {// If the query reached back less far than the card lists, a day with a note// would render as a day she missed, the one sentence this must never say.const { from, limit } = checkinWindow(ROSA, '2026-09-30')const days = expectedDays('2026-09-30', CHECKIN_ASKED_FROM)expect(from).toBe(days[days.length - 1])expect(limit).toBeGreaterThanOrEqual(days.length)})})describe('the four things that must not look alike', () => {const read = (store: CheckinStore): string => checkinBlock(sam, { [ROSA]: store }, EVENING)it('a store nobody has set up says so, and does not say "no notes"', () => {const html = read({ state: 'unconfigured' })expect(html).toMatch(/not set up/i)expect(html).not.toMatch(/no note</i)})it('a store that refused says so, in its own words', () => {const html = read({ state: 'failed', error: 'D1_ERROR: no such table: checkin' })expect(html).toContain('no such table')expect(html).toMatch(/missing, not empty/i)})it('a day with no note says "no note" and is counted as missed', () => {const html = read(ok([]))expect(html).toMatch(/no note/i)expect(html).toMatch(/missed/i)})it('a day with a note shows the note', () => {const html = read(ok([note()]))expect(html).toContain('Answered the new leads and sent two proofs.')expect(html).toContain('Whether a rush order needs a second proof.')})it('a question left blank renders as blank, not as an empty line', () => {const html = read(ok([note({ need: '' })]))expect(html).toMatch(/left blank/i)})it('the count and the list under it account for each other', () => {// Rule 9, which this board learned when My Day's header said 13 and its// list said nothing was on the board.const html = read(ok([note()]))const written = /(\d+) written/.exec(html)?.[1]expect(written).toBe('1')})it('a missing person entry is a failure, not an empty log', () => {// A reader whose store read never returned must not be shown a clean slate.const html = checkinBlock(sam, {}, EVENING)expect(html).toMatch(/could not be read/i)})})describe('the author’s own card', () => {const mine = (store: CheckinStore, now = EVENING, outcome = null): string =>checkinBlock(rosa, { [ROSA]: store }, now, outcome)it('offers the form, and names exactly who will read it', () => {const html = mine(ok([]))expect(html).toContain('action="/checkin"')for (const r of CHECKIN_READERS) expect(html).toContain(r)})it('carries the day it is writing, so a stale tab can be caught server-side', () => {expect(mine(ok([]))).toContain('name="day" value="2026-09-15"')})it('is folded shut in the afternoon and open in the evening', () => {expect(mine(ok([]), AFTERNOON)).toMatch(/opens at 4:00 PM/)expect(mine(ok([]), EVENING)).toMatch(/not written yet/)})it('accepts an early submission anyway, since a form that refuses typed text loses it', () => {expect(mine(ok([]), AFTERNOON)).toContain('action="/checkin"')})it('pre-fills tonight’s note for an edit, and says edits are kept', () => {const html = mine(ok([note()]))expect(html).toContain('Answered the new leads and sent two proofs.')expect(html).toMatch(/Edits are kept/i)})it('says out loud when a note is being filed against yesterday', () => {const html = mine(ok([]), AFTER_MIDNIGHT)expect(html).toMatch(/past midnight/i)expect(html).toContain('name="day" value="2026-09-15"')})it('still shows the boxes when the store could not be read', () => {// A read failure is not a write failure, and hiding the form guarantees the// loss that showing it only risks.const html = mine({ state: 'failed', error: 'D1_ERROR: timeout' })expect(html).toContain('action="/checkin"')expect(html).toMatch(/may not\s+save/i)})it('shows her what she wrote on earlier days', () => {const html = mine(ok([note({ day: '2026-09-14', did: 'day one' })]))expect(html).toContain('day one')})})describe('the outcome banner', () => {it('only ever echoes one of our own words back into the page', () => {expect(parseOutcome('saved')).toBe('saved')expect(parseOutcome('<img src=x onerror=1>')).toBeNull()expect(parseOutcome(null)).toBeNull()})it('says nothing was saved when nothing was', () => {const html = checkinBlock(rosa, { [ROSA]: ok([]) }, EVENING, 'empty')expect(html).toMatch(/nothing was saved/i)const failed = checkinBlock(rosa, { [ROSA]: ok([]) }, EVENING, 'failed')expect(failed).toMatch(/not<\/b> saved/i)})})describe('user text cannot become markup', () => {it('escapes a note that tries to close a tag', () => {const nasty = note({ did: '</textarea><img src=x onerror=alert(1)>' })for (const v of [sam, rosa]) {const html = checkinBlock(v, { [ROSA]: ok([nasty]) }, EVENING)expect(html).not.toContain('<img src=x')expect(html).toContain('<img src=x')}})it('escapes it inside the edit box too, which is the easier one to miss', () => {const html = checkinBlock(rosa, { [ROSA]: ok([note({ did: '</textarea><b>x' })]) }, EVENING)expect(html).not.toContain('</textarea><b>')})it('escapes a store error, which is a string from somewhere else entirely', () => {const html = checkinBlock(sam, { [ROSA]: { state: 'failed', error: '<script>x</script>' } }, EVENING)expect(html).not.toContain('<script>x')})})describe('stamping the block into a pre-rendered page', () => {const PAGE = `<html><body><div>before</div>${checkinPlaceholder()}<div>after</div></body></html>`it('replaces the placeholder and everything inside it', () => {const out = stampCheckin(PAGE, '<p>the card</p>')expect(out).toContain('<p>the card</p>')expect(out).not.toContain(CHECKIN_START)expect(out).not.toContain(CHECKIN_END)expect(out).not.toMatch(/lives on the live board/)expect(out).toContain('before')expect(out).toContain('after')})it('an empty block leaves nothing behind, not an empty shell', () => {const out = stampCheckin(PAGE, '')expect(out).toBe('<html><body><div>before</div><div>after</div></body></html>')})it('a page with no placeholder is returned untouched', () => {const plain = '<html><body>nothing here</body></html>'expect(stampCheckin(plain, '<p>card</p>')).toBe(plain)})it('the placeholder itself says why it is empty, rather than being empty', () => {// A file render has no Worker behind it. Silence there is a feature that// silently is not there (rule 8).expect(checkinPlaceholder()).toMatch(/lives on the live board/i)})})