// ⛔ AN EMPTY PAGE STILL HAS TO SAY SOMETHING TRUE.
//
// Ours said "Nothing yet." — which is the one thing the reader already knew. Bluesky's empty feed
// says what is empty, why it is empty, and puts the two buttons that fix it on the page. That is
// live for us right now: stortron's timeline is empty because it follows nobody, and the page has
// never once said so.
//
// ⛔ AND IT MUST NOT CONFLATE THE CAUSES. "You follow nobody" and "you follow people and nothing has
// replicated" are different problems with different fixes — the same distinction EDGE-GRAPH §4
// makes about edges, applied to a page. One sentence for both would be the UI version of drawing a
// converged edge and a resolved one the same way.
import test from 'brittle'
import { timeline, following, notifications, rooms, knocks, mentions, empty } from '../lib/pages.js'
const strip = (h) => h.replace(/<style[\s\S]*?<\/style>/g, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ')
test('★★ an empty timeline says WHICH kind of empty it is', async (t) => {
const nobody = strip(timeline([], { empty: { follows: 0 } }))
t.ok(/follow/i.test(nobody), 'names the cause: nobody is followed')
t.absent(/replicat/i.test(nobody), 'and does not blame replication for it')
const waiting = strip(timeline([], { empty: { follows: 3 } }))
t.ok(/3/.test(waiting), 'says how many feeds are being waited on')
// ⛔ The opposite mistake: telling someone who follows three people to go find some.
t.absent(/follow (some|nobody)/i.test(waiting), 'does not tell them to follow somebody')
})
test('★ an empty page OFFERS the thing that fixes it', async (t) => {
// A sentence with no way out is a dead end. Every empty state here ends in a link that is the
// next step, which is the whole difference between Bluesky's empty feed and ours.
for (const [name, html] of [
['timeline', timeline([], { empty: { follows: 0 } })],
['following', following([], [], null)],
['rooms', rooms([])],
['knocks', knocks([])],
['mentions', mentions([], null)],
['notifications', notifications([], null)]
]) {
const box = html.slice(html.indexOf('class="empty"'))
t.ok(html.includes('class="empty"'), name + ' uses the shared empty state')
t.ok(/<a [^>]*href="\/|<button/.test(box), name + ' offers a next step')
}
})
test('⚠ a non-empty page has no empty state', async (t) => {
// The obvious regression: rendering the explanation above real content.
const rows = [{ author: 'ab'.repeat(32), collection: 'post', name: 'p1', body: 'hi', createdAt: '2026-09-01T00:00:00Z' }]
t.absent(timeline(rows, { empty: { follows: 0 } }).includes('class="empty"'))
})
test('⚠ empty() escapes what it is given', async (t) => {
const out = empty('t', '<script>x</script>', [{ href: '/x', label: '<b>y</b>' }])
t.absent(out.includes('<script>'))
t.absent(out.includes('<b>y</b>'))
})