mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
The portal's header offered Continue with plan, View plans and Review answers (and Open app to a member already in the app). It offers Continue with plan to a member who has not subscribed and Review application to everyone. On a phone the link's Share button is its icon, so the link itself keeps the room.
416 lines
23 KiB
TypeScript
416 lines
23 KiB
TypeScript
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 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)
|
||
}
|
||
})
|