mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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]>
156 lines
6.5 KiB
TypeScript
156 lines
6.5 KiB
TypeScript
// Live marketing-site E2E + visual capture.
|
|
//
|
|
// Drives the real, deployed hanzo.ai (or any BASE_URL) with a real browser:
|
|
// - renders the core marketing views (home, pricing, zen, open-source, dev, chat)
|
|
// - checks the header and footer destinations resolve
|
|
// - sweeps header/footer links and fails on any 404/5xx
|
|
// - clicks real footer links and confirms each destination renders
|
|
//
|
|
// Every step writes a PNG to SHOT_DIR so the run doubles as a visual record.
|
|
//
|
|
// Run against production:
|
|
// BASE_URL=https://hanzo.ai \
|
|
// SHOT_DIR=/abs/path/to/screenshots \
|
|
// npx playwright test e2e/live-site.spec.ts --project=chromium
|
|
//
|
|
// SHOT_DIR defaults to test-results/live-shots (git-ignored) when unset.
|
|
|
|
import { test, expect, type Page } from '@playwright/test'
|
|
import { mkdirSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
|
|
const SHOT_DIR = process.env.SHOT_DIR || join('test-results', 'live-shots')
|
|
mkdirSync(SHOT_DIR, { recursive: true })
|
|
|
|
const shot = (name: string) => join(SHOT_DIR, name)
|
|
|
|
// Scroll the whole page in viewport-sized steps so framer-motion
|
|
// `whileInView` sections actually animate in before we screenshot,
|
|
// then return to the top for a clean full-page capture.
|
|
async function settle(page: Page) {
|
|
await page.waitForLoadState('domcontentloaded')
|
|
await page.waitForLoadState('networkidle').catch(() => {})
|
|
await page.evaluate(async () => {
|
|
const step = Math.round(window.innerHeight * 0.85)
|
|
for (let y = 0; y < document.body.scrollHeight; y += step) {
|
|
window.scrollTo(0, y)
|
|
await new Promise((r) => setTimeout(r, 120))
|
|
}
|
|
window.scrollTo(0, 0)
|
|
await new Promise((r) => setTimeout(r, 250))
|
|
})
|
|
}
|
|
|
|
async function capture(page: Page, path: string, file: string) {
|
|
const resp = await page.goto(path, { waitUntil: 'commit' })
|
|
// Static export serves real 200s for prerendered routes.
|
|
expect(resp, `no response for ${path}`).toBeTruthy()
|
|
expect(resp!.status(), `${path} returned ${resp!.status()}`).toBeLessThan(400)
|
|
await settle(page)
|
|
const body = await page.locator('body').innerText()
|
|
// Signed-out rooms (e.g. /chat auth card) render at ~138 characters.
|
|
expect(body.length, `${path} body too short (stub?)`).toBeGreaterThan(100)
|
|
await page.screenshot({ path: shot(file), fullPage: true })
|
|
return body
|
|
}
|
|
|
|
// Core marketing views the task requires a screenshot of.
|
|
const VIEWS: Array<[string, string]> = [
|
|
['/', '00-home.png'],
|
|
['/pricing', '01-pricing.png'],
|
|
['/zen', '02-zen.png'],
|
|
['/open-source', '03-open-source.png'],
|
|
['/chat', '05-chat.png'],
|
|
]
|
|
|
|
test.describe('hanzo.ai — core marketing views render', () => {
|
|
for (const [path, file] of VIEWS) {
|
|
test(`renders ${path}`, async ({ page }) => {
|
|
test.setTimeout(60000)
|
|
const body = await capture(page, path, file)
|
|
// Every marketing page has a visible hero heading.
|
|
await expect(page.locator('h1, h2').first()).toBeVisible()
|
|
// No client-side error boundary / Next 404 text leaked into the view.
|
|
expect(body).not.toMatch(/This page could not be found|404\s*\|/i)
|
|
})
|
|
}
|
|
})
|
|
|
|
// Header + footer destinations. Internal routes must be < 400; we record
|
|
// any offender. External links (github/discord/docs) are probed by HEAD.
|
|
const INTERNAL_LINKS = [
|
|
'/pricing', '/enterprise', '/zen', '/agents', '/chat', '/search',
|
|
'/crawl', '/vector', '/cloud', '/iam', '/kms', '/console',
|
|
'/insights', '/blockchain/chains', '/blockchain/exchange', '/blockchain/wallets',
|
|
'/blockchain/pay', '/about', '/careers', '/security', '/press',
|
|
'/privacy', '/terms',
|
|
]
|
|
|
|
test.describe('hanzo.ai — link integrity (no 404/5xx)', () => {
|
|
test('all header/footer internal routes resolve', async ({ page }) => {
|
|
test.setTimeout(120000)
|
|
const broken: string[] = []
|
|
for (const href of INTERNAL_LINKS) {
|
|
const resp = await page.goto(href, { waitUntil: 'commit' }).catch(() => null)
|
|
const status = resp?.status() ?? 0
|
|
if (!resp || status >= 400) broken.push(`${href} -> ${status || 'no response'}`)
|
|
}
|
|
expect(broken, `broken links:\n${broken.join('\n')}`).toEqual([])
|
|
})
|
|
|
|
test('all rendered footer links resolve', async ({ page }) => {
|
|
test.setTimeout(120000)
|
|
await page.goto('/', { waitUntil: 'commit' })
|
|
await page.waitForLoadState('networkidle').catch(() => {})
|
|
// Same-origin footer links, in EITHER shape. `[href^="/"]` alone found zero
|
|
// and read as "the footer renders nothing": the apex footer is HanzoFooter
|
|
// from @hanzogui/shell, which writes its own-site links absolutely
|
|
// (`https://hanzo.ai/dev`), not as root-relative paths. Selecting on the
|
|
// path rather than the spelling is what makes this test about reachability
|
|
// instead of about one package's URL style.
|
|
const hrefs = await page.locator('footer a[href]').evaluateAll((els) =>
|
|
Array.from(
|
|
new Set(
|
|
els
|
|
.map((e) => (e as HTMLAnchorElement).getAttribute('href') ?? '')
|
|
.filter((h) => h.startsWith('/') || h.startsWith('https://hanzo.ai/'))
|
|
.map((h) => (h.startsWith('/') ? h : h.replace('https://hanzo.ai', ''))),
|
|
),
|
|
),
|
|
)
|
|
expect(hrefs.length, 'footer should render many same-origin links').toBeGreaterThan(10)
|
|
const broken: string[] = []
|
|
for (const href of hrefs) {
|
|
const resp = await page.goto(href!, { waitUntil: 'commit' }).catch(() => null)
|
|
const status = resp?.status() ?? 0
|
|
if (!resp || status >= 400) broken.push(`${href} -> ${status || 'no response'}`)
|
|
}
|
|
expect(broken, `broken footer links:\n${broken.join('\n')}`).toEqual([])
|
|
})
|
|
})
|
|
|
|
test.describe('hanzo.ai — clicking footer links navigates', () => {
|
|
// Real clicks (not just goto) on footer anchors; confirm each renders.
|
|
const CLICKS: Array<[string, string, string]> = [
|
|
['Privacy', '/privacy', '08-link-privacy.png'],
|
|
]
|
|
for (const [label, expectedPath, file] of CLICKS) {
|
|
test(`footer "${label}" -> ${expectedPath}`, async ({ page }) => {
|
|
test.setTimeout(60000)
|
|
await page.goto('/', { waitUntil: 'commit' })
|
|
await page.waitForLoadState('networkidle').catch(() => {})
|
|
// Either spelling — see the note on same-origin shapes above.
|
|
const link = page
|
|
.locator(`footer a[href="${expectedPath}"], footer a[href="https://hanzo.ai${expectedPath}"]`)
|
|
.first()
|
|
await link.scrollIntoViewIfNeeded()
|
|
await expect(link).toBeVisible()
|
|
await link.click()
|
|
await page.waitForURL(`**${expectedPath}`, { timeout: 15000 })
|
|
await settle(page)
|
|
await expect(page.locator('h1, h2').first()).toBeVisible()
|
|
await page.screenshot({ path: shot(file), fullPage: true })
|
|
})
|
|
}
|
|
})
|