375 lines
25 KiB
Gherkin
375 lines
25 KiB
Gherkin
Feature: Media page, part 1: browse and manage
|
||
|
||
As a verified RenewCMS owner
|
||
I want /media to show every image in my library, let me search, filter, sort and switch view, and
|
||
let me inspect, copy, rename and delete one image at a time
|
||
So that I can manage the library from one place, with the page always reflecting what the
|
||
server actually holds
|
||
|
||
Background:
|
||
Given /media is a verified route (meta.auth "verified", meta.title "Media") rendered as
|
||
MediaPage inside DashboardShell, per docs/design/routes.md
|
||
And the sidebar gains the item "Media" (icon images) between "Articles" and "Users", in the
|
||
desktop sidebar and in the mobile drawer, it is the active item on /media, and the
|
||
breadcrumb reads "Media" (mobile title "Media")
|
||
And this slice is SPA only: the API from slice A already exists and is used unchanged, and
|
||
no Go code, use case or repository changes
|
||
And upload, multi-select, the bulk bar, the Duplicates tab content, duplicate merge,
|
||
drag-and-drop and the rate-limited state are specified in
|
||
features/media_upload_selection_duplicates.feature
|
||
And useMedia is a composable over useApi. It exposes the list state (items, counts, loading,
|
||
error), the request functions for list, usage, rename and delete, and the copy helper, and
|
||
it maps ApiError and NetworkError to the states below
|
||
And MediaPage calls GET /v1/media/ with the query built from the URL, always with an explicit
|
||
page and size (page=1&size=25 by default, or the stored size), and every request carries
|
||
the jwt cookie through useApi
|
||
And this slice is amended by features/media_pagination_api.feature and
|
||
features/media_pagination_screen.feature: limit=100 and next_cursor are gone, the body is
|
||
{ items, total, page, size, counts }, and the list is paginated
|
||
And search, tab, sort and view live in the URL query string so the view can be linked and
|
||
survives a reload: ?q= (search text), ?filter= ("unused" or "duplicates"; absent means
|
||
All), ?sort= ("oldest", "name" or "largest"; absent means Newest), ?view= ("list"; absent
|
||
means grid); a value outside these sets is dropped and the default applies; the names
|
||
mirror the API's own q, filter and sort parameters
|
||
And the header shows the title "Media", the subtitle "Every image you've uploaded, ready to
|
||
use in any article." and a primary "Upload" button (icon upload) on the right; on mobile it is
|
||
the full-width 48 px button pinned to the bottom
|
||
And the toolbar holds the tabs "All", "Unused" and "Duplicates" with a count each, then the
|
||
search field (placeholder "Search by filename"), the sort menu (options "Newest", "Oldest",
|
||
"Name", "Largest", sent as sort=newest, oldest, name, largest) and the grid/list toggle
|
||
(icons layout-grid and list); below md the toolbar stacks: search, tabs, then sort and toggle
|
||
And the tab counts come from the response's counts object (all, unused, duplicates). The
|
||
Duplicates count is a number of groups, drawn as an info-tinted badge only while it is
|
||
above 0. The All and Unused counts are drawn as plain muted mono numbers, as in the board
|
||
And the Duplicates tab selects the groups view and ?filter=duplicates is a valid value
|
||
And the grid is 5 columns on desktop and 2 on mobile, with a 16 px gap (10 px on mobile),
|
||
and each card is one button holding MediaThumb (4:3, a checkerboard behind PNG and SVG),
|
||
the filename (medium weight, ellipsis), "size · dimensions" and the MediaBadges; an SVG's
|
||
dimensions read "Vector"
|
||
And sizes read "8 KB" and "860 KB" (whole KB) below 1 MB and "3.9 MB" (one decimal, 1024
|
||
base) from 1 MB up; dimensions read "4032 × 3024"; dates read "12 Sep 2026" (absolute,
|
||
never relative); the Type column reads JPEG, PNG, WEBP or SVG from content_type
|
||
And the MediaBadges are: "Unused" (warning-soft fill, icon unlink) when used_in is 0,
|
||
"Duplicate" (info-soft fill, icon copy) when duplicate is true, and "Used in N"
|
||
(outline, toned text) when used_in is above 0; in the detail panel the article rows use
|
||
"Online" (icon globe, inverted fill) and "Draft" (icon pencil-line, outline)
|
||
And each card's accessible name reads "north-wall.jpg, 3.9 MB, used in 2 articles",
|
||
"kitchen-before.webp, 1.1 MB, used in 1 article" (singular for 1) or "window-frame.jpg,
|
||
1.8 MB, unused"
|
||
And list view is a table from md up (columns Filename with a 40 px thumbnail and the
|
||
Duplicate badge, Dimensions, Size, Type, Used in, Uploaded by, and Uploaded with an
|
||
arrow-down as the sorted column; 36 px header, 56 px rows) and a card list below md (56 px
|
||
thumbnail, filename, "size · dimensions · type", badges plus "uploader · date", and a
|
||
trailing chevron-right); in the table the "Used in" cell is an underlined count that
|
||
opens the detail panel, and reads the Unused badge when used_in is 0
|
||
And the checkbox column of the table (36 px), the 44 px checkbox column of the card list and
|
||
the "Select all" row above the grid are specified in
|
||
features/media_upload_selection_duplicates.feature
|
||
And the detail panel is a USlideover: 460 px on the right on desktop (a close button x),
|
||
full screen on mobile with an arrow-left back button; its title is the filename; the body
|
||
holds the 16:10 preview (4:3 on mobile), the actions "Copy URL" (icon link), "Rename"
|
||
(icon pencil) and "Delete" (icon trash-2, error-coloured text), the duplicate alert when
|
||
the image is a duplicate, the metadata list (Type, Size, Dimensions, Uploaded, Uploaded by,
|
||
and URL in mono) and the "Used in" section
|
||
And opening the panel for an image with used_in above 0 calls GET /v1/media/{id}/usage; for an
|
||
image with used_in 0 no usage call is sent
|
||
And the dialogs (Rename, Delete) are UModal with sm:max-w-md, a bottom sheet on mobile and
|
||
Cancel first; while a request runs the confirm button shows a loader-circle, both buttons
|
||
are disabled, and the dialog cannot be dismissed by Cancel, Escape or a backdrop click; a
|
||
failure shows an error alert at the top of the dialog body and the dialog stays open
|
||
And every success toast lasts 5 s; toast ids follow the action-plus-id convention:
|
||
"media-url-copied-{id}", "media-renamed-{id}", "media-deleted-{id}", "media-not-found-{id}"
|
||
And the library is this 12-image fixture, newest first (id, filename, size, dimensions, type,
|
||
used in, uploader, uploaded, flags), from the board's own script:
|
||
| id | filename | size | dimensions | type | used | uploaded by | uploaded | flags |
|
||
| 1 | north-wall.jpg | 3.9 MB | 4032 × 3024 | JPEG | 2 | Marta Alves | 12 Sep 2026 | |
|
||
| 2 | kitchen-after.webp | 1.2 MB | 2400 × 1600 | WEBP | 3 | Inès Duarte | 8 Sep 2026 | |
|
||
| 3 | kitchen-before.webp | 1.1 MB | 2400 × 1600 | WEBP | 1 | Inès Duarte | 8 Sep 2026 | |
|
||
| 4 | logo-renew.svg | 8 KB | Vector | SVG | 5 | jonas | 5 Sep 2026 | alpha |
|
||
| 5 | floorboards-copy.jpg | 2.4 MB | 3600 × 2400 | JPEG | 3 | Marta Alves | 2 Sep 2026 | duplicate |
|
||
| 6 | IMG_2208.jpg | 2.4 MB | 3600 × 2400 | JPEG | 2 | jonas | 30 Aug 2026 | duplicate |
|
||
| 7 | IMG_4471.jpg | 3.1 MB | 3600 × 2400 | JPEG | 2 | Marta Alves | 29 Aug 2026 | duplicate |
|
||
| 8 | window-frame.jpg | 1.8 MB | 3000 × 2000 | JPEG | 0 | Inès Duarte | 28 Aug 2026 | |
|
||
| 9 | stone-wall-detail.jpg | 3.1 MB | 3600 × 2400 | JPEG | 1 | jonas | 21 Aug 2026 | duplicate |
|
||
| 10 | bathroom-tiles.png | 860 KB | 1800 × 1200 | PNG | 0 | Marta Alves | 4 Aug 2026 | alpha |
|
||
| 11 | lime-plaster.jpg | 2.1 MB | 3200 × 2133 | JPEG | 2 | Inès Duarte | 19 Jul 2026 | |
|
||
| 12 | floorboards.jpg | 2.4 MB | 3600 × 2400 | JPEG | 4 | Inès Duarte | 14 Jun 2026 | duplicate |
|
||
with counts { all: 12, unused: 2, duplicates: 2 }, total 12, page 1, size 25
|
||
|
||
Scenario: Default, many images
|
||
Given GET /v1/media/?filter=all&sort=newest&page=1&size=25 resolves 200 with the 12-image fixture
|
||
When the user navigates to /media
|
||
Then the breadcrumb reads "Media" (mobile title "Media")
|
||
And the tabs read "All 12", "Unused 2", "Duplicates 2", with "All" selected, and the
|
||
Duplicates count drawn as an info-tinted badge
|
||
And the grid shows the 12 images in exactly the fixture's order, 5 per row on desktop and 2
|
||
on mobile, each card with its thumbnail, filename, "size · dimensions" and badges
|
||
And the "Select all" row above the grid reads "Select all" and "12 images"
|
||
And the toolbar shows "Search by filename", the sort "Newest" and the grid toggle selected
|
||
|
||
Scenario Outline: Each card's badges and accessible name follow the image
|
||
Given the Default state has loaded
|
||
Then the card for "<filename>" has the accessible name "<name>" and the badges "<badges>"
|
||
|
||
Examples:
|
||
| filename | name | badges |
|
||
| north-wall.jpg | north-wall.jpg, 3.9 MB, used in 2 articles | Used in 2 |
|
||
| kitchen-before.webp | kitchen-before.webp, 1.1 MB, used in 1 article | Used in 1 |
|
||
| window-frame.jpg | window-frame.jpg, 1.8 MB, unused | Unused |
|
||
| floorboards.jpg | floorboards.jpg, 2.4 MB, used in 4 articles | Duplicate, Used in 4 |
|
||
| logo-renew.svg | logo-renew.svg, 8 KB, used in 5 articles | Used in 5 |
|
||
|
||
Scenario: Grid, few images, keeps card size
|
||
Given GET /v1/media/ resolves 200 with ids 1, 4 and 8 and counts { all: 3, unused: 1, duplicates: 0 }
|
||
When the user navigates to /media
|
||
Then three cards render at the same size as in the 12-image grid, not stretched
|
||
And a checkerboard shows behind the SVG thumbnail of "logo-renew.svg"
|
||
And the tabs read "All 3", "Unused 1" and "Duplicates" with no count badge
|
||
|
||
Scenario: List view, desktop, is a table
|
||
Given the Default state has loaded at md width or more
|
||
When the user activates the list icon in the toggle
|
||
Then the URL gains ?view=list
|
||
And a table shows the columns Filename, Dimensions, Size, Type, Used in, Uploaded by and
|
||
Uploaded (with an arrow-down), 12 rows of 56 px under a 36 px header, in fixture order
|
||
And each Filename cell shows a 40 px thumbnail, the filename and, for duplicates, the
|
||
Duplicate badge; the Used in cell shows an underlined count, or the Unused badge
|
||
for id 8 and id 10
|
||
And the Uploaded by cells read the uploader and the Uploaded cells read "12 Sep 2026"
|
||
|
||
Scenario: List view, mobile, is a card list
|
||
Given the Default state has loaded below md width with ?view=list
|
||
Then a card list replaces the grid; each row shows a 56 px thumbnail, the filename,
|
||
"3.9 MB · 4032 × 3024 · JPEG", the badges followed by "Marta Alves · 12 Sep 2026", and a
|
||
trailing chevron-right
|
||
When the user taps a row
|
||
Then the detail panel opens for that image
|
||
|
||
Scenario Outline: The query string is the source of truth for search, tab, sort and view
|
||
Given GET /v1/media/ is mocked
|
||
When the user opens /media<url>
|
||
Then GET /v1/media/ is sent with "<request>" and page=1&size=25
|
||
And the control shows "<control>"
|
||
|
||
Examples:
|
||
| url | request | control |
|
||
| ?filter=unused | filter=unused&sort=newest | tab "Unused" selected |
|
||
| ?sort=largest | filter=all&sort=largest | sort "Largest" |
|
||
| ?sort=name | filter=all&sort=name | sort "Name" |
|
||
| ?sort=oldest | filter=all&sort=oldest | sort "Oldest" |
|
||
| ?q=wall | q=wall&filter=all | search field "wall" |
|
||
| ?view=list | filter=all&sort=newest | list icon selected |
|
||
| ?filter=bogus&sort=zzz | filter=all&sort=newest | tab "All" and sort "Newest", URL kept clean |
|
||
| ?filter=duplicates | filter=all&sort=newest | tab "All" selected |
|
||
|
||
Scenario: Changing a control writes the query string and reloads the list
|
||
Given the Default state has loaded
|
||
When the user types "wall" in the search field
|
||
Then after a 120 ms debounce the URL gains ?q=wall and GET /v1/media/?q=wall&filter=all&sort=newest&page=1&size=25 is sent
|
||
When the user selects the "Unused" tab
|
||
Then the URL reads ?q=wall&filter=unused and the list is requested again
|
||
When the user picks "Largest" in the sort menu
|
||
Then the URL gains sort=largest and the list is requested again
|
||
And selecting "All" removes filter from the URL, "Newest" removes sort, and the grid icon removes view
|
||
|
||
Scenario: Loading, skeleton cards after 150 ms
|
||
Given GET /v1/media/ has not yet resolved
|
||
When the user navigates to /media
|
||
Then nothing is drawn for the first 150 ms
|
||
And after 150 ms 10 skeleton cards (6 on mobile) replace the images, the list container is
|
||
aria-busy="true", and the toolbar stays visible
|
||
When the request resolves 200 with the 12-image fixture
|
||
Then the skeleton cards are replaced by the Default state's cards
|
||
And the same skeleton appears on a retry and on a change of search, tab or sort, in grid and
|
||
list view alike
|
||
|
||
Scenario: Empty library, first run
|
||
Given GET /v1/media/ resolves 200 with zero items and counts { all: 0, unused: 0, duplicates: 0 } and no q
|
||
When the user navigates to /media
|
||
Then the toolbar and the tabs are hidden
|
||
And one panel shows the images icon, title "No images yet", text "Upload your first image. You can use it in any article.", a primary "Upload an image" button, and under it "PNG, JPEG, WEBP, SVG · max 10 MB each"
|
||
|
||
Scenario: No results, search and filter
|
||
Given the Default state has loaded
|
||
When the user selects "Unused" and types "wall" and GET /v1/media/?q=wall&filter=unused&sort=newest&page=1&size=25 resolves 200 with zero items
|
||
Then a panel replaces the images with the search icon, title `No images match "wall"`, text "Nothing matches in Unused. Check the spelling, or clear the filters." and an outline "Clear filters" button
|
||
When the user activates "Clear filters"
|
||
Then the search field is emptied, the tab returns to "All", ?q and ?filter leave the URL, and the Default cards reappear
|
||
|
||
Scenario: Hover, focus and keyboard
|
||
Given the Default state has loaded on desktop
|
||
Then every card is one button and one tab stop
|
||
When the pointer rests on the second card ("kitchen-after.webp")
|
||
Then its border darkens (border-accented) and it gains the pop shadow
|
||
When the third card ("kitchen-before.webp") takes keyboard focus
|
||
Then it shows a 2 px primary ring with a 2 px offset and its accessible name reads "kitchen-before.webp, 1.1 MB, used in 1 article"
|
||
When the user presses an arrow key on a card
|
||
Then focus moves to the neighbouring card across the grid (left and right by one, up and down by one row, stopping at the edges)
|
||
When the user presses Enter on a card
|
||
Then the detail panel opens for that image
|
||
When the user presses Space on a card
|
||
Then the card's selection toggles and no panel opens
|
||
|
||
Scenario: Detail panel, in use
|
||
Given the Default state has loaded and GET /v1/media/1/usage resolves 200 with [{ id: 10, title: "How we price a bathroom renovation", published: true }, { id: 11, title: "Winter checklist for timber windows", published: false }]
|
||
When the user activates the "north-wall.jpg" card
|
||
Then the slideover opens with the title "north-wall.jpg", a 16:10 preview and the actions "Copy URL", "Rename" and "Delete" (Delete in error-coloured text)
|
||
And the metadata list reads Type "JPEG", Size "3.9 MB", Dimensions "4032 × 3024", Uploaded "12 Sep 2026", Uploaded by "Marta Alves" and URL in mono
|
||
And the "Used in" section reads "Used in 2 articles" over one bordered row per article: the title as an underlined link to /article/{id}/edit, a badge "Online" (icon globe, inverted) for "How we price a bathroom renovation" or "Draft" (icon pencil-line, outline) for "Winter checklist for timber windows", and an arrow-up-right
|
||
And the footnote reads "Counted from the text of your articles each time you open this page."
|
||
When the user closes the panel (x, the back arrow on mobile, or Escape)
|
||
Then the panel closes and focus returns to the "north-wall.jpg" card
|
||
|
||
Scenario: Detail panel, unused
|
||
Given the Default state has loaded
|
||
When the user activates the "window-frame.jpg" card
|
||
Then no usage request is sent
|
||
And the "Used in" section reads "Not used in any article" over the text "This image is safe to delete. No article shows it."
|
||
|
||
Scenario: Detail panel, duplicate
|
||
Given the Default state has loaded, GET /v1/media/?hash=<sha256 of id 12> resolves 200 with ids 12, 5 and 6, and GET /v1/media/12/usage resolves 200
|
||
When the user activates the "floorboards.jpg" card
|
||
Then an info alert reads "Identical to 2 other images" over "floorboards-copy.jpg and IMG_2208.jpg have the same content." with an action "Review duplicates" (icon arrow-right)
|
||
And the action is enabled and selects the Duplicates tab
|
||
|
||
Scenario: The Used in count in list view opens the panel
|
||
Given the Default state has loaded with ?view=list at md width or more
|
||
When the user activates the underlined "2" in the "north-wall.jpg" row
|
||
Then the detail panel opens for "north-wall.jpg" with its "Used in 2 articles" list
|
||
|
||
Scenario: Copy URL
|
||
Given the detail panel is open for "north-wall.jpg"
|
||
When the user activates "Copy URL"
|
||
Then "{origin}{url}" (the absolute address of the image) is written to the clipboard
|
||
And a success toast appears: icon circle-check, id "media-url-copied-1", title "URL copied", text "Paste it wherever you need the image address."
|
||
|
||
Scenario: Rename, dialog
|
||
Given the detail panel is open for "north-wall.jpg"
|
||
When the user activates "Rename"
|
||
Then a dialog opens: title "Rename image", description "Only the filename changes. The URL stays the same, so articles that use this image keep working."
|
||
And the field "Filename" holds "north-wall" with the mono suffix ".jpg" fixed after it and the help "Letters, numbers, dashes and underscores. The .jpg ending stays."
|
||
And the buttons read "Cancel" then "Rename"
|
||
|
||
Scenario Outline: Rename, validation, nothing is sent
|
||
Given the Rename dialog is open for "north-wall.jpg"
|
||
When the user enters "<value>" and submits or leaves the field
|
||
Then the field shows the error "Enter a filename." with a circle-alert icon and the help text is replaced
|
||
And no PATCH is sent
|
||
|
||
Examples:
|
||
| value |
|
||
| |
|
||
| --- |
|
||
| !!! |
|
||
|
||
Scenario: Rename, saving
|
||
Given the Rename dialog is open for "north-wall.jpg" with "north-wall-01" entered
|
||
When the user activates "Rename"
|
||
Then PATCH /v1/media/1 is sent with { "filename": "north-wall-01" }
|
||
And the field and both buttons are disabled, the confirm button reads "Renaming…" with a loader-circle, and Cancel, Escape and a backdrop click do not close the dialog
|
||
|
||
Scenario Outline: Rename, failed, the dialog stays and keeps the text
|
||
Given a rename is in flight for "north-wall.jpg" with "north-wall-01" entered
|
||
When PATCH /v1/media/1 <failure>
|
||
Then the dialog stays open with an error alert "Couldn't rename the image. Your text is still here. Try again in a moment." at the top of its body
|
||
And "north-wall-01" is still in the field and Cancel and Rename are enabled again
|
||
|
||
Examples:
|
||
| failure |
|
||
| resolves with a 500 response |
|
||
| never reaches the server |
|
||
|
||
Scenario: Rename, done
|
||
Given a rename is in flight for "north-wall.jpg" with "north-wall-01" entered
|
||
When PATCH /v1/media/1 resolves 200 with the item, filename "north-wall-01.jpg"
|
||
Then the dialog closes, the panel title reads "north-wall-01.jpg", the card or row for id 1 reads "north-wall-01.jpg" in place (the list is not reordered), and the URL row is unchanged
|
||
And a success toast appears: icon circle-check, id "media-renamed-1", title "Image renamed", text `It's now "north-wall-01.jpg". The URL is the same.`
|
||
|
||
Scenario: Delete, unused image
|
||
Given the detail panel is open for "window-frame.jpg" (used in 0)
|
||
When the user activates "Delete"
|
||
Then a dialog opens: title "Delete this image?", description `"window-frame.jpg" will be deleted from your library. No article uses it. This can't be undone.`, no body, "Cancel" and a danger "Delete image"
|
||
|
||
Scenario: Delete, image in use
|
||
Given the detail panel is open for "north-wall.jpg" with its 2 articles loaded from GET /v1/media/1/usage
|
||
When the user activates "Delete"
|
||
Then a dialog opens: title "Delete an image that's in use?", description `"north-wall.jpg" is used in 2 articles. If you delete it, they will show a broken image. This can't be undone.`
|
||
And the body lists the two affected articles, each with its title and an "Online" or "Draft" badge, "Cancel" and a danger "Delete image"
|
||
|
||
Scenario: Delete, in progress
|
||
Given the Delete dialog is open for "window-frame.jpg"
|
||
When the user activates "Delete image"
|
||
Then DELETE /v1/media/8 is sent
|
||
And both buttons are disabled, the confirm button reads "Deleting…" with a loader-circle, aria-busy is set, and Cancel, Escape and a backdrop click do not close it
|
||
|
||
Scenario Outline: Delete, failed, the image stays
|
||
Given a delete is in flight for "window-frame.jpg"
|
||
When DELETE /v1/media/8 <failure>
|
||
Then the dialog stays open with an error alert "Couldn't delete the image, so it's still there. Try again in a moment." and both buttons enabled again
|
||
And the "window-frame.jpg" card is untouched behind the dialog
|
||
|
||
Examples:
|
||
| failure |
|
||
| resolves with a 500 response |
|
||
| never reaches the server |
|
||
|
||
Scenario: Delete, done
|
||
Given a delete is in flight for "window-frame.jpg" (id 8, the 8th card)
|
||
When DELETE /v1/media/8 resolves 204
|
||
Then the dialog and the panel close and the card leaves the grid
|
||
And focus moves to the next card ("stone-wall-detail.jpg"); when the deleted card was the last, focus moves to the page heading
|
||
And a success toast appears: icon circle-check, id "media-deleted-8", title "Image deleted", text `"window-frame.jpg" is gone from your library.`
|
||
And the list is then requested again in the background, with no skeleton, to refresh the counts and the Duplicate flags
|
||
|
||
Scenario Outline: Image already gone, a 404 on any action removes it
|
||
Given the detail panel or a dialog is open for "window-frame.jpg"
|
||
When <trigger> resolves 404
|
||
Then the panel and any dialog close and the image leaves the list
|
||
And an info toast appears: icon info, id "media-not-found-8", title "That image no longer exists", text "It was deleted somewhere else, so we removed it from the library."
|
||
And the list is then requested again in the background to refresh the counts
|
||
|
||
Examples:
|
||
| trigger |
|
||
| DELETE /v1/media/8 |
|
||
| PATCH /v1/media/8 |
|
||
| GET /v1/media/8/usage (on open) |
|
||
|
||
Scenario Outline: List failed to load
|
||
Given GET /v1/media/ <failure>
|
||
When the user navigates to /media
|
||
Then an error alert replaces the images: icon circle-x, title "Couldn't load your images", text "Check your connection and try again.", and a "Try again" button (icon rotate-cw)
|
||
And the toolbar stays visible
|
||
When the user activates "Try again"
|
||
Then the skeleton cards show and GET /v1/media/ is sent again, and on success the Default state replaces the alert
|
||
|
||
Examples:
|
||
| failure |
|
||
| resolves with a 500 response |
|
||
| never reaches the server |
|
||
|
||
Scenario: Offline
|
||
Given the Default state has loaded
|
||
When navigator.onLine becomes false
|
||
Then a banner with the wifi-off icon reads "You're offline. You can look through the images that are loaded, but uploading, renaming and deleting need a connection."
|
||
And the loaded cards stay browsable and the detail panel still opens and "Copy URL" still works
|
||
And "Upload", "Rename" and "Delete" are disabled
|
||
When navigator.onLine becomes true
|
||
Then the banner disappears and "Rename" and "Delete" are enabled again
|
||
|
||
Scenario Outline: Session errors are global
|
||
When any /v1/media call resolves <status>
|
||
Then the user is sent to <destination> by useApi, and no local media copy is shown
|
||
|
||
Examples:
|
||
| status | destination |
|
||
| 401 | /login?redirect=/media&reason=expired |
|
||
| 403 | /register/pending (unverified user) |
|
||
|
||
Scenario: The sidebar item and the mobile drawer
|
||
Given the user is signed in and verified
|
||
When the sidebar renders
|
||
Then it lists Home, Articles, Media (icon images), Users, Integration, Settings in that order
|
||
And on /media, "Media" is the active item, on desktop and in the mobile drawer
|
||
When the user activates "Media"
|
||
Then the router goes to /media
|