chadtmiller.com/knotless
Git hosted in your atproto account, on any stock PDS, fast enough for agents.
git clone knotless://chadtmiller.com/knotlessneeds the knotless git helper. set it up
// 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
}