import { expect, test, type Page } from '@playwright/test' import { authenticate, mockChatSocket, mockHermesApi, TEST_ACCESS_KEY } from './fixtures' const inputPlaceholder = 'Type a message... (Enter to send, Shift+Enter for new line)' const sampleSession = { id: 'sess-1', title: 'First session', source: 'cli', created_at: 1760000000000, updated_at: 1760000000000, message_count: 2, input_tokens: 0, output_tokens: 0, reasoning_tokens: 0, } async function sendChatMessage(page: Page, message: string) { const input = page.getByPlaceholder(inputPlaceholder) await expect(input).toBeVisible() await input.fill(message) await page.getByRole('button', { name: 'Send' }).click() } async function waitForRunSessionId(page: Page, index = 0) { const handle = await page.waitForFunction((runIndex) => { const state = (window as any).__PW_CHAT_SOCKET__ const runs = state?.emitted?.filter((item: any) => item.event === 'run') || [] const run = runs[runIndex] return run ? { sessionId: run.payload?.session_id } : null }, index) return handle.jsonValue() as Promise } // Covers the Phase 006 manual checklist rows for streaming, markdown rendering, // and the run lifecycle, so a rendering regression fails in CI instead of only // showing up when someone opens the chat page by hand. test('streams a run and renders markdown, code, math and mermaid without leaking Arco into the bubble', async ({ page }) => { await authenticate(page, TEST_ACCESS_KEY, 'research') const api = await mockHermesApi(page) await mockChatSocket(page) await page.goto('/#/hermes/chat') await sendChatMessage(page, 'render everything') const { sessionId } = await waitForRunSessionId(page) await expect(page.getByRole('button', { name: 'Stop' })).toBeVisible() await page.evaluate((sid) => { const socket = (window as any).__PW_CHAT_SOCKET__.latest const delta = (text: string) => socket.__trigger('message.delta', { event: 'message.delta', session_id: sid, run_id: 'r1', delta: text }) delta('Hello **bold** and `code`') delta('\n\n```js\nconst a = 1\n```\n') delta('\n$$\nE = mc^2\n$$\n') delta('\n```mermaid\ngraph TD;\nA-->B;\n```\n') }, sessionId) const bubble = page.locator('.message').last() await expect(bubble.locator('strong')).toHaveText('bold') await expect(bubble.locator('code')).toHaveCount(2) await expect(bubble.locator('.katex')).toHaveCount(1) // mermaid renders asynchronously into the .mermaid-diagram placeholder await expect(bubble.locator('.mermaid-diagram svg')).toHaveCount(1) // ยง1 red line: no Arco component inside the message bubble await expect(bubble.locator('.arco-btn, .arco-tag, .arco-input')).toHaveCount(0) // the mock socket has no server-side cancel, so end the run the way the server would await page.evaluate((sid) => { const socket = (window as any).__PW_CHAT_SOCKET__.latest socket.__trigger('run.completed', { event: 'run.completed', session_id: sid, run_id: 'r1', output: 'done', inputTokens: 5, outputTokens: 3, }) }, sessionId) await expect(page.getByRole('button', { name: 'Stop' })).toHaveCount(0) expect(api.unexpectedRequests).toEqual([]) }) test('opens the session model dialog and the session search from the chat header', async ({ page }) => { await authenticate(page, TEST_ACCESS_KEY, 'research') await mockHermesApi(page, { sessions: [sampleSession] }) await mockChatSocket(page) await page.goto('/#/hermes/chat') await page.locator('.session-item').first().click({ button: 'right' }) const ctxPopup = page.locator('.arco-trigger-popup').filter({ hasText: 'Rename' }) await expect(ctxPopup).toBeVisible() await ctxPopup.locator('.arco-dropdown-option', { hasText: 'Set model' }).click() const modelModal = page.locator('.arco-modal').filter({ hasText: 'Set Session Model' }) await expect(modelModal).toBeVisible() await expect(modelModal.getByPlaceholder('Search models...')).toBeVisible() await page.keyboard.press('Escape') await page.getByRole('button', { name: /search/i }).first().click() await expect(page.getByPlaceholder(/search/i).first()).toBeVisible() })