app.spec.ts
Language TypeScript6.6 KB150 lines
import AxeBuilder from '@axe-core/playwright'import { expect, test, type Page } from '@playwright/test'const sizes = [{ name: 'desktop', width: 1280, height: 800 },{ name: 'small', width: 760, height: 520 },]const themes = ['light', 'dark'] as constasync function openApp(page: Page, theme: 'light' | 'dark', reduce = false) {await page.emulateMedia({ colorScheme: theme })if (reduce) {const cdp = await page.context().newCDPSession(page)await cdp.send('Emulation.setEmulatedMedia', {features: [{ name: 'prefers-reduced-transparency', value: 'reduce' }],})}await page.goto('/')await expect(page.getByTestId('composer-input')).toBeVisible()}async function noSidewaysScroll(page: Page) {const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth,)expect(overflow).toBeLessThanOrEqual(0)}for (const size of sizes) {for (const theme of themes) {test.describe(`${size.name} ${theme}`, () => {test.use({ viewport: { width: size.width, height: size.height } })test('opens a chat, sends, streams and shows the cost', async ({ page }) => {await openApp(page, theme)await expect(page.locator('html')).toHaveAttribute('data-theme', theme)await noSidewaysScroll(page)await page.getByRole('button', { name: /Tidy a bash backup script/ }).click()await expect(page.getByTestId('msg-assistant').first()).toContainText('rsync')await expect(page.getByTestId('cost-line').first()).toContainText(/in, .* out, \$/)await expect(page.getByTestId('chat-total')).toContainText('$')await page.getByTestId('composer-input').fill('Explain the index I should add in SQL')await expect(page.getByTestId('estimate')).toContainText('tokens in')await page.getByTestId('send').click()await expect(page.getByTestId('msg-assistant').last()).toContainText('CREATE INDEX')await expect(page.getByTestId('msg-assistant').last().getByTestId('cost-line')).toBeVisible()await noSidewaysScroll(page)})test('model picker shows prices and filters', async ({ page }) => {await openApp(page, theme)await page.getByTestId('model-button').click()const picker = page.getByRole('dialog', { name: 'Choose a model' })await expect(picker).toContainText('$0.09 / $0.55')await picker.getByRole('tab', { name: 'Vision' }).click()await expect(picker.getByRole('option')).toHaveCount(6)await picker.getByRole('tab', { name: 'Local' }).click()await expect(picker.getByRole('option').first()).toContainText('llama3.2')await expect(picker.getByRole('option').first()).toContainText('free')await picker.getByRole('option').first().click()await expect(page.getByTestId('model-button')).toContainText('llama3.2')await noSidewaysScroll(page)})})}}test('stop keeps the partial reply', async ({ page }) => {await openApp(page, 'light')await page.getByTestId('composer-input').fill('Tell me about caching, at length')await page.getByTestId('send').click()await expect(page.getByTestId('stop')).toBeVisible()await page.waitForTimeout(150)await page.getByTestId('stop').click()await expect(page.getByTestId('msg-assistant').last()).toContainText('Stopped.')await expect(page.getByTestId('msg-assistant').last().getByTestId('cost-line')).toContainText('est.')})test('search finds a message and opens its chat', async ({ page }) => {await openApp(page, 'dark')await page.keyboard.press('ControlOrMeta+k')await page.getByTestId('search-input').fill('bm25')const hit = page.locator('.hits li').first()await expect(hit).toContainText('How FTS5 bm25 ranking works')await expect(hit.locator('mark').first()).toHaveText(/bm25/i)await hit.click()await expect(page.locator('.chat-title')).toHaveText('How FTS5 bm25 ranking works')})test('usage and settings open and read cleanly', async ({ page }) => {await openApp(page, 'light')await page.getByTestId('spend-meter').click()await expect(page.getByRole('dialog', { name: 'Usage' })).toContainText('By model')await expect(page.getByTestId('usage-total')).toContainText('$')await page.keyboard.press('Escape')await page.keyboard.press('ControlOrMeta+,')const settings = page.getByRole('dialog', { name: 'Settings' })await expect(settings).toContainText('Monthly budget')await expect(settings.getByTestId('shortcut')).toBeVisible()await noSidewaysScroll(page)})test('reduced transparency makes the panels opaque', async ({ page }) => {await openApp(page, 'light', true)const blur = await page.locator('.composer').evaluate((el) => getComputedStyle(el).backdropFilter)expect(blur === 'none' || /blur\(0px\)/.test(blur)).toBe(true)})test('the quick prompt answers and shows the cost', async ({ page }) => {await page.setViewportSize({ width: 640, height: 420 })await page.goto('/quick.html')await page.getByLabel('Quick prompt').fill('What should I index in SQL?')await page.getByLabel('Quick prompt').press('Enter')await expect(page.locator('.quick-body')).toContainText('CREATE INDEX')await expect(page.getByTestId('cost-line')).toBeVisible()await noSidewaysScroll(page)})test('an Enter pressed before the quick prompt has loaded is not lost', async ({ page }) => {// Hold back the IPC client, as a slow first start would.await page.route(/mockClient.*\.js/, async (route) => {await new Promise((r) => setTimeout(r, 1500))await route.continue()})await page.setViewportSize({ width: 640, height: 420 })await page.goto('/quick.html')await page.getByLabel('Quick prompt').fill('What should I index in SQL?')await page.getByLabel('Quick prompt').press('Enter')await expect(page.locator('.quick-body')).toContainText('CREATE INDEX', { timeout: 8000 })})for (const theme of themes) {test(`no contrast or labeling problems (${theme})`, async ({ page }) => {await openApp(page, theme)await page.getByRole('button', { name: /Compare two pricing tiers/ }).click()await expect(page.getByTestId('msg-assistant')).toBeVisible()// axe ships newer Playwright types than the pinned runner; the Page is the same object.const results = await new AxeBuilder({ page } as unknown as ConstructorParameters<typeof AxeBuilder>[0]).withTags(['wcag2a', 'wcag2aa']).analyze()const problems = results.violations.map((v) =>`${v.id}: ${v.nodes.map((n) => n.target.join(' ')).slice(0, 4).join(' | ')}`,)expect(problems).toEqual([])})}