import { test, expect } from '@playwright/test'; // M1-3: Playwright tests for the Mask state (RED first — the live Mask UI // lands in M1-5; the pure state machine + geometry are unit-tested in // tests/unit/maskState.test.js). // // Spec ground truth (docs/CAPTURE.md): // - "Approve is blocked when the mask has no slab pixels. User retakes." // - "If the file is HEIC and the browser cannot decode it, show a one-tap // notice to retake the photo as JPEG." // - 2 knobs: sheet Auto/Green/Black, sensitivity 0–100 (slider + minus/plus). // Edge offset and feather are NOT user-exposed in M1 (core params only). // - Sensitivity slider updates immediately; overlay recomputes 2.0 s after // the last change. const SLAB_FIXTURE = 'tests/fixtures/slab-green.jpg'; const SHEET_ONLY_FIXTURE = 'tests/fixtures/sheet-only-green.jpg'; const HEIC_FIXTURE = 'tests/fixtures/slab.heic'; test.describe('Mask state (M1-5 live UI)', () => { test('HEIC input that the browser cannot decode shows a one-tap retake notice', async ({ page }) => { // 1. Land on Add photo. await page.goto('/'); // 2. Provide an HEIC file the browser cannot decode. const input = page.locator('input[type="file"]'); await input.setInputFiles(HEIC_FIXTURE); // 3. The app must surface a one-tap 'retake as JPEG' notice — never a // silent crash, never a server-side decode. const notice = page.getByRole('alert', { name: /retake/i }); await expect(notice).toBeVisible(); await expect(notice).toHaveText(/retake as JPEG/i); // 4. Tapping it returns to Add photo. await notice.tap(); await expect(page.getByRole('button', { name: /add photo/i })).toBeVisible(); }); test('Approve is blocked while the mask has no slab pixels', async ({ page }) => { // 1. Load a photo of the sheet only (no slab). await page.goto('/'); const input = page.locator('input[type="file"]'); await input.setInputFiles(SHEET_ONLY_FIXTURE); // 2. The Mask state opens with the overlay visible. await expect(page.getByText(/mask/i)).toBeVisible(); // 3. Approve must be disabled — the mask has no slab pixels. const approve = page.getByRole('button', { name: /approve/i }); await expect(approve).toBeDisabled(); // 4. Retake and load a photo with a real slab -> approve enables. await page.getByRole('button', { name: /retake/i }).tap(); await page.locator('input[type="file"]').setInputFiles(SLAB_FIXTURE); await expect(approve).toBeEnabled(); }); test('keys the green sheet and shows the 2 spec knobs', async ({ page }) => { await page.goto('/'); await page.locator('input[type="file"]').setInputFiles(SLAB_FIXTURE); // Sheet control with Auto/Green/Black. const sheet = page.getByRole('radiogroup', { name: /sheet/i }); await expect(sheet.getByRole('radio', { name: /auto/i })).toBeChecked(); await expect(sheet.getByRole('radio', { name: /green/i })).toBeVisible(); await expect(sheet.getByRole('radio', { name: /black/i })).toBeVisible(); // Sensitivity slider 0–100 with minus/plus. const slider = page.getByRole('slider', { name: /sensitivity/i }); await expect(slider).toBeVisible(); await expect(slider).toHaveAttribute('min', '0'); await expect(slider).toHaveAttribute('max', '100'); await expect(page.getByRole('button', { name: /decrease sensitivity/i })).toBeVisible(); await expect(page.getByRole('button', { name: /increase sensitivity/i })).toBeVisible(); // Edge offset and feather are NOT in the spec — they must not be // user-exposed in the Mask UI. await expect(page.getByRole('slider', { name: /edge offset/i })).not.toBeVisible(); await expect(page.getByRole('slider', { name: /feather/i })).not.toBeVisible(); // Overlay visible (photo with checkerboard on removed px). await expect(page.locator('[data-testid="mask-overlay"]')).toBeVisible(); }); test('sensitivity slider updates the control immediately', async ({ page }) => { await page.goto('/'); await page.locator('input[type="file"]').setInputFiles(SLAB_FIXTURE); const slider = page.getByRole('slider', { name: /sensitivity/i }); await slider.fill('80'); // The control value is reflected immediately, not after the 2 s debounce. await expect(slider).toHaveValue('80'); }); test('approve advances to Length axis after a non-empty mask', async ({ page }) => { await page.goto('/'); await page.locator('input[type="file"]').setInputFiles(SLAB_FIXTURE); // Let the initial recompute land so stats are non-empty. await page.waitForTimeout(2200); await page.getByRole('button', { name: /approve/i }).tap(); await expect(page.getByText(/length axis/i)).toBeVisible(); }); });