mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
_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]>
73 lines
3.4 KiB
TypeScript
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()
|
|
}
|
|
})
|