mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
The corner names an option by its label and its English hint, so the exact name 'العربية' matched nothing on CI. The open menu's backdrop filter fades out with the ground, and was read one frame early (blur(2.96e-08px)). Claude-Session: https://claude.ai/code/session_01BLLMyBFSvAfCt7ZGqYuiEz
103 lines
4.8 KiB
TypeScript
103 lines
4.8 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test'
|
||
import { serveExport } from './export'
|
||
|
||
/**
|
||
* THE BAR IS GLASS ON EVERY PAGE, AND A FIRST SCREEN'S LIGHT RUNS BEHIND IT.
|
||
*
|
||
* The bar (components/home/shell.tsx) stood in the page's flow, clear at rest,
|
||
* over nothing but the page's black ground, so above the creative pages'
|
||
* paintings and the kit heroes' glow it read as a black band. It is glass now,
|
||
* at rest and scrolled, in both schemes: a translucent fill over a blur. A first screen whose ground is a picture or a
|
||
* glow starts at the top of the window, behind it (`under`,
|
||
* components/shell/dock.ts), with its heading still clear of the bar. Under an
|
||
* open menu the bar is the plane's opaque ground, on purpose: a filtered bar is
|
||
* a backdrop root, and the veil would blur the bar instead of the page.
|
||
*/
|
||
|
||
let site: Awaited<ReturnType<typeof serveExport>>
|
||
test.beforeAll(async () => {
|
||
site = await serveExport()
|
||
})
|
||
test.afterAll(async () => {
|
||
await site.close()
|
||
})
|
||
|
||
const BAR = 'header[data-slot="masthead"]'
|
||
|
||
/** The alpha of a computed colour: `rgba(…, a)`, `color(srgb … / a)`, or 1 when none is written. */
|
||
const alpha = (color: string) => {
|
||
const slash = /\/\s*([\d.]+)\s*\)$/.exec(color)
|
||
if (slash) return Number(slash[1])
|
||
const rgba = /^rgba\([^)]*,\s*([\d.]+)\)$/.exec(color)
|
||
return rgba ? Number(rgba[1]) : 1
|
||
}
|
||
|
||
const ground = (page: Page) =>
|
||
page.locator(BAR).evaluate((el) => {
|
||
const c = getComputedStyle(el)
|
||
return { fill: c.backgroundColor, filter: c.backdropFilter || c.getPropertyValue('-webkit-backdrop-filter') }
|
||
})
|
||
|
||
async function glass(page: Page, where: string) {
|
||
const { fill, filter } = await ground(page)
|
||
expect(alpha(fill), `${where}: the bar's fill is translucent (${fill})`).toBeGreaterThan(0)
|
||
expect(alpha(fill), `${where}: the bar's fill is translucent (${fill})`).toBeLessThan(1)
|
||
expect(filter, `${where}: the bar frosts what is under it`).toContain('blur(')
|
||
}
|
||
|
||
async function open(page: Page, path: string, scheme: 'dark' | 'light', width = 1440) {
|
||
await page.route('https://api.hanzo.ai/**', (route) => route.abort())
|
||
await page.emulateMedia({ colorScheme: scheme })
|
||
await page.setViewportSize({ width, height: 900 })
|
||
await page.goto(`${site.url}${path}`)
|
||
await expect(page.locator(BAR)).toBeVisible({ timeout: 20_000 })
|
||
await expect.poll(() => page.evaluate(() => document.documentElement.className)).toContain(`t_${scheme}`)
|
||
}
|
||
|
||
for (const scheme of ['dark', 'light'] as const) {
|
||
test(`the bar is glass at rest and scrolled on every kind of page, ${scheme}`, async ({ page }) => {
|
||
// The landing, a plain page, Studio, a creative model, Zen, a kit page, a
|
||
// product page on a lit hero, and Enso's dot field.
|
||
for (const path of ['/', '/pricing', '/studio', '/koma', '/zen', '/customers', '/edge', '/enso']) {
|
||
await open(page, path, scheme)
|
||
await glass(page, `${path} at rest`)
|
||
await page.evaluate(() => window.scrollTo(0, 900))
|
||
await glass(page, `${path} scrolled`)
|
||
}
|
||
})
|
||
}
|
||
|
||
for (const width of [1440, 390]) {
|
||
test(`a first screen's picture or glow starts behind the bar, and its heading stays clear of it, ${width}`, async ({ page }) => {
|
||
// The six creative models and Zen open on a painting; the kit pages on a
|
||
// glow; six product pages on a lit ground; Enso on its dot field.
|
||
for (const path of ['/koma', '/iro', '/mori', '/oto', '/koe', '/uta', '/zen', '/customers', '/research', '/trust', '/edge', '/identity', '/functions', '/playground', '/operative', '/realtime', '/enso']) {
|
||
await open(page, path, 'dark', width)
|
||
const at = await page.evaluate((bar) => {
|
||
const first = document.querySelector('main section')!
|
||
const h1 = first.querySelector('h1')!
|
||
return {
|
||
top: first.getBoundingClientRect().top,
|
||
bar: document.querySelector(bar)!.getBoundingClientRect().bottom,
|
||
heading: h1.getBoundingClientRect().top,
|
||
}
|
||
}, BAR)
|
||
expect(at.top, `${path}: the first screen starts at the top of the window, behind the bar`).toBe(0)
|
||
expect(at.heading, `${path}: its heading stands below the bar`).toBeGreaterThan(at.bar)
|
||
}
|
||
})
|
||
}
|
||
|
||
test('an open menu stands the bar on the plane’s opaque ground, with no blur', async ({ page }) => {
|
||
await open(page, '/pricing', 'dark')
|
||
await page.locator('html[data-hydrated]').waitFor({ state: 'attached', timeout: 30_000 })
|
||
const trigger = page.locator(BAR).getByRole('button', { name: 'Products', exact: true })
|
||
await expect(async () => {
|
||
await trigger.hover()
|
||
await expect(page.locator('#menu-products')).toBeVisible({ timeout: 1000 })
|
||
}).toPass({ timeout: 15_000 })
|
||
await expect.poll(async () => alpha((await ground(page)).fill)).toBe(1)
|
||
// The filter fades out with the ground, so it is read at the end of that fade.
|
||
await expect.poll(async () => (await ground(page)).filter).toBe('none')
|
||
})
|