91 lines
6.8 KiB
Markdown
91 lines
6.8 KiB
Markdown
# 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.
|