Files

460 lines
26 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 })
// A member who paid has nothing to buy: no Continue with plan, and no View plans beside it.
await expect(page.getByRole('link', { name: 'Continue with plan' })).toHaveCount(0)
await expect(page.getByRole('link', { name: 'View plans' })).toHaveCount(0)
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"]')
}
// A row that scrolls on its own (the share card's topics) holds what is past its
// edge: scrolled to, not off the screen, so long as the row itself fits.
const scrolled = (e: Element) => {
for (let a = e.parentElement; a; a = a.parentElement) {
if (/^(auto|scroll)$/.test(getComputedStyle(a).overflowX)) return a.getBoundingClientRect().right <= edge
}
return false
}
const pane = document.querySelector('main, [data-slot="pane"], section') ?? document.body
const past = [...pane.querySelectorAll('*')].filter((e) => seen(e) && e.getBoundingClientRect().right > edge && !scrolled(e)).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 leads(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.getByRole('button', { name: 'Submit application' }).click()
await expect(out.getByRole('heading', { name: 'Create an account to finish 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)
}
})
test('a stranger arriving at /beta starts the application first, creates an account at the end, and Edit answers returns to the questionnaire', async ({ page }) => {
await local(page)
await quote(page)
await leads(page)
// Stale filed state in localStorage / hz_beta_saved from a prior session never blocks a signed-out visitor on load.
await page.addInitScript(() => {
if (sessionStorage.getItem('beta.stale.seeded')) return
sessionStorage.setItem('beta.stale.seeded', '1')
const stale = JSON.stringify({ program: 'beta', step: 7, filed: true, got: { builds: ['AI agents'] } })
localStorage.setItem('hanzo.beta', stale)
localStorage.setItem('hz_beta_saved', stale)
})
await page.goto(`${base}/beta`)
await page.locator('html[data-hydrated]').waitFor({ state: 'attached', timeout: 30_000 })
await expect(page.getByRole('heading', { name: 'Get there first.' })).toBeVisible({ timeout: 20_000 })
await expect(page.getByText('Save your application.')).toHaveCount(0)
await expect(page.getByRole('button', { name: 'Edit answers' })).toHaveCount(0)
// Jump to the final step with three answers in, submit, and verify account creation appears at the end.
await page.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 page.reload()
await page.locator('html[data-hydrated]').waitFor({ state: 'attached', timeout: 30_000 })
await page.getByRole('button', { name: 'Submit application' }).click()
await expect(page.getByRole('heading', { name: 'Create an account to finish your application.' })).toBeVisible({ timeout: 20_000 })
await expect(page.getByRole('link', { name: 'Create account' })).toHaveAttribute('href', /\/signup\?next=%2Fbeta/)
await expect(page.getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', /\/login\?next=%2Fbeta/)
// Edit answers is offered after submitting answers and returns to the questionnaire.
const edit = page.getByRole('button', { name: 'Edit answers' })
await expect(edit).toBeVisible()
await edit.click()
await expect(page.getByRole('button', { name: 'Submit application' })).toBeVisible()
})