Files
Cursor Agentandz c3af929c56 site: one API, one account, better cheaper faster
The bar is Models, Dev, Platform, Pricing, Docs, Enterprise, Log in, and Get API key. The homepage is seven sections. Launch pages stay in the tree and drop out of the index through lib/launch.ts. /platform forwards to /cloud. Model and repository floors come from the snapshots the build already keeps.

Co-authored-by: z <[email protected]>
2026-10-26 21:53:40 +00:00

300 lines
18 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 Locator, type Page } from '@playwright/test'
import { serveExport } from './export'
import { heard, stream } from './heard'
import { issuer, jwt, type Options, type Seen } from './issuer'
import { PLANS, serve } from './api'
import { signIn } from '../session'
import { AUP_VERSION, TERMS_VERSION } from '@hanzo/app/lib/terms'
/**
* A CHECKOUT'S SIGN-IN KEEPS THE CHECKOUT, AND GOES BACK TO IT.
*
* pay sends a buyer with no session to sign in with the checkout as `?next=`
* (hanzo-inc/pay lib/auth `login`). A campaign's checkout carries its plan, a
* coupon and the campaign's tags. The sign-in page is that checkout's first
* step (components/auth/page.tsx, lib/auth/sale): /login stays the sign-in, so
* a returning customer is one step from the checkout, the offer is said across
* the top in pay's words from commerce's quote, and the controls are pay's 56px
* at 16px. Every way in — a provider through IAM's authorize, a
* new address proven by a code, a known one by its password — lands on the
* checkout exactly as it was asked for (components/onboarding/flow.tsx), past
* the terms where IAM says they are owed.
*
* IAM and commerce are answered here (./issuer, the quote below); what is
* measured is the page, and the address the browser ends on.
*/
/** The checkout a campaign's buyer was on: plan, seats, coupon and the campaign. */
const CHECKOUT =
'/pay/confirm/card?plan=dev&quantity=1&coupon=50OFFFOUNDER&utm_source=hanzo&utm_medium=email&utm_campaign=again-welcome&utm_content=offer'
const NEXT = encodeURIComponent(CHECKOUT)
const TOKEN = jwt({ sub: 'ada', name: 'ada', email: '[email protected]', orgs: [{ org: 'ada', role: 'owner' }] })
let base: string
let stop: () => Promise<void>
test.beforeAll(async () => {
const served = await serveExport()
base = served.url
stop = served.close
})
test.afterAll(async () => stop())
/** The catalog as commerce quotes it for `coupon`: half off each paid plan's first month. */
function quote(coupon: string | null) {
if (!coupon) return PLANS
return PLANS.map((r) =>
r.price > 0 && !r.contactSales && r.category === 'personal'
? { ...r, couponCode: coupon, couponPercent: 50, couponFirstCents: r.price / 2 }
: { ...r, couponError: 'not_applicable' },
)
}
interface World extends Seen {
/** What IAM was told the person accepted (PUT /v1/iam/terms). */
accepted: unknown[]
}
/**
* The world, answered: this origin and IAM (./issuer), commerce's quote, and
* IAM's word on the account's terms — accepted, or owed by an account made a
* moment ago — wherever the page asks them (`api()`). Everything else off this
* machine fails.
*/
async function world(page: Page, { owed = false, ...options }: Pick<Options, 'reply' | 'authorize'> & { owed?: boolean } = {}): Promise<World> {
await page.route('**/*', (route) => (new URL(route.request().url()).origin === base ? route.continue() : route.abort()))
const seen = await issuer(page, { site: base, token: TOKEN, ...options })
const accepted: unknown[] = []
await page.route(
(u) => ['/v1/billing/plans', '/v1/iam/preferences', '/v1/iam/account', '/v1/iam/terms'].includes(u.pathname),
(route) => {
const req = route.request()
const url = new URL(req.url())
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' }
if (req.method() === 'OPTIONS') return route.fulfill({ status: 204, headers })
if (url.pathname === '/v1/billing/plans') return route.fulfill({ headers, json: quote(url.searchParams.get('coupon')) })
if (url.pathname === '/v1/iam/terms') {
accepted.push(JSON.parse(req.postData() || '{}'))
return route.fulfill({ headers, json: { terms: TERMS_VERSION, aup: AUP_VERSION } })
}
if (url.pathname === '/v1/iam/preferences') return route.fulfill({ headers, json: owed ? {} : { terms: { terms: TERMS_VERSION, aup: AUP_VERSION } } })
return route.fulfill({ headers, json: { name: 'ada', displayName: '', email: '[email protected]', createdTime: owed ? new Date().toISOString() : '2024-01-01T00:00:00Z' } })
},
)
return Object.assign(seen, { accepted })
}
/** The sign-in card. */
const card = (page: Page) => page.getByRole('region', { name: /^(Log in|Sign up)$/ })
/** The checkout the browser landed on, as a record of its query, once it is there. */
async function landed(page: Page): Promise<Record<string, string>> {
await page.waitForURL((u) => u.pathname === '/pay/confirm/card', { timeout: 30_000 })
return Object.fromEntries(new URL(page.url()).searchParams)
}
/** The checkout as it was asked for, billed to the one organization the account works in (`TOKEN`). */
const ASKED = { ...Object.fromEntries(new URL(CHECKOUT, 'https://hanzo.ai').searchParams), org: 'ada' }
/** A control's height and type, as the browser lays them out. */
async function measure(control: Locator): Promise<{ height: number; font: string }> {
return control.evaluate((el) => ({ height: Math.round(el.getBoundingClientRect().height), font: getComputedStyle(el).fontSize }))
}
/** Where the sign-in is written down for the callback (lib/auth/destination). */
const destination = (page: Page) => page.evaluate(() => sessionStorage.getItem('signin.destination'))
for (const width of [1440, 390]) {
test(`at ${width}px /login from a coupon checkout stays the sign-in, with its offer, at pay's measure`, async ({ page }) => {
await world(page)
await page.setViewportSize({ width, height: 900 })
await page.goto(`${base}/login?next=${NEXT}`)
await expect(page.getByRole('status').filter({ hasText: '50% off your first month of Pro' }), 'the offer, in pay’s words').toBeVisible({ timeout: 20_000 })
expect(new URL(page.url()).pathname, 'the route stands').toBe('/login')
expect(new URL(page.url()).searchParams.get('next'), 'the checkout rides on, whole').toBe(CHECKOUT)
await expect(page.locator('#hero-title')).toHaveText('Sign in to Hanzo AI')
await expect(page.locator('[data-slot="hero-demo"]'), 'no demo').toHaveCount(0)
await expect(page.getByText('Get Started', { exact: true }), 'no eyebrow').toHaveCount(0)
expect(await destination(page), 'written down for the callback, coupon and campaign in it').toBe(CHECKOUT)
for (const control of [
card(page).getByRole('button', { name: /Continue with Google$/ }),
card(page).getByRole('button', { name: /Continue with GitHub$/ }),
card(page).getByRole('button', { name: 'Continue with email' }),
card(page).getByLabel('Email'),
])
await expect.poll(() => measure(control), { message: 'pay’s 56px control at 16px' }).toEqual({ height: 56, font: '16px' })
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), 'no sideways scroll').toBe(true)
})
}
test('before its script runs, a checkout’s sign-in holds the landing’s hero off the screen, and a plain one does not', async ({ page }) => {
await world(page)
// No script past the page's own: what paints before hydration.
await page.route((u) => u.pathname.startsWith('/_next/static/') && u.pathname.endsWith('.js'), (route) => route.abort())
const hero = page.locator('[data-slot="hero"]')
await page.goto(`${base}/signup?next=${NEXT}`)
await expect(hero).toHaveCSS('visibility', 'hidden')
await page.goto(`${base}/signup?next=%2Fchat`)
await expect(hero).toHaveCSS('visibility', 'visible')
})
test('a provider from the checkout’s sign-up is one hop through IAM’s authorize, and lands on the checkout as it was asked for', async ({ page }) => {
const seen = await world(page, { authorize: (url) => `${base}/auth/callback?code=c0de&state=${url.searchParams.get('state')}` })
const events = await stream(page)
await page.setViewportSize({ width: 390, height: 844 })
await page.goto(`${base}/signup?next=${NEXT}`)
await card(page).getByRole('button', { name: /Continue with Google$/ }).click()
expect(await landed(page), 'plan, coupon and campaign, exactly').toEqual(ASKED)
expect(seen.authorize, 'one authorize').toHaveLength(1)
const q = seen.authorize[0].searchParams
expect(seen.authorize[0].origin, 'on this site').toBe(base)
expect(q.get('provider')).toBe('provider-google')
expect(q.get('signup'), 'asked as an account to make').toBe('true')
expect(q.get('client_id')).toBe('hanzo-app')
expect(q.get('redirect_uri')).toBe(`${base}/auth/callback`)
expect(q.get('state'), 'the CSRF binding').toBeTruthy()
expect(await heard(events, 'signup_submitted')).toMatchObject({ method: 'google' })
})
test('a new address from the checkout’s sign-up is proven by the link’s code, made, and lands on the checkout as it was asked for', async ({ page }) => {
const seen = await world(page, { reply: { '/v1/iam/login': (body) => ({ status: 'ok', data: body.create ? 'c0de' : 'SignupRequired' }) } })
await page.goto(`${base}/signup?next=${NEXT}`)
await card(page).getByLabel('Email').fill('[email protected]')
// The link at once, no password asked: the account the terms make has none.
await card(page).getByRole('button', { name: 'Continue with email' }).click()
await card(page).getByRole('button', { name: 'Enter verification code' }).click()
await card(page).getByRole('textbox', { name: 'Verification code' }).fill('123456')
await card(page).getByRole('button', { name: 'Verify email address' }).click()
await page.getByRole('checkbox', { name: /^I agree to Hanzo's Terms of Service and Acceptable Use Policy/ }).click()
await page.getByRole('button', { name: 'Create account' }).click()
expect(await landed(page)).toEqual(ASKED)
expect(seen.posted.map((p) => p.path)).toEqual(['/v1/iam/verification-codes', '/v1/iam/login', '/v1/iam/login'])
expect(JSON.parse(seen.posted[2].body), 'the terms, accepted with the account').toMatchObject({ create: true, terms: TERMS_VERSION, aup: AUP_VERSION })
expect(seen.authorize, 'no authorize page').toEqual([])
})
test('an account that exists signs in from the checkout’s sign-in and lands on the checkout as it was asked for', async ({ page }) => {
const seen = await world(page, { reply: { '/v1/iam/auth/identifier': { status: 'ok', data: { account: true, password: true } } } })
await page.goto(`${base}/login?next=${NEXT}`)
await card(page).getByLabel('Email').fill('[email protected]')
// An account with a password is sent its link at once, the password a press away.
await card(page).getByRole('button', { name: 'Continue with email' }).click()
await card(page).getByRole('button', { name: 'Enter password instead' }).click()
await card(page).getByRole('textbox', { name: 'Password' }).fill('correct horse')
await card(page).getByRole('button', { name: 'Continue', exact: true }).click()
expect(await landed(page)).toEqual(ASKED)
expect(seen.posted.map((p) => p.path), 'the link, then the password in its place').toEqual(['/v1/iam/verification-codes', '/v1/iam/login'])
})
test('an account a provider made a moment ago agrees to the terms once, then lands on the checkout as it was asked for', async ({ page }) => {
const seen = await world(page, { owed: true, authorize: (url) => `${base}/auth/callback?code=c0de&state=${url.searchParams.get('state')}` })
await page.goto(`${base}/signup?next=${NEXT}`)
await card(page).getByRole('button', { name: /Continue with GitHub$/ }).click()
await page.waitForURL((u) => u.pathname === '/onboarding', { timeout: 30_000 })
await expect(page.getByRole('heading', { name: 'Create your account' })).toBeVisible({ timeout: 30_000 })
await page.getByRole('checkbox', { name: /^I agree to Hanzo's Terms of Service and Acceptable Use Policy/ }).click()
await page.getByRole('button', { name: 'Create account' }).click()
expect(await landed(page)).toEqual(ASKED)
expect(seen.accepted, 'IAM recorded the versions').toEqual([{ terms: TERMS_VERSION, aup: AUP_VERSION }])
})
test('a checkout with no coupon keeps the route’s mode and the checkout’s measure', async ({ page }) => {
await world(page)
const plain = '/pay/confirm/card?plan=max-5x'
await page.goto(`${base}/login?next=${encodeURIComponent(plain)}`)
await expect(page.locator('#hero-title')).toHaveText('Sign in to Hanzo AI', { timeout: 20_000 })
expect(new URL(page.url()).pathname, 'no coupon, no move').toBe('/login')
await expect(page.locator('[data-slot="offer"]')).toHaveCount(0)
await expect(page.locator('[data-slot="hero-demo"]')).toHaveCount(0)
// Written once the page has asked IAM who is here (components/auth/panel `Gate`), after the title is drawn.
await expect.poll(() => destination(page)).toBe(plain)
})
test('a sign-in IAM refused tries again on the checkout’s own sign-in', async ({ page }) => {
await world(page)
await page.addInitScript((to) => {
if (!sessionStorage.getItem('gate.aimed')) {
sessionStorage.setItem('gate.aimed', '1')
sessionStorage.setItem('signin.destination', to)
}
}, CHECKOUT)
await page.goto(`${base}/auth/callback?error=access_denied`)
await page.getByRole('button', { name: 'Try again' }).click()
await page.waitForURL((u) => u.pathname === '/login')
expect(new URL(page.url()).searchParams.get('next')).toBe(CHECKOUT)
await expect(page.getByRole('status').filter({ hasText: '50% off your first month of Pro' }), 'the offer is said again').toBeVisible({ timeout: 20_000 })
})
test('/pricing: a paid plan is Get and its name, the sign-up carrying its checkout signed out, the checkout itself signed in', async ({ page }) => {
await serve(page)
await page.goto(`${base}/pricing`)
const pro = page.getByRole('region', { name: 'Pro', exact: true }).getByRole('link', { name: 'Get Pro', exact: true })
const out = new URL((await pro.getAttribute('href', { timeout: 20_000 }))!, base)
expect(out.pathname, 'signed out: the sign-up').toBe('/signup')
expect(new URL(out.searchParams.get('next')!, base).pathname).toBe('/pay/confirm/card')
expect(new URL(out.searchParams.get('next')!, base).searchParams.get('plan')).toBe('dev')
await expect(page.getByRole('region', { name: 'Max', exact: true }).getByRole('link', { name: 'Get Max', exact: true })).toHaveCount(1)
await signIn(page)
await page.reload()
// Signed in: the picker, which asks for terms still owed and opens the checkout.
await expect.poll(async () => new URL((await pro.getAttribute('href'))!, base).pathname, { timeout: 20_000, message: 'signed in: through the picker' }).toBe('/onboarding')
const next = new URL(new URL((await pro.getAttribute('href'))!, base).searchParams.get('next')!, base)
expect([next.pathname, next.searchParams.get('plan')]).toEqual(['/pay/confirm/card', 'dev'])
})
test('Free chosen on /pricing, then a paid plan: the new account still agrees to the terms before its checkout', async ({ page }) => {
const seen = await world(page, { owed: true, authorize: (url) => `${base}/auth/callback?code=c0de&state=${url.searchParams.get('state')}` })
await serve(page)
await page.goto(`${base}/pricing`)
await page.getByRole('region', { name: 'Free', exact: true }).getByRole('link', { name: 'Try Hanzo' }).click({ timeout: 20_000 })
await page.waitForURL((u) => u.pathname === '/signup')
expect(await page.evaluate(() => sessionStorage.getItem('signin.free')), 'Free is written down for this tab').toBe('1')
await page.goto(`${base}/signup?next=${NEXT}`)
await card(page).getByRole('button', { name: /Continue with Google$/ }).click()
await page.waitForURL((u) => u.pathname === '/onboarding', { timeout: 30_000 })
await expect(page.getByRole('heading', { name: 'Create your account' }), 'the terms, not the shortcut Free takes').toBeVisible({ timeout: 30_000 })
await page.getByRole('checkbox', { name: /^I agree to Hanzo's Terms of Service and Acceptable Use Policy/ }).click()
await page.getByRole('button', { name: 'Create account' }).click()
expect(await landed(page)).toEqual(ASKED)
expect(seen.accepted).toEqual([{ terms: TERMS_VERSION, aup: AUP_VERSION }])
})
test('a quote commerce never answers holds the checkout no longer than a moment', async ({ page }) => {
await world(page)
await signIn(page, { orgs: [{ org: 'ada', role: 'owner' }] })
// Registered last, so it answers first: the quote hangs.
await page.route((u) => u.pathname === '/v1/billing/plans', () => {})
await page.addInitScript(() => sessionStorage.setItem('checkout.coupon', '50OFF'))
await page.goto(`${base}/onboarding?next=${encodeURIComponent('/pay/confirm/card?plan=dev')}`)
expect(await landed(page), 'the checkout as it was asked for, no coupon it was never quoted').toEqual({ plan: 'dev', org: 'ada' })
})
test('a coupon this tab keeps, quoted for the plan, rides on to a checkout that names none', async ({ page }) => {
await world(page)
await signIn(page, { orgs: [{ org: 'ada', role: 'owner' }] })
await page.addInitScript(() => sessionStorage.setItem('checkout.coupon', '50OFF'))
await page.goto(`${base}/onboarding?next=${encodeURIComponent('/pay/confirm/card?plan=dev&returnUrl=https%3A%2F%2Fhanzo.ai%2F')}`)
expect(await landed(page)).toEqual({ plan: 'dev', returnUrl: 'https://hanzo.ai/', coupon: '50OFF', org: 'ada' })
})
// The bar's control is Get API Key. The hero still holds the sign-up panel.
for (const width of [1440, 390]) {
test(`at ${width}px the bar’s Get API Key is the console, and the hero still signs up`, async ({ page }) => {
await world(page)
await page.setViewportSize({ width, height: 900 })
await page.goto(`${base}/`)
const panel = page.locator('[data-landing] [data-slot="hero"] [data-slot="signin"]')
await expect(panel.getByLabel('Email')).toBeFocused({ timeout: 20_000 })
const bar = page.getByRole('banner')
if (width < 1024)
await expect(async () => {
await bar.getByRole('button', { name: 'Open menu' }).click()
await expect(page.getByRole('dialog', { name: 'Menu' })).toBeVisible({ timeout: 1000 })
}).toPass({ timeout: 15000 })
const key = page.getByRole(width < 1024 ? 'dialog' : 'banner').getByRole('link', { name: /^Get API Key/ })
await expect(key).toHaveAttribute('href', 'https://platform.hanzo.ai/api-keys')
await expect(panel.getByLabel('Email')).toBeVisible()
})
}