Files
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

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()
}
})