Files
zooqueen 0665e2154d The page says it plainly, and every door opens where it says
The landing carries the new arc in full — the crew band among the sections — and
reads in the voice the site now keeps: short lines, one idea each, nothing a
reader has to decode. The console is platform.hanzo.ai on every page, and the
API-key door is the page the console itself opens, /api-keys. Nothing a visitor
can see calls itself deprecated or frozen.

Playwright found and the sweeps closed: Zen Mono's feature settings collapsed
"--" into one cell, so the terminal drew the wrong command — code type now sets
its own font features; the fixed composer and the corner mark ride over content
on a scrim rather than through it; the logo strip fades at its ends instead of
cutting words; the phone in the product composition is a 9:19.5 device layered
over the browser rather than a squashed panel; the workforce and the connected
systems break the run of centred sections.

The header keeps the shell's own gutter and glass. Two site rules written for a
56px workspace rail that no longer exists forced the bar's padding to a negative
value the browser read as zero, and an opaque ground over the shell's glass; both
are gone, so the bar sits where the mark does and shares one material with any
menu that opens from it.

@hanzogui/shell 8.3.13.
2026-09-07 02:45:17 -07:00

106 lines
5.0 KiB
TypeScript

// The Skills product surface, asserted against the running dev server.
//
// `/skills` is a marketing product page (app/(marketing)/skills/page.tsx) that
// renders through the shared `ProductLanding` kit. `e2e/products-audit.spec.ts`
// already walks it as one of ~90 slugs, but only for the SHAPE every product
// page shares — a 200, some <h1>, an OSS block, a deploy CTA. Nothing asserts
// what THIS page says: its heading, the six things it calls a skill, and the
// two addresses a reader leaves by. That is the gap this file fills.
//
// It drives the running dev server rather than the export — the copy is client
// content behind `ProductLanding` — so it lives in the `chromium` project and
// cannot gate a build, the same rule the older live-surface specs follow.
//
// BASE_URL=http://localhost:3000 pnpm exec playwright test --project=chromium e2e/skills.spec.ts
//
// `BASE` already defaults to :3000 (e2e/app.ts), so the command runs without the
// variable too.
import { test, expect } from '@playwright/test'
import { mkdirSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { BASE, quiet } from './app'
// The repo's screenshot output dir — gitignored, the same tree
// `e2e/products-audit.spec.ts` writes its shots into.
const SHOTS = join(dirname(fileURLToPath(import.meta.url)), '../tests/screenshots')
// The six things the page calls a skill — the cards a reader actually sees, read
// off the live DOM rather than invented: they are the `what.pillars` titles in
// app/(marketing)/skills/page.tsx. A card dropped from that grid fails here
// rather than shipping a page that quietly says less than it did.
const SKILLS = [
'A skill is a markdown file',
'Gateway skills do the finding',
'Install it as a plugin',
'Written small on purpose',
'Cross-referenced, not duplicated',
'Add your own',
]
// Where a reader leaves by, asserted as a real href rather than link text alone
// — that is what catches a CTA repointed at a near-miss, the class of bug this
// repo's link audits keep finding. The source is one anchor and the key is the
// other; docs publishes no Skills page, so the page offers no docs link and
// `e2e/docs-links.spec.ts` holds that address on its measured-404 list.
const GITHUB = 'https://github.com/hanzoai'
const KEY = 'https://platform.hanzo.ai/api-keys'
test.describe.configure({ timeout: 120_000 })
test.describe('the Skills page says what Skills is', () => {
test.beforeAll(() => mkdirSync(SHOTS, { recursive: true }))
test('renders the hero, the six skills, and the way out', async ({ page }) => {
const resp = await page.goto(`${BASE}/skills`, { waitUntil: 'domcontentloaded' })
expect(resp?.status(), '/skills did not answer 200').toBe(200)
await quiet(page) // take the dev-server overlay out of the shot
// One heading, and it names the page. `headings.spec.ts` holds this for the
// app rooms; a product page owes a crawler and a screen reader the same one.
const h1 = page.locator('h1')
await expect(h1, '/skills must ship exactly one <h1>').toHaveCount(1)
await expect(h1).toHaveText('Skills')
// The one-line what-it-is, and the badge that names the repo.
await expect(
page.getByText('Development knowledge your coding agent loads only when it needs it'),
).toBeVisible()
await expect(page.getByText('hanzoai/skills').first()).toBeVisible()
// Every skill the grid claims to hold — the page's actual content, and the
// reason this file exists beside the generic product audit.
for (const name of SKILLS) {
await expect(
page.getByRole('heading', { name, exact: true }),
`the "${name}" card is missing from /skills`,
).toBeVisible()
}
// The final call to action, and the two addresses a reader leaves by, by
// real href rather than link text alone.
await expect(page.getByRole('heading', { name: 'Get started with Skills' })).toBeVisible()
await expect(
page.locator(`a[href="${GITHUB}"]`).first(),
'no anchor points at the Hanzo GitHub org',
).toBeVisible()
await expect(
page.locator(`a[href="${KEY}"]`).first(),
'no anchor points at the console, where the key comes from',
).toBeVisible()
// The instrument arrived. gui compiles every style prop to an atomic class
// in an external sheet; a document that loaded none answers every layout
// question with the base rule, so the hero section would read `block` and a
// screenshot would be of an unstyled page. Check the instrument before
// trusting the picture — the house rule from e2e/gates/workspace.spec.ts.
// The hero is the first <section> (a `render="section"` XStack, so flex).
const display = await page.locator('section').first().evaluate((el) => getComputedStyle(el).display)
expect(display, 'the hero section is not flex — the gui stylesheet did not arrive').toBe('flex')
// Evidence, never the assertion.
await page.screenshot({ path: join(SHOTS, 'skills.png'), fullPage: true }).catch(() => {})
})
})