Files
hanzo.ai/e2e/products.spec.ts
zeekayandHanzo Dev dff1d5f1e3 A signed-in spec starts past the organization picker
The one seeder in e2e/credential.ts now marks the browser as the bearer's
and chooses the first organization on the token, the way a returning
person's browser already has; the three specs that copied the seeder use
it. The other spec edits say what the site does now: money is at
pay.hanzo.ai, the header has no products door, ledger is the private
repo, a fresh board carries four projects, and the landing composer is
measured after it is scrolled into view.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 03:07:17 -07:00

120 lines
5.3 KiB
TypeScript

// Product-page integrity tests for the cloud taxonomy.
//
// - Every leaf in the products mega-menu must serve a real page
// (200 + non-empty body) — bespoke product pages AND generated
// cloud-primitive overviews. No dead links, no empty stubs.
// - Every leaf page's hero (h1) + body signature must be UNIQUE across the
// whole taxonomy — no two leaves share content.
// - Every github.com/hanzoai/<repo> link rendered on a leaf page resolves.
//
// Run:
// 1. serve the static build (or `pnpm dev`) on :8084
// 2. pnpm exec playwright test e2e/products.spec.ts
//
// CI / live smoke: BASE_URL=https://hanzo.ai pnpm exec playwright test
//
// The paths are READ from the taxonomy, not mirrored beside it. They used to be
// a hand-kept copy told to stay "in lockstep", and it had drifted to four pages
// that no longer exist (/cloud/rerank, /cloud/jobs, /cloud/cost, /engine) and a
// /blockchain section the taxonomy dropped — so the suite was testing a site
// nobody was shipping while the real leaves went unchecked. A list that has to
// be kept in lockstep by hand is a list that will not be.
import { test, expect } from '@playwright/test'
import { cloudCategories } from '../lib/data/cloud-primitives'
// Off-property leaves are another host's to keep alive, and probing them here
// would make this suite red on somebody else's outage.
const LEAF_PATHS = [
...new Set(
cloudCategories
.flatMap((c) => c.items)
.map((i) => i.href)
.filter((h) => h.startsWith('/')),
),
]
test.describe('every mega-menu leaf serves a real page', () => {
for (const path of LEAF_PATHS) {
test(`${path} returns 200 with non-empty body`, async ({ page }) => {
const resp = await page.goto(path)
expect(resp?.status(), `${path} returned ${resp?.status()}`).toBe(200)
const body = await page.locator('body').innerText()
expect(body.length, `${path} body too short`).toBeGreaterThan(200)
})
}
})
test.describe('leaf pages have unique hero content', () => {
test('every leaf page has a unique h1 + body signature', async ({ page }) => {
test.setTimeout(LEAF_PATHS.length * 3000) // ~1.5s a page, doubled for a cold cache
const seenH1 = new Map<string, string>()
const seenSig = new Map<string, string>()
const conflicts: string[] = []
for (const path of LEAF_PATHS) {
await page.goto(path)
const h1 = (await page.locator('h1').first().innerText().catch(() => '')).trim()
const body = (await page.locator('main, body').first().innerText().catch(() => '')).trim()
const sig = body.replace(/\s+/g, ' ').slice(0, 600)
if (h1) {
const prev = seenH1.get(h1)
if (prev && prev !== path) conflicts.push(`H1 "${h1}" shared by ${prev} and ${path}`)
else seenH1.set(h1, path)
}
if (sig) {
const prev = seenSig.get(sig)
if (prev && prev !== path) conflicts.push(`Body signature shared by ${prev} and ${path}`)
else seenSig.set(sig, path)
}
}
if (conflicts.length) console.log('\nUNIQUENESS CONFLICTS:\n' + conflicts.join('\n'))
expect(conflicts, conflicts.join('\n')).toEqual([])
})
})
test.describe('leaf pages link to real GitHub URLs', () => {
// A 404 here means one of two very different things, and the test used to
// report them as one. Asked ANONYMOUSLY, all 19 of these answer 404; asked
// AUTHENTICATED, all 19 EXIST and are PRIVATE — none is misnamed and none is
// absent. So the site is not link-rotted, it advertises repositories that
// have not been published, and the remedy is to publish them or to stop
// offering them. Neither is a link fix, and neither belongs to this repo.
//
// The ratchet is the same one scripts/audit-catalog.mjs runs, and it may only
// SHRINK. A 404 that is not named here fails — that is a NEW unpublished repo
// reaching a page. A repo named here that starts answering also fails, so
// publishing one forces the deletion of its own exemption.
//
// Re-measure with: gh api repos/hanzoai/<name> --jq .private
const PRIVATE = new Set([
'amqp', 'app', 'billing', 'bootnode', 'chat', 'console', 'console2',
'extension', 'ledger', 'licensing', 'operator', 'payments', 'pricing',
'skills', 'studio', 'treasury', 'world', 'zen', 'zt',
])
for (const path of LEAF_PATHS) {
test(`${path}: github.com/hanzoai/<repo> links resolve`, async ({ page, request }) => {
test.setTimeout(60000)
await page.goto(path)
const hrefs = await page.locator('a[href^="https://github.com/hanzoai/"]').evaluateAll(
(els) => Array.from(new Set(els.map((el) => (el as HTMLAnchorElement).href)))
)
for (const href of hrefs) {
// The REPO segment alone — a deep link (…/extension/releases/latest) is
// still that repo, and matching the whole path misses the ratchet.
const name = href.replace(/^https:\/\/github\.com\/hanzoai\//, '').split('/')[0]
const r = await request.head(href, { maxRedirects: 5, timeout: 15000 })
const dead = r.status() === 404
if (PRIVATE.has(name)) {
expect(dead, `${href} answers ${r.status()} — it is published, so drop it from PRIVATE`).toBe(true)
} else {
expect(dead, `${href} returned ${r.status()}`).toBe(false)
}
}
})
}
})