chadtmiller.com/knotless

Git hosted in your atproto account, on any stock PDS, fast enough for agents.

git clone knotless://chadtmiller.com/knotless

needs the knotless git helper. set it up

main
71 lines · 3.4 KB Raw History
// Before the screenshots: what the pages show, from fixtures.json, played
// back in front of the pages from web/build. With SHOTS_LIVE=1, or with no
// recording, a world instead: the one `just shots-up` keeps up if it is
// there, else a new one for this run, and knotless-serve from this build in
// front of it. The pages are written to
// target/shots/views.json, and those whose HTML is as it was when their
// baselines were taken, in every project, to target/shots/unchanged.json:
// views.spec.mjs skips them before a browser page is opened. So too the
// pages whose cards are shot, to cards.json, and the cards whose URL is
// as it was, as `card:<name>`. knotless-serve draws the cards, so they are
// shot only from a world: from fixtures, cards.json is null and
// cards.spec.mjs skips them. Returns the teardown.

import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { base, create, page, root, saved, serve, until } from './world.mjs'
import { card, digest } from './digest.mjs'
import { front, recording } from './fixtures.mjs'
import config from './playwright.config.mjs'

const here = join(root, 'harness/shots')
const projects = config.projects.map((p) => p.name).filter((p) => p !== 'card')

const kept = async () => {
  if (!existsSync(saved)) return null
  const world = JSON.parse(readFileSync(saved, 'utf8'))
  // Kept up from before cards were shot, it is seeded differently.
  if (!world.cards) return null
  const up = await fetch(`${world.net.pds}/xrpc/com.atproto.server.describeServer`).then((r) => r.ok, () => false)
  return up ? world : null
}

export default async function setup() {
  const recorded = process.env.SHOTS_LIVE ? null : recording()
  let world = recorded ? { views: recorded.views } : await kept()
  let teardown
  if (recorded) {
    const stand = await front({ recorded: recorded.answers })
    teardown = async () => stand.kill()
  } else if (world) {
    const server = await serve(world)
    teardown = async () => server.kill()
    // The index is on disk already; this waits only for the mirrors.
    await until('the pages', async () => (await page(world.views.pull)).includes('Merged as'))
  } else {
    world = await create()
    teardown = async () => world.stop()
  }
  mkdirSync(join(root, 'target/shots'), { recursive: true })
  writeFileSync(join(root, 'target/shots/views.json'), JSON.stringify(world.views, null, 2))
  const cards = recorded ? null : world.cards
  writeFileSync(join(root, 'target/shots/cards.json'), JSON.stringify(cards, null, 2))
  const unchanged = []
  if (!process.env.SHOTS_ALL) {
    const at = (name, project, ext) => join(here, 'baseline', process.platform, `${name}-${project}.${ext}`)
    const same = (name, project, sum) =>
      existsSync(at(name, project, 'png')) &&
      existsSync(at(name, project, 'sha256')) &&
      readFileSync(at(name, project, 'sha256'), 'utf8') === sum
    for (const [name, path] of Object.entries(world.views)) {
      const sum = digest(await (await fetch(base + path)).text())
      if (projects.every((p) => same(name, p, sum))) unchanged.push(name)
    }
    for (const [name, path] of Object.entries(cards ?? {})) {
      const sum = digest(card(await (await fetch(base + path)).text()))
      if (same(name, 'card', sum)) unchanged.push(`card:${name}`)
    }
  }
  writeFileSync(join(root, 'target/shots/unchanged.json'), JSON.stringify(unchanged))
  return teardown
}