mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
161 lines
6.5 KiB
TypeScript
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()
|
|
})
|
|
})
|