Files
zeekay 19a71cf416 gates: the consent sheet answers #choices from @hanzo/ui
The fragment gate found the page that answers #choices among the root
providers' own modules, and that handler now lives in the consent sheet
they mount from @hanzo/ui. It reads that module's built files too, so a
link to #choices counts as answered on every page again.
2026-10-25 22:39:52 -07:00

216 lines
9.5 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join, relative } from 'node:path'
/**
* A CONTROL THAT NAMES A DESTINATION LEADS TO ONE.
*
* Measured on the live site before this gate: forty-odd controls across
* thirteen published pages — /pricing, /ai, /payments, /realtime, /analytics,
* /identity, /edge, /functions, /machines, /datastore, /team, /commerce,
* /press — were a `<button>` with no handler. "Get Started", "Contact Sales",
* "Schedule a Demo", "View Documentation", "Start building" under the rate
* card. Each one rendered, took the pointer, and did nothing, which is the one
* thing a buyer cannot tell apart from a broken site.
*
* Nothing caught it because nothing CAN, from the export. A button with a
* handler and a button without one serialize to the same bytes, so this reads
* the source instead — the same way claims.spec reads it — and fails on the
* commit that adds the control.
*
* Two shapes are dead:
*
* - a `<button>` or `<Button>` with no `onClick`, no `onPress`, no `asChild`
* and not a form's submit, whose label names somewhere to go, or which
* wears `.action` — the site's role class for a call to action;
* - an address of `#`, which is a link to the top of the page it is on.
*
* Comments and template strings are blanked first: a comment may quote the
* defect it describes, and a code sample may show a `<Button>` it does not
* render.
*/
const ROOTS = ['app/(marketing)', 'components']
/** The words a control uses when it promises to take you somewhere. */
const DESTINATION =
/\b(get started|sign ?up|start|try|contact|sales|demo|documentation|docs|api reference|pricing|book|create .*account|deploy now|learn more|explore|download|github|browse|examples?|application|purchase)\b/i
/** What makes a control do something: a handler, a child that navigates, a
* form, or props spread in from a caller that may carry any of those. A spread
* is an ATTRIBUTE — `{...rest}` after whitespace — and not the `{{ ...KEY }}`
* of a style object, which carries nothing but paint. */
const ACTS = /\b(onClick|onPress|asChild)\b|type=["']submit["']|\s\{\s*\.\.\./
function files(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const path = join(dir, name)
if (statSync(path).isDirectory()) files(path, out)
else if (name.endsWith('.tsx')) out.push(path)
}
return out
}
/** The source with comments and template strings blanked, newlines kept, so a
* match still reports its own line. */
function code(src: string): string {
const blank = (s: string) => s.replace(/[^\n]/g, ' ')
return src
.replace(/\/\*[\s\S]*?\*\//g, blank)
.replace(/(^|[^:"'`])\/\/[^\n]*/g, (m, lead: string) => lead + blank(m.slice(lead.length)))
.replace(/`[^`]*`/g, (m) => '`' + blank(m.slice(1, -1)) + '`')
}
/** Inside an open `<form>`: a button there submits it. */
function inForm(src: string, at: number): boolean {
return src.lastIndexOf('<form', at) > src.lastIndexOf('</form>', at)
}
function dead(): string[] {
const found: string[] = []
for (const root of ROOTS) {
for (const path of files(root)) {
const src = code(readFileSync(path, 'utf8'))
const line = (at: number) => `${relative('.', path)}:${src.slice(0, at).split('\n').length}`
for (const m of src.matchAll(/<(button|Button)\b((?:[^>{}]|\{(?:[^{}]|\{[^{}]*\})*\})*)>/g)) {
const [whole, tag, attrs] = m
if (ACTS.test(attrs) || inForm(src, m.index)) continue
const close = src.indexOf(`</${tag}>`, m.index + whole.length)
const label = src
.slice(m.index + whole.length, close)
.replace(/<[^>]*>|\{[^}]*\}/g, ' ')
.replace(/\s+/g, ' ')
.trim()
if (DESTINATION.test(label) || /className=["']action["']/.test(attrs)) {
found.push(`${line(m.index)}: <${tag}> ${label.slice(0, 60)}`)
}
}
for (const m of src.matchAll(/\b(href|link)(=|:\s*)["'{]*#["'}]/g)) {
found.push(`${line(m.index)}: an address of '#'`)
}
}
}
return found
}
test('every control that names a destination leads to one', () => {
expect(dead(), 'these render, take the pointer, and go nowhere').toEqual([])
})
/**
* A FRAGMENT NAMES A PLACE ON ITS PAGE.
*
* `#x` scrolls to the element whose id is x, and with no such element it does
* nothing at all — the same dead click as `#`, only harder to see. /analytics
* shipped three: #start-free-trial, #request-demo and #learn-more, none drawn.
*
* A page is drawn from its route folder, its component folder and the modules
* it imports, so the id is looked for there, as any quoted `x`: an id may come
* from data, as `<section id={lane}>` does with lane="runtime". Every page is
* also drawn inside app/providers.tsx, whose controls answer a fragment on any
* page, so what it imports counts too: its own modules, and the consent sheet it
* mounts from @hanzo/ui, which opens on `#choices`.
*/
/** The file an `@/` import names, if it is one. */
function resolved(spec: string): string | null {
for (const ext of ['.tsx', '.ts', '/index.tsx', '/index.ts']) {
const path = spec.replace(/^@\//, '') + ext
try {
if (statSync(path).isFile()) return path
} catch {
/* not this spelling */
}
}
return null
}
/** The files of a package module the providers mount, read from its built folder. */
function mounted(spec: string): string[] {
const dir = dirname(createRequire(join(process.cwd(), 'package.json')).resolve(spec))
return readdirSync(dir).filter((n) => /\.js$/.test(n)).map((n) => join(dir, n))
}
/** What every page carries: the root providers, the modules they import, and the consent sheet. */
const EVERYWHERE = ((path: string) => {
const src = code(readFileSync(path, 'utf8'))
const imported = [...src.matchAll(/from\s+['"](@\/[^'"]+)['"]/g)].map((i) => resolved(i[1]))
const files = [...imported.filter((f): f is string => f !== null), ...mounted('@hanzo/ui/consent')]
return [src, ...files.map((f) => code(readFileSync(f, 'utf8')))].join('\n')
})('app/providers.tsx')
test('every fragment names an id its page draws', () => {
const missing: string[] = []
for (const root of ROOTS) {
for (const path of files(root)) {
const src = code(readFileSync(path, 'utf8'))
for (const m of src.matchAll(/\b(?:href|link|to)(?:=|:\s*)["'{]*#([A-Za-z][-\w]*)/g)) {
const dir = dirname(path)
const imported = [...src.matchAll(/from\s+['"](@\/[^'"]+)['"]/g)].map((i) => resolved(i[1]))
const near = [...readdirSync(dir).filter((n) => /\.tsx?$/.test(n)).map((n) => join(dir, n)), ...imported]
.filter((f): f is string => f !== null)
.map((f) => code(readFileSync(f, 'utf8')))
.join('\n')
// A page's own `href="#x"` must not answer itself: on the page the id is a
// quoted `x`; the providers may state the fragment they answer, `'#x'`.
if (!new RegExp(`["'\`]${m[1]}["'\`]`).test(near) && !new RegExp(`["'\`]#?${m[1]}["'\`]`).test(EVERYWHERE)) {
missing.push(`${relative('.', path)}:${src.slice(0, m.index).split('\n').length}: #${m[1]}`)
}
}
}
}
expect(missing, 'these name a place their page does not draw').toEqual([])
})
/**
* ONE SALES ADDRESS.
*
* A buyer asking for sales is sent to /contact-sales, which books the call and
* shows its mailbox as text. Controls elsewhere wrote to three mailboxes —
* sales@, solutions@ and enterprise@ — behind labels that never showed which,
* and whether the other two reach anyone is not something the site can know.
* The sales page's own address is the only one a control may write to.
*/
test('every sales address is the one the sales page shows', () => {
const shown = readFileSync('app/(marketing)/contact-sales/page.tsx', 'utf8').match(/mailto:([\w.+-]+@hanzo\.ai)/)?.[1]
expect(shown, '/contact-sales shows a mailbox').toBeTruthy()
const other: string[] = []
for (const root of ROOTS) {
for (const path of files(root)) {
const src = code(readFileSync(path, 'utf8'))
for (const m of src.matchAll(/mailto:((?:sales|solutions|enterprise)@hanzo\.ai)/g)) {
if (m[1] !== shown) other.push(`${relative('.', path)}:${src.slice(0, m.index).split('\n').length}: ${m[1]}`)
}
}
}
expect(other, `these write to a sales mailbox other than ${shown}`).toEqual([])
})
/**
* NOTHING CALLED FREE ENDS AT A PAYWALL.
*
* /signup ends at /chat, whose workspace runs on the paid plan and shows only
* Subscribe until it is live. A control that says "free" and goes there makes a
* promise the next screen breaks. Those open the console's key page (KEYS in
* components/home/nav-data.ts) instead.
*/
test('no control that says free opens /signup', () => {
const paywalled: string[] = []
for (const root of ROOTS) {
for (const path of files(root)) {
const src = code(readFileSync(path, 'utf8'))
for (const m of src.matchAll(/<(a|Link)\b((?:[^>{}]|\{(?:[^{}]|\{[^{}]*\})*\})*)>/g)) {
const [whole, tag, attrs] = m
if (!/href=["']\/signup["']/.test(attrs)) continue
const close = src.indexOf(`</${tag}>`, m.index + whole.length)
const label = src.slice(m.index + whole.length, close).replace(/<[^>]*>|\{[^}]*\}/g, ' ')
if (/\bfree\b/i.test(label)) {
paywalled.push(`${relative('.', path)}:${src.slice(0, m.index).split('\n').length}: ${label.replace(/\s+/g, ' ').trim()}`)
}
}
}
}
expect(paywalled, 'these say free and end at the paid workspace').toEqual([])
})