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.
102 lines
5.4 KiB
TypeScript
102 lines
5.4 KiB
TypeScript
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`) })
|
||
})
|
||
}
|