From acd392c29d80c7cef34dd0c234e323a3b5259ee6 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Mon, 5 Oct 2026 02:03:24 +0200 Subject: [PATCH] feat: integration page (PART 3/3) --- .gitignore | 2 +- docs/api.md | 39 + features/integration.feature | 234 ++++-- features/integration.qa.md | 140 ++-- web/src/components/ApiFieldTable.vue | 76 ++ web/src/components/ApiRouteRow.vue | 24 + web/src/components/CodeBlock.vue | 8 +- web/src/components/CodeText.vue | 14 + web/src/components/EndpointCard.vue | 208 +++-- web/src/components/SectionIndex.vue | 83 ++ web/src/components/SessionOnlyGroup.vue | 74 ++ web/src/composables/useShikiHighlighter.ts | 2 +- web/src/data/__tests__/apiCatalogue.spec.ts | 559 +++++++++++++ web/src/data/apiCatalogue.ts | 756 ++++++++++++++++++ web/src/i18n/messages.ts | 66 ++ web/src/i18n/spa.spec.ts | 5 +- web/src/lib/apiReference.ts | 28 + web/src/views/IntegrationView.vue | 500 +++++++++--- .../views/__tests__/IntegrationView.spec.ts | 602 +++++++++++--- web/visual/states.ts | 104 ++- 20 files changed, 3115 insertions(+), 409 deletions(-) create mode 100644 docs/api.md create mode 100644 web/src/components/ApiFieldTable.vue create mode 100644 web/src/components/ApiRouteRow.vue create mode 100644 web/src/components/CodeText.vue create mode 100644 web/src/components/SectionIndex.vue create mode 100644 web/src/components/SessionOnlyGroup.vue create mode 100644 web/src/data/__tests__/apiCatalogue.spec.ts create mode 100644 web/src/data/apiCatalogue.ts create mode 100644 web/src/lib/apiReference.ts diff --git a/.gitignore b/.gitignore index 469a4ca..67c4664 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ # DB *.db -data/ +/data/ # Build bin diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..b525ed9 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,39 @@ +# API reference + +The full, searchable reference is the Integration page of the running app (`/integration`). It +renders `web/src/data/apiCatalogue.ts`, which is the single source for this page. This file is for +readers without the UI. + +## Base URL +The origin that serves the app, for example `https://cms.example`. Every path starts with `/v1`, +for example `https://cms.example/v1/article/`. + +## Authentication +- Public routes need nothing: `GET /v1/article/` and `GET /v1/article/{id}`. They return online + articles only. +- Everything else needs a signed-in browser session (the `jwt` cookie) or an API key: + `Authorization: Bearer rcms_...`. Create keys at `/integration/api-keys`. A key acts as its + creator, with that user's role, and shows in full once. +- A key cannot call `POST /v1/auth/verify`, `DELETE /v1/auth/register`, `/v1/api-key/*` or + `/v1/system/*`. Those answer 403 "This action needs a real session." An invalid, unknown, expired + or revoked key answers 401, even on public routes. +- Each key can make 120 requests per minute. Beyond that: 429 with a `Retry-After` header. + +## Routes a key can call (20) +| Group | Routes | +| --- | --- | +| Articles | GET /v1/article/, GET /v1/article/{id}, POST /v1/article/, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish, PATCH /v1/article/{id}/unpublish, DELETE /v1/article/{id} | +| Media | GET /v1/media/, POST /v1/media/, PATCH /v1/media/{id}, DELETE /v1/media/{id}, POST /v1/media/merge, GET /v1/media/{id}/usage | +| Users and invites | GET /v1/user/, PATCH /v1/user/me, POST /v1/user/invite (owner only), DELETE /v1/user/invite/{id} (owner only), POST /v1/user/invite/{id}/renew (owner only), GET /v1/auth/me | +| Settings | GET /v1/settings | + +Article routes accept `?format=markdown|html|json` (default `markdown`). `GET /v1/media/` accepts +`q`, `hash`, `cursor`, `filter`, `sort` and `limit`. + +## Errors +Every error is a plain-text body with one sentence; the status is in the HTTP status line. The +statuses are 400, 401, 403, 404, 409, 429 and 500. Messages are listed on the Integration page. + +## Notes +- Images in `body` use paths like `/static/uploadedImages/.png`. Prefix them with the base URL. +- Browsers can call the API only from origins in the server's CORS setting. diff --git a/features/integration.feature b/features/integration.feature index 768efa5..ef7f5c6 100644 --- a/features/integration.feature +++ b/features/integration.feature @@ -1,63 +1,193 @@ -Feature: Integration screen +Feature: Integration screen (API reference) - As a verified RenewCMS owner - I want /integration to show my site's base API URL and ready-to-use endpoint examples - So that I, or the developer who builds my public site, can wire it up without reading the - source + As a developer building a frontend or an API-key client for a RenewCMS site + I want /integration to be a complete, searchable API reference + So that I can call every route a key may call without reading the source Background: Given /integration is a verified route (meta.auth "verified") rendered as IntegrationPage inside DashboardShell, per docs/design/routes.md And the screen makes no /v1 calls; api: none (static) - And the PageHeader shows title "Connect your website", description "A guide for the developer - who builds your public site.", breadcrumb "Integration" (mobile title "Integration"), and no - actions - And an introductory paragraph reads "RenewCMS is headless: it stores and publishes your - articles but doesn't show them to visitors. Your website fetches published articles from the - two addresses below and displays them in its own design." - And a "Base URL" row shows window.location.origin in a monospace box, with a "Copy" button - (icon copy) beside it - And two EndpointCard entries follow, in this order: - | method | path | description | - | GET | /v1/article/ | Lists every online article. | - | GET | /v1/article/{id} | Returns one online article. Drafts and unknown ids both return 404. | - And each EndpointCard's fetch example uses the current origin as the base of its URL: - | path | fetch example | - | /v1/article/ | const res = await fetch('{origin}/v1/article/')\nconst articles = await res.json() | - | /v1/article/{id} | const res = await fetch('{origin}/v1/article/12')\nif (res.status === 404) return notFound()\nconst article = await res.json() | - And each EndpointCard has fetch, curl and Response tabs, with fetch active by default, and its - CodeBlock is labelled "JavaScript" with its own ghost "Copy" button - And the curl tab's content is a plain curl command built from the current origin (e.g. - "curl {origin}/v1/article/" and "curl {origin}/v1/article/12"), and the Response tab's - content is a JSON sample built from the real domain Article fields; neither tab is active by - default, and neither is visually checked, since the fetch tab is the one shown in the board - And three info notes follow the endpoint cards, in this order: - - "Anonymous callers only ever get online articles. Drafts stay private until you publish - them." - - "Images in body use paths like /static/uploadedImages/.png. Prefix them with the - base URL before rendering." - - "Browsers can only call the API from origins listed in the server's CORS setting. Ask - whoever runs the server to add your site's address." + And the PageHeader shows breadcrumb "Integration", title "API reference" and description + "Everything a developer needs to build on RenewCMS." + And the endpoint catalogue is a typed static data file at web/src/data/apiCatalogue.ts, and the view only renders it + And the catalogue holds 20 key-callable endpoints in 4 expandable groups, in this order: + | group | routes | endpoints | + | Articles | 7 | GET /v1/article/, GET /v1/article/{id}, POST /v1/article/, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish, PATCH /v1/article/{id}/unpublish, DELETE /v1/article/{id} | + | Media | 6 | GET /v1/media/, POST /v1/media/, PATCH /v1/media/{id}, DELETE /v1/media/{id}, POST /v1/media/merge, GET /v1/media/{id}/usage | + | Users and invites | 6 | GET /v1/user/, PATCH /v1/user/me, POST /v1/user/invite, DELETE /v1/user/invite/{id}, POST /v1/user/invite/{id}/renew, GET /v1/auth/me | + | Settings | 1 | GET /v1/settings | + And the "Session only" group has 13 non-expandable rows in 3 sub-groups: + | sub-group | rows | + | System · owner only | GET /v1/system/update, POST /v1/system/update/check, POST /v1/system/update/download, POST /v1/system/update/restart | + | Sign in and account | POST /v1/auth/login, POST /v1/auth/logout, POST /v1/auth/register, POST /v1/auth/verify, DELETE /v1/auth/register, POST /v1/auth/password-reset | + | API key management | GET /v1/api-key, POST /v1/api-key, DELETE /v1/api-key/{id} | + And a "Public auth flow" sub-list follows with 6 non-expandable rows and the chip "Public": + | method | path | description | + | GET | /v1/ | Hello world. | + | GET | /v1/auth/registration | Whether registration is open. | + | POST | /v1/auth/password-reset/confirm | Set a new password with the emailed code. | + | GET | /v1/auth/invite | Check an invite code. | + | POST | /v1/auth/invite/accept | Accept an invite. | + | GET | /healthz | Plain text "ok". Outside /v1. | + And every catalogue endpoint has a non-empty description, method, path, access chip, key chip, + query-parameter table (or "No query parameters."), request-body table (or "No request body."), + fetch, curl and Response examples, and error captions - Scenario: Default — the page loads with the app's own origin - Given the app is served from origin "{origin}" + Scenario: Default, every card collapsed When the user navigates to /integration - Then the Base URL row shows exactly "{origin}" - And the Base URL "Copy" button reads "Copy", not "Copied" - And both EndpointCards' fetch examples use "{origin}" as the base of their URL + Then the intro reads "RenewCMS is headless: it stores your content and your own code displays it. There are two ways in. Public read-only access lets a website read online articles. API keys let a script or service manage articles, media and users without the back-office." + And a "Base URL" row shows window.location.origin in a monospace box with a "Copy" button (icon copy) + And every EndpointCard is collapsed as a 44 px row: method badge, mono path, one-line description, chips, chevron + And the sticky section index lists the sections on desktop and a "Jump to" select lists them on mobile - Scenario: Copied — the Base URL copy button - Given the user is on /integration, served from origin "{origin}" - When the user activates the Base URL row's "Copy" button + Scenario Outline: Method badge and chips per endpoint + Then shows the badge, access chip "" and key chip "API key OK" + Examples: + | method | path | access | + | GET | /v1/article/ | Public | + | GET | /v1/article/{id} | Public | + | POST | /v1/article/ | Verified user | + | DELETE | /v1/article/{id} | Verified user | + | GET | /v1/media/ | Verified user | + | PATCH | /v1/user/me | Verified user | + | GET | /v1/auth/me | Verified user | + | POST | /v1/user/invite | Owner only | + | DELETE | /v1/user/invite/{id} | Owner only | + | POST | /v1/user/invite/{id}/renew | Owner only | + | GET | /v1/settings | Verified user | + + Scenario: Owner-only invite cards, user-approved divergence from the board + Then the three invite cards show "Owner only", where the board shows "Verified user" + And each lists a 403 error "Only the owner can do this." + And each says it is rate limited and that too many requests answer 429, with no number + + Scenario: Group scrolled, one card expanded + When the user activates the "POST /v1/article/" row + Then the row's button reports aria-expanded "true" + And a "Query parameters" table lists "format", string, optional, "markdown, html or json. Default markdown." + And a "Request body" table lists: + | Name | Type | Required | Rule | + | title | string | Yes | 3 to 200 characters. Can't be changed later. | + | body | string | Yes | Up to 10 000 characters. | + And tabs fetch, curl and Response show, fetch active, with a CodeBlock labelled "JavaScript" and a ghost "Copy" button + And an "Errors" list shows a status chip with a one-line caption for 400, 401 + + Scenario: Query parameters table, new UI beyond the board, user-approved + Then the "Query parameters" table sits above the "Request body" table in each card + And it lists: + | route | name | type | required | rule | + | every /v1/article route | format | string | no | markdown, html or json. Default markdown. | + | GET /v1/media/ | q | string | no | Search text. | + | GET /v1/media/ | hash | string | no | SHA-256 of the content, lowercased. | + | GET /v1/media/ | cursor | string | no | The next_cursor of the previous page. | + | GET /v1/media/ | filter | string | no | all, unused or duplicates. Default all. | + | GET /v1/media/ | sort | string | no | newest, oldest, name or largest. Default newest. | + | GET /v1/media/ | limit | number | no | 1 to 100. Default 24. | + And a card without query parameters shows "No query parameters." + + Scenario Outline: Request body fields come from the handler validate tags + Then lists these fields: + Examples: + | method | path | fields | + | POST | /v1/article/ | title string required 3-200; body string required max 10000 | + | PATCH | /v1/article/{id} | body string required max 10000 | + | PATCH | /v1/media/{id} | filename string required | + | POST | /v1/media/merge | keep_id number required >0; merge_ids number[] required min 1 each >0 | + | POST | /v1/media/ | multipart/form-data, 1 file field named "file" | + | PATCH | /v1/user/me | display_name string required max 60 | + | POST | /v1/user/invite | email string required valid email | + | GET | /v1/article/ | "No request body." | + + Scenario: Response samples use the real domain JSON field names + Then the GET /v1/article/{id} Response sample has exactly the keys id, title, body, body_format, is_online, created_at, updated_at + And the sample has no "images" key and no "status" key + And POST /v1/media/merge has the keys articles_updated and merged + And GET /v1/media/ has the keys items, next_cursor and counts + And GET /v1/media/{id}/usage items have the keys id, title, published + And PATCH /v1/article/{id}/publish and /unpublish samples are articles with is_online true and false + + Scenario: Article body format + Then a note reads "Article routes accept `?format=markdown`, `?format=html` or `?format=json` to choose how `body` is returned. With `json`, `body` is the stored JSON structure. Articles have `body_format: markdown` by default." + And every article card's Errors list includes 400 "The format must be one of: markdown, html, json." + + Scenario: Authentication section + Then card "Public" reads "No key needed. Two read-only routes, and only online articles are returned." and names GET /v1/article/ and GET /v1/article/{id} + And card "API key" reads "Send `Authorization: Bearer rcms_…`. Requests act as the user who created the key, with that user's role (owner or admin). A key never has more rights than its creator." + And its CodeBlock has tabs curl and fetch for GET /v1/auth/me, using the current origin + And it says "Keys start with `rcms_` and are shown in full once, when you create them." + And a "Manage API keys" link navigates to /integration/api-keys + And Note 1 reads "A key can't create, list or revoke API keys, verify or cancel a registration, or call anything under `/v1/system`. Those routes answer 403 "This action needs a real session." An invalid, unknown, expired or revoked key answers 401, even on public routes, and so does a key whose user was deleted or unverified." + And Note 2 reads "Each key can make 120 requests per minute. Beyond that the API answers 429 with a `Retry-After` header." + + Scenario: Session only group + Then the banner reads "These routes are made for a signed-in browser session. With an API key, verify, DELETE /auth/register, /api-key/* and /system/* answer 403 "This action needs a real session."" + And each row has a dashed border and muted fill, uses no method colour and cannot be expanded + And rows that answer 403 to a key show the lock icon and the chip "Session only" + And Log in, Log out, Register and Reset password show the caption "Made for the browser. A key is ignored here, with no 403." and no 403 claim + And no /v1/system route appears in any expandable group + And the "Public auth flow" rows carry the chip "Public" and no lock icon + + Scenario: Errors section + Then the intro reads "Every error is a plain-text body with a single sentence. The status code is in the HTTP status line, and the Content-Type is text/plain." + And the table lists: + | status | message | + | 400 | The request cannot be processed due to a mismatch in the format of the body. / The expiry must be a date in the future. | + | 401 | You must be logged in to access this resource. | + | 403 | This action needs a real session. / Only the owner can do this. / You must verify your account to access this resource. | + | 404 | Article not found / Image not found / Invite not found / API key not found | + | 409 | There's already a pending invite for this address. / This invite has already been accepted. | + | 429 | Too many requests. Please try again later. | + | 500 | Internal server error | + And a sample shows "HTTP/1.1 403 Forbidden", "Content-Type: text/plain", a blank line, and "This action needs a real session." + And the table does not use the SPA copy of docs/design/states.md + + Scenario Outline: Per-endpoint error captions use the real strings + Then lists "" + Examples: + | method | path | status | caption | + | POST | /v1/media/ | 400 | the image could not be read | + | POST | /v1/media/ | 413 | the file exceeds the maximum upload size | + | POST | /v1/media/ | 415 | the file must be a PNG, JPEG, WEBP, or SVG image | + | GET | /v1/media/ | 400 | The media query is invalid. | + | POST | /v1/media/merge | 400 | Choose an image to keep and at least one other image to merge into it. | + | POST | /v1/media/merge | 409 | Some of the images to merge changed. Refresh and try again. | + | PATCH | /v1/media/{id} | 400 | Enter a filename. | + | POST | /v1/article/ | 400 | Article title must be between 3 and 200 characters. | + | GET | /v1/article/{id} | 400 | The server expects the ID to be in the format of an unsigned 32-bit integer (uint32). | + | PATCH | /v1/user/me | 400 | Display name must be 60 characters or fewer. | + | POST | /v1/user/invite | 409 | This address already has an account. | + | POST | /v1/user/invite | 409 | There's already a pending invite for this address. | + | POST | /v1/user/invite/{id}/renew | 409 | This invite has already been accepted. | + | POST | /v1/user/invite | 403 | Only the owner can do this. | + + Scenario: Notes + Then the image-prefix note reads "Images in body use paths like /static/uploadedImages/.png. Prefix them with the base URL before rendering." + And the CORS note reads "Browsers can only call the API from origins listed in the server's CORS setting. Ask whoever runs the server to add your site's address." + + Scenario: Copied, Base URL + Given the app is served from origin "{origin}" + When the user activates the Base URL "Copy" button Then "{origin}" is written to the clipboard - And the button's icon changes to check and its text changes to "Copied" - And the change is announced to assistive technology - When 2 seconds elapse without a further click - Then the button reverts to icon copy and text "Copy" + And the button shows icon check and "Copied", announced to assistive technology + When 2 seconds elapse + Then the button reverts to icon copy and "Copy" - Scenario: Each EndpointCard's own Copy button is independent of the Base URL's - Given the user is on /integration - When the user activates the "/v1/article/" EndpointCard's "JavaScript" code block's "Copy" - button - Then that card's own code is written to the clipboard, and only that button reads "Copied" - And the Base URL row's "Copy" button and the other EndpointCard's "Copy" button are unaffected + Scenario: Copied, CodeBlock is independent of Base URL + Given a card is expanded + When the user activates that CodeBlock's "Copy JavaScript example" button + Then that card's exact code string is written to the clipboard and only that button reads "Copied" + And the Base URL button and other CodeBlocks are unaffected + + Scenario: Examples use the current origin and the API key header + Then every fetch and curl example starts from "{origin}" and carries no ?format= query + And examples for non-public routes carry "Authorization: Bearer rcms_…" + And examples for public routes carry no Authorization header + + Scenario: Navigation + When the user activates a section in the index (desktop) + Then the page scrolls to that anchor and the item reports aria-current "true" + And on mobile the "Jump to" select opens and lists the same sections + + Scenario: The page stays static + Then no /v1 request is made while the page renders or while any card is expanded diff --git a/features/integration.qa.md b/features/integration.qa.md index 90be9b0..4558bf0 100644 --- a/features/integration.qa.md +++ b/features/integration.qa.md @@ -1,66 +1,98 @@ -# QA: Integration screen +# QA: Integration screen (API reference) ## Automated (Vitest + Vue Test Utils, `task test:web`) Mount IntegrationPage inside DashboardShell with a resolved verified session and a test router at -/integration. Stub `window.location.origin` (or the equivalent composable/helper the coder uses to -read it) to a fixed value for the test, and stub `navigator.clipboard.writeText` per case. No -`/v1` mocks are needed; the screen makes no calls. +/integration. Stub `window.location.origin` to `https://cms.renew.example` (the board's sample) and +`navigator.clipboard.writeText` per case. No `/v1` mocks are needed. A spec fails if any `/v1` call +is made. -1. **Default** — mount at /integration with origin stubbed to a fixed value (e.g. - `https://cms.renew.example`, matching the design board's sample so the assertions read the same - as the visual reference); assert breadcrumb "Integration"; assert the PageHeader title - "Connect your website" and description "A guide for the developer who builds your public - site."; assert the intro paragraph text; assert the Base URL row shows exactly the stubbed - origin; assert the Base URL "Copy" button reads "Copy"; assert both EndpointCards render in - order (GET /v1/article/, GET /v1/article/{id}) with their descriptions and their fetch examples - containing the stubbed origin; assert the fetch tab is active by default on both cards; assert - the three info notes render in order. -2. **Copied** — from Default, use fake timers; click the Base URL row's "Copy" button; assert - `navigator.clipboard.writeText` was called with the stubbed origin; assert the button's icon is - check and its text is "Copied"; assert the change is exposed to assistive technology (e.g. an - aria-live region or a status role holds "Copied"); advance the fake timer by 2 s; assert the - button reverts to icon copy and text "Copy". -3. **EndpointCard's own Copy button is independent** — from Default, click the "/v1/article/" - card's "JavaScript" CodeBlock's "Copy" button; assert `navigator.clipboard.writeText` was called - with that card's exact code string; assert only that button reads "Copied"; assert the Base URL - button still reads "Copy" and the other card's Copy button still reads "Copy". This behavior is - Vitest-only; it has no visual-check state of its own (see Visual below). -4. **curl and Response tabs render, unchecked visually** — from Default, switch the "/v1/article/" - card to its curl tab; assert its content is a plain curl command built from the stubbed origin - (e.g. "curl https://cms.renew.example/v1/article/"); switch to its Response tab; assert its - content is a JSON sample built from the real domain Article fields. Neither tab's content is - asserted pixel-for-pixel anywhere, since the design board only ever shows the fetch tab active. +Catalogue spec (pure data, no mount, imports web/src/data/apiCatalogue.ts): +1. The catalogue holds exactly the 20 expected key-callable routes from an explicit hand-maintained + list in the spec (Articles 7, Media 6, Users and invites 6, Settings 1), each once, with no other + expandable route. Session only has the 13 rows and Public auth flow the 6 rows listed in the + feature, from the same kind of explicit list. +2. Every catalogue route has a non-empty `description`. An empty or missing one fails the spec and + names the route. +3. Every route has method, path, access chip, key chip, a query-parameter list and a body-field list + (possibly empty), and fetch, curl and Response examples. +4. The Response sample of each route parses as JSON and uses only keys from a fixture of the real + fields: + - article: id, title, body, body_format, is_online, created_at, updated_at; + - media item: id, filename, url, size, content_type, width, height, sha256, uploaded_by, + created_at, used_in, duplicate; + - media list: items, next_cursor, counts (all, unused, duplicates); + - merge: articles_updated, merged; + - usage: id, title, published; + - user list item: id, kind, username, display_name, email, role, status, created_at; + - invite: id, kind, email, status, created_at, expires_at; + - settings: version. + Any other key fails the spec. "images" and "status" are explicitly rejected on articles. +5. Request-body tables equal the validate tags (title 3-200, body max 10000, filename, keep_id, + merge_ids, display_name, email). The multipart field is "file". Query tables: `format` on every + article route, and q, hash, cursor, filter, sort, limit on GET /v1/media/. +6. The three invite routes carry "Owner only", a 403 "Only the owner can do this." caption and the + rate-limit sentence. No /v1/system route is in an expandable group. Login, Log out, Register and + Reset password rows carry no 403 claim. Verify, DELETE /auth/register, /api-key/* and + /system/* are the only rows that claim 403. +7. Error captions equal fixtures copied from api/pkg/apperr/messages.go and the image domain + errors (413, 415, 400 on upload). + +Component specs, one per design state: +1. **Default**: breadcrumb, title "API reference", description, intro, Base URL equals the stubbed + origin, all cards collapsed (aria-expanded "false"), section index entries present, the three + invite rows show "Owner only". +2. **Group scrolled, one card expanded**: click the POST /v1/article/ row. Assert the Query + parameters table above the Request body table, the body rows, tabs fetch/curl/Response with fetch + active, the "JavaScript" CodeBlock, and the Errors list with 400 and 401. +3. **Copied: base URL**: fake timers; click Copy. Clipboard gets the origin; check icon and "Copied"; + announced; reverts after 2 s. +4. **Copied: CodeBlock**: expand a card; click "Copy JavaScript example". Clipboard gets that code, + only that button reads "Copied", and it reverts after 2 s. +5. **Authentication**: both cards, curl/fetch tabs for GET /v1/auth/me, the "Manage API keys" link + href /integration/api-keys, Note 1, Note 2. +6. **Session only**: the corrected banner; lock icon and "Session only" chip only on rows that + answer 403; the caption on the login family; no expand control; the "Public auth flow" rows with + the "Public" chip. +7. **Errors**: table rows 400, 401, 403, 404, 409, 429, 500 with the real strings and the 403 + sample block. +8. **Navigation**: clicking an index item sets aria-current and scrolls to its anchor; the mobile + "Jump to" lists the same sections. +9. **Tabs**: curl and Response render for a card. curl contains the stubbed origin and the + Authorization header only on non-public routes. Not visually checked. +10. **Notes**: format note, image-prefix note, CORS note. ## Visual (`task visual:check -- integration`, desktop and mobile, light and dark) -Reference: `docs/design/screens/integration.md` and -`docs/design/source/RenewCMS Screens Back-office.dc.html`, group "Integration"; layout detail from -`docs/design/source/AppFrame.dc.html`'s `isInteg` block. +Reference: docs/design/screens/integration.md; board "Integration" drawn by +docs/design/source/IntegrationPage.dc.html. Serve the SPA under `https://cms.renew.example`. Mocks +reuse the board's script data: BASE, KEY, group and endpoint descriptions, the request bodies +(for example the "Choosing tiles for a small bathroom" article). Response samples are not compared; +the Response tab is never active in a board frame. -The visual-check setup for this screen serves the SPA under `https://cms.renew.example` via -Playwright request routing, so the real `window.location.origin` the running app reads matches the -board's sample text exactly; no production test hook or origin stub is needed in app code. +Pre-approved exceptions: the user pre-approved visual exceptions for Session only, Group scrolled +one card expanded, Errors and Default. For each of these four states the coder measures the real +diff, sets the smallest `maxPercent` that is sufficient, writes the reason, and records +`approvedBy` "Florian Sylvain". The other states must still pass 10 %. -- [ ] Default state: PageHeader ("Connect your website" / description), intro paragraph, Base URL - row with monospace text and a "Copy" button (icon copy), two EndpointCards in order with - their GET badge, path, description, fetch/curl/Response tabs (fetch active), JavaScript - CodeBlock and its own "Copy" button, and the three info notes below — matches the board - exactly at desktop and mobile, light and dark. Sample data for the mocked run: origin - `https://cms.renew.example`, served as above. -- [ ] Copied state: the Base URL button shows the check icon and "Copied" text, in the success - color, matching the board's `copied: true` frame. This state is timer-driven, not - date-driven, so it is not clock-sensitive and gets a real visual check, not a skip. -- [ ] Keyboard/focus (manual, not a screenshot check): Tab order reaches the Base URL "Copy" - button, then each EndpointCard's tab strip (fetch/curl/Response, reachable and operable with - arrow keys per UTabs), then that card's own CodeBlock "Copy" button, then the pre's - `tabindex="0"` for scrolling; Enter/Space activates each Copy button. -- [ ] Hover (manual): ghost Copy buttons show a hover state consistent with other ghost buttons - elsewhere in the app (UserMenu, ArticleTable row menu). +- [ ] Default (exception: owner-only chips and rate-limit line) +- [ ] Group scrolled, one card expanded (exception: Query parameters table) +- [ ] Copied: base URL (must pass 10 %) +- [ ] Copied: CodeBlock (must pass 10 %) +- [ ] Authentication (must pass 10 %; Note 1 text changed) +- [ ] Session only (exception: banner, 13th row, captions, Public auth flow block) +- [ ] Errors (exception: 409 row) +- [ ] Navigation (must pass 10 %) -## Manual verification +None of these states is clock-sensitive, so none is skipped. -- Confirm the real running app's `window.location.origin` (e.g. `https://your-domain.example` in - production, or `http://localhost:8080` under `task dev`) renders correctly in the Base URL row - and in both fetch examples, since Vitest only ever exercises a stubbed origin, and - `visual:check` only ever exercises the routed `https://cms.renew.example` origin. +## Manual +- Keyboard and focus order: index, Base URL Copy, each row (Enter/Space), tab strip (arrow keys), + CodeBlock Copy, the pre (tabindex 0), Jump to on mobile. +- Hover and motion on ghost Copy buttons and rows. +- Find-in-page and a deep link to an `#ep-...` anchor. +- Real origin in a production build and under `task dev`. +- Run each documented example against a running server with a real `rcms_` key, including the + 403 on a session-only route and the owner-only 403 for an admin key on the invite routes. This + needs a real server and is not automated. +- docs/api.md matches the catalogue (hand check). diff --git a/web/src/components/ApiFieldTable.vue b/web/src/components/ApiFieldTable.vue new file mode 100644 index 0000000..fc402fe --- /dev/null +++ b/web/src/components/ApiFieldTable.vue @@ -0,0 +1,76 @@ + + + diff --git a/web/src/components/ApiRouteRow.vue b/web/src/components/ApiRouteRow.vue new file mode 100644 index 0000000..9e5a312 --- /dev/null +++ b/web/src/components/ApiRouteRow.vue @@ -0,0 +1,24 @@ + + + diff --git a/web/src/components/CodeBlock.vue b/web/src/components/CodeBlock.vue index f654d34..2e49eb9 100644 --- a/web/src/components/CodeBlock.vue +++ b/web/src/components/CodeBlock.vue @@ -30,7 +30,7 @@ function onCopy() {