mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
A cached Kai decision is 0.3 ms against a hosted call's 133 ms, which the clock on the page states as over 200×; the hero and the page's description now say the same.
210 lines
10 KiB
TypeScript
210 lines
10 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
||
import { read, serveExport } from './export'
|
||
import { contract, issuer, type Options, type Seen } from './issuer'
|
||
|
||
/**
|
||
* THE APEX IS THE LANDING FOR A STRANGER AND THE APP FOR A MEMBER.
|
||
*
|
||
* `/` is two things at once and neither may cost the other. It is hanzo.ai's
|
||
* search result and its share card, so the export has to keep answering a
|
||
* crawler with the whole landing and the metadata that describes it; and it is
|
||
* the address a reader who is ALREADY signed in types, who wants the app and
|
||
* not the pitch for it — so for them `/` IS the app (app/_apex.tsx), at the same
|
||
* address, with nowhere to be sent.
|
||
*
|
||
* `output: 'export'` decides HOW. There is no server at runtime — no
|
||
* middleware, no `redirect()`, no cookie read while rendering — so `/` ships as
|
||
* one static file with the landing in it, the same file for everybody, and the
|
||
* only thing that can tell the two readers apart runs in the browser. The
|
||
* consequence this file exists for is that a naive answer PAINTS THE PITCH
|
||
* FIRST and swaps it afterwards, which is worse than not doing it at all.
|
||
*
|
||
* So the measurement is a frame, not a URL: every frame the document draws
|
||
* before the app is mounted is asked whether the landing's header is on it.
|
||
*/
|
||
|
||
/** What `app/page.tsx` declares. The head has to keep saying exactly this. */
|
||
const TITLE = 'Hanzo AI — Better, Faster, Cheaper. Pick Three.'
|
||
const DESCRIPTION =
|
||
'Better, cheaper, faster. Pick all three. Frontier AI up to 200× faster, 400× cheaper. Build with autonomous coding agents, foundation models, and open cloud infrastructure.'
|
||
|
||
const PAINTS = 'gate.paints'
|
||
|
||
/**
|
||
* Record, per document, whether the browser ever painted it.
|
||
*
|
||
* Installed before anything else in every document, so the observer is already
|
||
* watching when the apex's own boot script runs. `buffered: true` because a
|
||
* paint that happened before the observer was constructed is still a paint.
|
||
* `requestAnimationFrame` is kept beside it as the weaker of the two signals —
|
||
* it fires at a rendering opportunity, which precedes a paint but does not
|
||
* prove one — because a run where the frame fired and the paint did not is
|
||
* worth being able to read in the failure message.
|
||
*/
|
||
async function watchPaint(page: Page): Promise<void> {
|
||
await page.addInitScript((key: string) => {
|
||
const here = location.pathname
|
||
const mark = (how: string) => {
|
||
try {
|
||
const seen = JSON.parse(localStorage.getItem(key) || '{}')
|
||
if (!seen[here]) seen[here] = []
|
||
if (!seen[here].includes(how)) seen[here].push(how)
|
||
localStorage.setItem(key, JSON.stringify(seen))
|
||
} catch {
|
||
/* a browser that refuses storage cannot be measured this way; the
|
||
assertions below read an absent record as an absent paint, so the
|
||
store refusing would report a false pass — it does not refuse here */
|
||
}
|
||
}
|
||
try {
|
||
new PerformanceObserver((list) => {
|
||
for (const entry of list.getEntries()) mark(entry.name)
|
||
}).observe({ type: 'paint', buffered: true })
|
||
} catch {
|
||
/* no paint timing in this engine */
|
||
}
|
||
requestAnimationFrame(() => mark('frame'))
|
||
}, PAINTS)
|
||
}
|
||
|
||
const paints = (page: Page): Promise<Record<string, string[]>> =>
|
||
page.evaluate((key: string) => JSON.parse(localStorage.getItem(key) || '{}'), PAINTS)
|
||
|
||
/**
|
||
* A session that can be ENDED — the one thing `signIn` cannot give.
|
||
*
|
||
* The shared helper writes its tokens from an init script, which runs in every
|
||
* document of the tab. That is right for every other spec and fatal for this
|
||
* one: `logout()` clears the browser and navigates, and the init script puts
|
||
* the tokens straight back before the page it lands on can read them, so the
|
||
* reader arrives signed in again and is forwarded into the room. Seeded once,
|
||
* with the tab itself remembering that it has been, the sign-out is real.
|
||
*/
|
||
async function seedOnce(page: Page, options?: Options): Promise<Seen> {
|
||
const seen = await issuer(page, options)
|
||
await page.addInitScript(() => {
|
||
if (sessionStorage.getItem('gate.seeded')) return
|
||
sessionStorage.setItem('gate.seeded', '1')
|
||
const part = (v: unknown) =>
|
||
btoa(JSON.stringify(v)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||
const exp = Math.floor(Date.now() / 1000) + 3600
|
||
const token = `${part({ alg: 'none', typ: 'JWT' })}.${part({
|
||
sub: 'gate',
|
||
name: 'gate',
|
||
email: '[email protected]',
|
||
exp,
|
||
orgs: [{ org: 'hanzo', role: 'owner' }],
|
||
})}.`
|
||
localStorage.setItem('hanzo_iam_access_token', token)
|
||
localStorage.setItem('hanzo_iam_id_token', token)
|
||
localStorage.setItem('hanzo_iam_expires_at', String(exp * 1000))
|
||
})
|
||
return seen
|
||
}
|
||
|
||
test.describe('the apex', () => {
|
||
let base: string
|
||
let stop: () => Promise<void>
|
||
|
||
test.beforeAll(async () => {
|
||
const served = await serveExport()
|
||
base = served.url
|
||
stop = served.close
|
||
})
|
||
test.afterAll(async () => stop())
|
||
|
||
test('still describes itself to a crawler', async () => {
|
||
// The bytes, not the browser: a crawler reads the file and runs none of it.
|
||
const html = read('index.html')
|
||
expect(html, 'the apex title').toContain(`<title>${TITLE}</title>`)
|
||
expect(html, 'the apex description').toContain(DESCRIPTION)
|
||
expect(html, 'the canonical is the apex itself').toContain(
|
||
'<link rel="canonical" href="https://hanzo.ai"/>',
|
||
)
|
||
expect(html, 'the share card').toContain('property="og:title"')
|
||
expect(html, 'the share card names the apex').toContain('content="https://hanzo.ai"')
|
||
expect(html, 'the twitter card').toContain('name="twitter:card"')
|
||
// And the landing itself is still IN the file. The forwarding is a script;
|
||
// if it had become a build-time decision this would be a shell.
|
||
expect(html, 'the landing ships in the apex document').toContain('data-slot="masthead"')
|
||
expect(html.length, 'the whole landing, not a stub').toBeGreaterThan(100_000)
|
||
})
|
||
|
||
test('answers a stranger with the landing, and stays at /', async ({ page }) => {
|
||
await watchPaint(page)
|
||
await page.goto(`${base}/`)
|
||
await expect(page.locator('header[data-slot="masthead"]')).toBeVisible({ timeout: 20_000 })
|
||
expect(new URL(page.url()).pathname, 'nobody signed in, nobody forwarded').toBe('/')
|
||
// The landing painted, which is both the point and the proof that the probe
|
||
// above can see a paint at all — an assertion that only ever reads "no
|
||
// paint" is an assertion that would pass on a broken probe. Polled: the
|
||
// observer hears a paint in a task after the frame, so the masthead can be
|
||
// laid out and visible a moment before its paint is on the record.
|
||
await expect
|
||
.poll(async () => (await paints(page))['/'] ?? [], { message: 'the landing paints for a stranger' })
|
||
.toContain('first-contentful-paint')
|
||
})
|
||
|
||
|
||
|
||
test('answers a signed-in reader with the app, at /, and never paints the pitch at them', async ({ page }) => {
|
||
// Every frame until the app is up: is the landing's header on it?
|
||
await page.addInitScript(() => {
|
||
const seen: string[] = []
|
||
;(window as unknown as { __frames: string[] }).__frames = seen
|
||
const look = () => {
|
||
if (document.querySelector('[data-slot="app-rail"]')) return
|
||
const header = document.querySelector('header[data-slot="masthead"]')
|
||
if (header?.checkVisibility()) seen.push('landing')
|
||
requestAnimationFrame(look)
|
||
}
|
||
requestAnimationFrame(look)
|
||
})
|
||
await page.route(
|
||
(u) => u.pathname.startsWith('/v1/') && !u.pathname.startsWith('/v1/iam/'),
|
||
(route) => {
|
||
const path = new URL(route.request().url()).pathname
|
||
// A reader who pays: the app stands behind the organization gate (app/_web.tsx).
|
||
if (path === '/v1/billing/tier') return route.fulfill({ json: { tier: { name: 'dev', displayName: 'Dev' }, balance: { currency: 'usd', prepaidAvailable: 0 } } })
|
||
if (path === '/v1/billing/balance') return route.fulfill({ json: { balance: 0, holds: 0, available: 0 } })
|
||
return route.request().url().includes('/stream')
|
||
? route.fulfill({ status: 200, contentType: 'text/event-stream', body: '' })
|
||
: route.fulfill({ json: { sessions: [], data: [] } })
|
||
},
|
||
)
|
||
await seedOnce(page)
|
||
await page.goto(base + '/')
|
||
await expect(page.locator('main textarea').first(), 'the app opens in Chat').toBeVisible({ timeout: 20_000 })
|
||
const column = page.getByRole('navigation', { name: 'Hanzo' })
|
||
await expect(page.getByRole('navigation', { name: 'Products' }).getByRole('button', { name: 'Chat', exact: true }), 'the app’s rail leads with Chat').toHaveAttribute('aria-current', 'page')
|
||
await expect(column.getByText('Hanzo AI', { exact: true }), 'the app names itself').toBeVisible()
|
||
expect(new URL(page.url()).pathname, 'the app is the apex itself, not a trip').toBe('/')
|
||
expect(await page.evaluate(() => (window as unknown as { __frames: string[] }).__frames), 'no frame showed the landing').toEqual([])
|
||
await expect(page.locator('header[data-slot="masthead"]')).toBeHidden()
|
||
})
|
||
|
||
test('signs out on this site: IAM is told by calls answered here, and the page is /login?from=logout', async ({ page }) => {
|
||
// Nothing of hanzo.id is navigated to. The tokens go back and the session
|
||
// ends with calls to this origin (lib/auth/client `useSignOut`, @hanzo/iam
|
||
// `end`), and the page a sign-out lands on does not ask for the session
|
||
// just ended.
|
||
const seen = await seedOnce(page, { site: base })
|
||
const posted = () => seen.posted.map((p) => p.path)
|
||
await page.goto(`${base}/pricing`)
|
||
|
||
const me = page.locator('header[data-slot="masthead"] button[aria-haspopup="menu"]')
|
||
await expect(me).toBeVisible({ timeout: 20_000 })
|
||
await me.click()
|
||
const out = page.getByRole('menuitem', { name: 'Sign out' })
|
||
await expect(out).toBeVisible({ timeout: 20_000 })
|
||
await out.click()
|
||
|
||
await page.waitForURL((u) => u.pathname === '/login' && u.searchParams.get('from') === 'logout', { timeout: 20_000 })
|
||
await expect(page.getByRole('region', { name: /^(Log in|Sign up)$/ }).getByLabel('Email'), 'the sign-in, to come straight back').toBeVisible({ timeout: 20_000 })
|
||
expect(posted(), 'the tokens handed back and the session ended, here').toEqual(expect.arrayContaining(['/v1/iam/oauth/revoke', '/v1/iam/oauth/logout']))
|
||
expect(posted(), 'no ask for the session just ended').not.toContain('/v1/iam/login')
|
||
contract(seen)
|
||
expect(await page.evaluate(() => localStorage.getItem('hanzo_iam_access_token')), 'the browser is signed out').toBeNull()
|
||
})
|
||
})
|