Files
RenewCMS/features/editor_image_upload.qa.md
T

91 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.