Files
RenewCMS/features/editor_image_upload.qa.md
T

6.8 KiB
Raw Blame History

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.

  1. 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.
  2. Dragging over shows "Drop to upload" — with the uploader open idle, fire a dragover on the drop zone; assert the text changes to "Drop to upload".
  3. Dropping a file on the editor opens the uploader already uploading — with the uploader closed, dispatch a drop event with a file before.png (image/png, 3.9 MB) on the editor surface; assert ImageUploader opens and an XHR to /v1/article/12/image is created with that file, and the uploading state (not idle) is shown.
  4. 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), and north-wall.tif (image/png, 14.2 MB), choose the file via the UFileUpload; assert no XHR is created and the rejected state shows the exact title/text/confirm/cancel copy from the feature's Examples.
  5. "Choose another file" returns to idle — from the rejected state, click "Choose another file"; assert the idle state re-renders.
  6. 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 field file; assert cancel reads "Cancel upload"; fire an upload.progress event with loaded: 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".
  7. Closing during an upload aborts the XHR — from the uploading state, click "Cancel upload"; assert the fake XHR's abort was called, the modal closes, and the editor's body and dirty state are unchanged from before the upload started.
  8. 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"}; assert insertImage (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, icon i-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.
  9. A 500 or network failure shows "Upload failed" and keeps the article unchanged, Examples- driven — for (a) the fake XHR firing load with status 500 and (b) firing error; 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 no insertImage call and the editor's body/dirty state is unchanged.
  10. "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/image with the same file, and the uploading state is shown again.
  11. 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 no insertImage call.
  12. 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 no insertImage call.

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-valuenow updates 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) against task dev with a live SQLite file and the real POST /v1/article/{id}/image handler, since features/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 drop event is synthetic; confirm a real OS-level drag of an image file onto the editor in a real browser.