192 lines
12 KiB
Gherkin
192 lines
12 KiB
Gherkin
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 |
|