mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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', '/platform', '/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 })
|
|
})
|
|
}
|
|
})
|