Files
Ty 88b9c47200 UAT-4: remove non-spec controls (edge offset, feather) from Mask UI
Spec CAPTURE.md §Mask lists exactly two controls: Sheet and Sensitivity. Edge offset and feather nudge buttons were extra. Removed from the UI; core params (edgeOffset, feather) remain in maskState.js, still unit-tested.
2026-09-14 07:24:42 -07:00

94 lines
4.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});
});