Feature: Wiring the editor's image upload flow As a verified RenewCMS owner writing an article I want the editor's image button (and dropping a file onto the editor) to open an upload modal that validates the file, shows progress, and inserts the result at my cursor So that I can add images to an article's body without leaving the editor, with clear feedback when a file is rejected or the upload fails Background: Given EditorView.vue listens for ArticleEditor's "request-image" event and, on it, opens ImageUploader (a UModal titled "Add an image") with the current article's id And dropping a file directly onto the editor surface also opens ImageUploader, with that file already provided to it instead of an empty idle state And ImageUploader validates a chosen or dropped file in the browser, checking both its MIME type and its file extension against png/jpeg/webp/svg, and that its size is at most 10 × 1024 × 1024 bytes, before sending anything And a file that passes validation is uploaded via a new useImageUpload composable, using XMLHttpRequest (not useApi) so upload progress is observable, as multipart/form-data with the file under the field name "file", to POST /v1/article/{articleId}/image, per features/upload_article_image.feature's already-implemented and already-mounted contract And a 201 response's {"location": "..."} is inserted into ArticleEditor's content at the current cursor position via an exposed insertImage(src, alt) method, with alt ""; this moves useArticleDraft's state to "dirty" the same way any other body edit does And closing the modal while an upload is in flight aborts the XMLHttpRequest And a 401 response is handled entirely by useApi's global handling (features/api_client_global_error_handling.feature) and is out of scope for new scenarios here; the same is true of a 404 (the article was deleted elsewhere), which the editor's existing not-found handling already covers (features/editor_core.feature) Scenario: Opening the uploader from the toolbar's image button shows the idle state Given the user is on /article/12/edit with the editor focused When the user activates the toolbar's "Image" button Then ImageUploader opens titled "Add an image" And it shows "Drop an image here", a "Choose file" action, and the help text "PNG, JPEG, WEBP, SVG · max 10 MB" And its cancel action reads "Cancel" And no request has been sent Scenario: Dragging a file over the editor surface shows the dragging-over state Given ImageUploader is open in the idle state When the user drags a file over the drop zone Then the drop zone reads "Drop to upload" Scenario: Dropping a valid file onto the editor opens the uploader already uploading it Given the user is on /article/12/edit with the editor focused and ImageUploader is not open When the user drops a file named "before.png", content type "image/png", size 3.9 MB, onto the editor surface Then ImageUploader opens titled "Add an image" And POST /v1/article/12/image is sent with that file under the field name "file" And the uploading state is shown, not the idle state Scenario Outline: Choosing a file the browser can reject never sends a request Given ImageUploader is open in the idle state for article 12 When the user chooses a file named "", content type "", size Then no request is sent to POST /v1/article/12/image And the rejected state shows the title "\"\" can't be used" And its text reads "" And its confirm action reads "Choose another file" and its cancel action reads "Cancel" Examples: | filename | content_type | size | description | | plan.pdf | application/pdf | 1.2 MB | Use a PNG, JPEG, WEBP or SVG image, up to 10 MB. | | disguised.png | application/pdf | 1.2 MB | Use a PNG, JPEG, WEBP or SVG image, up to 10 MB. | | north-wall.tif | image/png | 14.2 MB | north-wall.tif is 14.2 MB. Images can be up to 10 MB. | Scenario: "Choose another file" from the rejected state returns to idle Given ImageUploader is in the rejected state for a file named "plan.pdf" When the user activates "Choose another file" Then the idle state is shown again, with the file picker available Scenario: A valid upload shows progress and its cancel action reads "Cancel upload" Given ImageUploader is open in the idle state for article 12 When the user chooses a file named "north-wall.jpg", content type "image/jpeg", size 3.9 MB Then POST /v1/article/12/image is sent with that file under the field name "file" And the uploading state shows "north-wall.jpg 0%" and "Uploading 0.0 of 3.9 MB" And its cancel action reads "Cancel upload" When the upload reports 2.4 MB sent of 3.9 MB Then the uploading state shows "north-wall.jpg 62%" and "Uploading 2.4 of 3.9 MB" Scenario: Closing the dialog during an upload cancels it Given ImageUploader is uploading a file named "north-wall.jpg" to article 12 When the user activates "Cancel upload" (or otherwise closes the modal) Then the in-flight XMLHttpRequest is aborted And the modal closes And the editor's body is unchanged and remains in its previous dirty/clean state Scenario: A successful upload inserts the image at the cursor and marks the draft dirty Given the user is on /article/12/edit in the Saved state and ImageUploader is uploading a file named "north-wall.jpg" to article 12 When POST /v1/article/12/image resolves 201 with {"location": "/static/uploadedImages/north-wall.jpg"} Then the uploading state briefly shows "Image added" And the image at "/static/uploadedImages/north-wall.jpg" is inserted into the editor's content at the cursor via insertImage And SaveStatus reads "Unsaved changes" and the Save button becomes enabled And a toast appears: color success, icon i-lucide-circle-check, title "Image added", description "Save to keep it in the article." And the modal closes 600 ms after the response resolves Scenario Outline: A server-side upload failure keeps the article unchanged and offers a retry Given ImageUploader is uploading a file named "north-wall.jpg" to article 12 When POST /v1/article/12/image Then the failed state shows the title "Upload failed" And its text reads "The server didn't accept the file. Your article hasn't changed." And its confirm action reads "Try again" And the editor's body is unchanged, and no image is inserted Examples: | failure | | resolves with a 500 response | | never reaches the server | Scenario: "Try again" re-attempts the upload with the same file Given ImageUploader is in the failed state for a file named "north-wall.jpg", article 12, after a 500 response When the user activates "Try again" Then POST /v1/article/12/image is sent again with that same file under the field name "file" And the uploading state is shown Scenario: A server 400 for a type the client should have caught shows the same rejected copy Given ImageUploader is uploading a file named "notes.pdf" to article 12 (a type that defensively passed the browser's own check, e.g. a MIME type the browser misreported) When POST /v1/article/12/image resolves 400 Then the rejected state shows the title "\"notes.pdf\" can't be used" And its text reads "Use a PNG, JPEG, WEBP or SVG image, up to 10 MB." And its confirm action reads "Choose another file" And the editor's body is unchanged, and no image is inserted Scenario: A server 400 for a size the client should have caught shows the same size-named copy Given ImageUploader is uploading a file named "north-wall.tif" to article 12 (a file that defensively passed the browser's own size check somehow, e.g. its reported size changed between selection and send) When POST /v1/article/12/image resolves 400 Then the rejected state shows the title "\"north-wall.tif\" can't be used" And its text reads "north-wall.tif is 14.2 MB. Images can be up to 10 MB." And its confirm action reads "Choose another file" And the editor's body is unchanged, and no image is inserted