308 lines
19 KiB
Gherkin
308 lines
19 KiB
Gherkin
Feature: Sites and Authors list pagination
|
||
|
||
As a RenewCMS owner (Sites) or any signed-in role (Authors)
|
||
I want the Sites and Authors lists split into pages with a page-size menu, server-side search and shareable URLs
|
||
So that I can browse a long directory quickly and come back to the same page
|
||
|
||
Background:
|
||
Given /settings/sites (SitesView, owner only) and /authors (AuthorsView, every role) are the verified routes of features/sites_authors_screens.feature, per docs/design/routes.md, docs/design/screens/sites.md group "Sites: pagination" and docs/design/screens/authors.md group "Authors: pagination"
|
||
And the Paginator and the page-size select are the Articles and Media ones (AppPaginator, docs/design/components.md "Paginator"); nothing new is drawn except the single-page variant below
|
||
And the screens request GET /v1/site/?q=Q&page=P&size=S and GET /v1/author/?q=Q&page=P&size=S through useApi only, with relative URLs, always sending page and size, and q only when it is not empty
|
||
And the total is read from the X-Total-Count response header, and the body stays the array of sites (Sites) or the envelope {items,total,page,size} (Authors)
|
||
And a request that fails or carries no X-Total-Count is treated as a failed load
|
||
And page, size and q live in the URL as ?page=&size=&q=; sizes are 5, 25, 50, 100 and the default is 25; the defaults (page 1, size 25, no q) are omitted from the URL
|
||
And an invalid page (not a positive integer) or an invalid size (not 5, 25, 50 or 100) is replaced silently by the default with a history replace and no notice, and the request carries the default
|
||
And a change to search or to size resets page to 1 and drops it from the URL; search keeps its existing debounce
|
||
And the URL changes when a response succeeds, not when the request is sent, and Browser Back returns to the previous page of results
|
||
And a page past the last page (a 200 with items [] and total above 0) is replaced in the URL (no history entry) by ?page=<last>, <last> is requested, and the info notice "That page doesn't exist. Showing the last page." is shown
|
||
And the summary reads "Showing {from}–{to} of {total}" with locale-formatted numbers (thousands separators above 999), computed from total (the filtered count)
|
||
And the footer sits below the list: summary on the left, the page-size select next to it, the page controls right-aligned; below md the summary and the select share one row (wrapping onto two if needed), then Previous, "Page X of Y" and Next on one row with 44 px targets and a label that never wraps
|
||
And the page controls are First, Previous, page numbers with ellipses marking skipped ranges once there are more than 7 pages, Next and Last; the current page uses --rc-sand fill, --rc-on-sand text, a --ui-primary border and aria-current="page"; disabled controls are at 40 % opacity with aria-disabled
|
||
And on a single page (total <= size, total > 0) the controls and the page-size select are not rendered and the footer holds only the count line "{N} sites" or "{N} authors" in --ui-text-muted ("1 site", "1 author"); this replaces the AuthorsView rule that showed the pager only above 25 items
|
||
And the count line or the footer appears whenever the list has loaded with total above 0, including the existing "Default" frames
|
||
And the same footer is shown to owner, admin and read-only roles on Authors; Sites stays owner only (403 page for the others)
|
||
And SitesView gains a search field with the placeholder "Search by name or key", matching name, key and base URL on the server; Authors keeps "Search by name or slug"
|
||
And the places that need every site (deep link ?edit=<key>, duplicate-key check, ReassignDialog destinations, the form's default-site text) read the unpaged GET /v1/site/ and never the current page
|
||
And keyboard and aria are those of Articles: Tab reaches the page-size select then the controls, Enter or Space activates, ArrowDown opens the page-size list, Enter selects, Esc closes and returns focus; every control shows a 2 px --ui-primary focus outline with a 2 px offset
|
||
And every new visible string goes through t(...) with the English text as the key and its French entry in web/src/i18n/messages.ts; the placeholders are written {0}, as in the other messages; the new strings are "Search by name or key" (French "Rechercher par nom ou clé"), "{0} sites" and "1 site" ("{0} sites", "1 site"), "{0} authors" and "1 author" ("{0} auteurs", "1 auteur"), and 'No sites match "{0}"' (French 'Aucun site ne correspond à « {0} »'); the other strings reuse the Articles and Media entries
|
||
And the sites fixture is 61 sites (3 pages at size 25) and the authors fixture is 128 authors (6 pages at size 25), with generated rows, unless stated
|
||
|
||
Rule: Page states, one set per screen
|
||
|
||
Scenario Outline: 1 First page
|
||
Given <screen> resolves GET <path>?page=1&size=25 with 25 rows and X-Total-Count <total>
|
||
When the user navigates to <route>
|
||
Then the summary reads "<summary>", the page-size select reads "25 per page" and the controls read "Page 1 of <pages>"
|
||
And First and Previous are disabled with aria-disabled, page 1 is current, Next and Last are enabled
|
||
And below md Previous is disabled
|
||
And the URL has no page and no size
|
||
|
||
Examples:
|
||
| screen | path | route | total | pages | summary |
|
||
| Sites | /v1/site/ | /settings/sites | 61 | 3 | Showing 1–25 of 61 |
|
||
| Authors | /v1/author/ | /authors | 128 | 6 | Showing 1–25 of 128 |
|
||
|
||
Scenario Outline: 2 Middle page
|
||
Given <screen> resolves GET <path>?page=<page>&size=25 with 25 rows and X-Total-Count <total>
|
||
When the user navigates to <route>?page=<page>
|
||
Then the summary reads "<summary>" and the label reads "Page <page> of <pages>"
|
||
And page <page> is current with --rc-sand, --rc-on-sand, a --ui-primary border and aria-current="page"
|
||
And every control is enabled
|
||
|
||
Examples:
|
||
| screen | path | route | page | total | pages | summary |
|
||
| Sites | /v1/site/ | /settings/sites | 2 | 61 | 3 | Showing 26–50 of 61 |
|
||
| Authors | /v1/author/ | /authors | 3 | 128 | 6 | Showing 51–75 of 128 |
|
||
|
||
Scenario Outline: 3 Last page
|
||
Given <screen> resolves GET <path>?page=<pages>&size=25 with <rows> rows and X-Total-Count <total>
|
||
When the user navigates to <route>?page=<pages>
|
||
Then the summary reads "<summary>" and the label reads "Page <pages> of <pages>"
|
||
And Next and Last are disabled with aria-disabled and Previous and First are enabled
|
||
|
||
Examples:
|
||
| screen | path | route | pages | rows | total | summary |
|
||
| Sites | /v1/site/ | /settings/sites | 3 | 11 | 61 | Showing 51–61 of 61 |
|
||
| Authors | /v1/author/ | /authors | 6 | 3 | 128 | Showing 126–128 of 128 |
|
||
|
||
Scenario Outline: 4 Single page
|
||
Given <screen> resolves GET <path>?page=1&size=25 with <total> rows and X-Total-Count <total>
|
||
When the user navigates to <route>
|
||
Then the footer shows only "<line>"
|
||
And the page controls and the page-size select are not rendered
|
||
And no "Showing" summary is rendered
|
||
|
||
Examples:
|
||
| screen | path | route | total | line |
|
||
| Sites | /v1/site/ | /settings/sites | 4 | 4 sites |
|
||
| Authors | /v1/author/ | /authors | 6 | 6 authors |
|
||
|
||
Scenario Outline: 4b Singular count line
|
||
Given <screen> resolves its list with 1 row and X-Total-Count 1
|
||
When the user navigates to <route>
|
||
Then the footer shows only "<line>"
|
||
|
||
Examples:
|
||
| screen | route | line |
|
||
| Sites | /settings/sites | 1 site |
|
||
| Authors | /authors | 1 author |
|
||
|
||
Scenario Outline: 4c Five per page with a few rows still pages
|
||
Given <screen> resolves GET <path>?size=5&page=1 with 5 rows and X-Total-Count <total>
|
||
When the user navigates to <route>?size=5
|
||
Then the summary "Showing 1–5 of <total>", the page-size select "5 per page" and the controls read "Page 1 of 2"
|
||
|
||
Examples:
|
||
| screen | path | route | total |
|
||
| Sites | /v1/site/ | /settings/sites | 6 |
|
||
| Authors | /v1/author/ | /authors | 6 |
|
||
|
||
Scenario Outline: 5 Page size menu
|
||
Given the user is on <route>?page=<page>
|
||
When the user opens the page-size select (it opens upward) and chooses "<label>"
|
||
Then <path>?page=1&size=<size> is requested and, on success, the URL is <url>
|
||
And the list shows page 1
|
||
|
||
Examples:
|
||
| route | path | page | label | size | url |
|
||
| /settings/sites | /v1/site/ | 2 | 5 per page | 5 | /settings/sites?size=5 |
|
||
| /settings/sites | /v1/site/ | 2 | 25 per page | 25 | /settings/sites |
|
||
| /settings/sites | /v1/site/ | 2 | 50 per page | 50 | /settings/sites?size=50 |
|
||
| /settings/sites | /v1/site/ | 2 | 100 per page | 100 | /settings/sites?size=100 |
|
||
| /authors | /v1/author/ | 3 | 5 per page | 5 | /authors?size=5 |
|
||
| /authors | /v1/author/ | 3 | 25 per page | 25 | /authors |
|
||
| /authors | /v1/author/ | 3 | 50 per page | 50 | /authors?size=50 |
|
||
| /authors | /v1/author/ | 3 | 100 per page | 100 | /authors?size=100 |
|
||
|
||
Scenario Outline: 6 Loading a page
|
||
Given <route>?page=<page> has loaded and the request for page <next> has not resolved
|
||
When the user activates Next (or Previous, a page number, or changes the size)
|
||
Then the rows are replaced by skeleton rows (8 on desktop, 6 below md) and the list is aria-busy="true"
|
||
And the pager stays, at 40 % opacity, every control and the page-size select are disabled and not focusable
|
||
And the summary reads "Loading page <next>…" and the page number shown is still <page>
|
||
And the URL is unchanged until the request resolves
|
||
When the request resolves 200
|
||
Then the rows, the summary and the URL of page <next> appear
|
||
|
||
Examples:
|
||
| route | page | next |
|
||
| /settings/sites | 2 | 3 |
|
||
| /authors | 3 | 4 |
|
||
|
||
Scenario Outline: 7 Error loading a page keeps the previous rows
|
||
Given <route>?page=<page> is shown
|
||
When the user activates Next and the request for page <next> <failure>
|
||
Then the rows of page <page> stay, the pager keeps "Page <page>" and is enabled again, and the URL stays <route>?page=<page>
|
||
And an inline error above the pager reads "Couldn't load page <next>. Showing page <page>.", "Check your connection and try again." and a "Retry" action
|
||
When the user activates "Retry"
|
||
Then the request for page <next> is sent again and, on 200, page <next> replaces the rows, the URL becomes ?page=<next> and the error goes
|
||
|
||
Examples:
|
||
| route | page | next | failure |
|
||
| /settings/sites | 2 | 3 | resolves with a 500 response |
|
||
| /settings/sites | 2 | 3 | never reaches the server |
|
||
| /authors | 3 | 4 | resolves with a 500 response |
|
||
| /authors | 3 | 4 | never reaches the server |
|
||
|
||
Scenario Outline: 8 Empty after search
|
||
Given <path>?q=<q>&page=1&size=25 resolves 200 with [] and X-Total-Count 0
|
||
When the user searches "<q>"
|
||
Then the existing empty state shows "<title>", "Check the spelling, or search for a shorter part of the name." and "Clear search"
|
||
And the pager, the summary and the count line are not rendered
|
||
When the user activates "Clear search"
|
||
Then the URL is <route>, page 1 is requested and the list returns
|
||
|
||
Examples:
|
||
| route | path | q | title |
|
||
| /settings/sites | /v1/site/ | blog | No sites match "blog" |
|
||
| /authors | /v1/author/ | zoe | No authors match "zoe" |
|
||
|
||
Scenario Outline: 9 Search resets to page 1
|
||
Given the user is on <route>?page=<page>
|
||
When the user types "<q>" in the search field and the debounce elapses
|
||
Then <path>?q=<q>&page=1&size=25 is requested
|
||
And on success the URL is <route>?q=<q> (page omitted) and the summary reads "<summary>"
|
||
And a size change resets the page the same way
|
||
|
||
Examples:
|
||
| route | path | page | q | summary |
|
||
| /settings/sites | /v1/site/ | 4 | renew | Showing 1–25 of 27 |
|
||
| /authors | /v1/author/ | 5 | mar | Showing 1–25 of 31 |
|
||
|
||
Scenario Outline: 10 Long labels on mobile
|
||
Given the viewport is 390 px wide and <route>?page=<page> resolves with X-Total-Count <total>
|
||
Then the summary "<summary>" and the page-size select share one row and wrap onto two if needed
|
||
And Previous, the label "<label>" and Next stay on one row, the two buttons flex and the label never wraps
|
||
And totals above 999 use thousands separators in both labels
|
||
|
||
Examples:
|
||
| route | page | total | summary | label |
|
||
| /settings/sites | 6 | 312 | Showing 126–150 of 312 | Page 6 of 13 |
|
||
| /authors | 12 | 1284 | Showing 276–300 of 1,284 | Page 12 of 52 |
|
||
|
||
Rule: URL rules
|
||
|
||
Scenario Outline: Invalid values fall back silently
|
||
When the user navigates to <route>?size=30&page=abc
|
||
Then <path>?page=1&size=25 is requested and the URL is replaced by <route>
|
||
And no notice is shown
|
||
|
||
Examples:
|
||
| route | path |
|
||
| /settings/sites | /v1/site/ |
|
||
| /authors | /v1/author/ |
|
||
|
||
Scenario Outline: Page 0 and negative pages fall back
|
||
When the user navigates to <route>?page=<page>
|
||
Then page 1 is requested and the URL is replaced by <route>
|
||
|
||
Examples:
|
||
| route | page |
|
||
| /settings/sites | 0 |
|
||
| /settings/sites | -2 |
|
||
| /authors | 0 |
|
||
| /authors | -2 |
|
||
|
||
Scenario Outline: A page out of range goes to the last page with a notice
|
||
Given <path>?page=99&size=25 resolves 200 with [] and X-Total-Count <total>
|
||
When the user navigates to <route>?page=99
|
||
Then the URL is replaced (no history entry) by <route>?page=<last> and page <last> is requested
|
||
And the info notice reads "That page doesn't exist. Showing the last page."
|
||
And the last-page state is shown
|
||
|
||
Examples:
|
||
| route | path | total | last |
|
||
| /settings/sites | /v1/site/ | 61 | 3 |
|
||
| /authors | /v1/author/ | 128 | 6 |
|
||
|
||
Scenario Outline: A page out of range with a total of 0 shows the empty state
|
||
Given <path>?page=5&size=25 resolves 200 with [] and X-Total-Count 0
|
||
When the user navigates to <route>?page=5
|
||
Then the empty state shows, there is no redirect, no notice, and the URL is <route>
|
||
|
||
Examples:
|
||
| route | path |
|
||
| /settings/sites | /v1/site/ |
|
||
| /authors | /v1/author/ |
|
||
|
||
Scenario Outline: Combined q and page in the URL
|
||
When the user navigates to <route>?q=<q>&page=2
|
||
Then <path>?q=<q>&page=2&size=25 is requested and the search field holds "<q>"
|
||
|
||
Examples:
|
||
| route | path | q |
|
||
| /settings/sites | /v1/site/ | renew |
|
||
| /authors | /v1/author/ | mar |
|
||
|
||
Scenario Outline: Browser Back returns to the previous page of results
|
||
Given the user went from <route> to <route>?page=2 with Next
|
||
When the user uses Browser Back
|
||
Then the URL is <route> and page 1 is requested
|
||
|
||
Examples:
|
||
| route |
|
||
| /settings/sites |
|
||
| /authors |
|
||
|
||
Rule: Sites search
|
||
|
||
Scenario: The Sites search field
|
||
Given the owner is on /settings/sites
|
||
Then a search field with the placeholder "Search by name or key" is shown above the list
|
||
When the owner types "renew" and the debounce elapses
|
||
Then GET /v1/site/?q=renew&page=1&size=25 is requested and the list shows the matches
|
||
|
||
Scenario: A Sites search that matches the default site only shows what the API returns
|
||
Given GET /v1/site/?q=journal&page=1&size=25 resolves 200 with only "Journal" and X-Total-Count 1
|
||
When the owner searches "journal"
|
||
Then only "Journal" is listed and the footer shows "1 site"
|
||
|
||
Rule: Existing behaviour that must not regress
|
||
|
||
Scenario Outline: The unpaged site list still feeds the dependent features
|
||
Given the paged list shows page 2 of <total> sites
|
||
When the owner opens ?edit=<key>, blurs a duplicate key in the add form, or opens the delete dialog
|
||
Then GET /v1/site/ is requested without page and size and the result is used, not the paged rows
|
||
|
||
Examples:
|
||
| total | key |
|
||
| 61 | journal |
|
||
|
||
Scenario Outline: Read-only roles see the same Authors pager
|
||
Given the session role is "<role>"
|
||
When the user navigates to /authors?page=2
|
||
Then the same footer, summary and controls are shown, with no Add author button and no row menu
|
||
|
||
Examples:
|
||
| role |
|
||
| member |
|
||
|
||
Scenario Outline: Owner and admin see the same Authors pager with the management actions
|
||
Given the session role is "<role>"
|
||
When the user navigates to /authors?page=2
|
||
Then the same footer, summary and controls are shown, with the Add author button and the row menu
|
||
|
||
Examples:
|
||
| role |
|
||
| owner |
|
||
| admin |
|
||
|
||
Scenario: Deleting the last row of the last page
|
||
Given the user is on the last page of /authors with a single row and deletes it (204)
|
||
Then the previous page is requested and the URL is replaced by it
|
||
|
||
Scenario: Adding or deleting refreshes the total
|
||
Given a list of 25 rows (total 25, count line only)
|
||
When a row is added and the list reloads with total 26
|
||
Then the footer shows the pager and the summary "Showing 1–25 of 26"
|
||
|
||
Rule: Design conformance of the visual gate
|
||
|
||
Scenario: The Create author mobile states pass at the default budget
|
||
Given the editor states "Create author: dialog", "Create author: for me", "Create author: slug taken" and "Create author: failed" at 390x844
|
||
Then each capture is within 10 % of the board frame, with no per-state exception in web/visual/states.ts
|
||
|
||
Scenario: Integration "6. Errors" passes at the default budget
|
||
Given the board frame of Integration "6. Errors" is drawn with the 403 note
|
||
Then every capture of "6. Errors" is within 10 %, desktop and mobile, light and dark, with no maxPercent exception
|