Files
hanzo.ai/e2e/gates/forward.spec.ts
zeekay 8bb3c633d2 consent: @hanzo/ui's one sheet, and our stream counts every visitor
components/consent/Consent.tsx and Choices.tsx are gone. app/providers.tsx
mounts Consent from @hanzo/ui/consent 8.29.14: a bar with Manage
preferences, Accept all and a × that stores essential cookies only, and
a Cookie Preferences sheet that opens from any link to #choices, the
footer's Privacy choices included. Its switches start off until chosen.

@hanzo/event 0.3.61 makes the stream first party: every page view and
event goes for every visitor, stating the grants it went under, so the
provider no longer takes `enabled` from consent. Consent gates Google
and Meta alone, through startTags.

The conversion and forward gates now hold that no platform hears of a
visit before Accept or after a refusal, while our stream counts it under
no grant. Help's corner words gain the Font and Arabic font rows
@hanzo/ui 8.29.12 added to Words.
2026-10-25 22:05:45 -07:00

109 lines
5.0 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { serveExport } from './export'
import { bare, person, stream, type Wire } from './heard'
import { asked, issuer, type Seen as Issued } from './issuer'
/**
* HANZO.CHAT COUNTS THE VISIT, THEN OPENS THE CHAT AT HANZO.AI.
*
* Its root is this export's /forward (universe infra/aws/routes/sites.yaml,
* router `hanzo-chat`). For a visitor who accepted, the page reports to
* hanzo.chat's own GA4 stream, the Meta Pixel and our stream, and leaves for
* hanzo.ai/chat once they have taken the page view — carrying the query, and
* never through hanzo.id. A visitor who has not answered is counted nowhere and
* is not held: every visitor is asked before anything is measured.
*
* hanzo.chat is answered from the export on this machine, `/` as /forward;
* gtag.js and fbevents.js are stubs, the second one sending the page view the
* way Meta's library does, so what is measured is what left the page.
*/
const HANZO_CHAT = 'G-GRQBMNDTT6'
const PIXEL = '1790308611893001'
/** Meta's library, as far as this gate needs it: drain the stub's queue into /tr hits. */
const FBEVENTS = `(function(){var f=window.fbq,id='';f.queue.splice(0).forEach(function(a){
if(a[0]==='init')id=a[1];if(a[0]==='track')new Image().src='https://www.facebook.com/tr?id='+id+'&ev='+a[1]});
f.callMethod=function(){}})()`
let base: string
let stop: () => Promise<void>
test.beforeAll(async () => {
const served = await serveExport()
base = served.url
stop = served.close
})
test.afterAll(async () => stop())
type Seen = { ga: string[]; meta: string[]; ai: string[]; iam: Issued; events: Wire[] }
async function edge(page: Page): Promise<Seen> {
await person(page)
await page.route('**/*', (route) => (new URL(route.request().url()).origin === base ? route.continue() : route.abort()))
const seen: Seen = { ga: [], meta: [], ai: [], iam: null, events: [] }
await page.route('https://www.googletagmanager.com/**', (route) => {
seen.ga.push(new URL(route.request().url()).searchParams.get('id') ?? '')
return route.fulfill({ status: 200, contentType: 'text/javascript', body: '' })
})
await page.route('https://connect.facebook.net/**', (route) => route.fulfill({ status: 200, contentType: 'text/javascript', body: FBEVENTS }))
await page.route('https://www.facebook.com/tr**', (route) => {
const q = new URL(route.request().url()).searchParams
seen.meta.push(`${q.get('id')} ${q.get('ev')}`)
return route.fulfill({ status: 200, contentType: 'image/gif', body: '' })
})
await page.route('https://hanzo.chat/**', async (route) => {
const u = new URL(route.request().url())
const path = u.pathname === '/' ? '/forward' : u.pathname
return route.fulfill({ response: await route.fetch({ url: `${base}${path}${u.search}` }) })
})
await page.route('https://hanzo.ai/**', (route) => {
seen.ai.push(route.request().url())
return route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><title>Hanzo</title>' })
})
seen.iam = await issuer(page)
seen.events = await stream(page)
return seen
}
test('hanzo.chat reports to its own stream, the Pixel and ours, then opens the chat at hanzo.ai', async ({ page }) => {
const seen = await edge(page)
await page.goto('https://hanzo.chat/?q=hello')
await page.waitForURL((u) => u.hostname === 'hanzo.ai', { timeout: 20_000 })
expect(bare(page.url())).toBe('https://hanzo.ai/chat?q=hello')
expect(seen.ga).toEqual([HANZO_CHAT])
expect(seen.meta).toEqual([`${PIXEL} PageView`])
expect(seen.events.map((e) => e.event)).toContain('$pageview')
expect(seen.ai.map(bare)).toEqual(['https://hanzo.ai/chat?q=hello'])
expect(asked(seen.iam)).toEqual([])
})
test.describe('a visitor who has not answered the cookie question', () => {
// A browser that holds no choice for hanzo.chat (playwright.config ANSWERED holds one).
test.use({ storageState: { cookies: [], origins: [] } })
test('is counted by no platform on hanzo.chat, and is not held there', async ({ page }) => {
const seen = await edge(page)
const start = Date.now()
await page.goto('https://hanzo.chat/?q=hello')
await page.waitForURL((u) => u.hostname === 'hanzo.ai', { timeout: 20_000 })
expect(Date.now() - start).toBeLessThan(12_000)
expect(bare(page.url())).toBe('https://hanzo.ai/chat?q=hello')
expect(seen.ga).toEqual([])
expect(seen.meta).toEqual([])
expect(seen.events.filter((e) => String(e.properties?.consent ?? '') !== ''), 'our own stream only, under no grant').toEqual([])
})
})
test('a blocked tag holds no reader on hanzo.chat', async ({ page }) => {
const seen = await edge(page)
// Meta's host dropped, as a blocker drops it: the library never arrives.
await page.route('https://connect.facebook.net/**', (route) => route.abort())
const start = Date.now()
await page.goto('https://hanzo.chat/')
await page.waitForURL((u) => u.hostname === 'hanzo.ai', { timeout: 20_000 })
expect(Date.now() - start).toBeLessThan(12_000)
expect(bare(page.url())).toBe('https://hanzo.ai/chat')
expect(seen.ga).toEqual([HANZO_CHAT])
})