186 lines
12 KiB
Gherkin
186 lines
12 KiB
Gherkin
Feature: Media page pagination
|
||
|
||
As a verified owner or admin with many images
|
||
I want the Media library split into pages with a page-size menu and shareable URLs
|
||
So that I can browse a large library quickly and come back to the same page
|
||
|
||
Background:
|
||
Given /media is the verified Media route (features/media_browse.feature), per docs/design/routes.md "Pagination in the URL (Articles, Media)" and docs/design/screens/media.md states 44-52
|
||
And the Paginator and PageSizeSelect are the existing components used by Articles (AppPaginator.vue); no new component is created
|
||
And the page requests GET /v1/media/?q=Q&filter=F&sort=S&variants=V&page=P&size=Z and reads {items, total, page, size, counts} (features/media_pagination_api.feature); q and variants are sent only when set; page and size are always sent explicitly; the tab counts come from counts and do not change while searching
|
||
And page and size live in the URL: ?page=&size=, next to the existing q, filter, sort, variants and view; the defaults (page 1, size 25) are omitted from the URL
|
||
And an invalid or missing size falls back to the stored size (features/media_page_size_memory.feature) or 25, and an invalid page (not an integer >= 1) falls back to 1, both silently replaced in the URL with history.replace and never shown as an error
|
||
And a change to search, tab, sort, view, the variants filter or the page size resets page to 1 (and drops it from the URL); search keeps its 120 ms debounce
|
||
And the URL changes when a response succeeds, not when the request is sent
|
||
And the summary reads "Showing {from}–{to} of {total}" with locale-formatted numbers, from total (the filtered count), under the grid or list
|
||
And the Paginator has First, Previous, page numbers with ellipses marking skipped ranges, Next, Last, and the page-size select "5 per page", "25 per page", "50 per page", "100 per page" (it opens upward)
|
||
And the current page uses --rc-sand fill, --rc-on-sand text and a --ui-primary border and has aria-current="page"; disabled controls are at 40 % opacity; buttons are 32 px (44 px on mobile)
|
||
And on mobile the paginator is Previous, "Page N of M" and Next
|
||
And the paginator is hidden when total fits on one page; the summary and the page-size select stay
|
||
And the paginator sits under the grid in grid view and under the table (md and up) or card list (below md) in list view; its exact placement is taken from the board frame docs/design/source/MediaFrame.dc.html, not from components.md prose
|
||
And keyboard: Tab reaches the page-size select, first, previous, each page, next, last; Enter or Space activates; ArrowDown opens the page-size list, Enter selects, Esc closes; every control shows a 2 px --ui-primary focus outline with a 2 px offset
|
||
And the Duplicates tab (?filter=duplicates) is not paginated: it loads the whole duplicate set with size=100, requesting page 1, 2, ... until the loaded count reaches total, groups it by sha256 as today, shows no paginator, summary or page-size select, and drops page and size from the URL
|
||
And "Select all" covers the images on the current page only and reads "{n} images" for that page; changing page, size, search, tab, sort or view clears the selection
|
||
And the silent background refresh after a delete, upload or merge reloads the current page with no skeleton; when the current page has become empty and is past the last page, the list steps back to the last page and replaces the URL
|
||
And the first load and a change of search, tab or sort keep the existing skeleton after 150 ms and the "Couldn't load your images" alert (media.md states 4 and 42); the page-change error below applies only when images are already shown
|
||
And the fixture below is total 312, size 25 (13 pages) unless stated, with the board's own sample data
|
||
|
||
Scenario: 44 First page
|
||
Given GET /v1/media/?filter=all&sort=newest&page=1&size=25 resolves 200 with 25 items, total 312 and counts {all 312, unused 2, duplicates 2}
|
||
When the user navigates to /media
|
||
Then the summary reads "Showing 1–25 of 312"
|
||
And First and Previous are disabled, page 1 is current, Next and Last are enabled
|
||
And the URL has no page or size
|
||
|
||
Scenario: 45 Middle page
|
||
Given GET ...&page=7&size=25 resolves 200 with 25 items and total 312
|
||
When the user navigates to /media?page=7
|
||
Then the summary reads "Showing 151–175 of 312"
|
||
And page 7 is current with --rc-sand / --rc-on-sand and aria-current="page"
|
||
And ellipses mark the skipped ranges and every control is enabled
|
||
|
||
Scenario: 46 Last page
|
||
Given GET ...&page=13&size=25 resolves 200 with 12 items and total 312
|
||
When the user navigates to /media?page=13
|
||
Then the summary reads "Showing 301–312 of 312"
|
||
And Next and Last are disabled
|
||
|
||
Scenario: Single page
|
||
Given GET /v1/media/ resolves 200 with 12 items, total 12 and counts {all 12, unused 2, duplicates 2}
|
||
When the user navigates to /media
|
||
Then the summary reads "Showing 1–12 of 12" and the page-size select stays
|
||
And the paginator (First ... Last) is not rendered
|
||
|
||
Scenario: 47 List view, desktop and mobile
|
||
Given page 2 of 13 is shown with ?view=list
|
||
Then at md width or more the paginator sits under the table with the page numbers
|
||
And below md it sits under the card list as Previous, "Page 2 of 13", Next with 44 px controls and no page numbers
|
||
And in grid view on mobile the paginator is the same Previous, "Page 2 of 13", Next
|
||
|
||
Scenario: 48 Loading next page
|
||
Given the first page has loaded and GET ...&page=2 has not resolved
|
||
When the user activates Next
|
||
Then skeleton cards keep the grid height and the list is aria-busy="true"
|
||
And every paginator control is disabled and the summary reads "Loading page 2…"
|
||
When the request resolves 200
|
||
Then the images and summary of page 2 appear and the URL becomes /media?page=2
|
||
|
||
Scenario Outline: 49 Error loading a page keeps the previous images
|
||
Given page 6 is shown
|
||
When the user activates Next and GET ...&page=7 <failure>
|
||
Then the page-6 images stay, the URL stays /media?page=6 and the controls are re-enabled
|
||
And a notice reads "Couldn't load page 7. Showing page 6." with a "Retry" action
|
||
When the user activates "Retry"
|
||
Then GET ...&page=7 is sent again and, on 200, page 7 replaces the images, the URL becomes ?page=7 and the notice goes
|
||
|
||
Examples:
|
||
| failure |
|
||
| resolves with a 500 response |
|
||
| never reaches the server |
|
||
|
||
Scenario: 50 Page out of range redirects to the last page with a notice
|
||
Given GET ...&page=99 resolves 200 with items [] and total 312
|
||
When the user navigates to /media?page=99
|
||
Then the URL is replaced (no history entry) by /media?page=13 and GET ...&page=13 is sent
|
||
And the notice reads "That page doesn't exist. Showing the last page."
|
||
And the last-page state (46) is shown
|
||
|
||
Scenario: Out of range with total 0 shows the empty state, no redirect
|
||
Given GET ...&page=5 resolves 200 with items [] and total 0 and counts.all 0
|
||
When the user navigates to /media?page=5
|
||
Then the "No images yet" empty state shows and the URL is /media
|
||
|
||
Scenario: Out of range after a search with total 0 shows no results
|
||
Given GET ...&q=zzzz&page=5 resolves 200 with items [], total 0 and counts.all 312
|
||
When the user navigates to /media?q=zzzz&page=5
|
||
Then the no-results state (media.md state 6) shows and the URL is /media?q=zzzz
|
||
|
||
Scenario Outline: Search, tab, sort, view and variants changes reset to page 1
|
||
Given the user is on /media?page=7
|
||
When the user <action> and the request resolves
|
||
Then GET ... carries page=1
|
||
And on success the URL has no page
|
||
And the tab counts are unchanged by a search
|
||
|
||
Examples:
|
||
| action |
|
||
| types "wall" in the search field and the debounce elapses |
|
||
| selects the "Unused" tab |
|
||
| picks "Largest" in the sort menu |
|
||
| activates the list icon in the grid toggle |
|
||
| applies the "With variants" filter |
|
||
|
||
Scenario Outline: 51 Page size menu
|
||
Given the user is on /media?page=3
|
||
When the user opens the page-size select (it opens upward) and chooses "<label>"
|
||
Then GET ...&page=1&size=<size> is sent and, on success, the URL is <url>
|
||
|
||
Examples:
|
||
| label | size | url |
|
||
| 5 per page | 5 | /media?size=5 |
|
||
| 25 per page | 25 | /media |
|
||
| 50 per page | 50 | /media?size=50 |
|
||
| 100 per page | 100 | /media?size=100 |
|
||
|
||
Scenario Outline: Invalid URL values fall back silently
|
||
When the user navigates to /media?<query>
|
||
Then GET ... is sent with page=1 and size=25 and the URL is replaced by /media with no history entry
|
||
And no error or notice is shown
|
||
|
||
Examples:
|
||
| query |
|
||
| size=10&page=abc |
|
||
| page=0 |
|
||
| page=-2 |
|
||
| page=1.5 |
|
||
| size=0 |
|
||
| size=200 |
|
||
|
||
Scenario: Pagination is kept across the other controls
|
||
Given the URL is /media?size=50&sort=largest&filter=unused
|
||
When the user activates Next
|
||
Then every request carries size=50, sort=largest and filter=unused and the URL keeps them
|
||
|
||
Scenario: 52 Focus-visible
|
||
When the user tabs to Next
|
||
Then Next shows a 2 px --ui-primary outline with a 2 px offset
|
||
And the same ring shows on every paginator control and on the page-size select
|
||
|
||
Scenario: Keyboard, the page-size list
|
||
When the user focuses the page-size select and presses ArrowDown
|
||
Then the list opens; Enter selects the highlighted size; Esc closes it and returns focus to the select
|
||
|
||
Scenario: Select all covers the current page only
|
||
Given page 2 of 13 is shown with 25 images
|
||
Then the "Select all" row reads "Select all" and "25 images"
|
||
When the user activates "Select all"
|
||
Then 25 images are selected and the bulk bar announces "25 selected"
|
||
When the user activates Next and page 3 loads
|
||
Then no image is selected and the bulk bar is gone
|
||
|
||
Scenario: The Duplicates tab loads the whole set and shows no paginator
|
||
Given 130 images are duplicates, GET ...&filter=duplicates&sort=newest&page=1&size=100 resolves 200 with 100 items and total 130
|
||
And GET ...&filter=duplicates&page=2&size=100 resolves 200 with the remaining 30 items
|
||
When the user navigates to /media?filter=duplicates&page=4&size=5
|
||
Then page 1 and page 2 are requested with size=100, page 2 only because 100 < 130
|
||
And the groups are built from the 130 images, with no paginator, summary or page-size select
|
||
And the URL is replaced by /media?filter=duplicates
|
||
|
||
Scenario: Leaving the Duplicates tab returns to the paginated list
|
||
Given the Duplicates tab is shown
|
||
When the user selects the "All" tab
|
||
Then GET ...&filter=all&page=1&size=25 is sent and the paginator is shown
|
||
|
||
Scenario: Deleting the last image of the last page
|
||
Given the user is on the last page with a single image and deletes it (204)
|
||
Then the list reloads the previous page in the background and the URL is replaced by it
|
||
And the tab counts are recomputed from the new response
|
||
|
||
Scenario: A background refresh keeps the current page
|
||
Given page 7 is shown and the user renames, uploads or merges
|
||
Then the list is requested again in the background for page 7 with no skeleton
|
||
|
||
Scenario: Hash lookups do not touch the page
|
||
When the detail panel or the upload duplicate check looks up copies of an image
|
||
Then GET /v1/media/?hash=H&page=1&size=100 is sent, the URL does not change and the paginator is unaffected
|