SPB Git

Repository Public

phone.spec.ts 6.4 KB 122 lines typescript

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
Files main
Menu

Theme