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:
@@ -468,11 +468,17 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.preview {
|
.preview {
|
||||||
width: 100%;
|
max-width: 100%;
|
||||||
|
max-height: 50vh;
|
||||||
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
.busy {
|
.busy {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const SLAB_FIXTURE = 'tests/fixtures/slab-green.jpg';
|
||||||
|
|
||||||
// E2E smoke: the app serves and the health route responds.
|
// 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 }) => {
|
test('M1-1 harness smoke: app serves + health ok', async ({ page, request }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
await expect(page).toHaveTitle(/SlabUploader/);
|
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();
|
const body = await res.json();
|
||||||
expect(body.ok).toBe(true);
|
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);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user