6.8 KiB
QA: Wiring the editor's image upload flow
Automated (Vitest + Vue Test Utils, task test:web)
Mock XMLHttpRequest (a fake implementing open/setRequestHeader/send/abort and firing
upload.progress, load, error) for POST /v1/article/{id}/image; useApi/fetch is not
involved in this slice. Mount EditorPage inside DashboardShell at /article/12/edit with the
editor_core.qa.md fixture article (id 12, title "How we price a bathroom renovation", is_online
false), GET already resolved 200.
- Toolbar button opens the idle state — click the toolbar's "Image" button; assert ImageUploader opens titled "Add an image", showing "Drop an image here", "Choose file", "PNG, JPEG, WEBP, SVG · max 10 MB", cancel "Cancel"; assert no XHR was created.
- Dragging over shows "Drop to upload" — with the uploader open idle, fire a
dragoveron the drop zone; assert the text changes to "Drop to upload". - Dropping a file on the editor opens the uploader already uploading — with the uploader
closed, dispatch a
dropevent with a filebefore.png(image/png, 3.9 MB) on the editor surface; assert ImageUploader opens and an XHR to/v1/article/12/imageis created with that file, and the uploading state (not idle) is shown. - Rejected — Examples-driven on type, extension mismatch, and size — for each of
plan.pdf(application/pdf, 1.2 MB),disguised.png(application/pdf, 1.2 MB), andnorth-wall.tif(image/png, 14.2 MB), choose the file via theUFileUpload; assert no XHR is created and the rejected state shows the exact title/text/confirm/cancel copy from the feature's Examples. - "Choose another file" returns to idle — from the rejected state, click "Choose another file"; assert the idle state re-renders.
- Uploading shows live progress and "Cancel upload" — choose
north-wall.jpg(image/jpeg, 3.9 MB); assert the XHR is created with that file under fieldfile; assert cancel reads "Cancel upload"; fire anupload.progressevent withloaded: 2.4 * 1024 * 1024, total: 3.9 * 1024 * 1024; assert the text reads "north-wall.jpg 62%" and "Uploading 2.4 of 3.9 MB". - Closing during an upload aborts the XHR — from the uploading state, click "Cancel
upload"; assert the fake XHR's
abortwas called, the modal closes, and the editor's body and dirty state are unchanged from before the upload started. - A 201 inserts at the cursor and marks the draft dirty — from the uploading state, resolve
the fake XHR with status 201 and body
{"location": "/static/uploadedImages/north-wall.jpg"}; assertinsertImage(or the editor's exposed method) was called with that location; assert SaveStatus now reads "Unsaved changes" and Save is enabled; assert a toast appears with color success, iconi-lucide-circle-check, title "Image added", description "Save to keep it in the article."; advance fake timers 600 ms and assert the modal has closed. - A 500 or network failure shows "Upload failed" and keeps the article unchanged, Examples-
driven — for (a) the fake XHR firing
loadwith status 500 and (b) firingerror; assert the failed state shows title "Upload failed", text "The server didn't accept the file. Your article hasn't changed.", confirm "Try again"; assert noinsertImagecall and the editor's body/dirty state is unchanged. - "Try again" resends the same file — from the failed state for
north-wall.jpg, click "Try again"; assert a new XHR is created for/v1/article/12/imagewith the same file, and the uploading state is shown again. - A server 400 for a type the client should have caught shows the rejected copy — from an
uploading state for
notes.pdf, resolve the fake XHR with status 400; assert the rejected state shows title ""notes.pdf" can't be used", text "Use a PNG, JPEG, WEBP or SVG image, up to 10 MB.", confirm "Choose another file"; assert noinsertImagecall. - A server 400 for a size the client should have caught shows the size-named rejected copy
— from an uploading state for
north-wall.tif, resolve the fake XHR with status 400; assert the rejected state shows title ""north-wall.tif" can't be used", text "north-wall.tif is 14.2 MB. Images can be up to 10 MB.", confirm "Choose another file"; assert noinsertImagecall.
Visual (manual verification — desktop and mobile, light and dark)
Reference: docs/design/screens/editor.md states "Upload: choose", "Upload: in progress",
"Upload: rejected", "Upload: inserted", and docs/design/source/RenewCMS Screens Back- office.dc.html. Run task visual:check -- editor for each state below; add each state's mock
setup to web/visual/states.ts first, reusing editor_core.qa.md's fixture article (id 12, title
"How we price a bathroom renovation", draft, "Today, 11:02", 3 images) plus the sample file data
from the board: filename "north-wall.jpg", size 3.9 MB, progress 62% / 2.4 of 3.9 MB uploaded.
- Upload: choose — modal "Add an image", idle state, cancel "Cancel".
- Upload: in progress — modal "Add an image", uploading "north-wall.jpg 62%" / "Uploading 2.4 of 3.9 MB", cancel "Cancel upload".
- Upload: rejected — modal "Add an image", rejected state for a sample rejected file per the board's script, cancel "Cancel", confirm "Choose another file".
- Upload: inserted — toast "Image added" / "Save to keep it in the article.", editor status "draft", save "dirty".
- Mobile (all of the above at 390×844): same modal layout constraints as other editor modals (leading arrow-left button, trailing ellipsis in the header behind it).
- Keyboard/focus: the drop zone is reachable by Tab and Enter opens the file picker; focus
moves into the modal on open and is trapped there; Esc during "Upload: in progress"
behaves like "Cancel upload" (aborts, closes); the progress bar's
role="progressbar"/aria-valuenowupdates are announced without spamming on every progress tick.- Motion: modal open/close follows the standard 240 ms timing; the success state's 600 ms auto-close is not visually checked as a capture (it's a timing behavior, not a static state) — cover only via the Vitest fake-timer assertion above.
Manual verification
- Real POST round trip against a running server — Vitest only mocks
XMLHttpRequest; confirm a real upload (200/400/401/404/500) againsttask devwith a live SQLite file and the realPOST /v1/article/{id}/imagehandler, sincefeatures/upload_article_image.feature's httptest suite covers the handler in isolation but never through this SPA flow. - Real drag-and-drop from the OS file picker — jsdom's
dropevent is synthetic; confirm a real OS-level drag of an image file onto the editor in a real browser.