# QA: Crop and resize step in the editor's image uploader ## Automated (Vitest + Vue Test Utils, `task test:web`) Mount EditorPage inside DashboardShell at `/article/12/edit` with the editor_core.qa.md fixture article (id 12, "How we price a bathroom renovation", is_online false), GET already resolved 200. Reuse editor_image_upload.qa.md's fake-XHR setup for the upload itself. Stub the chosen file's decoded natural size (e.g. mock `Image`/`createImageBitmap` or whatever the component uses to read `naturalWidth`/`naturalHeight`) so tests control it deterministically, and stub the crop/resize step's own output producer (whatever function turns the frame rect + width preset into a Blob) as a spy, asserting it is called with the frame's rect in natural-image pixels and the resolved output width/height, and stubbing its returned Blob's size — without asserting how it renders internally (canvas/OffscreenCanvas is a coder implementation detail). 1. **A raster file opens "Crop and resize" instead of uploading** — choose `north-wall.jpg` (image/jpeg, 3.9 MB, natural 4032×3024); assert no XHR is created; assert the modal title is "Crop and resize", description "north-wall.jpg · 3.9 MB", ratio "Original" and width "1600 px" selected, mono line "Uploads as 1600 × 1200 px · about 420 KB", footer "Choose another file" / "Upload image". 2. **An SVG bypasses the step** — choose `diagram.svg` (image/svg+xml, 40 KB); assert an XHR to `/v1/media/` is created immediately and the crop step never renders. 3. **Ratio presets lock the frame, Examples-driven (16:9 / 4:3 / 1:1)** — from the opened step, select each ratio pill; assert the frame's reported rect and the mono line's output size match the feature's Examples table. 4. **Width preset recomputes the output** — select width "800 px"; assert the mono line reads "Uploads as 800 × 600 px" (assert dimensions exactly; assert only that the size figure changed to a smaller value, not a specific number). 5. **A crop narrower than the width shows the info row and keeps its own size** — resize the frame (via its resize API/spy) to 640×640 with ratio "1:1", width "1600 px"; assert the mono line reads "Uploads as 640 × 640 px · about {size}" and the info row reads exactly "The crop is 640 px wide, so it's uploaded at that size. Enlarging it would make it blurry." 6. **Free ratio removes the constraint** — select ratio "Free"; assert a subsequent resize call with independent width/height is accepted without being coerced back to a ratio. 7. **Arrow and Shift+Arrow keys move/resize** — focus the frame; fire `ArrowRight`; assert the frame's x increases by 1 px and the mono line's size is unchanged; fire `Shift+ArrowRight`; assert the frame's width increases by 1 px and the mono line updates. 8. **Rotate cycles 90° and Reset restores defaults** — activate "Rotate"; assert the reported rotation is 90°; activate "Reset"; assert ratio "Original", width "1600 px", the frame back to the whole unrotated image, and the mono line "Uploads as 1600 × 1200 px · about 420 KB". 9. **"Choose another file" discards the step** — activate it; assert no XHR was created and the idle state renders. 10. **A newly chosen file reopens at defaults** — from idle after "Choose another file" out of a 16:9/800px step, choose `sunset.jpg` (image/jpeg, 2.1 MB, natural 3000×2000); assert the new crop step opens with ratio "Original", width "1600 px", frame as the whole new image (not the previous file's ratio/width). 11. **"Upload image" sends the cropped/resized file** — from the default 1600×1200/420 KB step, activate "Upload image"; assert the XHR is created with a file whose declared size is 420 KB (not 3.9 MB); assert the uploading state shows "Uploading 0.0 of 420 KB"; fire an `upload.progress` with `loaded: 180 * 1024, total: 420 * 1024`; assert "north-wall.jpg 43%" and "Uploading 180 of 420 KB". ## Visual (manual verification via `task visual:check -- editor` — desktop and mobile, light and dark) Reference: `docs/design/screens/editor.md` states "Upload: crop and resize", "Upload: crop to a ratio", "Upload: crop narrower than the width", and the updated "Upload: in progress"; board `docs/design/source/RenewCMS Screens Back-office.dc.html`, group "Editor". Add each state's mock setup to `web/visual/states.ts`, reusing editor_core.qa.md's fixture article (id 12, "How we price a bathroom renovation", draft) and the sample file "north-wall.jpg · 3.9 MB". - [ ] Upload: crop and resize — modal "Crop and resize" / "north-wall.jpg · 3.9 MB", ratio "Original", width "1600 px", cancel "Choose another file", confirm "Upload image". - [ ] Upload: crop to a ratio — same modal, ratio "16:9" selected, frame locked to that ratio. - [ ] Upload: crop narrower than the width — ratio "1:1", width "1600 px", frame resized to 640×640, info row "The crop is 640 px wide, so it's uploaded at that size. Enlarging it would make it blurry." - [ ] Upload: in progress (updated) — modal "Add an image", uploading "north-wall.jpg 43%" / "Uploading 180 of 420 KB", cancel "Cancel upload". - [ ] Mobile (all of the above at 390×844): same modal layout constraints as other editor modals. - [ ] Keyboard/focus: the crop frame is reachable by Tab; its `aria-label` is "Crop area. Arrow keys move it, Shift and arrow keys resize it."; Arrow/Shift+Arrow move and resize it without moving focus elsewhere; the aspect-ratio and width radiogroups follow standard roving-tabindex radiogroup keyboard behavior (arrow keys between pills, one tab stop per group); "Rotate" and "Reset" are reachable and named by their visible text. - [ ] Motion: no auto-close or timed transition in this step; frame drag/resize follows pointer with no easing delay (not part of the 10 % capture budget, spot-checked only). ## Manual verification - **Real image decoding and resizing** — Vitest stubs `naturalWidth`/`naturalHeight` and the output producer; confirm real JPEG/PNG/WEBP files of varying orientations and EXIF rotation crop, rotate and resize correctly against `task dev` with a live server, since jsdom has no real canvas/image pipeline. - **Real POST of the resized file** — confirm the actual uploaded bytes decode to the expected pixel dimensions server-side, complementing `features/upload_article_image.feature`'s httptest coverage of the handler in isolation.