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]>
196 lines
8.1 KiB
TypeScript
196 lines
8.1 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
import { rail } from './rail'
|
|
import { serveExport } from './export'
|
|
import { signIn, answer } from '../session'
|
|
|
|
/**
|
|
* SETTINGS IS THE COLUMN'S SECOND LEVEL, AND THE ROOM IS WHERE ITS PANE DRAWS.
|
|
*
|
|
* The two halves are in different subtrees — rows in the sidebar, pane in
|
|
* `main` — so nothing else in the suite can tell "the list opened" from "the
|
|
* list opened and the pane it points at is empty". That gap is exactly where a
|
|
* takeover would come back: a pane rendered over the room reads identically to
|
|
* one rendered in it until somebody measures which element holds it.
|
|
*
|
|
* It drives the export, so what it measures is the bytes that ship.
|
|
*/
|
|
|
|
const SHELL = '[data-slot="workspace"]'
|
|
/** The column Settings takes over. The places it is reached from lead it. */
|
|
const RAIL = '[data-slot="list"]'
|
|
const PLACES = '[data-slot="places-list"]'
|
|
|
|
/**
|
|
* A roster, so "the column gave the rooms up" is a change and not a locator
|
|
* that never matched. Without it the chat list draws its folders only if the
|
|
* platform answered, and an empty column passes a disappearance for free.
|
|
*/
|
|
async function roster(page: import('@playwright/test').Page) {
|
|
await signIn(page)
|
|
await answer(page, '/v1/team/rooms', {
|
|
rooms: [{ id: 'r1', space: 'acme', name: 'general', direct: false, private: false, archived: false, members: [] }],
|
|
})
|
|
await answer(page, '/v1/agents', { agents: [] })
|
|
await answer(page, '/v1/agents/chat/conversations', { conversations: [] })
|
|
await answer(page, '/v1/agents/sessions', { sessions: [] })
|
|
await answer(page, '/v1/projects', [])
|
|
await answer(page, '/v1/channels/inbox', { items: [] })
|
|
}
|
|
|
|
async function openSettings(page: import('@playwright/test').Page) {
|
|
await rail(page, RAIL)
|
|
const settingsBtn = page.getByRole('button', { name: 'Settings', exact: true }).first()
|
|
await expect(async () => {
|
|
if (!(await settingsBtn.isVisible().catch(() => false))) {
|
|
await page.getByRole('button', { name: 'More' }).click().catch(() => {})
|
|
}
|
|
await expect(settingsBtn).toBeVisible({ timeout: 1000 })
|
|
}).toPass({ timeout: 20_000 })
|
|
await settingsBtn.click()
|
|
await expect(page.getByRole('button', { name: 'Workspace', exact: true })).toBeVisible({
|
|
timeout: 20_000,
|
|
})
|
|
}
|
|
|
|
test('the column swaps to Settings and swaps back', async ({ page }) => {
|
|
await roster(page)
|
|
const site = await serveExport()
|
|
try {
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
await page.goto(`${site.url}/home`)
|
|
|
|
const column = page.locator(RAIL).first()
|
|
// Home's column: the places, then the room's own lists.
|
|
await rail(page, RAIL)
|
|
await expect(page.locator(PLACES).getByRole('button', { name: 'Chat', exact: true })).toBeVisible()
|
|
await expect(column.getByRole('button', { name: 'Cloud & Platform', exact: true })).toBeVisible({
|
|
timeout: 20_000,
|
|
})
|
|
|
|
await openSettings(page)
|
|
|
|
// SWAPPED, not stacked. A row list that merely appeared under the rooms
|
|
// would pass every assertion below and still be the wrong shape.
|
|
await expect(
|
|
column.getByRole('button', { name: 'Cloud & Platform', exact: true }),
|
|
'the room list gives the column up while Settings holds it',
|
|
).toHaveCount(0)
|
|
await expect(column.getByRole('button', { name: 'General', exact: true })).toBeVisible()
|
|
|
|
// NOT A MODAL. The takeover this replaced was a dialog; nothing may be one.
|
|
await expect(page.locator('[data-slot="dialog-content"]')).toHaveCount(0)
|
|
await expect(page.locator('[role="dialog"]')).toHaveCount(0)
|
|
|
|
// Back to the rooms, by the row that says where it goes.
|
|
await page.getByRole('button', { name: 'Workspace', exact: true }).click()
|
|
await expect(column.getByRole('button', { name: 'Cloud & Platform', exact: true })).toBeVisible({
|
|
timeout: 20_000,
|
|
})
|
|
await expect(column.getByRole('button', { name: 'General', exact: true })).toHaveCount(0)
|
|
} finally {
|
|
await site.close()
|
|
}
|
|
})
|
|
|
|
test('a chosen row draws its pane inside main', async ({ page }) => {
|
|
await roster(page)
|
|
const site = await serveExport()
|
|
try {
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
await page.goto(`${site.url}/home`)
|
|
|
|
// What the room has before Settings opens, so the absence asserted at the
|
|
// end is a change rather than a locator that never matched anything.
|
|
const main = page.locator('main').first()
|
|
const composer = main.getByPlaceholder(/Ask anything|Message /)
|
|
await expect(composer).toHaveCount(1, { timeout: 20_000 })
|
|
|
|
await openSettings(page)
|
|
|
|
for (const [label, says] of [
|
|
['General', /Appearance|Theme|Dark/i],
|
|
['Capabilities', /What models and agent swarms are granted access to/i],
|
|
['Personal agent', /Nothing is bound to this surface yet/i],
|
|
] as const) {
|
|
await page.locator(RAIL).first().getByRole('button', { name: label, exact: true }).click()
|
|
// The heading is the pane's, and it is in `main` — the assertion that
|
|
// separates "drawn in the room" from "drawn over it".
|
|
await expect(main.getByText(label, { exact: true }).first()).toBeVisible({ timeout: 20_000 })
|
|
await expect(main).toContainText(says)
|
|
// And the row it came from says it is the current one.
|
|
await expect(
|
|
page.locator('[aria-current="true"]').filter({ hasText: label }).first(),
|
|
).toBeVisible()
|
|
}
|
|
|
|
// The room is PUT AWAY while a pane is up: a composer still standing under
|
|
// it would mean the pane is an overlay after all.
|
|
await expect(composer, 'the room gives main up to the pane').toHaveCount(0)
|
|
} finally {
|
|
await site.close()
|
|
}
|
|
})
|
|
|
|
/**
|
|
* The cloud rows lead somewhere real, and each is judged by what it can prove.
|
|
*
|
|
* Models reads `/v1/models`, so the pane states what came back — including
|
|
* nothing, when nothing does. Base, Functions and Realtime have no read this app
|
|
* makes, so each goes to the page that already documents the product, and the
|
|
* proof is arriving there.
|
|
*/
|
|
test('the Platform group reaches the cloud surfaces', async ({ page }) => {
|
|
await signIn(page)
|
|
const site = await serveExport()
|
|
try {
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
await page.goto(`${site.url}/home`)
|
|
await openSettings(page)
|
|
|
|
const column = page.locator(RAIL).first()
|
|
await expect(column.getByText('Platform', { exact: true })).toBeVisible()
|
|
for (const row of ['Models', 'Base', 'Functions', 'Realtime'] as const) {
|
|
await expect(column.getByRole('button', { name: row, exact: true })).toBeVisible()
|
|
}
|
|
|
|
// Models is a pane, and it says what it read rather than a made-up tally.
|
|
await column.getByRole('button', { name: 'Models', exact: true }).click()
|
|
const main = page.locator('main').first()
|
|
await expect(main).toContainText(/models answer at \/v1\/models|No model answers|did not answer|Reading the catalog/, {
|
|
timeout: 20_000,
|
|
})
|
|
|
|
// Base LEAVES, and the address is the proof.
|
|
await column.getByRole('button', { name: 'Base', exact: true }).click()
|
|
await page.waitForURL(/\/base$/, { timeout: 20_000 })
|
|
await expect(page.getByRole('heading', { level: 1 }).first()).toBeVisible()
|
|
} finally {
|
|
await site.close()
|
|
}
|
|
})
|
|
|
|
test('Settings fits a phone, and the pane is not under the drawer', async ({ page }) => {
|
|
await signIn(page)
|
|
const site = await serveExport()
|
|
try {
|
|
await page.setViewportSize({ width: 390, height: 844 })
|
|
await page.goto(`${site.url}/home`)
|
|
await openSettings(page)
|
|
|
|
await page.locator(RAIL).first().getByRole('button', { name: 'General', exact: true }).click()
|
|
// The drawer floats OVER the room on a phone, so choosing a row has to put
|
|
// it away or the pane is drawn behind the list that opened it.
|
|
await expect(page.locator(RAIL).first()).toBeHidden({ timeout: 20_000 })
|
|
await expect(page.locator('main').first().getByText('General', { exact: true }).first()).toBeVisible()
|
|
|
|
const over = await page.evaluate(
|
|
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
)
|
|
expect(over, 'settings on a phone must not scroll sideways').toBeLessThanOrEqual(0)
|
|
|
|
await expect(page.locator(SHELL).first()).toBeVisible()
|
|
} finally {
|
|
await site.close()
|
|
}
|
|
})
|