Files
hanzo.ai/e2e/gates/picker.spec.ts
zeekay 37ff5ac382 gates: the composer's model menu, its suggestions and its agent shelf as they are
The model word under the composer opens @hanzo/build's own menu since 0.2.73
(Search models, Enso's row, model rows carrying data-model and data-paused),
not a panel with @hanzo/ui ModelPicker inside it, and effort is a word of its
own. chat.ts reads that menu; picker, limits, upgrade and named measure it.
ModelPicker's research-preview row has no counterpart in build's menu, which
lists what /v1/models serves, so picker.spec stops asserting it and LLM.md
says where Zen 7 is asked for instead. The usage bars are UsageMeter rows
keyed by class (usage-meter-row), as the settings gates already read them.

An empty chat shows @hanzo/build Suggestions since @hanzo/rooms 0.1.83, and a
starter does its act on the press; starters and local-cloud hold that, with
the kept rows answered so no model is asked. Agents are seated from
+ › Agents, which lists the org's own agents; together.spec gives the org the
core team's rows and holds the same room, briefs and names. The thread on
/models is found by its accessible name: rooms' Ask no longer draws a
placeholder, so TryChat stops passing one.

Claude-Session: https://claude.ai/code/session_01BLLMyBFSvAfCt7ZGqYuiEz
2026-10-22 13:05:18 -07:00

100 lines
4.4 KiB
TypeScript

import { expect, test, type Locator, type Page } from '@playwright/test'
import { FREE, PAID, arrive, chip, effort, enso, panel, row } from './chat'
import { serveExport } from './export'
/**
* THE WORD UNDER THE COMPOSER NAMES WHAT ANSWERS, AND ITS MENU IS SOLID.
*
* The model's word says Enso while Enso routes the turn and names the model
* once the reader picks one; the effort is a word of its own beside it while
* the model takes one. The word opens the model menu (@hanzo/build Tune, since
* 0.2.73 its own menu: Search models, Enso's row, the catalog by family), on
* screen with an opaque ground, and the search narrows its rows. Held at a
* laptop and a phone; the `webkit` project runs this file in WebKit too.
*
* The research preview's row ("Request access") was @hanzo/ui ModelPicker's,
* which appends `RESEARCH` to the catalog. The composer stopped drawing that
* picker when @hanzo/build drew its own menu over `GET /v1/models`, so the menu
* lists what the gateway serves and no preview; /research-access is where Zen 7
* is asked for.
*/
const SIZES = [
{ width: 1440, height: 900 },
{ width: 390, height: 844 },
] as const
/** The alpha of an element's computed background colour; 1 is a solid surface. */
function ground(el: Locator): Promise<number> {
return el.evaluate((node) => {
const paint = getComputedStyle(node).backgroundColor
if (paint === 'transparent') return 0
const slash = paint.match(/\/\s*([\d.]+)(%?)\s*\)$/)
if (slash) return slash[2] ? Number(slash[1]) / 100 : Number(slash[1])
const parts = paint.match(/\(([^)]+)\)/)?.[1].split(',') ?? []
return parts.length === 4 ? Number(parts[3]) : 1
})
}
/** The surface is inside the window and nothing behind it shows through. */
async function solid(page: Page, surface: Locator) {
await expect(surface).toBeVisible()
const view = page.viewportSize()!
const box = (await surface.boundingBox())!
expect(box.x, 'left edge on screen').toBeGreaterThanOrEqual(0)
expect(box.y, 'top edge on screen').toBeGreaterThanOrEqual(0)
expect(box.x + box.width, 'right edge on screen').toBeLessThanOrEqual(view.width)
expect(box.y + box.height, 'bottom edge on screen').toBeLessThanOrEqual(view.height)
await expect.poll(() => ground(surface), { message: 'an opaque ground' }).toBe(1)
}
test.describe('the model under the composer', () => {
let base: string
let stop: () => Promise<void>
test.beforeAll(async () => {
const served = await serveExport()
base = served.url
stop = served.close
})
test.afterAll(async () => stop())
for (const size of SIZES) {
test(`at ${size.width}px it names the model picked, and its menu opens solid and on screen`, async ({ page }) => {
await page.setViewportSize(size)
await arrive(page, base, 'dev', FREE.id)
// A model kept by id is named by the catalog. Enso Free names no effort.
await expect(chip(page)).toHaveAttribute('aria-label', `Model: ${FREE.name}`)
await expect(chip(page)).toContainText(FREE.name)
await expect(effort(page)).toHaveCount(0)
await chip(page).click()
await solid(page, panel(page))
await row(page, PAID.id).click()
await expect(panel(page)).toHaveCount(0)
await expect(chip(page)).toHaveAttribute('aria-label', `Model: ${PAID.name}`)
await expect(chip(page)).toContainText(PAID.name)
await expect(chip(page)).not.toContainText('Enso')
// Back to the router: the word says Enso, and its effort stands beside it.
await chip(page).click()
await enso(page).click()
await expect(chip(page)).toHaveAttribute('aria-label', 'Model: Enso')
await expect(effort(page)).toHaveAttribute('aria-label', 'Effort: Medium')
// The search narrows the rows to the models it matches, and keeps the menu solid.
await chip(page).click()
await solid(page, panel(page))
await panel(page).getByPlaceholder('Search models').fill('zen')
await expect(row(page, PAID.id)).toBeVisible()
await expect(row(page, FREE.id)).toHaveCount(0)
await expect(enso(page), 'Enso stands for every ask, not a match').toHaveCount(0)
await page.keyboard.press('Escape')
await expect(panel(page)).toHaveCount(0)
await expect(chip(page)).toHaveAttribute('aria-label', 'Model: Enso')
expect(await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1)).toBe(false)
})
}
})