mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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.
144 lines
7.7 KiB
TypeScript
144 lines
7.7 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test'
|
||
import { serveExport } from './export'
|
||
import { signIn } from '../session'
|
||
import { AUP_VERSION, TERMS_VERSION } from '@hanzo/app/lib/terms'
|
||
|
||
/**
|
||
* AN OFFER LINK OPENS PAY'S PLANS WITH ITS CODE AND ITS CAMPAIGN.
|
||
*
|
||
* /onboarding?coupon= keeps the code in the tab and, for a stranger, sends the
|
||
* reader through the sign-up and back carrying it and the campaign. Signed in,
|
||
* past the terms where IAM says they are owed, a new account is asked its one
|
||
* question under the offer, with Continue with Free beside it, and either
|
||
* answer opens pay's plans (/pay/cart) with the code, the campaign, the
|
||
* organization worked in and the way back to the app; an account past its first
|
||
* run opens the plans at once. With no code nothing is quoted.
|
||
*/
|
||
|
||
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 NOW = () => new Date(Date.now() - 60_000).toISOString()
|
||
const CAMPAIGN = { utm_source: 'hanzo', utm_medium: 'email', utm_campaign: 'poolparty' }
|
||
|
||
interface Heard {
|
||
/** Every catalog read, by its query. */
|
||
quotes: string[]
|
||
/** The organizations billing was asked about. */
|
||
tiers: string[]
|
||
}
|
||
|
||
/** The platform as a spec wants it: an account IAM made `made`, with the terms on record unless `owed`, on Free. */
|
||
async function platform(page: Page, { made = NOW(), owed = false }: { made?: string; owed?: boolean } = {}): Promise<Heard> {
|
||
const heard: Heard = { quotes: [], tiers: [] }
|
||
await page.route(
|
||
(u) => u.pathname.startsWith('/v1/') && !u.pathname.startsWith('/v1/iam/oauth/'),
|
||
(route) => {
|
||
const req = route.request()
|
||
const url = new URL(req.url())
|
||
const path = url.pathname
|
||
if (path === '/v1/billing/tier') {
|
||
heard.tiers.push(req.headers()['x-org-id'] ?? '-')
|
||
return route.fulfill({
|
||
json: {
|
||
user: 'gate',
|
||
plan: '',
|
||
tier: { name: 'free', displayName: 'free', dailyCreditsCents: 0, maxAgents: 1, unlimitedAgents: false },
|
||
balance: { currency: 'usd', prepaidAvailable: 0, creditsRemaining: 0, dailyRemaining: 0, effectiveAvailable: 0 },
|
||
},
|
||
})
|
||
}
|
||
if (path === '/v1/billing/balance') return route.fulfill({ json: { balance: 0, holds: 0, available: 0 } })
|
||
if (path === '/v1/iam/preferences') return route.fulfill({ json: owed ? {} : { terms: { terms: TERMS_VERSION, aup: AUP_VERSION } } })
|
||
if (path === '/v1/iam/terms') return route.fulfill({ json: { terms: TERMS_VERSION, aup: AUP_VERSION } })
|
||
if (path === '/v1/billing/plans') {
|
||
heard.quotes.push(url.search)
|
||
const coupon = url.searchParams.get('coupon')
|
||
return route.fulfill({
|
||
json: coupon ? [{ slug: 'dev', name: 'Pro', price: 2000, couponCode: coupon, couponPercent: 50, couponFirstCents: 1000 }] : [],
|
||
})
|
||
}
|
||
if (path === '/v1/iam/account') return route.fulfill({ json: { name: 'gate', displayName: '', email: '[email protected]', createdTime: made } })
|
||
if (path.endsWith('/stream')) return route.fulfill({ status: 200, contentType: 'text/event-stream', body: '' })
|
||
return route.fulfill({ json: {} })
|
||
},
|
||
)
|
||
return heard
|
||
}
|
||
|
||
/** pay's plans, once the page has left for them: the query, read as one record. */
|
||
async function plans(page: Page): Promise<Record<string, string>> {
|
||
await page.waitForURL((u) => u.pathname === '/pay/cart', { timeout: 30_000 })
|
||
return Object.fromEntries(new URL(page.url()).searchParams)
|
||
}
|
||
|
||
// A stranger is asked the question too, under the offer, and signs in on pay's
|
||
// plans (b2de8afa): the tab keeps the code, and an answer opens the plans with
|
||
// it, the campaign and the way back.
|
||
test('a stranger with a coupon and a next is asked the question, and Personal opens pay\'s plans carrying the code and the way back', async ({ page }) => {
|
||
await platform(page)
|
||
await page.goto(`${base}/onboarding?${new URLSearchParams({ next: '/dev', coupon: '50OFF' })}`)
|
||
await expect(page.getByRole('heading', { name: 'How will you use Hanzo?' })).toBeVisible({ timeout: 30_000 })
|
||
await page.getByRole('link', { name: /^Personal/ }).click()
|
||
expect(await plans(page)).toEqual({ coupon: '50OFF', returnUrl: `${base}/dev`, tab: 'individual' })
|
||
})
|
||
|
||
test('a stranger with a coupon keeps it and the campaign, and Business opens pay\'s plans with both', async ({ page }) => {
|
||
await platform(page)
|
||
await page.setViewportSize({ width: 390, height: 844 })
|
||
await page.goto(`${base}/onboarding?${new URLSearchParams({ coupon: '50OFF', ...CAMPAIGN })}`)
|
||
await expect(page.getByRole('heading', { name: 'How will you use Hanzo?' })).toBeVisible({ timeout: 30_000 })
|
||
await expect.poll(() => page.evaluate(() => sessionStorage.getItem('checkout.coupon')), { message: 'the tab keeps the code' }).toBe('50OFF')
|
||
await page.getByRole('link', { name: /^Business/ }).click()
|
||
expect(await plans(page)).toEqual({ coupon: '50OFF', ...CAMPAIGN, returnUrl: `${base}/chat`, tab: 'team' })
|
||
})
|
||
|
||
for (const width of [1440, 390]) {
|
||
test(`at ${width}px a new account with an offer link is asked its question under the offer, Free beside it, and an answer opens pay's plans with the code and campaign`, async ({ page }) => {
|
||
await signIn(page, { orgs: [{ org: 'gate-org', role: 'owner' }] })
|
||
await platform(page)
|
||
await page.setViewportSize({ width, height: 900 })
|
||
await page.goto(`${base}/onboarding?${new URLSearchParams({ coupon: '50OFF', ...CAMPAIGN })}`)
|
||
await expect(page.getByRole('heading', { name: 'How will you use Hanzo?' })).toBeVisible({ timeout: 30_000 })
|
||
await expect(page.getByRole('status').filter({ hasText: '50% off your first month of Pro' }), 'the offer, said as pay says it').toBeVisible()
|
||
// Free is chosen on pay's plans, which come back to the app (1aa7f96a); this page offers no Free button.
|
||
await expect(page.getByRole('button', { name: 'Continue with Free' })).toHaveCount(0)
|
||
await page.getByRole('link', { name: /^Personal/ }).click()
|
||
expect(await plans(page)).toEqual({ org: 'gate-org', coupon: '50OFF', ...CAMPAIGN, returnUrl: `${base}/chat`, tab: 'individual' })
|
||
})
|
||
}
|
||
|
||
test('a returning account with an offer link opens pay\'s plans with it at once', async ({ page }) => {
|
||
await signIn(page, { orgs: [{ org: 'gate-org', role: 'owner' }] })
|
||
await platform(page, { made: '2024-01-01T00:00:00Z' })
|
||
await page.goto(`${base}/onboarding?coupon=50OFF`)
|
||
expect(await plans(page)).toEqual({ org: 'gate-org', coupon: '50OFF', returnUrl: `${base}/chat` })
|
||
})
|
||
|
||
test('a new account a provider made agrees to the terms before the offer’s question', async ({ page }) => {
|
||
await signIn(page, { orgs: [{ org: 'gate-org', role: 'owner' }] })
|
||
await platform(page, { owed: true })
|
||
await page.setViewportSize({ width: 390, height: 844 })
|
||
await page.goto(`${base}/onboarding?coupon=50OFF`)
|
||
await page.getByRole('checkbox', { name: /^I agree to Hanzo's Terms/ }).click({ timeout: 30_000 })
|
||
await page.getByRole('button', { name: 'Create account' }).click()
|
||
await page.getByRole('link', { name: /^Business/ }).click({ timeout: 15_000 })
|
||
expect(await plans(page)).toEqual({ org: 'gate-org', coupon: '50OFF', returnUrl: `${base}/chat`, tab: 'team' })
|
||
})
|
||
|
||
test('with no code nothing is quoted and the plans open plain', async ({ page }) => {
|
||
await signIn(page, { orgs: [{ org: 'gate-org', role: 'owner' }] })
|
||
const heard = await platform(page)
|
||
await page.setViewportSize({ width: 1440, height: 900 })
|
||
await page.goto(`${base}/onboarding?next=%2Fchat`)
|
||
await page.getByRole('link', { name: /^Personal/ }).click({ timeout: 30_000 })
|
||
expect(await plans(page)).toEqual({ org: 'gate-org', returnUrl: `${base}/chat`, tab: 'individual' })
|
||
expect(heard.quotes, 'no quote is asked for').toEqual([])
|
||
})
|