mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
/platform was MOVED to /cloud; now the page lives at /platform and /cloud forwards there. cloud.hanzo.ai's root is the same page: deploy.yml copies platform.html over that host's index.html, and the canonical names hanzo.ai/platform. /cloud keeps its route for go get hanzo.ai/cloud and draws the same page with its canonical on /platform, so an edge still forwarding /platform to /cloud cannot bounce a reader between the two. Every internal link, the nav's Overview and Platform, the landing's stack link, the local server and the gates now name /platform.
164 lines
7.4 KiB
TypeScript
164 lines
7.4 KiB
TypeScript
// Structural audit + screenshot capture for every product page.
|
|
//
|
|
// Asserts (per slug):
|
|
// - Page returns 200
|
|
// - <h1> exists (hero)
|
|
// - At least one anchor pointing at platform.hanzo.ai/deploy/<slug> exists
|
|
// OR a [data-testid="deploy-cta"] anchor exists
|
|
// - At least one OSS attribution block exists (license + GitHub link)
|
|
// - One screenshot saved to tests/screenshots/products/<slug>.png
|
|
//
|
|
// Run:
|
|
// pnpm dev # terminal A — serve on :8084
|
|
// pnpm test e2e/products-audit.spec.ts
|
|
|
|
import { test, expect } from '@playwright/test'
|
|
import { mkdirSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = path.dirname(__filename)
|
|
|
|
const PRODUCT_SLUGS = [
|
|
// AI & Agents
|
|
'zen', 'agents', 'mcp', 'zap', 'llm', 'engine', 'jin', 'guard', 'skills',
|
|
// Developer
|
|
'playground', 'code', 'cli', 'desktop', 'gui', 'ui', 'extension', 'operative',
|
|
// Apps. `/chat` is not here: it is the chat itself now, not a page about it,
|
|
// so it carries no deploy CTA and no licence block. See e2e/chat.spec.ts.
|
|
'bot', 'app', 'search', 'crawl', 'base', 'commerce', 'payments', 'captable',
|
|
'dataroom', 'sign', 'billing', 'ledger', 'treasury', 'studio', 'computer',
|
|
'gallery', 'world',
|
|
// Compute
|
|
'functions', 'machines', 'edge', 'realtime', 'node', 'network', 'tunnel', 'registry',
|
|
// Data
|
|
'vector', 'sql', 'kv', 'datastore', 'storage', 'docdb', 'database',
|
|
// Async
|
|
'flow', 'auto', 'tasks', 'pubsub', 'mq', 'stream',
|
|
// Platform
|
|
'iam', 'kms', 'platform', 'dns', 'console', 'gateway', 'ingress', 'operator', 'visor',
|
|
'hsm', 'idv', 'authz',
|
|
// Observability
|
|
'insights', 'analytics', 'status', 'dashboards', 'telemetry', 'metrics', 'sentry', 'o11y',
|
|
]
|
|
|
|
// A LANDING is not a product leaf. These three answer, name themselves, and owe
|
|
// neither an OSS block nor a deploy CTA: `/dev` is the app room (its own gates
|
|
// and e2e/{pin,star,smoke,preview,down}.spec.ts), `/cloud` is the root of
|
|
// cloud.hanzo.ai rather than a page about a product, and `/enso` is a model
|
|
// family — hanzoai/enso is PRIVATE, so a licence block there would advertise a
|
|
// repository the public cannot open. Asserting the leaf shape on them failed
|
|
// four times over for a thing none of them is.
|
|
const LANDING_PATHS = ['/platform', '/enso']
|
|
|
|
const WEB3_SLUGS = [
|
|
'chains', 'exchange', 'wallets', 'indexer', 'nft', 'tokens', 'pay', 'bridge',
|
|
]
|
|
|
|
const SHOT_DIR = path.resolve(__dirname, '../tests/screenshots/products')
|
|
test.beforeAll(() => {
|
|
mkdirSync(SHOT_DIR, { recursive: true })
|
|
})
|
|
|
|
function checkPage(slug: string, urlPath: string) {
|
|
test(`product audit: ${urlPath}`, async ({ page }) => {
|
|
const resp = await page.goto(urlPath)
|
|
expect(resp?.status(), `${urlPath} returned ${resp?.status()}`).toBe(200)
|
|
|
|
// A REDIRECT STUB is a legitimate page shape, not a broken product page.
|
|
// `/status` calls window.location.replace() from a useEffect, so it leaves
|
|
// the moment it hydrates. Every assertion below then raced the navigation
|
|
// and the first to lose reported "missing <h1>" — a real element, on a page
|
|
// that was already gone.
|
|
//
|
|
// Detect it by WHERE WE ENDED UP, not by reading text off the stub: by the
|
|
// time a locator resolves, the stub's own markup may no longer be there.
|
|
// A page that forwards is fine as long as it lands somewhere real.
|
|
// Compare against the ORIGIN OF THE RESPONSE, not against page.url(): the
|
|
// stub can already have left by the time this runs, and resolving the
|
|
// expected URL relative to the current one then compares the destination
|
|
// with itself and finds no redirect at all. (Cost me a round trip.)
|
|
// Host AND path: `/code` forwards to `/dev` on the SAME host, so comparing
|
|
// hosts alone read a stub as a product page and asked it for a licence block.
|
|
const from = new URL(resp?.url() ?? urlPath, 'https://hanzo.ai')
|
|
await page.waitForTimeout(1200)
|
|
let landedOn = new URL(page.url())
|
|
const moved = () => landedOn.host !== from.host || landedOn.pathname !== from.pathname
|
|
|
|
// A forward can be SLOWER than the settle, and then this asks a page that is
|
|
// already leaving for its heading and reports the heading missing. `/status`
|
|
// ships exactly one and failed that way. Only wait the extra time where the
|
|
// symptom is present, so a page that is simply itself costs nothing.
|
|
if (!moved() && !(await page.locator('h1').first().isVisible().catch(() => false))) {
|
|
await page
|
|
.waitForURL((u) => u.toString() !== from.toString(), { timeout: 3000 })
|
|
.catch(() => {})
|
|
landedOn = new URL(page.url())
|
|
}
|
|
|
|
if (moved()) {
|
|
const resp2 = await page.request.get(page.url()).catch(() => null)
|
|
expect(
|
|
resp2?.status() ?? 0,
|
|
`${urlPath} forwards to ${page.url()}, which does not serve`,
|
|
).toBeLessThan(400)
|
|
return
|
|
}
|
|
|
|
const h1 = page.locator('h1').first()
|
|
await expect(h1, `${urlPath} missing <h1>`).toBeVisible()
|
|
|
|
// OSS attribution.
|
|
//
|
|
// When this fails, the page is usually RIGHT and the metadata is wrong. A
|
|
// page cannot honestly show "open source · Apache-2.0 · view on GitHub"
|
|
// for a repo the public cannot open, and `/enso` and `/cloud` are exactly
|
|
// that: products-metadata declares `license: 'Apache-2.0'` and a
|
|
// `github_repo`, while hanzoai/enso and hanzoai/cloud are both PRIVATE.
|
|
// So fix the claim, not the page — publish the repo, or stop asserting a
|
|
// licence for it. Same open question as the 72 private repos the OSS
|
|
// catalog cites.
|
|
const ossAttribution = page.locator('[data-testid="oss-attribution"]')
|
|
const ossPresent =
|
|
(await ossAttribution.count()) > 0 ||
|
|
(await page.locator('text=/Open source/i').first().count()) > 0
|
|
expect(
|
|
ossPresent,
|
|
`${urlPath} shows no OSS attribution. If products-metadata declares a ` +
|
|
`license + github_repo for this slug, one of the two is lying — check ` +
|
|
`whether that repo is actually public before "fixing" the page.`,
|
|
).toBe(true)
|
|
|
|
// Deploy CTA. The href form is deliberately loose: the per-product
|
|
// `/deploy/<slug>` path never resolved (the console has no such route) and
|
|
// now points at the bare `/deploy`, so matching on the prefix keeps this
|
|
// assertion about "there is a way to deploy" rather than about a URL shape
|
|
// that has already changed once.
|
|
const deployTestId = page.locator('[data-testid="deploy-cta"]')
|
|
const deployHref = page.locator(`a[href*="platform.hanzo.ai/deploy"]`)
|
|
const deployPresent = (await deployTestId.count()) > 0 || (await deployHref.count()) > 0
|
|
expect(deployPresent, `${urlPath} missing Deploy to Cloud CTA`).toBe(true)
|
|
|
|
// Screenshot — full page so the entire hero + footer is captured.
|
|
await page.screenshot({
|
|
path: path.join(SHOT_DIR, `${slug.replace('/', '_')}.png`),
|
|
fullPage: true,
|
|
})
|
|
})
|
|
}
|
|
|
|
function checkLanding(urlPath: string) {
|
|
test(`landing: ${urlPath}`, async ({ page }) => {
|
|
const resp = await page.goto(urlPath)
|
|
expect(resp?.status(), `${urlPath} returned ${resp?.status()}`).toBe(200)
|
|
await expect(page.locator('h1').first(), `${urlPath} missing <h1>`).toBeVisible()
|
|
})
|
|
}
|
|
|
|
test.describe('product page structure', () => {
|
|
for (const slug of PRODUCT_SLUGS) checkPage(slug, `/${slug}`)
|
|
for (const p of LANDING_PATHS) checkLanding(p)
|
|
for (const slug of WEB3_SLUGS) checkPage(`blockchain_${slug}`, `/blockchain/${slug}`)
|
|
})
|