diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index b6485f2..789b7e3 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -468,11 +468,17 @@ border-radius: 8px; border: 1px solid #ddd; background: #f5f5f5; + display: flex; + align-items: center; + justify-content: center; } .preview { - width: 100%; + max-width: 100%; + max-height: 50vh; + width: auto; height: auto; display: block; + object-fit: contain; } .busy { position: absolute; diff --git a/frontend/tests/e2e/smoke.spec.js b/frontend/tests/e2e/smoke.spec.js index 1405c4a..d2bf2ea 100644 --- a/frontend/tests/e2e/smoke.spec.js +++ b/frontend/tests/e2e/smoke.spec.js @@ -1,7 +1,8 @@ import { test, expect } from '@playwright/test'; +const SLAB_FIXTURE = 'tests/fixtures/slab-green.jpg'; + // E2E smoke: the app serves and the health route responds. -// Later cards extend this to walk the full 5-state capture flow. test('M1-1 harness smoke: app serves + health ok', async ({ page, request }) => { await page.goto('/'); await expect(page).toHaveTitle(/SlabUploader/); @@ -10,3 +11,35 @@ test('M1-1 harness smoke: app serves + health ok', async ({ page, request }) => const body = await res.json(); expect(body.ok).toBe(true); }); + +// Regression: the preview canvas must fit the full photo within the +// viewport without cropping. Spec: "Preview fits the device width. +// Height is capped so Mask and Measure controls stay on screen." +test('preview canvas fits full photo without cropping', async ({ page }) => { + await page.goto('/'); + await page.locator('input[type="file"]').setInputFiles(SLAB_FIXTURE); + await page.waitForTimeout(2500); // mask recompute + + const canvas = page.locator('canvas.preview'); + await expect(canvas).toBeVisible(); + + // Get the canvas's rendered (CSS) size and its intrinsic (pixel) size. + const box = await canvas.boundingBox(); + const intrinsic = await canvas.evaluate((c) => ({ w: c.width, h: c.height })); + + // The rendered size must fit within the viewport. + const vp = page.viewportSize(); + expect(box.width).toBeLessThanOrEqual(vp.width); + expect(box.height).toBeLessThanOrEqual(vp.height * 0.55); // 50vh cap + tolerance + + // Aspect ratio must be preserved (no distortion). + const renderedRatio = box.width / box.height; + const intrinsicRatio = intrinsic.w / intrinsic.h; + expect(renderedRatio).toBeCloseTo(intrinsicRatio, 1); + + // The canvas must not be clipped by its container. + const wrap = page.locator('.preview-wrap'); + const wrapBox = await wrap.boundingBox(); + expect(box.width).toBeLessThanOrEqual(wrapBox.width + 1); + expect(box.height).toBeLessThanOrEqual(wrapBox.height + 1); +});