mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
Every import, the catalog pin and the docs name @hanzo/app and github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and components/rooms.tsx is components/app.tsx.
124 lines
5.4 KiB
TypeScript
124 lines
5.4 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test'
|
|
import { serveExport } from './export'
|
|
import { signIn, answer } from '../session'
|
|
|
|
/**
|
|
* ⌘K IN THE APP FINDS WHAT IS NOT IN THE ROOM.
|
|
*
|
|
* The chord used to focus the column's filter box, so the control that offered
|
|
* to "Search Hanzo" narrowed the one list already on screen — a project was
|
|
* unreachable from /chat and a conversation unreachable from /dev, by the
|
|
* shortcut whose whole promise is that you do not have to know where a thing
|
|
* lives.
|
|
*
|
|
* So the assertion is deliberately CROSS-ROOM: stand in /inbox, whose column
|
|
* lists no projects, and find a project. A test that searched for what the
|
|
* column already lists would pass against the old filter box too.
|
|
*/
|
|
|
|
/**
|
|
* The palette's field. The palette is @hanzo/app' one search (Palette.tsx,
|
|
* rooms 0.1.9x): the workspace frame (Shell) mounts it on ⌘K in place of the
|
|
* old `OrgCommandPalette`, whose field read "Search commands".
|
|
*/
|
|
const FIELD = 'input[placeholder="Search or jump to…"]'
|
|
const ROW = '[role="option"]'
|
|
|
|
/**
|
|
* Open the palette by the chord.
|
|
*
|
|
* Retried, because `load` fires when the bytes are down and the listener is
|
|
* attached by an effect some milliseconds later — one press races hydration and
|
|
* goes red for a reason that is not the palette. Pressing only while the field
|
|
* is absent means a retry cannot toggle shut a palette that just opened.
|
|
*/
|
|
async function open(page: Page): Promise<void> {
|
|
await expect(async () => {
|
|
if ((await page.locator(FIELD).count()) === 0) await page.keyboard.press('Meta+k')
|
|
await expect(page.locator(FIELD)).toBeFocused({ timeout: 1000 })
|
|
}).toPass({ timeout: 20000 })
|
|
}
|
|
|
|
/** The titles the palette offers for `query`. */
|
|
async function results(page: Page, query: string): Promise<string[]> {
|
|
await page.locator(FIELD).fill(query)
|
|
await expect(page.locator(ROW).first()).toBeVisible({ timeout: 5000 })
|
|
return page.locator(ROW).allInnerTexts()
|
|
}
|
|
|
|
test.describe('the workspace palette', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await signIn(page)
|
|
// A project row as cloud answers it: the palette keys a project on its slug
|
|
// and names it by its name (@hanzo/app Palette `projects`).
|
|
await answer(page, '/v1/projects', [
|
|
{ id: 'p-borealis', slug: 'borealis', name: 'Borealis' },
|
|
{ id: 'p-other', slug: 'cartography', name: 'Cartography' },
|
|
])
|
|
await answer(page, '/v1/agents/chat/conversations', {
|
|
conversations: [{ id: 't-aurora', title: 'Aurora', updatedAt: new Date().toISOString() }],
|
|
})
|
|
})
|
|
|
|
test('finds a project from the inbox, and a room by name', async ({ page }) => {
|
|
const site = await serveExport()
|
|
try {
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
await page.goto(`${site.url}/inbox`)
|
|
// THE COMPOSER IS VISIBLE BEFORE IT CAN SEND. The organization gate added a
|
|
// render pass in front of every room, so a keystroke landing between paint and
|
|
// hydration is dropped — which a person typing straight after load would also
|
|
// lose. `data-hydrated` is the shell's own readiness, and the same signal
|
|
// `rail()` waits on.
|
|
await page.locator('[data-slot="workspace"][data-hydrated="true"]').first().waitFor({ timeout: 20_000 })
|
|
await open(page)
|
|
|
|
// THE CROSS-ROOM CLAIM. /inbox's column has no projects in it at all.
|
|
const projects = (await results(page, 'borealis')).join(' | ')
|
|
expect(projects, 'a project is unreachable from the inbox').toContain('Borealis')
|
|
|
|
// A destination answers to its own name.
|
|
const rooms = (await results(page, 'drive')).join(' | ')
|
|
expect(rooms, 'a room is unreachable by name').toContain('Drive')
|
|
|
|
// THE ROOM WITH NO OTHER DOOR. Home is off the rail by design and no
|
|
// control anywhere else points at it, so the palette is how it is
|
|
// reached at all — and it answers to the word a reader knows it by,
|
|
// which is not the brand its browser tab carries.
|
|
const home = (await results(page, 'home')).join(' | ')
|
|
expect(home, 'Home has no door at all').toContain('Home')
|
|
|
|
// ONE ROW PER DESTINATION. The estate's app registry sends Chat to
|
|
// hanzo.chat, which is the product this workspace subsumes — so without
|
|
// dropping it the palette offered two rows called Chat, one of which left
|
|
// for the thing the reader was already standing in.
|
|
const chat = (await results(page, 'chat')).filter((row) => row.startsWith('Chat'))
|
|
expect(chat, 'two rows called Chat').toHaveLength(1)
|
|
} finally {
|
|
await site.close()
|
|
}
|
|
})
|
|
|
|
test('the column keeps a filter, and only the bar claims search', async ({ page }) => {
|
|
const site = await serveExport()
|
|
try {
|
|
await page.setViewportSize({ width: 1440, height: 900 })
|
|
await page.goto(`${site.url}/inbox`)
|
|
|
|
// Two controls both saying Search did two different things.
|
|
// Found by ATTRIBUTE rather than by role: the drawer starts away in this
|
|
// harness, and a closed column is out of the accessibility tree — so the
|
|
// role query measures whether the sidebar is open, which is a different
|
|
// test that already exists in columns.spec.
|
|
const box = page.locator('input[aria-label="Filter sidebar"]')
|
|
await expect(box).toHaveAttribute('placeholder', 'Filter', { timeout: 20000 })
|
|
|
|
// And the chord no longer lands in it.
|
|
await open(page)
|
|
await expect(box).not.toBeFocused()
|
|
} finally {
|
|
await site.close()
|
|
}
|
|
})
|
|
})
|