OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface / test / nav.test.js

70 lines · javascript ref:
// ⛔ A ROW THAT MUST FIT N ITEMS BREAKS EVERY TIME N GROWS.
//
// The CSS carries its own history: "Eight items on one line at this width", then "Nine items… it has
// wrapped twice now", then "Ten items… it has wrapped three times". Each time the fix was to shrink
// the gap. There are eleven items now and it wraps again, leaving "sign out" orphaned on a second
// row — 64px of navigation above every page.
//
// Shrinking the gap a fourth time fixes this instance and schedules the next one. Bluesky and
// Mastodon both stack navigation vertically beside the column, which cannot wrap however many items
// there are, and there is 250px of empty gutter either side of our 38rem column to put it in.

import test from 'brittle'
import { layout, timeline } from '../lib/pages.js'

const css = (html) => html.slice(html.indexOf('<style>') + 7, html.indexOf('</style>'))

test('★★ the content is one element, so it can be placed beside the nav', async (t) => {
  const html = layout('timeline', '<p>hello</p>')
  // ⛔ Without a wrapper, `body` has nav plus every top-level element of the page as siblings — so a
  // grid on body would put the nav in column one, the first paragraph in column two, the second
  // back in column one. The wrapper is what makes a sidebar expressible at all.
  t.ok(html.includes('<main>'), 'a single content element')
  t.ok(html.indexOf('<main>') > html.indexOf('</nav>'), 'after the nav, so source order is reading order')
  t.ok(html.includes('</main></body>'))
})

test('★★ at a wide viewport the nav is a COLUMN, not a row', async (t) => {
  const c = css(timeline([], { empty: { follows: 0 } }))
  const wide = c.slice(c.indexOf('@media (min-width'))
  t.ok(wide.length > 0, 'there is a wide-viewport rule at all')
  t.ok(/flex-direction:column/.test(wide), 'the nav stacks')
  t.ok(/grid-template-columns/.test(wide), 'and the content sits beside it')
})

test('⛔ the narrow layout is unchanged — it is the one that has to work everywhere', async (t) => {
  const c = css(timeline([], { empty: { follows: 0 } }))
  const base = c.slice(0, c.indexOf('@media (min-width'))
  // A phone has no gutter to put a sidebar in. The row is the default and the sidebar is the
  // enhancement, not the other way round.
  t.ok(/nav\{[^}]*flex-wrap:wrap/.test(base), 'still wraps when it must')
  t.ok(/body\{[^}]*max-width:38rem/.test(base), 'and the column is still the column')
})

// ── the action row is ONE toolbar ─────────────────────────────────────────────────────────────

test('★ the reaction palette is not pushed to the far edge', async (t) => {
  const { post, layout } = await import('../lib/pages.js')
  const html = layout('x', post({ author: 'ab'.repeat(32), collection: 'post', name: 'p1', body: 'hi', createdAt: '2026-09-01T00:00:00Z' }, null))
  const css = html.slice(html.indexOf('<style>'), html.indexOf('</style>'))
  const rule = css.slice(css.indexOf('.reactions .quick{'))
  // ⛔ `margin-left:auto` put the emoji at the far right with a gap in the middle, so four words at
  // one end and three emoji at the other read as two unrelated toolbars. Mastodon spreads one set
  // of actions across the row; it does not have two.
  t.absent(/^\.reactions \.quick\{[^}]*margin-left:auto/.test(rule), 'the palette is not shoved right')
  const bar = css.slice(css.indexOf('.reactions{'), css.indexOf('.reactions form'))
  t.ok(/justify-content:space-between/.test(bar), 'the row spreads its items instead')
})

test('⚠ the file control is styled like the rest of the page', async (t) => {
  const { timeline } = await import('../lib/pages.js')
  const html = timeline([], { empty: { follows: 0 } })
  const css = html.slice(html.indexOf('<style>'), html.indexOf('</style>'))
  // The native control renders a grey OS button and the words "No file chosen" — the only unstyled
  // element on the page, and the one that most made it look unfinished. ::file-selector-button
  // styles the button and KEEPS the filename, which a hidden-input-plus-label trick would throw
  // away: without JavaScript nothing else can tell you which file you picked.
  t.ok(css.includes('::file-selector-button'), 'the button part is styled')
  t.ok(/::file-selector-button\{[^}]*border-radius:999px/.test(css), 'like every other button here')
})