Files
zeekayandHanzo Dev 18c16ecf2c Zen is the face, and the check can prove it
The site rendered in the system fallback: @hanzo/design 0.5.x replaced Geist
with Zen, and this app pinned 0.4.6, so the family every surface asked for was
one nothing declared any more. Bumping design alone strands it — @hanzo/ui
inlines a copy of that token block — so the pair moves together, to
@hanzo/design 0.5.21 and @hanzo/ui 8.3.4.

@hanzo/ui depends on none of its @hanzogui/* peers; it declares them, so this
app installs them. With only @hanzo/gui declared, pnpm resolved
@hanzogui/lucide-icons-2 a generation behind, which dragged a second
@hanzogui/core, and every @hanzo/ui component carrying an icon then read a
theme context no provider fills — prerender died on "Missing theme.". Declaring
the cohort, as hanzo.ai and the console already do, collapses it to one core.

The old font test matched /geist|system-ui|sans-serif/, so it passed on the
fallback: the exact failure it existed to catch. Both checks ask the font
loader now. shots.mjs asks for Zen Mono only where a page renders mono, because
a face loads when a glyph wants it; mutation-checked by aborting **/*.woff2.

CommandPalette was controlled, nothing ever owned it, and its global handler
could only close — so Cmd+K opened nothing and its test had never passed. A
palette is the thing Cmd+K opens, so it owns the shortcut and mounts itself.

51 pages, 414/414 gui classes covered, 36/36 e2e, 60/60 routes across
phone+desktop x light+dark.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 09:51:17 -07:00

170 lines
7.7 KiB
JavaScript

/* eslint-disable */
/**
* The visual check for a served build — what proves a styling change actually
* landed. Both failure modes in this stack are SILENT: @hanzo/gui ignores a prop
* it does not recognise, and an undefined CSS class or `var()` does nothing.
* Neither errors, neither fails typecheck. Only pixels settle it.
*
* It renders real routes in a real browser across the full matrix — phone and
* desktop, light and dark — and fails on what a CSS migration actually breaks:
* horizontal overflow, a page that renders no text, a theme that did not apply,
* and same-origin errors.
*
* BOTH THEMES is not thoroughness, it is the minimum. The partner wall passed
* this check while six of its eight marks were invisible on dark, because the
* check only ever looked at light.
*
* pnpm build && npx serve out -l 3399
* node scripts/shots.mjs [outDir]
*/
import { chromium } from '@playwright/test'
import { mkdirSync } from 'node:fs'
const BASE = process.env.SHOTS_BASE || 'http://localhost:3399'
const ORIGIN = new URL(BASE).origin
/** `missing` routes are EXPECTED to 404 — a 404 page that 200s is the bug, and
* one that renders three lines of text is doing its job, not failing. */
const ROUTES = [
{ path: '/' },
{ path: '/about' },
{ path: '/team' },
{ path: '/pricing' },
{ path: '/careers' },
{ path: '/contact' },
{ path: '/help' },
{ path: '/models' },
{ path: '/research' },
{ path: '/docs' },
{ path: '/open-source' },
{ path: '/press' },
{ path: '/status' },
{ path: '/products/zen' },
{ path: '/nope-404', missing: true },
]
/* The page surface each theme must actually paint. If a theme's tokens stop
resolving these are what change, so they are asserted rather than printed. */
const SURFACE = { light: 'rgb(255, 255, 255)', dark: 'rgb(0, 0, 0)' }
const OUT = process.argv[2] || 'shots'
mkdirSync(OUT, { recursive: true })
const browser = await chromium.launch()
let bad = 0
for (const theme of ['light', 'dark']) {
for (const width of [390, 1280]) {
const ctx = await browser.newContext({ viewport: { width, height: 900 } })
await ctx.addInitScript((t) => localStorage.setItem('theme', t), theme)
for (const { path, missing } of ROUTES) {
const page = await ctx.newPage()
const errors = []
/* A request to a third-party host cannot succeed from a local origin, and
the console noise that follows is the environment, not the page. It is
reported, never fatal — the page's own failure path is what is on test
here, and the page passing means that path worked. */
const offsite = []
page.on('console', (m) => m.type() === 'error' && errors.push(m.text()))
page.on('pageerror', (e) => errors.push('pageerror: ' + e.message))
page.on('requestfailed', (r) => {
const where = new URL(r.url()).origin === ORIGIN ? errors : offsite
where.push('requestfailed: ' + r.url())
})
const res = await page
.goto(BASE + path, { waitUntil: 'networkidle' })
.catch((e) => void errors.push('nav: ' + e.message))
await page.waitForTimeout(400)
const m = await page.evaluate(() => {
const de = document.documentElement
const cs = getComputedStyle(document.body)
/* The tap target, not the painted box. @hanzo/ui expands a control's hit
area with an `::after` overlay driven from `data-touch-y`, so a 32px
`size="sm"` button is a legal 44px target and counting its box would
report a problem that is not there. */
const small = [...document.querySelectorAll('a,button,summary,[role=button]')]
.filter((el) => el.getClientRects().length)
.filter((el) => {
const r = el.getBoundingClientRect()
const pad = parseFloat(el.dataset.touchY || '0')
return r.height > 0 && r.height + 2 * pad < 44
})
/* A button whose box is shorter than the frame @hanzo/ui asks for is a
button clipping its own label — the failure a pinned `height` used to
cause, and the reason 8.0.46 moved to `height:auto` + `minHeight`. */
const short = [...document.querySelectorAll('[data-slot="button"]')]
.filter((el) => el.getClientRects().length)
.filter((el) => {
const min = parseFloat(getComputedStyle(el).minHeight) || 0
return el.getBoundingClientRect().height + 0.5 < min
})
return {
overflow: de.scrollWidth - de.clientWidth,
bg: cs.backgroundColor,
font: cs.fontFamily.split(',')[0],
/* THE face, not the name of one. The stack falls back to
`ui-sans-serif, system-ui` silently — same computed value shape,
same green build — if the host stops declaring @font-face, so ask
the font loader whether it actually has the file. */
zen: document.fonts.check('16px Zen'),
/* A face loads when a glyph asks for it, so `check` is false on a page
that renders no mono at all — true of most of this site. Ask both
questions: does anything here render mono, and did that face arrive. */
mono: [...document.querySelectorAll('*')].some(
(el) => getComputedStyle(el).fontFamily.split(',')[0].replace(/["']/g, '').trim() === 'Zen Mono'
),
zenMono: document.fonts.check('16px "Zen Mono"'),
text: (document.body.innerText || '').trim().length,
h1: document.querySelector('h1')?.innerText?.slice(0, 48) ?? null,
smallTaps: small.length,
shortButtons: short.length,
/* every one-ink mark must resolve to a filter, in every theme */
inkless: [...document.querySelectorAll('img[alt$="logo"]')].filter(
(e) => !e.className.includes('hz-ink-')
).length,
}
})
const name = `${theme}-${width}${path.replace(/\//g, '_') || '_root'}.png`
await page.screenshot({ path: `${OUT}/${name}` })
const status = res?.status() ?? 0
const problems = []
if (m.overflow > 0) problems.push(`overflow ${m.overflow}px`)
if (!missing && m.text <= 200) problems.push(`no text (${m.text})`)
if (missing && status !== 404) problems.push(`expected 404, got ${status}`)
if (!missing && status !== 200) problems.push(`status ${status}`)
if (m.bg !== SURFACE[theme]) problems.push(`${theme} surface is ${m.bg}`)
if (m.inkless) problems.push(`${m.inkless} logo(s) with no hz-ink-*`)
if (!m.zen) problems.push('Zen did not load — the page is on the fallback face')
if (m.mono && !m.zenMono) problems.push('mono text is on the fallback face')
if (m.shortButtons) problems.push(`${m.shortButtons} button(s) shorter than their own minHeight`)
// Console noise is only the page's own once the offsite hosts are excluded
// — and a `missing` route is SUPPOSED to have served a 404, so the console
// line the browser logs about it is the expectation being met, not a fault.
const own = missing
? errors.filter((e) => !/status of 404/.test(e))
: errors
if (!offsite.length && own.length) problems.push(own.slice(0, 2).join(' | '))
if (problems.length) bad++
console.log(
`${problems.length ? 'BAD ' : 'ok '} ${theme.padEnd(5)} ${String(width).padEnd(5)} ${path.padEnd(
22
)} ovf=${m.overflow} text=${m.text} taps<44=${m.smallTaps} bg=${m.bg} font=${m.font} zen=${m.zen}${m.mono ? `/mono=${m.zenMono}` : ''}` +
(offsite.length ? ` offsite=${offsite.length}` : '') +
(problems.length ? `\n ${problems.join('\n ')}` : '')
)
await page.close()
}
await ctx.close()
}
}
await browser.close()
console.log(bad ? `FAILURES: ${bad}` : 'ALL GREEN')
process.exit(bad ? 1 : 0)