mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
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]>
120 lines
5.3 KiB
TypeScript
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|