188 lines
11 KiB
Gherkin
188 lines
11 KiB
Gherkin
Feature: Articles list pagination
|
||
|
||
As a verified RenewCMS user with many articles
|
||
I want the Articles list split into pages with a page-size menu, server-side search and tabs, and shareable URLs
|
||
So that I can browse a large list quickly and come back to the same page
|
||
|
||
Background:
|
||
Given /article is the verified Articles route (features/articles_list.feature), per docs/design/routes.md, docs/design/screens/articles.md group "Articles: pagination" and the Paginator in docs/design/components.md
|
||
And the route stays /article (the design's /articles is not adopted)
|
||
And the page requests GET /v1/article/?format=html&page=P&size=S&q=Q&status=T and reads {items, total, page, size, counts} (features/articles_pagination_api.feature); the tab counts "All", "Online", "Drafts" come from counts and do not change while searching
|
||
And page, size, q, status (tab) and sort live in the URL: ?page=&size=&q=&status=&sort=; the defaults (page 1, size 25, no q, tab All, sort updated_at:desc) are omitted from the URL and from the request only where the API default applies
|
||
And the sortable columns are Title (title), Status (status), Created (created_at) and Updated (updated_at); a header activation sends sort=<field>:<direction>, the active column shows an arrow icon for its direction, and sorting is done by the server over the whole list, never inside the loaded page
|
||
And an invalid sort in the URL falls back to updated_at:desc and is replaced in the URL
|
||
And the table caption reads "Articles, sorted by last updated, newest first" for the default sort and "Articles, sorted by {column}, {ascending|descending}" otherwise, with a French entry in messages.ts
|
||
And the mobile card list has no sort control; the sort parameter is still honoured from the URL there
|
||
And an invalid or missing size falls back to 25 and an invalid page (not an integer >= 1) falls back to 1, both replaced in the URL
|
||
And a change to search, tab, sort or 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)
|
||
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"
|
||
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 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 this slice amends features/articles_list.feature: search, tabs and sort are no longer computed client-side over the full list; the list, the counts and the empty states come from the server response
|
||
And HomeView, ArticleView and useArticles.listArticles migrate to the envelope: they read items (and counts or total where they count) and send explicit paging; no caller reads a bare array any more
|
||
And the fixture below is total 312, size 25 (13 pages) unless stated; the frames draw at most 10 rows
|
||
|
||
Scenario: 1 First page
|
||
Given GET /v1/article/?format=html&page=1&size=25 resolves 200 with 25 items and total 312
|
||
When the user navigates to /article
|
||
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: 2 Middle page
|
||
Given GET ...&page=7&size=25 resolves 200 with 25 items and total 312
|
||
When the user navigates to /article?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: 3 Last page
|
||
Given GET ...&page=13&size=25 resolves 200 with 12 items and total 312
|
||
When the user navigates to /article?page=13
|
||
Then the summary reads "Showing 301–312 of 312"
|
||
And Next and Last are disabled
|
||
|
||
Scenario: 4 Single page
|
||
Given GET /v1/article/ resolves 200 with 6 items, total 6 and counts {all 6, online 4, draft 2}
|
||
When the user navigates to /article
|
||
Then the summary reads "Showing 1–6 of 6" and the page-size select stays
|
||
And the paginator (First ... Last) is not rendered
|
||
And the tabs read "All 6" "Online 4" "Drafts 2"
|
||
|
||
Scenario: 5 Loading next page
|
||
Given the first page has loaded and GET ...&page=2 has not resolved
|
||
When the user activates Next
|
||
Then skeleton rows keep the table 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 rows and summary of page 2 appear and the URL becomes /article?page=2
|
||
|
||
Scenario Outline: 6 Error loading a page keeps the previous rows
|
||
Given page 6 is shown
|
||
When the user activates Next and GET ...&page=7 <failure>
|
||
Then the page-6 rows stay, the URL stays /article?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 rows, the URL becomes ?page=7 and the notice goes
|
||
|
||
Examples:
|
||
| failure |
|
||
| resolves with a 500 response |
|
||
| never reaches the server |
|
||
|
||
Scenario: 7 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 /article?page=99
|
||
Then the URL is replaced (no history entry) by /article?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 (3) is shown
|
||
|
||
Scenario: 7b Out of range with total 0 shows the empty state, no redirect
|
||
Given GET ...&page=5 resolves 200 with items [] and total 0
|
||
When the user navigates to /article?page=5
|
||
Then the "No articles yet" empty state shows and the URL is /article
|
||
|
||
Scenario: 8 Search resets to page 1
|
||
Given the user is on /article?page=7
|
||
When the user types "plaster" in the search field and the debounce elapses
|
||
Then GET ...&q=plaster&page=1 is sent
|
||
And on success the URL is /article?q=plaster (page omitted)
|
||
And the tab counts are unchanged
|
||
And the same reset happens for a tab change, a sort change and a page-size change
|
||
|
||
Scenario: 9 Empty after search
|
||
Given GET ...&q=tiles resolves 200 with items [], total 0 and counts {all 6, online 4, draft 2}
|
||
When the user searches "tiles"
|
||
Then the paginator and the summary are hidden
|
||
And the no-results state shows "No articles match "tiles"", "Check the spelling, or search for a shorter part of the title." and "Clear search"
|
||
And the tabs still read "All 6" "Online 4" "Drafts 2"
|
||
|
||
Scenario Outline: 10 Page size menu
|
||
Given the user is on /article?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 | /article?size=5 |
|
||
| 25 per page | 25 | /article |
|
||
| 50 per page | 50 | /article?size=50 |
|
||
| 100 per page | 100 | /article?size=100 |
|
||
|
||
Scenario: 10b Invalid URL values fall back
|
||
When the user navigates to /article?size=10&page=abc
|
||
Then GET ... is sent with page=1 and size=25 and the URL is replaced by /article
|
||
|
||
Scenario: 11 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: Tabs filter on the server
|
||
Given the Default list has loaded and the user is on /article?page=3
|
||
When the user selects the "Drafts" tab
|
||
Then GET ...&status=draft&page=1 is sent and, on success, the URL is /article?status=draft
|
||
And the tab counts are unchanged
|
||
|
||
Scenario Outline: Sort header, server-side, resets to page 1
|
||
Given the user is on /article?page=7
|
||
When the user activates the "<column>" header (first click)
|
||
Then GET ...&sort=<sort>&page=1 is sent
|
||
And on success the URL is /article?sort=<sort> (page omitted)
|
||
And the header shows the arrow icon for its direction and the tab counts are unchanged
|
||
|
||
Examples:
|
||
| column | sort |
|
||
| Title | title:asc |
|
||
| Status | status:asc |
|
||
| Created | created_at:asc |
|
||
| Updated | updated_at:asc |
|
||
|
||
Scenario: Activating the sorted header again toggles the direction
|
||
Given the list is sorted by Title ascending
|
||
When the user activates the "Title" header
|
||
Then GET ...&sort=title:desc&page=1 is sent and, on success, the URL is /article?sort=title:desc
|
||
|
||
Scenario: Returning to the default sort drops sort from the URL
|
||
Given the URL is /article?sort=title:asc
|
||
When the user activates the "Updated" header until it is updated_at:desc
|
||
Then the request sends sort=updated_at:desc and, on success, the URL has no sort
|
||
|
||
Scenario: Sort is kept across paging, page size, search and tabs
|
||
Given the URL is /article?sort=title:asc
|
||
When the user activates Next, or changes the size, or searches "plaster"
|
||
Then every request carries sort=title:asc and the URL keeps sort=title:asc (search, tab and size also reset the page)
|
||
|
||
Scenario: A sorted URL loads sorted
|
||
When the user navigates to /article?sort=created_at:desc&page=2
|
||
Then GET ... carries sort=created_at:desc&page=2 and the Created header shows the arrow-down icon
|
||
|
||
Scenario: Invalid sort in the URL falls back
|
||
When the user navigates to /article?sort=body:asc
|
||
Then GET ... carries no sort (the default applies), the Updated header shows the arrow-down icon and the URL is replaced by /article
|
||
|
||
Scenario: The caption follows the sort
|
||
Given the list is sorted by Title ascending
|
||
Then the table's visually hidden caption reads "Articles, sorted by title, ascending"
|
||
|
||
Scenario: Mobile, Previous, "Page N of M", Next
|
||
Given the viewport is below md and page 2 of 13 is shown
|
||
Then the paginator reads Previous, "Page 2 of 13", Next with 44 px controls and no page numbers
|
||
|
||
Scenario: Deleting the last row of the last page
|
||
Given the user is on the last page with a single row and deletes it (204)
|
||
Then the list reloads the previous page and the URL is replaced by it
|
||
And the tab counts are recomputed from the new response
|
||
|
||
Scenario: Home and the article view read the envelope
|
||
When HomeView or ArticleView load their articles
|
||
Then they call GET /v1/article/ with explicit paging and read items, never a bare array
|