Files
hanzo.ai/e2e/gates/beta.spec.ts
zeekay 4fd349aa06 beta: the portal says less, and opting in and sharing are one press
Prose that carried no decision or fact is gone: the hero, the questions, the
gate and every step are a heading and at most a line. What scores is said once
(the leaderboard step), and every claim the cloud does not keep is out.

Step 1 is one share card (the member's link, Copy, the device's share sheet on
phones, seven channels as icons, Customize behind a text button) and a quiet
invite-by-email card. Step 3 is one searched, grouped list with one control a
row: On when `opens` says so, Get access when it does not, Requested at once
after the press. Stars and follows moved under the leaderboard as Earn more.

The 1,800-line dashboard is a shell over share, rank, betas and alpha; what
they read is one hook (standing.ts); the questions are data (program.ts).
Dead access, card, hint and referral components are deleted. Every press is
44px, every input too, and a gate walks each screen at 390px.

scripts/brands and scripts/prebuild are TypeScript now; the three marks the
share row draws ship in a JSON only /beta loads. /beta's title and card say
what the page says.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-26 03:29:13 -07:00

406 lines
23 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test, type Page, type Route } from '@playwright/test'
import { serveExport } from './export'
import { BETAS, key } from '@hanzo/app/lib/betas'
import { signIn } from '../session'
/**
* /beta — a stranger's questionnaire in three bars, a third of it answered to
* apply, "Continue with plan" once that third is in, and local persistence across
* login/signup. A signed-in member has the portal on the same page: referral and
* social sharing, leaderboard, feature-flagged ecosystem betas, Alpha access.
*/
let base: string
let stop: () => Promise<void>
test.beforeAll(async () => {
const served = await serveExport()
base = served.url
stop = served.close
})
test.afterAll(async () => stop())
/** Nothing off this machine answers. */
const local = (page: Page) => page.route('**/*', (route) => (new URL(route.request().url()).origin === base ? route.continue() : route.abort()))
/** An answer from the API, with the CORS the browser asks for. */
const reply = (route: Route, json: unknown) => {
const req = route.request()
const headers = {
'access-control-allow-origin': req.headers().origin ?? base,
'access-control-allow-credentials': 'true',
'access-control-allow-headers': 'content-type, authorization, x-org-id',
'access-control-allow-methods': 'GET, POST, PUT',
}
if (req.method() === 'OPTIONS') return route.fulfill({ status: 204, headers })
return route.fulfill({ headers, json })
}
/** Commerce's quote for 50OFFBETA: half off Pro for three monthly cycles. */
const quote = (page: Page) =>
page.route(
(u) => u.pathname === '/v1/billing/plans' && u.searchParams.get('coupon') === '50OFFBETA',
(route) =>
reply(route, [
{ slug: 'free', name: 'Free', price: 0, couponError: 'not_applicable' },
{ slug: 'dev', name: 'Pro', price: 2000, couponCode: '50OFFBETA', couponPercent: 50, couponFirstCents: 1000, couponMonths: 3 },
]),
)
/** Every lead the page files. */
const leads = async (page: Page) => {
const got: Record<string, unknown>[] = []
await page.route('**/v1/marketing/leads', (route) => {
if (route.request().method() === 'POST') got.push(JSON.parse(route.request().postData() || '{}'))
return reply(route, { lead: 'lead_1', offer: 'none' })
})
return got
}
const ANSWERED = { builds: ['AI agents'], first: ['Dev, our coding agent'], agents: ['Claude Code'] }
const open = async (page: Page) => {
await page.goto(`${base}/beta`)
await page.locator('html[data-hydrated]').waitFor({ state: 'attached', timeout: 30_000 })
await expect(page.getByText('Early access to Dev, Studio, Kai, Enso and Zen.')).toBeVisible({ timeout: 20_000 })
await page.getByRole('button', { name: /Beta tester/ }).click()
await expect(page.getByRole('heading', { name: 'What do you build?' })).toBeVisible({ timeout: 20_000 })
}
test('the questionnaire is three bars, and a third answered opens Continue with plan, which saves locally and opens Pro checkout with 50OFFBETA', async ({ page }) => {
await local(page)
await quote(page)
await leads(page)
await page.goto(`${base}/beta`)
await page.locator('html[data-hydrated]').waitFor({ state: 'attached', timeout: 30_000 })
await expect(page.getByRole('link', { name: 'Book a call' })).toHaveAttribute('href', /^https:\/\/cal\./)
await open(page)
const bar = page.getByRole('progressbar')
await expect(bar.locator(':scope > *')).toHaveCount(3)
await expect(page.getByText('Step 1 of 3')).toBeVisible()
await expect(page.getByText(/Question \d+ of/)).toHaveCount(0)
const start = Number(await bar.getAttribute('aria-valuenow'))
await page.getByRole('checkbox', { name: /AI agents/ }).click()
expect(Number(await bar.getAttribute('aria-valuenow'))).toBeGreaterThan(start)
await page.getByRole('button', { name: 'Continue' }).click()
await expect(page.getByRole('heading', { name: 'What do you want to try first?' })).toBeVisible()
await page.getByRole('checkbox', { name: /Dev, our coding agent/ }).click()
await page.getByRole('button', { name: 'Continue' }).click()
// Two of eight answered: a third is three, so the plan is not offered yet.
await expect(page.getByRole('heading', { name: 'Which coding agents do you use today?' })).toBeVisible()
await expect(page.locator('#beta-plan-shortcut')).toHaveCount(0)
await expect(page.getByText('Answer 1 more to apply.')).toBeVisible()
await page.getByRole('checkbox', { name: /Claude Code/ }).click()
const shortcut = page.locator('#beta-plan-shortcut')
await expect(shortcut).toBeVisible()
await shortcut.click()
await page.waitForURL((u) => u.pathname === '/pay/cart', { timeout: 20_000 })
const to = new URL(page.url())
expect(Object.fromEntries(to.searchParams)).toMatchObject({ coupon: '50OFFBETA', returnUrl: `${base}/chat` })
const kept = JSON.parse((await page.evaluate(() => localStorage.getItem('hanzo.beta'))) ?? 'null')
expect(kept).toMatchObject({ program: 'beta', got: ANSWERED })
})
test('skipping cannot leave fewer than a third answered, and the last step files nothing below it', async ({ page }) => {
await local(page)
await quote(page)
const filed = await leads(page)
await open(page)
// Eight questions, three needed: five may be skipped, a sixth would leave too few ahead.
const skip = page.getByRole('button', { name: 'Skip' })
for (let i = 0; i < 5; i++) await skip.click()
await expect(skip).toBeDisabled()
// A saved application on the last step with one answer: neither way to apply is open.
await page.evaluate(() => localStorage.setItem('hanzo.beta', JSON.stringify({ program: 'beta', step: 7, got: { builds: ['AI agents'] } })))
await page.reload()
await page.locator('html[data-hydrated]').waitFor({ state: 'attached', timeout: 30_000 })
await expect(page.locator('#beta-checkout')).toBeDisabled({ timeout: 20_000 })
await expect(page.getByRole('button', { name: 'Submit application' })).toBeDisabled()
await expect(page.getByText('Answer 2 more to apply.').first()).toBeVisible()
expect(filed).toHaveLength(0)
})
test('signed in on final step, Continue with plan files the application and opens checkout', async ({ page }) => {
await local(page)
await quote(page)
await signIn(page, { name: 'ada', email: '[email protected]' })
const filed = await leads(page)
await page.route('**/v1/marketing/beta', (route) => reply(route, {}))
await page.addInitScript(() => {
if (sessionStorage.getItem('beta.kept')) return
sessionStorage.setItem('beta.kept', '1')
localStorage.setItem(
'hanzo.beta',
JSON.stringify({
program: 'beta',
step: 7,
got: { builds: ['AI agents'], first: ['Dev, our coding agent'], role: 'Founder' },
}),
)
})
await page.goto(`${base}/beta`)
await expect(page.locator('#beta-checkout')).toBeVisible({ timeout: 20_000 })
await page.locator('#beta-checkout').click()
await page.waitForURL((u) => u.pathname === '/pay/cart', { timeout: 30_000 })
expect(filed).toHaveLength(1)
expect(filed[0]).toMatchObject({ email: '[email protected]', program: 'beta', source: 'hanzo.ai/beta' })
expect(new URL(page.url()).searchParams.get('coupon')).toBe('50OFFBETA')
})
test('a member who opens Betas from inside the app gets the portal, not the plain list', async ({ page }) => {
await local(page)
await quote(page)
await signIn(page, { name: 'ada', email: '[email protected]' })
await page.route('**/v1/marketing/beta', (route) => route.fulfill({ status: 404, json: {} }))
await page.route('**/v1/flag/platform', (route) => reply(route, { flags: {} }))
await page.route('**/v1/flag/optin', (route) => reply(route, { on: false, blocked: false, public: [] }))
await page.goto(`${base}/`)
await page.getByRole('button', { name: 'Betas' }).first().click({ timeout: 30_000 })
await expect(page.getByText('Get there first.')).toBeVisible({ timeout: 20_000 })
await expect(page.getByText('Ecosystem Betas & Early Access')).toHaveCount(0)
await expect(page.getByText('New run')).toHaveCount(0)
// The opt-in for future experimental features is the first thing on the page.
await expect(page.locator('[data-slot="optin"]')).toBeVisible()
// The pane's own fill shows: the site's page glow is not painted over it as a black slab.
await expect(page.locator('[data-slot="glow"]')).toHaveCount(0)
})
test('a member who files picks Pro or Free once, then has the portal in the app with no builder sidebar', async ({ page }) => {
await local(page)
await quote(page)
await signIn(page, { name: 'ada', email: '[email protected]' })
const filed = await leads(page)
await page.route('**/v1/marketing/beta', (route) =>
route.request().method() === 'PUT' ? reply(route, { applied: true, program: 'beta', betas: [], points: 0, place: 7, of: 40, actions: [] }) : route.fulfill({ status: 404, json: {} }),
)
await page.route('**/v1/flag/platform', (route) => reply(route, { flags: {} }))
await page.route('**/v1/billing/tier', (route) => reply(route, { plan: 'free', tier: { name: 'free' } }))
await page.route('**/v1/billing/balance', (route) => reply(route, { available: 0 }))
await page.route('**/v1/referral', (route) => reply(route, { code: 'ada7', link: 'https://hanzo.ai/beta?ref=ada7', counts: { total: 0, signup: 0, qualified: 0 }, referrals: [] }))
await page.addInitScript(() => {
if (sessionStorage.getItem('beta.kept')) return
sessionStorage.setItem('beta.kept', '1')
localStorage.setItem('hanzo.beta', JSON.stringify({ program: 'beta', step: 7, got: { builds: ['AI agents'], first: ['Dev, our coding agent'], role: 'Founder' } }))
})
await page.goto(`${base}/beta`)
await expect(page.getByRole('button', { name: 'Betas' })).toBeVisible({ timeout: 20_000 })
await expect(page.getByText('New run')).toHaveCount(0)
await page.getByRole('button', { name: 'Submit application' }).click()
await expect(page.getByRole('heading', { name: 'Your application is in. Pick your plan.' })).toBeVisible({ timeout: 20_000 })
expect(filed).toHaveLength(1)
await expect(page.locator('#beta-plan-pro')).toBeVisible()
await page.locator('#beta-plan-free').click()
await expect(page.getByRole('button', { name: /1\. Share/ })).toBeVisible({ timeout: 20_000 })
await expect(page.getByRole('heading', { name: 'Your application is in. Pick your plan.' })).toHaveCount(0)
// A wheel over the pane reaches the foot of the portal, on a desktop and on a phone.
// (scrollIntoView would also scroll a clipped overflow:hidden box, which no reader can.)
for (const size of [{ width: 1440, height: 900 }, { width: 390, height: 844 }]) {
await page.setViewportSize(size)
await page.mouse.move(size.width / 2, size.height / 2)
for (let i = 0; i < 12; i++) await page.mouse.wheel(0, 800)
await expect(page.getByRole('link', { name: 'Referral dashboard' })).toBeInViewport()
}
})
test('a signed-in member at /beta has the portal: referral sharing, leaderboard, feature-flagged ecosystem betas, and Alpha access', async ({ page }) => {
await local(page)
await quote(page)
await signIn(page, { name: 'ada', email: '[email protected]' })
await leads(page)
await page.route('**/v1/marketing/beta', (route) =>
reply(route, { applied: true, program: 'beta', betas: ['crm'], points: 30, place: 120, of: 2400, actions: [] }),
)
await page.route('**/v1/flag/platform', (route) => reply(route, { flags: {} }))
await page.route('**/v1/referral', (route) =>
reply(route, { code: 'ada7', link: 'https://hanzo.ai/beta?ref=ada7', counts: { total: 2, signup: 1, qualified: 1 }, referrals: [] }),
)
await page.goto(`${base}/beta?checkout=success&subscription=sub_1`)
await expect(page.getByRole('heading', { name: 'You’re in the Hanzo beta.' })).toBeVisible({ timeout: 20_000 })
await expect(page.getByRole('link', { name: /Open app/ }).first()).toBeVisible()
await expect(page.getByRole('link', { name: 'Referral dashboard' })).toHaveAttribute('href', '/referral')
// The portal only: no builder sidebar beside it.
await expect(page.getByText('New run')).toHaveCount(0)
await expect(page.getByText('Upgrade plan')).toHaveCount(0)
// Each channel shares the member's own link, tagged with the channel it went out on.
for (const [name, channel] of [['Share on X', 'x'], ['Share on LinkedIn', 'linkedin'], ['Share on Bluesky', 'bluesky']] as const) {
const href = decodeURIComponent((await page.getByRole('link', { name, exact: true }).getAttribute('href')) ?? '')
expect(href).toContain('ref=ada7')
expect(href).toContain(`utm_source=${channel}`)
expect(href).toContain('utm_campaign=beta_referral')
}
await page.getByRole('button', { name: /2\. Leaderboard/ }).click()
await expect(page.locator('[data-slot="beta-leaderboard"]')).toBeVisible()
await page.getByRole('button', { name: /3\. Ecosystem Betas/ }).click()
await expect(page.locator('[data-slot="ecosystem-betas"]')).toBeVisible()
await page.getByRole('button', { name: /4\. Alpha & Feedback/ }).click()
await expect(page.locator('[data-slot="alpha-access"]')).toBeVisible()
})
test('the leaderboard is the cloud\'s board: names only for those who listed, the reader below the top, and a choice to list', async ({ page }) => {
await local(page)
await quote(page)
await signIn(page, { name: 'ada', email: '[email protected]' })
await leads(page)
await page.route('**/v1/marketing/beta', (route) => reply(route, { applied: true, program: 'beta', betas: [], points: 3, score: 41, place: 7, of: 2400, actions: [] }))
await page.route('**/v1/flag/platform', (route) => reply(route, { flags: {} }))
await page.route('**/v1/referral', (route) => reply(route, { code: 'ada7', link: 'https://hanzo.ai/beta?ref=ada7', counts: { total: 2, signup: 1, qualified: 1 }, referrals: [] }))
const you = { rank: 7, name: null, you: true, score: 41, referrals: 2, qualified: 1, tokens: '1M-10M', applied: '2026-10-01' }
await page.route('**/v1/leaderboard/beta**', (route) =>
reply(route, {
rows: [
{ rank: 1, name: 'zeekay', you: false, score: 900, referrals: 30, qualified: 9, tokens: '100M-1B', applied: '2026-09-02' },
{ rank: 2, name: null, you: false, score: 410, referrals: 12, qualified: 3, tokens: '10M-100M', applied: '2026-09-04' },
],
you,
of: 2400,
tokens: true,
referrals: true,
}),
)
let listed = false
await page.route('**/v1/leaderboard/optin', (route) => {
if (route.request().method() === 'PUT') listed = JSON.parse(route.request().postData() || '{}').listed
return reply(route, route.request().method() === 'PUT' ? { listed, handle: 'ada', canSet: true } : { user: { listed, handle: '', canSet: true }, org: { listed: false, display: '', canManage: false } })
})
await page.goto(`${base}/beta?checkout=success&subscription=sub_1`)
await page.getByRole('button', { name: /2\. Leaderboard/ }).click({ timeout: 20_000 })
const board = page.locator('[data-slot="beta-leaderboard"]')
await expect(board.getByText('@zeekay')).toBeVisible()
await expect(board.getByText('A builder')).toBeVisible()
await expect(board.getByText('#7')).toBeVisible()
await expect(board.getByText('2,400 on the waitlist')).toBeVisible()
await board.getByRole('button', { name: 'Show my name on the board' }).click()
await expect(board.getByRole('button', { name: 'Take my name off the board' })).toBeVisible()
expect(listed).toBe(true)
})
/** A member with a record, and the flags the cloud holds for them. */
async function member(page: Page, flags: Record<string, boolean>) {
await local(page)
await quote(page)
await signIn(page, { name: 'ada', email: '[email protected]' })
await leads(page)
await page.route('**/v1/marketing/beta', (route) => reply(route, { applied: true, program: 'beta', betas: [], points: 3, place: 7, of: 40, actions: [] }))
await page.route('**/v1/flag/platform', (route) => reply(route, { flags }))
await page.route('**/v1/flag/optin', (route) => reply(route, { on: false, blocked: false, public: [] }))
await page.route('**/v1/referral', (route) => reply(route, { code: 'ada7', link: 'https://hanzo.ai/beta?ref=ada7', counts: { total: 0, signup: 0, qualified: 0 }, referrals: [] }))
}
const betas = async (page: Page) => {
await page.goto(`${base}/beta?checkout=success&subscription=sub_1`)
await page.getByRole('button', { name: /3\. Ecosystem Betas/ }).click({ timeout: 20_000 })
return page.locator('[data-slot="ecosystem-betas"]')
}
test('a beta row says what it is for this member: On when the flags open it, Get access when they do not, and Requested at once after the press', async ({ page }) => {
const [open, closed] = BETAS
const asked: unknown[] = []
await member(page, { [key(open.id)]: true })
await page.route('**/v1/marketing/beta/request', (route) => {
asked.push(JSON.parse(route.request().postData() || '{}'))
return reply(route, { applied: true, program: 'beta', betas: [closed.id], points: 3, place: 7, of: 40, actions: [] })
})
const list = await betas(page)
const on = list.locator(`[data-beta="${open.id}"]`)
await expect(on.getByText('On', { exact: true })).toBeVisible({ timeout: 20_000 })
await expect(on.getByRole('button')).toHaveCount(0)
const row = list.locator(`[data-beta="${closed.id}"]`)
await expect(row.getByText('On', { exact: true })).toHaveCount(0)
await row.getByRole('button', { name: `Get access to ${closed.name}` }).click()
await expect(row.getByText('Requested', { exact: true })).toBeVisible()
await expect(row.getByRole('button')).toHaveCount(0)
expect(asked).toEqual([{ betas: [closed.id] }])
// One list, searched: a name narrows it, a stranger's name empties it.
const find = list.getByRole('textbox', { name: 'Search betas' })
await find.fill(closed.name)
await expect(list.locator(`[data-beta="${closed.id}"]`)).toBeVisible()
await expect(list.locator(`[data-beta="${open.id}"]`)).toHaveCount(closed.name.toLowerCase().includes(open.name.toLowerCase()) ? 1 : 0)
await find.fill('zzzz-not-a-beta')
await expect(list.locator('[data-beta]')).toHaveCount(0)
})
test('staff are on everywhere: every beta is On and none can be asked for', async ({ page }) => {
await member(page, Object.fromEntries(BETAS.map((b) => [key(b.id), true])))
const list = await betas(page)
await expect(list.locator('[data-beta]')).toHaveCount(BETAS.length, { timeout: 20_000 })
await expect(list.locator('[data-beta]').getByText('On', { exact: true })).toHaveCount(BETAS.length)
await expect(list.getByRole('button', { name: /^Get access/ })).toHaveCount(0)
})
/** Whether the page fits a phone: nothing scrolls sideways, nothing reaches past the edge, and every press is 44px. */
const fits = (page: Page) =>
page.evaluate(() => {
const edge = window.innerWidth + 1
const seen = (e: Element) => {
const r = e.getBoundingClientRect()
const s = getComputedStyle(e)
return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none' && !e.closest('[aria-hidden="true"]')
}
const pane = document.querySelector('main, [data-slot="pane"], section') ?? document.body
const past = [...pane.querySelectorAll('*')].filter((e) => seen(e) && e.getBoundingClientRect().right > edge).map((e) => `${e.tagName} ${(e.textContent ?? '').slice(0, 24)}`)
const small = [...pane.querySelectorAll('button, a[href], input, textarea, [role="radio"], [role="checkbox"]')]
// The opt-in switch is @hanzo/app's (Experimental), and its hit area is its own to size.
.filter((e) => seen(e) && !e.closest('[data-slot="optin"]') && !(e instanceof HTMLInputElement && e.tabIndex < 0) && e.getBoundingClientRect().height < 43.5)
.map((e) => `${e.tagName} ${(e.getAttribute('aria-label') ?? e.textContent ?? '').slice(0, 24)} ${Math.round(e.getBoundingClientRect().height)}`)
return { sideways: document.scrollingElement!.scrollWidth > window.innerWidth, past: past.slice(0, 4), small: small.slice(0, 6) }
})
test('on a phone every screen fits: the program picker, a question, the last one, the gate, the plan, and each step of the portal', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await member(page, {})
const none = { sideways: false, past: [], small: [] }
const kept = (extra: Record<string, unknown> = {}) =>
page.evaluate((v) => localStorage.setItem('hanzo.beta', JSON.stringify({ program: 'beta', step: 7, got: { builds: ['AI agents'], first: ['Dev, our coding agent'], agents: ['Claude Code'], role: 'Founder' }, ...v })), extra)
// A stranger: the picker, a question, the last question, the gate.
const out = await page.context().newPage()
await local(out)
await quote(out)
await out.setViewportSize({ width: 390, height: 844 })
await out.goto(`${base}/beta`)
await out.locator('html[data-hydrated]').waitFor({ state: 'attached', timeout: 30_000 })
await expect(out.getByText('Early access to Dev, Studio, Kai, Enso and Zen.')).toBeVisible({ timeout: 20_000 })
expect(await fits(out), 'the program picker').toEqual(none)
await out.getByRole('button', { name: /Beta tester/ }).click()
await expect(out.getByRole('heading', { name: 'What do you build?' })).toBeVisible({ timeout: 20_000 })
expect(await fits(out), 'a question').toEqual(none)
await out.evaluate(() => localStorage.setItem('hanzo.beta', JSON.stringify({ program: 'beta', step: 7, got: { builds: ['AI agents'], first: ['Dev, our coding agent'], agents: ['Claude Code'], role: 'Founder' } })))
await out.reload()
await expect(out.getByRole('button', { name: 'Submit application' })).toBeVisible({ timeout: 20_000 })
expect(await fits(out), 'the last question').toEqual(none)
await out.evaluate(() => localStorage.setItem('hanzo.beta', JSON.stringify({ program: 'beta', step: 7, filed: true, got: { builds: ['AI agents'] } })))
await out.reload()
await expect(out.getByRole('heading', { name: 'Save your application.' })).toBeVisible({ timeout: 20_000 })
expect(await fits(out), 'the gate').toEqual(none)
await out.close()
// A member with no record yet: the plan once, then the four steps.
await page.route('**/v1/marketing/beta', (route) => (route.request().method() === 'PUT' ? reply(route, { applied: true, program: 'beta', betas: [], points: 0, place: 7, of: 40, actions: [] }) : route.fulfill({ status: 404, json: {} })))
await page.goto(`${base}/beta`)
await page.locator('html[data-hydrated]').waitFor({ state: 'attached', timeout: 30_000 })
await kept()
await page.reload()
await page.getByRole('button', { name: 'Submit application' }).click({ timeout: 20_000 })
await expect(page.getByRole('heading', { name: 'Your application is in. Pick your plan.' })).toBeVisible({ timeout: 20_000 })
expect(await fits(page), 'the plan').toEqual(none)
await page.locator('#beta-plan-free').click()
for (const [tab, name] of [[/1\. Share/, 'share'], [/2\. Leaderboard/, 'leaderboard'], [/3\. Ecosystem Betas/, 'betas'], [/4\. Alpha & Feedback/, 'alpha']] as const) {
await page.getByRole('button', { name: tab }).click({ timeout: 20_000 })
await page.waitForTimeout(400)
expect(await fits(page), name).toEqual(none)
}
})