Files
zeekay 2b1448267a deps: ui 8.29.24, build 0.2.85, app 0.2.45, studio 0.1.8, appearance 0.2.12; the composer pages keep the foot
The set moves forward together: Studio 0.1.8 draws the one composer and no longer
imports `prompt`, which build 0.2.84 removed. @hanzo/ui's Consent draws its way to
the privacy choices at the foot of any page that mounts no link, which on a page
with a composer stood on the composer's own controls; those pages yield the foot
(app/room.css) and reach the choices from every marketing footer. The hosted gate
reads the phone menu's rail, where the apps are marks now.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-26 12:00:50 -07:00

102 lines
5.5 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
import { serveExport } from './export'
import { answer, signIn, type Who } from '../session'
/**
* THE APPS THAT DRAW INSIDE THE APP, at /apps/<id> (@hanzo/app lib/hosted).
*
* CRM, Commerce, CMS, Analytics and Insights stay standalone apps on hosts of
* their own. The app gives each reader who `opens` one a mark on the rail and a
* pane that frames the app's live host (`?embed=1`), and everyone else the Betas
* list's request path. The export is served under hanzo.ai, as host.spec.ts does,
* because a page on a developer's own machine opens every app and a flag would
* measure nothing; nothing here reaches the apps' hosts, a frame's page included.
*/
const MAPPED = ['hanzo.ai']
test.use({ launchOptions: { args: [`--host-resolver-rules=${MAPPED.map((host) => `MAP ${host} 127.0.0.1`).join(', ')}`] } })
let port: string
let stop: () => Promise<void>
test.beforeAll(async () => {
const served = await serveExport()
port = new URL(served.url).port
stop = served.close
})
test.afterAll(async () => stop())
const at = (path: string) => `http://hanzo.ai:${port}${path}`
const ALL = ['crm', 'commerce', 'cms', 'analytics', 'insights']
/** The reader, the cloud holding exactly these switches for them, and nothing off this machine answering. */
async function reader(page: Page, who: Who, ...held: string[]) {
await page.route('**/*', (route) => (new URL(route.request().url()).port === port ? route.continue() : route.abort()))
await signIn(page, who)
await answer(page, '/v1/flag/platform', { flags: Object.fromEntries(held.map((id) => [`beta.${id}`, true])) })
await answer(page, '/v1/marketing/beta', { betas: [] })
await answer(page, '/v1/flag/optin', { on: false, blocked: false, public: [] })
}
const ada: Who = { name: 'ada', email: '[email protected]' }
test('a member with beta.crm alone has CRM on the rail and its live host in the pane, and nothing else', async ({ page }) => {
await reader(page, ada, 'crm')
await page.goto(at('/apps/crm'))
await expect(page.locator('[data-slot="rail-app-crm"]')).toBeVisible({ timeout: 30_000 })
for (const id of ALL.slice(1)) await expect(page.locator(`[data-slot="rail-app-${id}"]`)).toHaveCount(0)
await expect(page.locator('[data-slot="rail-app-crm"]')).toHaveAttribute('aria-current', 'page')
const frames = page.locator('iframe')
await expect(frames).toHaveCount(1)
await expect(frames.first()).toHaveAttribute('src', 'https://crm.hanzo.ai/?embed=1')
// No sidebar beside it, as Tabs and Betas.
await expect(page.getByText('New chat')).toHaveCount(0)
})
test('staff, for whom every beta opens, have all five on the rail, each going to its own address', async ({ page }) => {
await reader(page, { name: 'zach', email: '[email protected]', orgs: [{ org: 'admin', role: 'admin' }] }, ...ALL)
await page.goto(at('/apps/cms'))
for (const id of ALL) await expect(page.locator(`[data-slot="rail-app-${id}"]`)).toBeVisible({ timeout: 30_000 })
await expect(page.locator('iframe')).toHaveAttribute('src', 'https://cms.hanzo.ai/admin?embed=1')
await page.locator('[data-slot="rail-app-insights"]').click()
await expect(page).toHaveURL(at('/apps/insights'))
await expect(page.locator('iframe')).toHaveAttribute('src', 'https://insights.hanzo.ai/?embed=1')
})
test('a SuperAdmin has all five with no switch held', async ({ page }) => {
await reader(page, { sub: 'z', name: 'z', email: '[email protected]', org: 'admin' })
await page.goto(at('/apps/commerce'))
for (const id of ALL) await expect(page.locator(`[data-slot="rail-app-${id}"]`)).toBeVisible({ timeout: 30_000 })
await expect(page.locator('iframe')).toHaveAttribute('src', 'https://commerce-admin.hanzo.ai/admin/?embed=1')
})
test('a reader the switch is off for is shown the request path at the address, and no frame', async ({ page }) => {
await reader(page, ada)
await page.goto(at('/apps/crm'))
await expect(page.locator('[data-slot="hosted-request"]')).toBeVisible({ timeout: 30_000 })
await expect(page.locator('iframe')).toHaveCount(0)
await expect(page.locator('[data-slot="rail-app-crm"]')).toHaveCount(0)
await expect(page.locator('[data-slot="beta-request"]')).toBeVisible()
})
test('a stranger at an address under /apps is sent to sign in and back to it, and nothing is framed', async ({ page }) => {
await page.route('**/*', (route) => (new URL(route.request().url()).port === port ? route.continue() : route.abort()))
await page.goto(at('/apps/crm'))
await expect(page).toHaveURL(/\/(login|signup)\?next=%2Fapps%2Fcrm$/, { timeout: 30_000 })
await expect(page.locator('iframe')).toHaveCount(0)
})
test('on a phone the pane is full-bleed under the bar, and the rail in the menu holds the apps as marks', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 })
await reader(page, ada, 'crm', 'analytics')
await page.goto(at('/apps/analytics'))
const frame = page.locator('iframe')
await expect(frame).toBeVisible({ timeout: 30_000 })
const box = (await frame.boundingBox())!
expect(box.width).toBeGreaterThanOrEqual(388)
expect(await page.evaluate(() => document.scrollingElement!.scrollWidth <= window.innerWidth)).toBe(true)
await page.getByRole('button', { name: 'Open sidebar' }).click()
const rail = page.locator('[data-slot="menu"] [data-slot="drawer-rail"]')
await expect(rail.locator('[data-slot="rail-app-crm"]')).toBeVisible()
await expect(rail.locator('[data-slot="rail-app-analytics"]')).toHaveAttribute('aria-current', 'page')
await expect(rail.locator('[data-slot="rail-app-cms"]')).toHaveCount(0)
})