// @ts-check // Conversation Playground Metrics view (all /api routes mocked; no live writes). const { test, expect } = require('./_guard'); const sse = (/** @type {any[]} */ evts) => evts.map(e => `data: ${JSON.stringify(e)}\n\n`).join(''); async function boot(page, { usage = true, lang = '' } = {}) { let n = 0; await page.route('**/api/**', route => route.request().method() === 'GET' ? route.fulfill({ json: {} }) : route.fulfill({ json: { ok: true } })); await page.route('**/api/conversation/turn', route => { n++; const stats = { type: 'stats', stt_ms: 100 + n, llm_ttft_ms: 200 + n * 10, llm_total_ms: 900, tts_ms: 300 + n, total_ms: 1500, usage: usage ? { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } : null }; return route.fulfill({ contentType: 'text/event-stream', body: sse([ { type: 'transcript', text: 'hi', stt_ms: null }, { type: 'token', delta: 'Hello.' }, { type: 'llm_done', text: 'Hello.', llm_ttft_ms: 1, llm_total_ms: 2 }, stats, { type: 'done' }]) }); }); await page.route('**/static/dist/main.min.js*', r => r.abort()); if (lang) await page.addInitScript(l => { try { localStorage.setItem('app-lang', l); } catch (e) {} }, lang); await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); await page.evaluate(() => window.navTo('s-conversation')); } async function turn(page, i) { await page.locator('#conv-text-input').fill('hello ' + i); await page.locator('#conv-text-input').press('Enter'); await expect(page.locator('#conv-turn-history .conv-hist-item')).toHaveCount(i); await expect(page.locator('#conv-send-btn')).toBeEnabled(); } test('3 turns -> 3 charts with 3 points, token sums, latency panel intact', async ({ page }) => { await boot(page); for (let i = 1; i <= 3; i++) await turn(page, i); // existing Latency panel unchanged for (const id of ['cpv-stt', 'cpv-ttft', 'cpv-llm', 'cpv-tts', 'cpv-total']) await expect(page.locator('#' + id)).not.toHaveText('—'); await expect(page.locator('#conv-stats-panel .conv-stats-head', { hasText: 'Turn history' })).toBeVisible(); await expect(page.getByRole('tab')).toHaveCount(0); await expect(page.locator('#conv-stats-panel .conv-chart')).toHaveCount(3); for (const k of ['stt', 'llm', 'tts']) { await expect(page.locator(`.conv-chart[data-metric="${k}"] circle`)).toHaveCount(3); await expect(page.locator(`.conv-chart[data-metric="${k}"] tbody tr`)).toHaveCount(3); } await expect(page.locator('.conv-chart[data-metric="llm"] svg')).toHaveAttribute('aria-label', 'LLM first token, last 230 ms, 3 points'); await expect(page.locator('.conv-chart[data-metric="llm"] svg')).toContainText('Time (ms)'); await expect(page.locator('.conv-chart[data-metric="llm"] svg')).toContainText('TTFB (LLM)'); const d = page.locator('.conv-chart[data-metric="stt"] details'); await expect(d).not.toHaveAttribute('open', ''); await d.locator('summary').click(); await expect(d.locator('table')).toBeVisible(); await expect(page.locator('#cm-tok-prompt')).toHaveText('30'); await expect(page.locator('#cm-tok-completion')).toHaveText('15'); await expect(page.locator('#cm-tok-total')).toHaveText('45'); expect(await page.locator('.conv-chart[data-metric="stt"] tbody td').first().textContent()).toMatch(/^\d{2}:\d{2}:\d{2}$/); // Clear resets await page.locator('#conv-clear-btn').click(); await expect(page.locator('.conv-chart')).toHaveCount(0); await expect(page.locator('#cm-empty')).toBeVisible(); await expect(page.locator('#cm-tok-total')).toHaveText('n/a'); }); test('no usage from the server shows n/a, never 0', async ({ page }) => { await boot(page, { usage: false }); await turn(page, 1); for (const id of ['prompt', 'completion', 'total']) await expect(page.locator('#cm-tok-' + id)).toHaveText('n/a'); await expect(page.locator('.conv-chart[data-metric="llm"] circle')).toHaveCount(1); }); test('empty state and German labels', async ({ page }) => { await boot(page, { lang: 'de' }); await expect(page.locator('#cm-empty')).toBeVisible(); await expect(page.locator('#cm-tok-total')).toHaveText('k. A.'); await expect(page.locator('#conv-stats-panel .conv-stats-head', { hasText: 'Token-Verbrauch' })).toBeVisible(); });