Files
RenewCMS/features/editor_image_upload.feature
T

192 lines
12 KiB
Gherkin
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.
Feature: The editor's Upload tab uploads to the library
As a verified RenewCMS owner writing an article
I want the Upload tab of the "Add an image" dialog to check my file, spot duplicates,
upload to my library and insert 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 this slice is SPA only for this file: the API from the media library slice is used
unchanged
And EditorView listens for ArticleEditor's "request-image" event and opens ImageUploader
(a UModal titled "Add an image") with the picker tabs, whose Library tab is specified in
features/editor_media_picker.feature
And dropping a file onto the editor surface opens ImageUploader on the Upload tab with that
file already provided, and no list request is sent until the Library tab is opened
And this file supersedes these statements of the earlier version: the upload goes to
POST /v1/media/ and no longer to POST /v1/article/{articleId}/image; the 201 body is the
library item { id, filename, url, size, content_type, width, height, sha256, uploaded_by,
created_at, used_in, duplicate }; there is no article id in any upload request and no 404
for a deleted article on upload
And ImageUploader validates a chosen or dropped file in the browser with validateImageFile
from web/src/lib/imageUpload.ts, size first (10485760 bytes allowed, one byte more is too
large), then MIME type and extension must both be allowed (png, jpeg, webp, svg), before
hashing or sending anything
And a file that passes validation is hashed with hashFile from web/src/lib/fileHash.ts (the
lib shared with MediaPage), and GET /v1/media/?hash=<sha256 hex> is sent through useApi;
when hashFile resolves null, or the lookup fails with 500 or a network error, the check
is skipped and the flow continues as if nothing matched
And a file with no match continues exactly as in features/editor_image_crop_resize.feature:
a JPEG, PNG or WEBP opens the crop and resize step, an SVG is uploaded immediately
And the upload is sent with XMLHttpRequest (not useApi) so progress is observable, as
multipart/form-data with the file under the field name "file" to POST /v1/media/, with the
jwt cookie
And a 201 inserts the item's url at the cursor through insertImage(item.url, "", item.id),
so the node carries data-image-id, and useArticleDraft moves to "dirty"
And closing the dialog while an upload is in flight aborts the XMLHttpRequest
And the tabs show only on the choose step (idle, the duplicate card and the Library tab);
the crop, uploading, rejected and failed steps keep the earlier look without tabs
And the footer on the Upload tab holds only the step's own actions ("Cancel" in idle);
there is no "Insert image" button on it
Rule: Opening
Scenario: The toolbar's image button opens on Upload when the library is empty
Given the user is on /article/12/edit with the editor focused
And GET /v1/media/?limit=24 resolves 200 with no item
When the user activates the toolbar's "Image" button
Then ImageUploader opens titled "Add an image" with the tabs "Upload" (icon upload) and "Library" (icon images)
And the Upload tab is selected and shows the image-plus icon, "Drop an image here, or choose a file" and "PNG, JPEG, WEBP, SVG · max 10 MB"
And its footer reads "Cancel" only
And no POST has been sent
Scenario: Dropping a file opens Upload with the file
Given the user is on /article/12/edit 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 on the Upload tab
And GET /v1/media/?hash=<sha256 of the file> is sent, and no request to GET /v1/media/?limit=24 is sent
And when the lookup resolves 200 with no item the crop and resize step shows for "before.png"
And no POST /v1/media/ has been sent
Scenario: Dragging a file over the drop zone shows the dragging-over state
Given ImageUploader is open on the Upload tab in the idle state
When the user drags a file over the drop zone
Then the drop zone reads "Drop to upload"
Rule: Browser checks, nothing is sent
Scenario Outline: Choosing a file the browser can reject sends nothing
Given ImageUploader is open in the idle state
When the user chooses a file named "<filename>", content type "<content_type>", size <size>
Then no hash lookup and no POST /v1/media/ is sent
And the rejected state shows the title "\"<filename>\" can't be used"
And its text reads "<description>"
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 "plan.pdf"
When the user activates "Choose another file"
Then the idle state is shown again, with the tabs
Rule: Already in your library
Scenario: A file whose content exists shows the duplicate card and sends nothing
Given ImageUploader is open in the idle state
And GET /v1/media/?hash=<sha256 of north-wall.jpg> resolves 200 with the image "north-wall.jpg" (id 1, 4032 × 3024, uploaded 12 Sep 2026 by Marta Alves)
When the user chooses a valid file named "north-wall.jpg" of 3.9 MB
Then the Upload tab shows a card with an info border: a 72 × 54 thumbnail of the existing image, the title "Already in your library" and the text "north-wall.jpg matches an image uploaded on 12 Sep 2026 by Marta Alves."
And the card has the buttons "Use existing" (primary) and "Upload anyway", stacked and full width below md
And no POST /v1/media/ has been sent and the crop step has not opened
Scenario: Several matches name the oldest one
Given the hash lookup resolves 200 with several images
Then the card names the oldest match
Scenario: A match without a known uploader drops the "by" part
Given the matched image's uploaded_by is empty
Then the text reads "north-wall.jpg matches an image uploaded on 12 Sep 2026."
Scenario: "Use existing" goes to the alt text step for the existing image
Given the duplicate card is showing for image id 1
When the user activates "Use existing"
Then the Library tab is selected with "north-wall.jpg" selected, its panel visible with the empty "Alt text (optional)" field, and "Insert image" enabled
And no POST /v1/media/ has been sent
Scenario: "Upload anyway" continues to crop and upload
Given the duplicate card is showing for "north-wall.jpg" (4032 × 3024)
When the user activates "Upload anyway"
Then the crop and resize step opens for that file
And the hash lookup is not repeated
And uploading from that step sends POST /v1/media/ and follows the normal states
Scenario: "Upload anyway" for an SVG uploads immediately
Given the duplicate card is showing for "diagram.svg"
When the user activates "Upload anyway"
Then POST /v1/media/ is sent with that file
Scenario Outline: The duplicate check is skipped when it cannot run
Given ImageUploader is open in the idle state
When the user chooses a valid file and <condition>
Then the flow continues to the crop step (or the upload for an SVG) with no card
Examples:
| condition |
| crypto.subtle is undefined (an insecure context) |
| GET /v1/media/?hash= resolves with a 500 response |
| GET /v1/media/?hash= never reaches the server |
Rule: Uploading
Scenario: A valid upload shows progress and its cancel action reads "Cancel upload"
Given the crop step was confirmed (or an SVG chosen) for "north-wall.jpg", 3.9 MB
Then POST /v1/media/ is sent with the 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 it 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 "north-wall.jpg"
When the user activates "Cancel upload" (or otherwise closes the dialog)
Then the in-flight XMLHttpRequest is aborted and the dialog closes
And the editor's body and its dirty or clean state are unchanged
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 "north-wall.jpg" is uploading
When POST /v1/media/ resolves 201 with the item { id: 13, url: "/static/uploadedImages/13.jpg", filename: "north-wall.jpg" }
Then the uploading state briefly shows "Image added"
And the image "/static/uploadedImages/13.jpg" is inserted at the cursor with alt "" and data-image-id 13
And SaveStatus reads "Unsaved changes" and Save is 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 dialog closes 600 ms after the response
Scenario Outline: A server answer for a file that passed the browser checks
Given ImageUploader is uploading "<file>" of <size>
When POST /v1/media/ resolves <status>
Then the <state> state shows the title "<title>"
And its text reads "<text>"
And its confirm action reads "<confirm>"
And no image is inserted and the editor's body is unchanged
Examples:
| file | size | status | state | title | text | confirm |
| notes.pdf | 210 KB | 415 | rejected | "notes.pdf" can't be used | Use a PNG, JPEG, WEBP or SVG image, up to 10 MB. | Choose another file |
| north-wall.tif | 14.2 MB | 413 | rejected | "north-wall.tif" can't be used | north-wall.tif is 14.2 MB. Images can be up to 10 MB. | Choose another file |
| broken.png | 3.0 MB | 400 | rejected | "broken.png" can't be used | Use a PNG, JPEG, WEBP or SVG image, up to 10 MB. | Choose another file |
| north-wall.jpg | 3.9 MB | 500 | failed | Upload failed | The server didn't accept the file. Your article hasn't changed. | Try again |
| north-wall.jpg | 3.9 MB | 429 | failed | Upload failed | The server didn't accept the file. Your article hasn't changed. | Try again |
| north-wall.jpg | 3.9 MB | none (network) | failed | Upload failed | The server didn't accept the file. Your article hasn't changed. | Try again |
Scenario: "Try again" re-attempts the upload with the same file
Given ImageUploader is in the failed state for "north-wall.jpg" after a 500
When the user activates "Try again"
Then POST /v1/media/ is sent again with that same file and no new hash lookup, and the uploading state shows
Rule: Sessions
Scenario Outline: Session errors are global
When <call> resolves <status>
Then the user is sent where useApi sends a <status> from /article/12/edit, and no local copy shows
Examples:
| call | status |
| POST /v1/media/ (XHR) | 401 |
| POST /v1/media/ (XHR) | 403 |
| GET /v1/media/?hash= | 401 |