7.7 KiB
QA: Sites and Authors list pagination (SPA)
SPA slice only. The Go API (page, size, q, X-Total-Count) already exists (features/sites_backend.feature, features/authors_backend.feature), so no new Go test is requested. If a contract below turns out wrong, report it as a backend defect. Un-run web tests are NOT RUN, never passing.
Automated (Vitest + Vue Test Utils, task test:web)
Mock useApi for GET /v1/site/?... and /v1/author/?... (the route key includes the query) per case, returning the body and the X-Total-Count header through the new headers-aware helper. Mount SitesView (owner session) and AuthorsView (owner, admin and member sessions) inside DashboardShell with a test router at /settings/sites and /authors, as in features/sites_authors_screens.qa.md. Fixtures: 61 sites at 25 per page (3 pages) and 128 authors at 25 per page (6 pages), built by helpers. Mock matchMedia for desktop and below-md. Do not freeze the system date; use fake timers only for the 150 ms skeleton delay and the search debounce. One test per scenario, Sites and Authors in the same file through it.each.
- First page: no page/size in the URL, requests carry page=1&size=25, "Showing 1–25 of 61" and "Showing 1–25 of 128", First and Previous aria-disabled, page 1 aria-current, total read from the header (body length ignored).
- Middle page (?page=2 on Sites, ?page=3 on Authors): summaries, aria-current on the page, every control enabled.
- Last page (?page=3, ?page=6): "Showing 51–61 of 61" and "Showing 126–128 of 128", Next and Last disabled.
- Single page: total 4 sites and 6 authors render the count line "4 sites" / "6 authors" only; no nav, no select, no "Showing" text. 4b: "1 site" and "1 author". 4c: size=5 with total 6 renders the full pager ("Page 1 of 2").
- Page size menu: 5/25/50/100 -> request page=1&size=N, URL after the response (/settings/sites?size=5, no size for 25); ArrowDown opens, Enter selects, Esc closes and returns focus.
- Loading a page: unresolved promise -> skeleton rows (8 desktop, 6 below md), aria-busy, pager 40 % and disabled, "Loading page 3…" / "Loading page 4…", page number unchanged, URL unchanged before the response; resolve -> rows and URL.
- Error loading a page (500 and network rejection): rows and URL kept, "Couldn't load page 3. Showing page 2." (Authors: page 4/3), "Check your connection and try again.", Retry resends page N and success clears the error and sets the URL. A first-load failure still shows "Couldn't load sites" / "Couldn't load authors" with "Try again".
- Empty after search: "No sites match "blog"" / "No authors match "zoe"", "Clear search" goes back to the bare route and page 1, no pager, summary or count line.
- Search resets to page 1: from ?page=4 (Sites) and ?page=5 (Authors) type the term (fake timers past the debounce) -> request q=...&page=1, URL /settings/sites?q=renew and /authors?q=mar after the response, "Showing 1–25 of 27" and "Showing 1–25 of 31". A size change resets the page too.
- Long labels on mobile: 312 sites page 6 and 1,284 authors page 12 -> "Showing 126–150 of 312", "Page 6 of 13", "Showing 276–300 of 1,284", "Page 12 of 52" with the thousands separator; label has no wrap class and Previous/Next carry the flex classes (the pixel layout is the visual check).
- URL rules: ?size=30&page=abc, ?page=0 and ?page=-2 -> default request and router.replace (assert no extra history entry and no notice). Out of range ?page=99 with total 61 / 128 -> router.replace to the last page, the second request for it and the notice "That page doesn't exist. Showing the last page."; with total 0 -> empty state, no redirect, no notice. ?q=renew&page=2 loads with the field filled. Browser Back returns to page 1.
- Sites search field: placeholder "Search by name or key" with its French string; q is sent, encoded; matches the API contract of features/sites_backend.feature.
- Unpaged site list for dependents: with page 2 shown, ?edit=journal, the duplicate-key blur and the ReassignDialog destinations call GET /v1/site/ with no page and no size and never use the paged rows.
- Roles: member on /authors?page=2 sees the same footer with no Add author button and no row menu; owner and admin see the same footer with the Add author button and the row menu.
- Delete the last row of the last page reloads the previous page and replaces the URL; adding the 26th item turns the count line into the pager.
- Authors regression: the old threshold behaviour (pager above 25 only) is gone; a test asserts the count line instead of the pager when total <= size.
- useApi: a unit test for the new headers-aware helper (header read, missing header treated as a failed load, 401/403/429 behaviour unchanged,
requestuntouched). - i18n: the new strings have a French entry ("Rechercher par nom ou clé", "{0} sites", "1 site", "{0} auteurs", "1 auteur", 'Aucun site ne correspond à « {0} »'); the existing key-parity spec in web/src/i18n/ must stay green.
- Existing specs amended (list them in the diff): SitesView and AuthorsView specs and their test kits in web/src/views/tests/directoryTestKit.ts (the list mock now answers a page and a total through the header), and the sites_authors_screens specs whose assumptions about the loaded list change (item 14 duplicate-key check, the deep link).
Visual (task visual:check -- sites, -- authors, -- editor, -- integration)
Run all four. The gate is hard: every capture within 10 % at 1440x900 and 390x844, in light and dark. Add no new exception. A failure, a missing setup or an unmocked /v1 call is listed for the user and not hidden.
Sites and Authors: add one setup per state in web/visual/states.ts, reusing the board's own sample data from the groups "Sites: pagination" and "Authors: pagination" in docs/design/source/RenewCMS Screens Back-office.dc.html (generated rows, totals 61/128, summaries, page labels). Take layout from the frame markup. States for each of the two screens: First page, Middle page, Last page, Single page, Page size menu, Loading a page, Error loading a page, Empty after search, Search resets to page 1, Long labels on mobile (10 each, 20 in total). None of them show a date or time, so none is skip: 'clock-sensitive'. The page-size menu opens upward. Mock GET /v1/site/?... and /v1/author/?... with the right X-Total-Count; the Loading state keeps its request pending.
Existing Sites and Authors states ("Default", "Only the default site", loading, error, row menu, forms, delete, read-only) are re-run: the count line now appears under every loaded list, so some may exceed 10 %. Report any that do, with the diff percentage, to the user.
Editor: remove the withMobileException wrapper and its boardOmitsMobileDialogReason constant from the four "Create author" states (dialog, for me, slug taken, failed) now that the board draws the mobile dialog; update the setups only where needed to match the frame, and do not hide the dialog. If a mobile capture still fails, list it.
Integration: remove the maxPercent: { desktop: 16, mobile: 22 } exception from "6. Errors" (the board now draws the 403 note). Remove no other Integration exception unless the user names it.
Manual
- Keyboard order and focus return: Tab to the page-size select, then First, Previous, each page, Next, Last; Enter and Space; ArrowDown, Enter, Esc; the 2 px --ui-primary focus ring with a 2 px offset on every control.
- Hover, motion and the reduced-motion behaviour of the skeleton pulse.
- Real round trip with
task dev: seed more than 25 sites and more than 25 authors; page, size and q survive a reload; browser Back after a size change; the footer count line on a single page. - Out-of-range fallback against a real shrinking list (delete rows in another tab).
- A small real list: owner and non-owner views of /authors keep the same footer.