mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +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]>
118 lines
5.0 KiB
TypeScript
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)')
|
|
})
|
|
})
|