# QA: SEO round, slice 4 (Sites defaults, Articles search status, Integration, suggestion dismissal) SPA slice only. No Go change: the API of `features/seo_data_backend.feature` and `features/seo_output_backend.feature` already exists, so this slice has no httptest part. A wrong contract is reported as a backend defect. Roles: the domain defines only `owner` and `admin`. "verified" is a verified account without either role. ## Automated SPA (Vitest + Vue Test Utils, `task test:web`) Mock `useApi`'s `request` per case: GET/POST/PATCH /v1/site/..., GET /v1/article/ (items with `seo_status` and `seo_suggestions`, `counts`, `total`), PATCH /v1/article/{id}, POST /v1/article/{id}/seo/check, and the media routes of the shared picker. Mount SitesView, ArticleView, IntegrationView and EditorView inside DashboardShell with a resolved session and a test router. Mock matchMedia for desktop and mobile. Use fake timers for the search debounce. Do not freeze the system date. The rendering of "Updated …" is not asserted. Report the web tests as run or NOT RUN, never as passing when not run. One spec file per numbered group, under `web/src/views/__tests__/` (`SeoSites*.spec.ts`, `SeoArticles*.spec.ts`, `SeoDismiss*.spec.ts`, `SeoIntegration*.spec.ts`). Regression: every existing Sites, Articles (ArticlesPagination, ArticlesPageSizeMemory, ArticlesSitesAuthors), Integration and EditorSeo* spec now sees the toolbar count request, the new fields and the dismiss buttons. Give them default mocks (count request answers total 3 on the request carrying `seo=suggestions`; sites with the three SEO fields) rather than loosening unmocked-call checks, and show that they still pass. 1. Sites form fields: order, copy of states 1 and 2 (edit and add), placeholders, helpers, the counter, no "(optional)" on Site name, the "How articles use these defaults" block and its placement under the preview block. 2. Live example: the Examples table (site name, name fallback, placeholder "Renew" fallback, `%title%` only, two `%site%`), updated on each keystroke, no request sent; global replace, not first occurrence. 3. Template validation: the four invalid values (`just text`, `%site%`, `%TITLE%`, empty) on blur and on submit, nothing sent, focus on the first invalid field, example hidden, error cleared on a valid value, 400 `invalid_title_template` from the server shows the same message. 4. Limits: maxlength 100, 200 and 320; the counter at 0, 45 and 320. 5. Save payloads: PATCH holds only changed fields (assert exact JSON) for template only and for name + description + robots; POST omits the three fields when untouched and sends them when changed; the discard dialog on a changed SEO field; a site without the fields opens with defaults and "Save changes" sends nothing. 6. Save failure and sessions: 500 and network failure keep the panel and the notice; 403 `owner_only` gives the existing toast; 401 goes to `/login?redirect=/settings/sites&reason=expired`; admin and verified users get the 403 page and no form. 7. SeoStatusChip: the status table (ready, suggestions 1 and 2, hidden with and without a count), placement in the Status cell and in the card meta row, tooltip/accessible name (singular and plural), the unusable items (no seo_status, suggestions with 0, unknown status), the row action that keeps the chip. 8. Toggle and URL: parse and write `seo` (`parseArticleQuery`, `toRouteQuery`, `sameArticleQuery`), page reset, q/status/sort/site/size kept, other changes keep seo, reset-to-1 table, opened with `?seo=suggestions&page=2`, unknown values dropped silently via replace (the six values), the request carries `seo=suggestions` only when on, tab counts unchanged. 9. Count: total 3, total 0, pending, failure (no count, no notice, no URL change); the extra request (`seo=suggestions&page=1&size=5`, with q, status and site) is made only while off, only when q/tab/site change, and the main response total is used while on. 10. Empty states: "Nothing needs attention" with its action (clears seo only), the not-in-design combinations (search, Draft, Online, a site), paginator hidden, Retry notice still works. 11. Single-site install: the Site column and filter are absent and everything above behaves the same. 12. Dismiss ×, rendering: one button per row of the panel list, none in the compact section list; the footnote; accessible name with the message interpolated; Tab order (after "Go to …"); Enter and Space; visible focus class. 13. Dismiss ×, behaviour: success sends exactly one PATCH with `seo_dismissed` equal to the saved list plus the code (assert exact JSON, no other field, no draft field); the button is busy and the row stays until 200; the row leaves; one new check follows; summary line and "All good" follow the check; the draft, the panel values and the editor status are untouched; focus table (first, second, last, and the last remaining row going to "Show dismissed"); 500 and network failure keep the row and show the toast, send no check; 401 mirrors the draft and redirects; 404 shows "Not found"; the three roles; Save and Publish stay enabled; "Show dismissed" sends `seo_dismissed: []` then a check. 14. Integration group: position after Articles, group description, 2 routes, both cards "Public" and "API key OK", expanded content (fields, query rows, tabs, error rows with the real strings), no duplicate route in Articles or Sites, `POST …/seo/check` still in Articles, index and Jump to order, deep link #seo, counts 8/2/10/6/1 and "1 route" for Sites, the 403 note, no /v1 request. 15. Catalogue spec: update the group list, the per-group counts, the Sites description, the "Public" assertions (they currently list `GET /v1/article/{id}/seo` and the sitemap with the Articles and Sites groups) and the 403 note. 16. i18n: every new `t()` key has a French entry (extend the i18n spec), the French plurals, one French walk through the four screens. ## Visual (`task visual:check -- `, desktop and mobile, light and dark) Setups go in `web/visual/states.ts`, one per state, with mocked /v1 and the board's own sample data. Hard gate: every capture within 10 %, no per-state exception. Reference frames: `DirectoryFrame.dc.html` (Sites form, SuggestionList), `AppFrame.dc.html` (Articles) and `IntegrationPage.dc.html`. - Sites: SEO defaults (5): Edit: SEO defaults (Renew, `f.sname`, template `%title% | %site%`, example "How we price a bathroom renovation | Renew"), Add: defaults, Template: custom (site name Journal, template `%title% – %site%`), Template: not valid, Save failed. The existing Sites form frames (7) now contain the three fields and the new block: update their setups with the board values. - Articles: search status (3): Status chips (rows: 2 suggestions, hidden, 3 suggestions, 1 suggestion), Needs attention on, Needs attention: none. Every existing Articles and pagination frame now shows the toggle with the count 3 and the chips of the board rows, so each setup mocks the count request with total 3. States whose "Updated" column or text is clock-sensitive follow what the existing setups already do (`skip: clock-sensitive`), and state 8 "Published" stays skipped. - Integration (8 existing states): the index and the mobile select now contain "Search and sharing", and the page has the new group (collapsed). Mock nothing new. The Navigation state's open select must list the new option. If the 403 note makes a capture exceed 10 %, report it to the user; do not add an exception. - Editor: the dismiss column is now implemented. Remove the five `withMobileException(..., dismissColumnReason)` wrappers (and the `dismissColumnReason` constant, `withMobileException` if nothing else uses it) in `web/visual/states.ts`, covering 'Panel: defaults', 'Panel: outside the soft ranges', 'Panel: cover without alt', 'Panel: choose cover' and the fifth state at line ~5828. Those states must pass `task visual:check -- editor` at the default 10 % in both viewports. If one still fails, report it; do not restore an exception. - Re-run Settings, Users, Profile and Home: report any capture above 10 %. Any failure needs the user's decision (an `approvedBy` exception or a design update). ## Final pass: slice 3 leftovers (required in the qa report) 1. Grep `web/src` (excluding `__tests__`) for text in templates not passed through `t(...)`, and for `title="..."`, `placeholder="..."` and `aria-label="..."` attributes with English literals, in the files of slice 3 (`SeoPanel.vue`, `SearchSharingSection.vue`, `SuggestionList.vue`, `ApiPreviewDialog.vue`, `SearchPreview.vue`, `SocialCardPreview.vue`, `SeoDisclosure.vue`, `EditorView.vue`, `seoCopy.ts`, `seoApiPreview.ts`, `seoPreview.ts`). 2. Extract every `t('…')` key used by those files and by the files of this slice, and confirm each has an entry in `web/src/i18n/messages.ts` (the i18n spec must cover them). Spot check on the current tree: no missing entry found among the editor strings sampled; the new ones for this slice are still to add. 3. Report findings by file and line. Fix those that belong to this slice. Anything else is reported to the user, not fixed silently. ## Exit conditions - All Articles, Sites and Integration frames (existing and the 8 new ones) pass `task visual:check`, plus Editor, Settings, Users, Profile and Home re-run with no regression above 10 %. - The slice 3 dismiss-column exceptions are gone from `web/visual/states.ts` and `grep -n dismissColumnReason web/visual/states.ts` finds nothing; the Editor panel frames pass without exception. - Vitest specs reported honestly, run or NOT RUN. - `bun lint` clean. No comment added. No Go file changed. ## Divergences between the design and the real API (feed back to docs/design) - Sitemap errors: the design says 404 "Site not found" and 400 "Invalid page or size."; the API answers 404 `site_not_found` and 400 with the generic body-mismatch message (`apperr.BodyErrorMessage`). The Integration cards use the real strings. - Article list: the design (routes.md) describes an array body with `X-Total-Count`; the code reads `{items, total, page, size, counts}`. Unchanged by this slice. - The 403 note under the Errors table has no layout in the design. - The 403 note under the Errors table is a design gap: it pushes the following rows down, so Integration "6. Errors" carries an approved exception (16 % desktop, 22 % mobile). - Hidden chip: the board wraps the "Hidden" chip under the Online badge in the 110 px Status column; the feature says it never wraps, and the app follows the feature. - Add: defaults: the board's Example row always falls back to "Renew"; the app falls back to the Name, then "Renew", as the feature requires. - Articles row 5: the design text says row 5 has 1 suggestion, but the board data draws no chip on it; the visual setup follows the board data. - Picker debt: on mobile the shared media picker sheet is taller and shows more tiles than the board, so Editor "Panel: choose cover" carries an approved mobile exception (29 %). The picker was not changed in this slice. ## Copy and behaviour not in the design (feed back to docs/design later) Each has a French entry in `web/src/i18n/messages.ts`. 1. Source of the "Needs attention" count (extra request, `total`). 2. Zero count shown as "0", no count while pending or on failure. 3. Empty-state precedence when `seo=suggestions` is combined with a search, a tab or a site. 4. Chips refresh only with the next list load after a row action. 5. Items without a usable status show no chip. 6. Example fallback to the name, then to "Renew" as site name. 7. Accessible name of the chip on touch. 8. The 403 row note on the Errors table. 9. Dismiss ×: accessible name "Dismiss suggestion: ", the failure toast "Couldn't dismiss the suggestion" / "Try again in a moment.", the busy state, the focus rules, and "Show dismissed" restoring all codes. ## Proposed French (for review, in `web/src/i18n/messages.ts`) | English key | French | | --- | --- | | Needs attention | À traiter | | Hidden | Masqué | | Hidden from search engines | Masqué des moteurs de recherche | | Nothing needs attention | Rien à traiter | | Every article has a clean check. Suggestions are optional, so this is just a bonus. | Tous les articles passent la vérification. Les suggestions sont facultatives, c'est un petit plus. | | Show all articles | Afficher tous les articles | | Site name | Nom du site | | Title template | Modèle de titre | | Use %title% for the article title and %site% for the site name. | Utilisez %title% pour le titre de l'article et %site% pour le nom du site. | | The template must contain %title%. | Le modèle doit contenir %title%. | | Example | Exemple | | Default description | Description par défaut | | How articles use these defaults | Comment les articles utilisent ces valeurs par défaut | | Dismiss suggestion: {0} | Ignorer la suggestion : {0} | | Couldn't dismiss the suggestion | Impossible d'ignorer la suggestion | | Show dismissed | Afficher les suggestions ignorées | | Search and sharing | Recherche et partage (already present) | ## Manual checks screenshots cannot cover - Keyboard: tab order through the Sites form (the new fields sit between the image and the robots policy), Space and Enter on "Needs attention", focus on the first invalid field, the dismiss button order and focus after a row leaves, the index and the Jump to select (Enter, Escape). - Hover and touch: the chip tooltips on desktop, the chip text read by a screen reader, the toggle's pressed state. - Real `task dev`: the owner sets a template and sees the title change in "What the frontend gets"; dismiss a suggestion, reload, and see it stay hidden and the article's chip count drop; the Articles filter agrees with `curl '…/v1/article/?seo=suggestions'`; the sitemap card matches `curl` on `/v1/site/renew/sitemap` and its X-Total-Count; read the screens in French.