Files
hanzo.ai/e2e/gates/held.spec.ts
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

80 lines
3.3 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { serveExport } from './export'
import { signIn } from '../session'
import { ready } from './chat'
/**
* THE APP UNDER THE ORGANIZATION GATE (app/_web.tsx, @hanzo/app `App` → `Orgs`).
*
* Off hanzo.team no plan stands in front of the app, so billing decides nothing
* here: a plan read that never answers holds nothing, and one that fails as the
* tab comes back takes nothing away, the reader's draft included. While the
* session itself settles the app is drawn under a veil and is inert there
* (@hanzo/app Orgs.test.ts holds that half).
*/
let base: string
let stop: () => Promise<void>
test.beforeAll(async () => {
const served = await serveExport()
base = served.url
stop = served.close
})
test.afterAll(async () => stop())
const TIER = { user: 'gate', tier: { name: 'dev', displayName: 'Pro' }, balance: { currency: 'usd', effectiveAvailable: 0 } }
/** Every platform read answered empty but the plan, which answers when `plan` resolves. */
async function platform(page: Page, plan: Promise<void>) {
await page.route(
(u) => u.pathname.startsWith('/v1/') && !u.pathname.startsWith('/v1/iam/'),
async (route) => {
const path = new URL(route.request().url()).pathname
if (path === '/v1/billing/tier') {
await plan
return route.fulfill({ json: TIER })
}
if (path === '/v1/billing/balance') return route.fulfill({ json: { balance: 0, holds: 0, available: 0 } })
if (path.endsWith('/stream')) return route.fulfill({ status: 200, contentType: 'text/event-stream', body: '' })
return route.fulfill({ json: {} })
},
)
}
test('a plan read that never answers holds nothing: the app opens and takes keys', async ({ page }) => {
await signIn(page)
await platform(page, new Promise<void>(() => {}))
await page.setViewportSize({ width: 1440, height: 900 })
await page.goto(`${base}/chat`)
await ready(page)
const held = page.locator('[data-slot="held"]')
await expect(held).not.toHaveAttribute('inert', '')
await expect(held).not.toHaveAttribute('aria-hidden', 'true')
const field = page.getByRole('textbox', { name: 'Ask anything' })
await field.click()
await page.keyboard.type('asked while billing says nothing')
await expect(field).toHaveValue('asked while billing says nothing')
})
test('a billing read that fails as the tab comes back keeps the reader in the room, with their draft', async ({ page }) => {
await signIn(page)
await platform(page, Promise.resolve())
await page.setViewportSize({ width: 1440, height: 900 })
await page.goto(`${base}/chat`)
await ready(page)
const field = page.getByRole('textbox', { name: 'Ask anything' })
await field.fill('half a question')
// Billing fails from here on, and the tab comes back.
await page.route(
(u) => u.pathname === '/v1/billing/tier',
(route) => route.fulfill({ status: 502, json: { status: 502, detail: 'billing is down' } }),
)
await page.evaluate(() => window.dispatchEvent(new Event('focus')))
await page.waitForTimeout(1000)
await expect(page.locator('[data-slot="held"]')).not.toHaveAttribute('inert', '')
await expect(page.getByRole('region', { name: /Subscribe|Choose your plan|Organizations/ })).toHaveCount(0)
await expect(field, 'the draft is where it was').toHaveValue('half a question')
})