Repository Public
1import { expect, test, type Page } from '@playwright/test';2import { apiGet, FIX, mission, ROUTES, seeded, watch } from './helpers';34test.beforeEach(async ({ page }) => {5 await seeded(page);6});78/** Nothing may be wider than the 390 px viewport. */9async function expectNoOverflow(page: Page): Promise<void> {10 const wide = await page.evaluate(() => {11 const vw = document.documentElement.clientWidth;12 const out: string[] = [];13 for (const el of document.querySelectorAll('main *, [role="dialog"] *')) {14 const r = el.getBoundingClientRect();15 if (r.width > 0 && r.right > vw + 1 && !el.closest('.overflow-x-auto, [class*="overflow-x-auto"], .sr-only')) out.push(`${el.tagName}.${String(el.className).slice(0, 60)} right=${Math.round(r.right)}`);16 }17 return out.slice(0, 5);18 });19 expect(wide, 'elements wider than the viewport').toEqual([]);20}2122test('phone: approve from Home — needs-you first, thumb-sized actions, badge follows', async ({ page }) => {23 const w = watch(page);24 await page.goto('/');25 await expect(page).toHaveURL(/\/home\//);26 await expect(page.getByTestId('mobile-shell')).toBeVisible();27 const nav = page.getByTestId('mobile-nav');28 await expect(nav.getByRole('link')).toHaveText([/Home/, /Missions/, /Approvals/]);29 await expect(nav.getByTestId('pending-badge')).toHaveText('2');30 // "Needs you" is the first thing on the screen31 const needs = page.getByTestId('needs-you');32 const cards = needs.getByTestId('wait-card');33 await expect(cards).toHaveCount(2);34 expect((await needs.boundingBox())!.y).toBeLessThan(140);35 const approval = needs.locator('[data-testid="wait-card"][data-kind="approval"]');36 await expect(approval).toContainText('Push aos/');37 await expect(approval.getByTestId('risk-badge')).toHaveText('External effect');38 const approve = approval.getByTestId('wait-approve');39 await approve.scrollIntoViewIfNeeded();40 const box = (await approve.boundingBox())!;41 expect(box.height).toBeGreaterThanOrEqual(44); // touch target42 expect(box.width).toBeGreaterThanOrEqual(150);43 await expectNoOverflow(page);44 await approve.tap();45 await expect(cards).toHaveCount(1);46 await expect(nav.getByTestId('pending-badge')).toHaveText('1');47 // the mission resumes: its card on Home leaves "waiting"48 const card = page.locator(`[data-testid="mission-card"]`).filter({ hasText: FIX.approval });49 await expect(card).not.toHaveAttribute('data-status', 'waiting');50 const m = await mission(page, FIX.approval);51 await expect.poll(async () => (await apiGet<{ status: string }>(page, `/missions/${m.id}`)).status, { timeout: 30_000 }).toBe('done');52 w.expectClean();53});5455test('phone: approvals tab — answer a question with an option; approve-and-remember needs a confirmation', async ({ page }) => {56 const w = watch(page);57 await page.goto('/home/');58 await page.getByTestId('tab-approvals').tap();59 await expect(page).toHaveURL(/\/approvals\//);60 await expect(page.getByTestId('page-title')).toHaveText('Approvals');61 await expectNoOverflow(page);62 const approval = page.locator('[data-testid="wait-card"][data-kind="approval"]');63 await approval.getByTestId('wait-remember').tap();64 await expect(page.getByTestId('remember-explain')).toBeVisible();65 await expectNoOverflow(page);66 await page.getByRole('dialog').getByRole('button', { name: 'Cancel' }).tap();67 await expect(approval).toBeVisible(); // cancelled: still pending68 await approval.getByTestId('wait-remember').tap();69 await page.getByTestId('wait-remember-confirm').tap();70 await expect(approval).toHaveCount(0);71 const question = page.locator('[data-testid="wait-card"][data-kind="question"]');72 await question.getByTestId('wait-option').first().tap();73 await expect(page.getByTestId('approvals-empty')).toBeVisible();74 await expect(page.getByTestId('mobile-nav').getByTestId('pending-badge')).toHaveCount(0);75 w.expectClean();76});7778test('phone: new mission sheet, mission timeline with composer, overview sheet', async ({ page }) => {79 const w = watch(page);80 await page.goto('/home/');81 await page.getByTestId('new-mission-page').tap();82 await expect(page.getByTestId('new-mission-form')).toBeVisible();83 await expectNoOverflow(page);84 await page.getByTestId('mission-objective').fill('Fix the invoice totals.');85 await page.getByTestId('criterion-add').tap();86 await page.getByTestId('criterion-input').fill('npm test passes');87 await page.getByTestId('model-claude-haiku-5-5').tap();88 await page.getByTestId('mission-submit').tap();89 await expect(page).toHaveURL(/\/mission\/\?id=/);90 await expect(page.getByTestId('mobile-nav')).toHaveCount(0); // the composer owns the bottom edge91 await expect(page.getByTestId('timeline-wait').getByTestId('wait-approve')).toBeVisible({ timeout: 30_000 });92 await page.getByTestId('timeline-scroll').evaluate((el) => { el.scrollTop = 0; });93 await expect(page.locator('[data-testid="tool-row"][data-tool="edit_file"]').first().getByTestId('diff-view')).toBeVisible();94 await expectNoOverflow(page);95 await page.getByTestId('composer-input').fill('Keep the public API unchanged.');96 await page.getByTestId('composer-send').tap();97 await expect(page.getByTestId('owner-message')).toContainText('Keep the public API unchanged.');98 await page.getByTestId('mission-overview').tap();99 await expect(page.getByRole('dialog', { name: 'Overview' }).getByTestId('criterion')).toHaveCount(1);100 await expect(page.getByRole('dialog', { name: 'Overview' }).getByTestId('run-node')).toHaveCount(1);101 w.expectClean();102});103104test('phone: every route renders inside 390 px without console errors or CSP violations', async ({ page }) => {105 const w = watch(page);106 const m = await mission(page, FIX.done);107 for (const route of [...ROUTES, `/mission/?id=${m.id}`]) {108 await page.goto(route);109 await expect(page.getByTestId('page-title').or(page.getByTestId('mission-title')).first()).toBeVisible();110 await page.waitForLoadState('networkidle');111 await expectNoOverflow(page);112 expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), route).toBe(true);113 }114 await page.goto('/home/');115 await page.getByTestId('nav-more').tap();116 await page.getByTestId('more-sheet').getByRole('radio', { name: 'Dark' }).tap();117 await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');118 await page.getByTestId('more-sheet').getByRole('link', { name: 'Spend' }).tap();119 await expect(page.getByTestId('spend-page')).toBeVisible();120 w.expectClean();121});122