Fix preview cropping: canvas fits within viewport, preserves aspect ratio

.preview was width:100% inside max-height:50vh container — tall photos
were clipped. Now: max-width:100%, max-height:50vh, width/height auto,
object-fit:contain, flex-centered in the wrap.

Added e2e regression test: loads a photo, verifies the canvas bounding
box fits the viewport, preserves aspect ratio, and is not clipped by
the container.
This commit is contained in:
Ty
2026-09-14 11:51:44 -07:00
parent 2d5ff95441
commit 8749b07611
2 changed files with 41 additions and 2 deletions
+7 -1
View File
@@ -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;
+34 -1
View File
@@ -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);
});