// What the Team forum's client half decides for itself (client/src/lib/teamForum.js). // // The point of this file is how LITTLE that is. Who may post, who may moderate, // whether an image renders and whether a post may be edited are all server // answers the panel reads. What is tested here is the three places the client // turns those answers into what a reader sees — and one property that is easy to // break by accident: the edit offer can only ever be withdrawn here, never // granted. import { test } from 'node:test' import assert from 'node:assert/strict' import { REPORT_REASONS, editOfferOpen, stripToText, threadSummary } from '../src/lib/teamForum.js' const NOW = new Date('2026-08-18T12:00:00Z').getTime() const inMinutes = (n) => new Date(NOW + n * 60_000).toISOString() // ── the edit offer ───────────────────────────────────────────────────────── test('the client can withdraw an edit offer and can never create one', () => { // The server said no. Nothing about a deadline changes that — a future // `editableUntil` on a post the server refused must not become an offer, or // the client would be granting a permission. assert.equal(editOfferOpen({ canEdit: false, editableUntil: inMinutes(10) }, NOW), false) assert.equal(editOfferOpen({ canEdit: false, editableUntil: null }, NOW), false) }) test('a deadline that has passed while the page sat open withdraws the offer', () => { assert.equal(editOfferOpen({ canEdit: true, editableUntil: inMinutes(5) }, NOW), true) // Same post, fifteen minutes of the reader staring at it later. assert.equal(editOfferOpen({ canEdit: true, editableUntil: inMinutes(5) }, NOW + 15 * 60_000), false) }) test('no deadline means no deadline, not no permission', () => { // Staff are not time-bounded, and `editableUntil: null` is how the server says // so. Reading it as "expired" would take the edit control away from exactly the // people whose authority does not expire. assert.equal(editOfferOpen({ canEdit: true, editableUntil: null }, NOW), true) }) test('an unparseable deadline closes the offer rather than opening it', () => { assert.equal(editOfferOpen({ canEdit: true, editableUntil: 'not a date' }, NOW), false) assert.equal(editOfferOpen(null, NOW), false) assert.equal(editOfferOpen(undefined, NOW), false) }) // ── round-tripping a body back into the composer ─────────────────────────── test('the image core generated is stripped, and the URL that made it survives', () => { // §5.5.3: the author wrote a URL, core emitted the at read time. Handing // the back would let an author edit markup they never wrote — and the // URL is what re-renders it, so nothing is lost by removing it. const rendered = '

https://x/a.png' + '

' const text = stripToText(rendered) assert.ok(!text.includes(' { assert.equal(stripToText('

One

Two

'), 'One\n\nTwo') assert.equal(stripToText('

One
Two

'), 'One\nTwo') // A paragraph carrying attributes is still a paragraph. assert.equal(stripToText('

One

\n

Two

'), 'One\n\nTwo') }) test('entities decode to what the author typed, and only once', () => { assert.equal(stripToText('

Tom & Jerry

'), 'Tom & Jerry') assert.equal(stripToText('

"quoted"

'), '"quoted"') // The one that bites: an author who typed a literal "