Files
hanzo.ai/e2e/gates/look.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

118 lines
5.0 KiB
TypeScript

import { test, expect, type Page } from '@playwright/test'
import { rail } from './rail'
import { serveExport } from './export'
import { signIn } from '../session'
/**
* HOW THE APP LOOKS IS A SETTING, AND IT TAKES EFFECT.
*
* Each case presses a control on Settings → General and then reads the ROOM,
* not the control: a pressed button that repaints nothing is the failure this
* exists to catch.
*
* The theme is read off the class gui's colours hang on (`t_dark` / `t_light`),
* not `data-theme`: that attribute is the IAM hook's own and moves even when
* the paint does not. Width is read as the composer's width, Accent as Send's
* fill after a reload, Font and Text size off the field itself.
*/
async function general(page: Page) {
// The column holds the door, and it may start away.
await rail(page, '[data-slot="sidebar"]')
await page.getByText('Settings', { exact: true }).filter({ visible: true }).first().click()
await page.getByText('General', { exact: true }).filter({ visible: true }).first().click()
await expect(page.getByText('Kept with your account', { exact: false })).toBeVisible()
}
const html = (page: Page) =>
page.evaluate(() => ({
cls: document.documentElement.className,
// The person's choice, as @hanzo/appearance keeps it on the device.
stored: (JSON.parse(localStorage.getItem('hanzo.appearance') || '{}') as { theme?: string }).theme ?? null,
}))
async function room(page: Page, path = '/home') {
await page.goto(path)
await expect(page.locator('textarea').filter({ visible: true }).first()).toBeVisible({ timeout: 20000 })
}
test.describe('Settings → General', () => {
let site: Awaited<ReturnType<typeof serveExport>>
test.beforeAll(async () => {
site = await serveExport()
})
test.afterAll(async () => {
await site.close()
})
test.use({ viewport: { width: 1440, height: 900 } })
test('each theme button turns the page that theme, and the choice survives a reload', async ({ page }) => {
await signIn(page)
await page.goto(`${site.url}/home`)
await general(page)
for (const [name, want] of [['Dark', 'dark'], ['Light', 'light'], ['Dark', 'dark']] as const) {
await page.getByRole('button', { name, exact: true }).filter({ visible: true }).first().click()
await expect.poll(async () => (await html(page)).cls).toContain(`t_${want}`)
expect(await html(page)).toEqual({ cls: expect.stringContaining(want), stored: want })
}
await page.reload()
await expect.poll(async () => (await html(page)).cls).toContain('t_dark')
})
for (const scheme of ['dark', 'light'] as const) {
test(`System follows the device when it prefers ${scheme}`, async ({ page }) => {
await page.emulateMedia({ colorScheme: scheme })
await signIn(page)
await page.goto(`${site.url}/home`)
await general(page)
await page.getByRole('button', { name: 'System', exact: true }).filter({ visible: true }).first().click()
await expect.poll(async () => (await html(page)).cls).toContain(`t_${scheme}`)
expect((await html(page)).stored).toBe('system')
})
}
test('Width, Font, Text size and Spacing each move the room', async ({ page }) => {
await signIn(page)
await room(page, `${site.url}/home`)
const measure = () =>
page.evaluate(() => {
const field = [...document.querySelectorAll('textarea')].find((t) => t.offsetParent) as HTMLTextAreaElement
const composer = field.closest('[data-slot="composer"]') ?? field
return {
width: Math.round(composer.getBoundingClientRect().width),
font: getComputedStyle(field).fontFamily,
size: getComputedStyle(field).fontSize,
density: getComputedStyle(document.documentElement).getPropertyValue('--density').trim(),
}
})
const before = await measure()
await general(page)
for (const choice of ['Narrow', 'Serif', 'Larger', 'Roomy']) {
await page.getByRole('button', { name: choice, exact: true }).filter({ visible: true }).first().click()
}
await room(page, `${site.url}/home`)
const after = await measure()
expect(after.width).toBeLessThan(before.width)
expect(after.font).not.toBe(before.font)
expect(parseFloat(after.size)).toBeGreaterThan(parseFloat(before.size))
expect(after.density).toBe('1.15')
})
test('an accent paints Send, and is still there after a reload', async ({ page }) => {
await signIn(page)
await room(page, `${site.url}/home`)
const send = () =>
page.evaluate(() => {
const s = [...document.querySelectorAll('[data-slot="composer-send"]')].find((e) => (e as HTMLElement).offsetParent)
return s ? getComputedStyle(s).backgroundColor : null
})
const ink = await send()
await general(page)
await page.getByRole('button', { name: 'Violet', exact: true }).filter({ visible: true }).first().click()
await page.reload()
await expect(page.locator('textarea').filter({ visible: true }).first()).toBeVisible({ timeout: 20000 })
await expect.poll(send).toBe('rgb(139, 92, 246)')
expect(ink).not.toBe('rgb(139, 92, 246)')
})
})