mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
Prose that carried no decision or fact is gone: the hero, the questions, the gate and every step are a heading and at most a line. What scores is said once (the leaderboard step), and every claim the cloud does not keep is out. Step 1 is one share card (the member's link, Copy, the device's share sheet on phones, seven channels as icons, Customize behind a text button) and a quiet invite-by-email card. Step 3 is one searched, grouped list with one control a row: On when `opens` says so, Get access when it does not, Requested at once after the press. Stars and follows moved under the leaderboard as Earn more. The 1,800-line dashboard is a shell over share, rank, betas and alpha; what they read is one hook (standing.ts); the questions are data (program.ts). Dead access, card, hint and referral components are deleted. Every press is 44px, every input too, and a gate walks each screen at 390px. scripts/brands and scripts/prebuild are TypeScript now; the three marks the share row draws ship in a JSON only /beta loads. /beta's title and card say what the page says. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
99 lines
5.2 KiB
TypeScript
99 lines
5.2 KiB
TypeScript
import { expect, test, type Page, type Route } from '@playwright/test'
|
|
import { serveExport } from './export'
|
|
import { heard, person, stream } from './heard'
|
|
import { signIn } from '../session'
|
|
|
|
/**
|
|
* SHARING, MEASURED WHERE IT CAN BE.
|
|
*
|
|
* Nobody can see a post go out on another network, and nothing here pretends to.
|
|
* What the cloud can know is what comes back: the member's own link leaves with the
|
|
* channel it was pressed for, the stream hears the press, a visitor who follows the
|
|
* link arrives with the code and the channel kept, and the code is claimed once, when
|
|
* they sign in. The score counts the claim (+1), not the press.
|
|
*/
|
|
|
|
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, popups included. */
|
|
const local = (page: Page) => page.context().route('**/*', (route) => (new URL(route.request().url()).origin === base ? route.continue() : route.abort()))
|
|
|
|
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 })
|
|
}
|
|
|
|
const CHANNELS = ['x', 'linkedin', 'facebook', 'bluesky', 'reddit', 'hn', 'email'] as const
|
|
const NAMES: Record<(typeof CHANNELS)[number], string> = { x: 'X', linkedin: 'LinkedIn', facebook: 'Facebook', bluesky: 'Bluesky', reddit: 'Reddit', hn: 'Hacker News', email: 'Email' }
|
|
|
|
test('each channel leaves with the member\'s own link tagged for it, and a press reaches the stream once, named for the channel', async ({ page }) => {
|
|
await local(page)
|
|
await person(page)
|
|
await signIn(page, { name: 'ada', email: '[email protected]' })
|
|
await page.route('**/v1/marketing/beta', (route) => reply(route, { applied: true, program: 'beta', betas: [], points: 0, place: 7, of: 40, 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: 0, signup: 0, qualified: 0 }, referrals: [] }))
|
|
const seen = await stream(page)
|
|
|
|
await page.goto(`${base}/beta?checkout=success&subscription=sub_1`)
|
|
for (const channel of CHANNELS) {
|
|
const link = page.getByRole('link', { name: `Share on ${NAMES[channel]}`, exact: true })
|
|
await expect(link).toBeVisible({ timeout: 20_000 })
|
|
const to = decodeURIComponent((await link.getAttribute('href')) ?? '')
|
|
expect(to, `${channel} carries the member's code`).toContain('ref=ada7')
|
|
expect(to).toContain(`utm_source=${channel}`)
|
|
expect(to).toContain('utm_medium=' + (channel === 'email' ? 'email' : 'social'))
|
|
expect(to).toContain('utm_campaign=beta_referral')
|
|
// A mail link opens the mail app, not a page; every other channel opens its own.
|
|
const popup = channel === 'email' ? null : page.context().waitForEvent('page')
|
|
await link.click()
|
|
if (popup) await (await popup).close()
|
|
const props = await heard(seen, 'waitlist_shared', (p) => p.method === channel)
|
|
expect(props).toMatchObject({ method: channel, program: 'beta', from: 'beta' })
|
|
}
|
|
// One press, one event: no channel is heard twice.
|
|
const shared = seen.filter((e) => e.event === 'waitlist_shared').map((e) => e.properties?.method)
|
|
expect(shared).toEqual([...CHANNELS])
|
|
})
|
|
|
|
test('a visitor who follows a shared link keeps the code and the channel, and the code is claimed once they sign in', async ({ page }) => {
|
|
await local(page)
|
|
await person(page)
|
|
const seen = await stream(page)
|
|
const claims: unknown[] = []
|
|
await page.route('**/v1/referral/claim', (route) => {
|
|
if (route.request().method() === 'POST') claims.push(JSON.parse(route.request().postData() || '{}'))
|
|
return reply(route, { recorded: true })
|
|
})
|
|
|
|
await page.goto(`${base}/beta?ref=ada7&utm_source=linkedin&utm_medium=social&utm_campaign=beta_referral`)
|
|
await heard(seen, 'beta_viewed')
|
|
expect(await page.evaluate(() => localStorage.getItem('hz_ref'))).toBe('ada7')
|
|
const kept = await page.evaluate(() => sessionStorage.getItem('hz_campaign') ?? '')
|
|
expect(Object.fromEntries(new URLSearchParams(kept))).toMatchObject({ ref: 'ada7', utm_source: 'linkedin', utm_medium: 'social', utm_campaign: 'beta_referral' })
|
|
expect(claims, 'a stranger claims nothing').toHaveLength(0)
|
|
|
|
// The same browser signs in: the code is claimed, once, and let go.
|
|
await signIn(page, { name: 'bob', email: '[email protected]' })
|
|
await page.route('**/v1/marketing/beta', (route) => reply(route, { applied: false, place: 0, of: 0, points: 0, betas: [], actions: [] }))
|
|
await page.route('**/v1/flag/platform', (route) => reply(route, { flags: {} }))
|
|
await page.goto(`${base}/beta`)
|
|
await expect.poll(() => claims.length, { timeout: 20_000 }).toBe(1)
|
|
expect(claims[0]).toEqual({ code: 'ada7' })
|
|
await expect.poll(() => page.evaluate(() => localStorage.getItem('hz_ref'))).toBeNull()
|
|
})
|