Files
hanzo.ai/e2e/gates/studio.spec.ts
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
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.
2026-10-22 19:07:20 -07:00

102 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test, type Page } from '@playwright/test'
import { serveExport } from './export'
import { signIn } from '../session'
/**
* STUDIO IS THE APP'S THIRD MODE.
*
* The rail carries Studio after Dev (@hanzo/app Rail) in light and dark on a
* desk, a phone's menu lists it first among the places its bar does not carry,
* and its press moves the app to /studio, a history entry and never a load. /studio stands behind the gate `/` does
* (app/studio/_door.tsx): a stranger reads Studio's landing, and a signed-in
* reader is in the app with @hanzo/studio in its content panel and Studio's
* places in the sidebar. Measured on the shipped bytes, the engine at
* studio.hanzo.ai answered from here.
*/
let base: string
let stop: () => Promise<void>
test.beforeAll(async () => {
const served = await serveExport()
base = served.url
stop = served.close
})
test.afterAll(async () => stop())
/** A signed-in reader whose platform reads, and Studio's engine, answer empty. */
async function member(page: Page) {
await signIn(page)
await page.route(
(u) => u.pathname.startsWith('/v1/') && !u.pathname.startsWith('/v1/iam/oauth/'),
(route) => (new URL(route.request().url()).pathname.endsWith('/stream') ? route.fulfill({ status: 200, contentType: 'text/event-stream', body: '' }) : route.fulfill({ json: {} })),
)
await page.context().route('https://studio.hanzo.ai/**', (route) => route.fulfill({ json: {} }))
}
const studio = (page: Page) => page.locator('[data-slot="frame"][data-app="studio"]')
test('a stranger at /studio reads Studio’s landing', async ({ page }) => {
await page.goto(`${base}/studio`)
await expect(page.getByText('Build AI pipelines on a canvas').first()).toBeVisible({ timeout: 20_000 })
await expect(studio(page)).toHaveCount(0)
expect(new URL(page.url()).pathname).toBe('/studio')
})
test('a signed-in reader at /studio is in the app, Studio in its panel and its places in the sidebar, and never meets the landing', async ({ page }) => {
await member(page)
await page.setViewportSize({ width: 1440, height: 900 })
await page.goto(`${base}/studio`)
await expect(studio(page)).toBeVisible({ timeout: 30_000 })
await expect(page.locator('[data-slot="app-head"]')).toContainText('Hanzo Studio')
await expect(page.locator('[data-slot="app-places"]').getByText('Library')).toBeVisible()
await expect(page.getByLabel('Starters').first()).toBeVisible({ timeout: 30_000 })
await expect(page.getByText('Build AI pipelines on a canvas')).toHaveCount(0)
await expect(page.locator('[data-slot="rail"]').first().getByRole('button', { name: 'Studio' })).toHaveAttribute('aria-current', 'page')
})
test('an address under /studio is Studio at that address', async ({ page }) => {
await member(page)
await page.goto(`${base}/studio/library`)
await expect(studio(page)).toBeVisible({ timeout: 30_000 })
expect(new URL(page.url()).pathname).toBe('/studio/library')
})
for (const scheme of ['light', 'dark'] as const) {
test(`the rail carries Studio after Dev, and its press moves the app there, ${scheme}, 1440`, async ({ page }) => {
await page.emulateMedia({ colorScheme: scheme })
await member(page)
await page.setViewportSize({ width: 1440, height: 900 })
await page.goto(`${base}/chat`)
const rail = page.locator('[data-slot="rail"]').first()
await expect(rail.getByRole('button', { name: 'Studio' })).toBeVisible({ timeout: 30_000 })
const marks = await rail.locator('[aria-label]').evaluateAll((els) => els.map((e) => e.getAttribute('aria-label')))
expect(marks.indexOf('Studio'), 'Studio follows Dev').toBe(marks.indexOf('Dev') + 1)
// Then Tabs, the fourth product the app draws, and the places after it.
expect(marks.indexOf('Tabs'), 'Tabs follows Studio').toBe(marks.indexOf('Studio') + 1)
expect(marks.indexOf('Library'), 'and leads Library').toBe(marks.indexOf('Tabs') + 1)
// A move of the app is no load: a mark of the page survives it.
await page.evaluate(() => ((window as unknown as { kept: number }).kept = 1))
await rail.getByRole('button', { name: 'Studio' }).click()
await expect(studio(page)).toBeVisible({ timeout: 30_000 })
expect(new URL(page.url()).pathname).toBe('/studio')
expect(await page.evaluate(() => (window as unknown as { kept?: number }).kept)).toBe(1)
await page.screenshot({ path: test.info().outputPath(`studio-${scheme}-1440.png`) })
})
test(`the phone's menu leads its places with Studio, ${scheme}, 390`, async ({ page }) => {
await page.emulateMedia({ colorScheme: scheme })
await member(page)
await page.setViewportSize({ width: 390, height: 844 })
await page.goto(`${base}/chat`)
await page.getByRole('button', { name: 'Open sidebar' }).first().click({ timeout: 30_000 })
const places = page.locator('[data-slot="drawer"] [data-slot="places"]')
await expect(places.getByRole('button', { name: 'Studio' })).toBeVisible()
const rows = await places.locator('[data-slot^="place-"]').evaluateAll((els) => els.map((e) => e.getAttribute('data-slot')))
expect(rows, 'Studio, then Tabs and More; Chat and Dev are the bar’s').toEqual(['place-studio', 'place-tabs', 'place-more'])
await places.getByRole('button', { name: 'Studio' }).click()
await expect(studio(page)).toBeVisible({ timeout: 30_000 })
await expect(page.locator('[data-slot="drawer"]'), 'the move puts the menu away').toHaveCount(0)
await page.screenshot({ path: test.info().outputPath(`studio-${scheme}-390.png`) })
})
}