Files
hanzo.ai/e2e/gates/thumb.spec.ts
fbc926f525 The app is @hanzo/rooms 0.1.55's App, at paths under /chat and /dev, behind the organization gate
_web.tsx draws rooms' App inside rooms' `Orgs`, as every room is: a reader
with no organization makes one, one with several picks one, and a team on Free
is asked for the Team plan before the app is drawn; a personal organization on
Free opens it. The app reads its place from the path: /chat and /chat/<id> are
Chat, a path under /dev is Dev. /?chat=<id> and /chat?thread=<id> move to
/chat/<id>, /?at= to /dev?at=, whose place the door replaces with its path, as
it does the builder's grammar rooms 0.1.35-0.1.38 wrote under /dev.

The gates read the app at its paths and the rooms' shell at /home, with new
Linux baselines for it; the limit banner, the model chip and the side panel
are held to what rooms 0.1.55 draws; the landing's budget is 445 KB, since
rooms' lib/host carries @hanzo/build's route grammar. app/gui.css is what the
build writes.


Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 15:05:24 -07:00

73 lines
3.4 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { serveExport } from './export'
import { signIn } from '../session'
/**
* THE BAR IS REACHABLE BY A THUMB.
*
* `@hanzo/design`'s base.css states a 44px floor for every control on a coarse pointer, and
* the rule names the kinds it applies to — so a control of a kind nobody
* remembered stands outside it. The organization switcher is IAM's own
* `<select>`, sized 28px by that package, and it was the smallest thing in the
* bar while the toggle beside it was 44.
*
* A COARSE POINTER, not merely a narrow window: the floor is a `pointer: coarse`
* rule, so a 390px desktop window does not exercise it and a gate written that
* way would pass over the defect. `isMobile` gives Chromium the pointer without
* needing WebKit.
*/
test.use({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true })
test('every control in the bar clears the thumb floor', async ({ page }) => {
await signIn(page)
const site = await serveExport()
try {
await page.goto(`${site.url}/home`)
// The bar this gate is about, named the way the shell names it rather than
// taken as whichever `<header>` the document happens to put first.
const bar = page.locator('header[data-slot="bar"]')
await expect(bar).toBeVisible({ timeout: 20_000 })
// WAIT FOR THE ROOM TO SAY IT IS READY, NOT FOR ONE CONTROL TO TURN UP.
// `Shell.tsx` computes `settled = hydrated && !isLoading`, stamps it on the
// workspace as `data-hydrated`, and renders the switcher in that same pass
// under `settled && isAuthenticated` — so the room's own mark is the moment
// the bar is worth measuring. Counting a `<select>` used one control as a
// stand-in for that, and a stand-in goes stale the day the control moves or
// is merely slow: this spent 20 s on an org switcher, resolved 0 elements
// 32 times, and never reached the assertion the gate exists for.
await expect(page.locator('[data-slot="workspace"]')).toHaveAttribute('data-hydrated', 'true', {
timeout: 30_000,
})
// The switcher is what made this gate necessary — IAM's own 28px `<select>`,
// a kind the 44px floor did not name. It leads the sidebar now, which a phone
// opens as a sheet from this bar, so the sheet is opened and it is measured
// there rather than quietly losing the coverage.
await bar.getByRole('button', { name: 'Open sidebar' }).click()
const switcher = page.locator('[data-slot="workspace-switcher"] select')
await expect(switcher, 'the sidebar no longer leads with the switcher').toHaveCount(1, { timeout: 30_000 })
const pick = await switcher.boundingBox()
expect(pick && pick.height, 'the switcher is under the thumb floor').toBeGreaterThanOrEqual(44)
await bar.getByRole('button', { name: 'Close sidebar' }).click()
const small = await bar.evaluate((el) => {
const out: string[] = []
for (const kid of Array.from(
el.querySelectorAll('button,[role=button],select,a[href]'),
)) {
const r = kid.getBoundingClientRect()
if (r.width === 0 || r.height === 0) continue
if (r.height < 44)
out.push(
`${kid.getAttribute('aria-label') || kid.tagName} ${Math.round(r.width)}x${Math.round(r.height)}`,
)
}
return out
})
expect(small, 'a control in the bar is under the thumb floor').toEqual([])
} finally {
await site.close()
}
})