12 KiB
QA: Localized public API, SEO output and existing surfaces
Accepted together with features/localization_public_api_and_surfaces.feature. One pair, full path: Go backend first (public routes, SEO, sitemap, languages read, MCP, llms), then the SPA, which mocks /v1 from docs/api/article-localization.md.
Backend (Go, task test, package test)
Build NewHandler(...)s over the real use cases wired to the in-memory fakes already used by test/article_translation_create_http_test.go, test/seo_output_support_test.go and test/site_support_test.go (reuse them, add no second copy; new fake types need unique names). Mount on a fresh chi.NewRouter() mirroring api/router.go: the two public routes under /v1/public with no auth middleware, /v1/article with OptionalAccess, /v1/site for the sitemap and languages. Call validator.InitValidator(), exercise with httptest. No live server, no SQLite file, no SMTP.
Fixture: the Background of the feature (journal with en default, fr fr_FR, de, ar, es disabled; story 5 with versions 10, 12, 11, 13, 14; stories 6 and 7; unassigned article 30; atelier with a published fr slug clash; notes without confirmed default). Add pt-BR versions 40 and 41 for the og:locale table.
- Exact lookup, Examples-driven over the table: status, body code, and for 200 the key set equals the anonymous
GET /v1/article/{id}key set plusalternates. Alternates order (default first, then tag), keysid, lang, story_id, slug, url, exclusion of the served version, drafts and disabled languages;urlis the pattern result, or JSONnullwhen the site has no pattern (assert the pattern-less case with the fake). - Never falls back and never leaks: wrong language with right slug, draft with owner cookie (cookie ignored), unpublished after unpublish, old slug after change, unassigned article, soft-deleted version: all
404 article_not_foundwith the identical body. A raw-response grep shows the id, slug and title of a hidden sibling appear nowhere. - Story resolution, Examples-driven over the decision table of the design (requested published, missing without fallback, missing with fallback, requested unpublished, both unavailable, default requested and unpublished, no lang, default changed, no confirmed default, disabled language explicit, other site, bad fallback values, bad lang, bad id). Assert
lang,requested_lang,fallbackon every 200 and that version 11 (de) is never returned as a third language. - Default change stability: after changing the default to fr, lang-omitted resolves to 12 while
?lang=en, the exact lookup andGET /v1/article/10are unchanged. - SEO object: key sets (additive
lang,alternates,og.locale_alternates,fallbacks.site_text_skipped); canonical table (pattern, override wins, no pattern keeps legacy, unassigned keeps legacy,{lang}keepspt-BR);fallbacks.canonicalvalues; og:locale Examples table includingenwith Site.Localeenandnull(empty string, alternates[]);og.locale_alternates; the description/title fallback tables andsite_text_skipped; the default-change limitation scenario. Existingtest/seo_output_*specs are amended only where they assert the exact key set, theog.localeof an assigned article, or the canonical of a site that has a pattern (unassigned and pattern-less cases must pass unchanged). - Sitemap: eligible versions only,
langkey, localized urls with a pattern and legacy urls without,X-Total-Count, paging invariants unchanged. - Languages read:
social_locale_statederivedwithen_GBfor bareenand Site.Localeen-GB,not_setfor bare Site.Locale; amendtest/site_languages_*accordingly. - MCP:
list_articlesforwardslang(assert the request URL with a stub HTTP client ininternal/infrastructure/mcpserver's existing test style), tool descriptions contain no "cannot be changed". - Catalogue drift:
test/api_catalogue_drift_test.gogreen with the two new routes;web/src/data/apiCatalogue.tscounts updated. - Layering: existing layering tests stay green; the public handler imports no persistence, models or gorm.
- Media usage (extend
TestMediaUsageintest/media_library_http_test.goand the usage cases oftest/media_merge_hardening_test.goandtest/seo_data_media_http_test.gorather than a parallel file; the fakes andNewGetMediaUsageUseCase(...)wiring already exist): fixture image 7 as in the feature's Rule "Media usage names the language and counts a story once". Assert: one article item per version, ordered by id (10, 11, 12, 20, 30); every article item carriesstory_idandlang(item 12 equals the exact JSON of the feature, key settype,id,title,published,fields,story_id,lang); the unassigned article 30 haslang""and is not grouped with any story; author, site and entry items have neither key (assert absence in the raw JSON); order articles, authors, sites, entries unchanged; unused image answers[];404 Image not foundand400 apperr.IdUint32ErrorMessageunchanged. Grouping check: items 10, 11, 12 sharestory_id5.GET /v1/media/givesused_in5 for image 7 (story 5 once, story 6, article 30, author, site) and the same count as before the slice on a one-language fixture. The delete-in-use count (unchanged endpoint, SPA side) is covered in the SPA list below. Amend only the usage assertions that compare an article item's exact JSON orused_inof an image shared by several versions of a story; every other media test must pass untouched.
Older features amended in the same commit (listed in the feature's scenario "Older features are amended in the same commit"): media_library.feature (Rule: Usage), media_field_library.feature (lines 11-12, articles heading, delete-in-use counts), media_browse.feature (usage mock), seo_data_backend.feature and sites_backend.feature (only exact usage key sets / used_in), seo_output_backend.feature, site_languages.feature, integration.feature; their .qa.md files follow.
Real-database runs are required (gate): task test:mysql and task test:postgres for a new persistence test in test/article_localization_storage_persistence_test.go (or a sibling file) that exercises the repository queries behind exact lookup and story resolution: same results on each driver, soft-deleted and offline rows excluded, unassigned excluded, EXPLAIN-free (assert correctness and the presence of idx_articles_site_slug and idx_articles_story_lang, not the plan). Run SQLite in task test. Run both real-DB tasks once, at the end of the backend stage. Use the podman shim if the Docker daemon is off.
Docs (the contract the SPA mocks): docs/api/article-localization.md, docs/api.md, docs/api/articles.md, docs/llms.md, docs/llm/ recipes, docs/release-notes.md.
Automated SPA (Vitest + Vue Test Utils, task test:web)
Mock useApi's request per case (no backend), from the documented contract. Mount EditorView in DashboardShell with a verified session (user id 7, username "marta") and reuse editorKit.ts and localizationKit.ts. Fixture: the Background (journal, story 5, version 12 open). Specs next to the code in __tests__/; extend the existing EditorSeo*.spec.ts files rather than adding a parallel set.
- Search and sharing, one test per state: loc.seo.noFallback (every heading, note, badge, row and help text listed in the feature), loc.slug.published, loc.url.unset (alert, button target
/settings/sites/journal/public-url, no canonical, no alternates), loc.l10.noSocial. - Fallback labels, Examples-driven over every
fallbackssource andsite_text_skippedvalue; both site-text articles (default language, other language). - Draft, canonical override, single-language site: alternates and drafts, override note, no language text on a one-language site.
- Edit slug: warning only when published; request body
{slug, if_revision}; 400invalid_slug, 409 slug conflict and 409revision_conflictoutcomes; focus returns to the Edit slug button. - Integration page: the four cards and sample shapes; catalogue entries for both routes; no "Titles can't be changed" sentence.
- Home: request uses
group=story; default-language title; the code chip only for non-default; French-only reader (loc.l12.homeFr); counts are articles; single-language site unchanged. - Activity: sentences with the language for every article action code that carries
params.lang(publish, unpublish, create, update, delete), English and French; events withoutlangunchanged. - Media usage: mock
GET /v1/media/7/usagewith items 10, 11, 12 (story_id 5; langs en, de, fr), 20 (story 6) and 30 (lang ""): the Articles section shows story 5 once with the versions EN, DE, FR listed and the heading counts articles (3 with story 6 and article 30), not 5 items; a one-language site lists as before; the delete-in-use dialog counts stories; items withoutstory_id/lang(older mocks) still render one row per item (existingmediaUsagespecs stay green). - Article menu labels: Publish, Unpublish and Delete name the language on multilingual sites only.
- API URL copy: the Details card text, copied text
/v1/article/14?lang=fr, toast "API URL copied", unassigned version copies withoutlang, clipboard failure handling as today. - SEO preview: breadcrumb, title, description fallback line, the note.
- Keyboard and focus: Edit slug dialog trap and Escape, row menu items unchanged.
Existing SPA specs to amend (and nothing else): EditorSeoPanel, EditorSeoSlug, EditorSeoFrench, SeoIntegration, IntegrationView, HomeView, ActivityView, media usage specs, seoApiPreview.spec.ts, apiCatalogue spec.
Visual (task visual:check -- <screen>)
Add setups in web/visual/states.ts with the board's own fixture data (Journal, locale-fixtures.js; the French title "Comment nous chiffrons une rénovation", slugs and URLs of the board). Screens and states, light and dark, 1440 and 390 (and 1024 where the table says so):
| Screen file | States |
|---|---|
| localization-metadata-api | loc.seo.noFallback, loc.slug.published, loc.url.unset, loc.l10.noSocial, loc.l10.api (see open question below) |
| localization-surfaces | loc.l12.home, loc.l12.homeFr, loc.l12.activity, loc.l12.media (1440, 1024, 390), loc.l12.menu, loc.l12.apiCopy, loc.l12.seoPreview, loc.l12.siteFallback (1440, 390) |
Clock-sensitive: loc.l12.activity and loc.l12.home show relative times if the SPA renders them from the current date; if so mark skip: 'clock-sensitive' and cover the language text in Vitest with a fixed event list. No date freezing, no threshold change, no exception without the user's explicit approval.
Decision D6 (user): the specification works around these design conflicts. For loc.l10.api, loc.l12.siteFallback, loc.l12.apiCopy and loc.l12.seoPreview the coder must stop and return the measured differences or route gaps to the user; it must not edit docs/design, add a skip, change a threshold or add an exception. The route language_disabled is not returned by any public route (a disabled language stays readable). Known gaps (reported at specification time, not waived): loc.l10.api is drawn with the alert "Proposed capability / These requests don't exist yet" and the story id "st_01", which will be false once this slice ships; loc.l12.siteFallback draws two different articles in one frame (French and Arabic), which no single SPA screen can show; loc.l12.apiCopy and loc.l12.seoPreview are drawn as stand-alone "Details" and "Search preview" pages whose route is not named in docs/design/routes.md.
Manual (not covered by screenshots or Vitest)
- Keyboard and focus order in the Edit slug dialog and the new alternates and social locale cards; hover and motion.
- Real browser: copy button writes to the real clipboard.
- Real embedded frontend build (
task build, run the binary):/integrationshows the new cards; no console errors. - Real SMTP is not involved.
- A real site with a Public URL pattern: open the canonical and an alternate URL in a browser is not possible (they point to the website, not RenewCMS); verify instead that the strings match the pattern.