diff --git a/src/app/features/desk/DeskView.test.tsx b/src/app/features/desk/DeskView.test.tsx index ca10ea66..9c60ed53 100644 --- a/src/app/features/desk/DeskView.test.tsx +++ b/src/app/features/desk/DeskView.test.tsx @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import { ScreenSize, ScreenSizeProvider } from '../../hooks/useScreenSize'; import { DeskEntry } from './DeskEntry'; import { DeskView, SIGN_IN_NOTE } from './DeskView'; +import { ForkList } from './forkContract'; import { STATES } from './fixtures'; import { setDeskSource } from './source'; @@ -121,6 +122,88 @@ describe('DeskView', () => { }); }); +describe("a question's age", () => { + const NOW = Date.parse('2026-11-11T12:00:00Z'); + const listRaised = (raisedAt: string | null): ForkList => { + const entry = STATES.noAnswers.forks[0]; + return { + ...STATES.noAnswers, + forks: [{ ...entry, card: { ...entry.card, raised_at: raisedAt } }], + }; + }; + const html = (raisedAt: string | null) => + renderToStaticMarkup( + + + + ); + it('reads days since the card was first admitted', () => { + expect(html('2026-11-06T12:00:00Z')).toContain('raised 5d ago'); + }); + it('reads hours inside a day, and just now inside an hour', () => { + expect(html('2026-11-11T07:30:00Z')).toContain('raised 4h ago'); + expect(html('2026-11-11T11:40:00Z')).toContain('raised just now'); + }); + it('shows no age when the card carries none, or an instant that does not parse or is ahead of the clock', () => { + ['null', 'yesterday', '2027-01-01T00:00:00Z', '2026-11-06', 'Nov 6 2026', '0', '2026-02-30T00:00:00Z'].forEach((v) => { + expect(html(v === 'null' ? null : v)).not.toContain('raised '); + }); + }); +}); + +describe('counts when the ids are withheld', () => { + const withheld = (): ForkList => { + const entry = STATES.noAnswers.forks[0]; + return { + ...STATES.noAnswers, + forks: [ + { + ...entry, + card: { ...entry.card, deciders: [], decider_count: 3 }, + record: { ...entry.record, answerer_count: 2 }, + }, + ], + }; + }; + it('shows how many deciders and answerers there are, naming none', () => { + const html = renderToStaticMarkup( + + + + ); + expect(html).toContain('3 deciders · 2 answerers'); + }); + it('shows no count line for a served count outside the contract', () => { + const bad = (card: object, record: object): string => { + const entry = STATES.noAnswers.forks[0]; + const list = { + ...STATES.noAnswers, + forks: [{ card: { ...entry.card, ...card }, record: { ...entry.record, ...record } }], + } as ForkList; + return renderToStaticMarkup( + + + + ); + }; + [ + [{ decider_count: 0 }, {}], + [{ decider_count: 65 }, {}], + [{ decider_count: undefined }, {}], + [{}, { answerer_count: -1 }], + [{}, { answerer_count: 1.5 }], + ].forEach(([c, r]) => { + const out = bad(c, r); + expect(out).not.toContain('undefined'); + expect(out).not.toMatch(/deciders? ·/); + }); + }); + it('says the singular for one', () => { + const html = view('noAnswers', ScreenSize.Desktop); + expect(html).toContain('1 decider · 0 answerers'); + }); +}); + describe('DeskEntry', () => { afterEach(() => setDeskSource(() => undefined)); const entry = (mind: string) => diff --git a/src/app/features/desk/DeskView.tsx b/src/app/features/desk/DeskView.tsx index e57bfa70..f83dcd82 100644 --- a/src/app/features/desk/DeskView.tsx +++ b/src/app/features/desk/DeskView.tsx @@ -6,11 +6,14 @@ import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { AnswerRow, Desk, buildDesk, DeskRow } from './buildDesk'; import { ForkList } from './forkContract'; import { deskMood } from './deskMood'; +import { raisedAge } from './age'; import * as css from './DeskView.css'; /** Slice 1 has no session: every viewer is signed out, and the note says why it cannot be used. */ export const SIGN_IN_NOTE = 'Answering opens with sign-in. Until then you can read the questions.'; +const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? '' : 's'}`; + const date = (iso: string) => iso.slice(0, 10); const OUTCOME: Record = { @@ -45,7 +48,7 @@ function Answers({ answers }: { answers: AnswerRow[] }) { ); } -function Row({ row }: { row: DeskRow }) { +function Row({ row, now }: { row: DeskRow; now: number }) { const phone = useScreenSizeContext() === ScreenSize.Mobile; // Phones list titles; a tap opens one. Wide screens show every row open. const [open, setOpen] = useState(!phone); @@ -77,6 +80,14 @@ function Row({ row }: { row: DeskRow }) { {open && ( <> + {raisedAge(row.raisedAt, now) && ( +

{raisedAge(row.raisedAt, now)}

+ )} + {row.deciderCount !== undefined && row.answererCount !== undefined && ( +

+ {`${plural(row.deciderCount, 'decider')} · ${plural(row.answererCount, 'answerer')}`} +

+ )}

{row.context}