Files
hanzo.ai/e2e/chat-workspace-ui.spec.ts

161 lines
6.5 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { credential } from './credential'
test.describe('Workspace Chat & Shell Requirements', () => {
async function ensureWorkspace(page: import('@playwright/test').Page) {
await page.waitForLoadState('domcontentloaded')
const orgPicker = page.getByRole('region', { name: 'Choose an organization' })
try {
if (await orgPicker.isVisible({ timeout: 2000 })) {
await orgPicker.getByRole('button', { name: 'hanzo', exact: true }).click()
await expect(orgPicker).toBeHidden({ timeout: 5000 })
}
} catch {}
}
test.beforeEach(async ({ page }) => {
const TOKEN = credential()
if (TOKEN) {
await page.addInitScript((t) => {
localStorage.setItem('hanzo_iam_access_token', t)
localStorage.setItem('hanzo_iam_id_token', t)
try {
const c = JSON.parse(atob(t.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')))
localStorage.setItem('hanzo_iam_expires_at', String(c.exp * 1000))
if (c.sub) localStorage.setItem('hanzo:who', c.sub)
} catch {}
localStorage.setItem('hanzo_iam_current_org', 'hanzo')
localStorage.setItem('hanzo_iam_org', 'hanzo')
localStorage.setItem('hanzo_org', 'hanzo')
localStorage.setItem('hanzo:org:picked', '1')
}, TOKEN)
}
})
test('chat composer has no Show Work, mic is beside Send on right, and inline model selector works', async ({ page }) => {
await page.goto('/chat')
await ensureWorkspace(page)
// 1. "Show work" button must be completely absent from the UI
const showWorkBtn = page.getByRole('button', { name: /show work/i })
await expect(showWorkBtn).toHaveCount(0)
// Also verify text "Show work" nowhere on the page
const showWorkText = page.getByText(/show work/i)
await expect(showWorkText).toHaveCount(0)
// 2. Composer exists
const composer = page.locator('textarea')
await expect(composer).toBeVisible({ timeout: 20000 })
// 3. Send button and Mic button exist
const sendBtn = page.locator("[data-slot='composer-send']").or(page.getByRole('button', { name: /send/i }))
await expect(sendBtn.first()).toBeVisible()
const micBtn = page.locator('[data-slot="prism-control"]').or(page.getByRole('button', { name: /mic|voice/i }))
await expect(micBtn.first()).toBeVisible()
// Verify visual order: Mic should be immediately next to Send on the right
const micBox = await micBtn.first().boundingBox()
const sendBox = await sendBtn.first().boundingBox()
expect(micBox).not.toBeNull()
expect(sendBox).not.toBeNull()
if (micBox && sendBox) {
// Both should be aligned horizontally
expect(Math.abs(micBox.y - sendBox.y)).toBeLessThan(25)
// Send button is to the right of Mic
expect(sendBox.x).toBeGreaterThan(micBox.x)
}
// 4. Inline ModelSelector is present in the composer
const modelTrigger = page.locator('[role="combobox"]').or(page.getByLabel(/model/i)).or(page.locator('button:has-text("enso"), button:has-text("Enso"), button:has-text("gpt"), button:has-text("claude")'))
await expect(modelTrigger.first()).toBeVisible()
})
test('crew displays in one horizontal scrolling row, every agent has a polished memoji portrait', async ({ page }) => {
await page.goto('/chat')
await ensureWorkspace(page)
// Scroller container
const crewScroller = page.locator('[data-slot="crew"]')
await expect(crewScroller).toBeVisible({ timeout: 20000 })
// Check flexWrap is nowrap and overflowX is auto
const flexWrap = await crewScroller.evaluate((el) => window.getComputedStyle(el).flexWrap)
expect(flexWrap).toBe('nowrap')
const overflowX = await crewScroller.evaluate((el) => window.getComputedStyle(el).overflowX)
expect(overflowX).toMatch(/auto|scroll/)
// Agent items inside crew
const agentButtons = crewScroller.locator('button')
const count = await agentButtons.count()
expect(count).toBeGreaterThan(1)
// Verify every agent disc has a polished memoji or image, never raw text initials or broken img
const discs = crewScroller.locator('img')
const imgCount = await discs.count()
expect(imgCount).toBeGreaterThan(0)
for (let i = 0; i < imgCount; i++) {
const img = discs.nth(i)
await expect(img).toBeVisible()
const src = await img.getAttribute('src')
expect(src).toBeTruthy()
expect(src).toMatch(/\.(png|jpg|jpeg|webp|svg)|^data:image\//)
// Ensure the image loaded successfully and has natural dimensions > 0
const naturalWidth = await img.evaluate((el: HTMLImageElement) => el.naturalWidth)
expect(naturalWidth).toBeGreaterThan(0)
}
// Ensure no agent disc displays raw question mark "?" or text initials
const rawInitials = crewScroller.locator('text="?"')
await expect(rawInitials).toHaveCount(0)
})
test('top-left org dropdown is removed and clicking Hanzo mark toggles org rail with way back', async ({ page }) => {
await page.goto('/chat')
await ensureWorkspace(page)
// Floating dropdown OrgSwitcherDropdown should not be in the DOM
const floatingMenu = page.locator('[data-slot="org-switcher-dropdown"]')
await expect(floatingMenu).toHaveCount(0)
// Mark button in the Rail
const markButton = page.locator('[data-slot="mark"] button')
await expect(markButton).toBeVisible({ timeout: 20000 })
// Initially places menu is showing
const places = page.locator('[data-slot="places"]')
await expect(places).toBeVisible()
const orgsList = page.locator('[data-slot="orgs"]')
await expect(orgsList).toHaveCount(0)
// Click mark button to switch to organizations view
await markButton.click()
// Now orgs list should be visible in the rail
await expect(page.locator('[data-slot="orgs"]')).toBeVisible()
// There must be an obvious way back to app menu (Back to App Menu / Apps button)
const backToAppsBtn = page.getByRole('button', { name: /back to app menu|apps/i }).first()
await expect(backToAppsBtn).toBeVisible()
// Click Back to Apps button
await backToAppsBtn.click()
// Should be back to places view
await expect(page.locator('[data-slot="places"]')).toBeVisible()
await expect(page.locator('[data-slot="orgs"]')).toHaveCount(0)
// Clicking mark button again also toggles to orgs and back
await markButton.click()
await expect(page.locator('[data-slot="orgs"]')).toBeVisible()
await markButton.click()
await expect(page.locator('[data-slot="places"]')).toBeVisible()
})
})