mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
components/family/Film is the one player: the still is the first paint, the loop is made only after hydration and only when motion is wanted, and one IntersectionObserver for every film on the page loads a loop just before it shows, plays it on screen and pauses it off. It fades in on `playing`, so the step from still to loop cannot be seen. The bento draws each card's film behind its words with no filter (the art is grey already), a lighter scrim, the lean and a small grow on hover; the line keeps to the left of the card and Enso runs its square master below 1024px. /enso, /kai, /zen and the six creative models open on Fold with the film where a page puts its preview, screened onto the dot field and drawn inverted in the light theme; Splash is that Fold with a model's words. The route Enso takes and the decision Kai forms move to the band under it. e2e/gates/films.spec.ts holds the files, their weight, the loops playing and pausing, the phone's square Enso, reduced motion and each page's one heading beside its film.
105 lines
5.0 KiB
TypeScript
105 lines
5.0 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 their film beside the dot field;
|
||
// 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 })
|
||
// The bar's first menu: Research, since the navigation the owner restored on
|
||
// 2026-10-26 (LLM.md "Navigation restored"), which has no Platform menu.
|
||
const trigger = page.locator(BAR).getByRole('button', { name: 'Research', exact: true })
|
||
await expect(async () => {
|
||
await trigger.hover()
|
||
await expect(page.locator('#menu-research')).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')
|
||
})
|