mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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.
216 lines
9.5 KiB
TypeScript
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([])
|
|
})
|