feat(integration): add image variants section and standardize code snippets
This commit is contained in:
1 parent
6f9ae6207a
commit
e6746f2be7
28 files changed
+2256
-422
No files matched your search
@@ -155,7 +155,7 @@ Feature: API keys screen
|
||||
"\"Nightly media import\" is ready. Copy the key and store it somewhere safe."
|
||||
And a warning alert (UAlert, color warning, triangle-alert icon) reads "You won't be able to
|
||||
see this key again."
|
||||
And the full key from the response is shown in a monospace box with a "Copy" button (icon copy)
|
||||
And the full key from the response is shown in a CodeSnippet with the single tab "API key" and a "Copy" button (icon copy, accessible name "Copy code"), focused when the modal opens
|
||||
And the note "Closing this window discards the key from the screen. Copy it first." is shown
|
||||
And the only button is a primary "Done"
|
||||
|
||||
@@ -181,8 +181,7 @@ Feature: API keys screen
|
||||
Given the key reveal is open
|
||||
When the user activates "Copy"
|
||||
Then the full key is written to the clipboard
|
||||
And the button shows icon check and text "Copied" in the success color, announced to assistive
|
||||
technology
|
||||
And the button shows icon check and text "Copied" in the success color, announced politely
|
||||
When 2 seconds elapse
|
||||
Then the button reverts to icon copy and text "Copy"
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ layout cases at desktop and below-md (mock `matchMedia`).
|
||||
errors clear when fixed.
|
||||
9. **Create key: name too long** - 75 characters; counter, error class and text, no request.
|
||||
10. **Create key: success** - one POST with `{"name":"Nightly media import"}` and no expires_at
|
||||
key; the reveal: title, text, warning alert, key equal to the mocked `key`, "Copy", note, only
|
||||
"Done".
|
||||
key; the reveal: title, text, warning alert, key equal to the mocked `key` inside a CodeSnippet with
|
||||
the single tab "API key", "Copy" (named "Copy code") focused on open, note, only "Done".
|
||||
11. **Create key: expiry** - date D + 30 days; assert expires_at is 23:59:59 local on that day
|
||||
with the local offset. Examples today (sent), yesterday (blocked), tomorrow (sent); build the
|
||||
dates from `new Date()` in the test.
|
||||
@@ -137,7 +137,7 @@ States:
|
||||
- [ ] Create key
|
||||
- [ ] Create key: validation
|
||||
- [ ] Create key: name too long
|
||||
- [ ] One-time reveal
|
||||
- [ ] One-time reveal (re-run after the CodeSnippet migration, within 10 %)
|
||||
- [ ] One-time reveal: copied (timer-driven, so it is checked)
|
||||
- [ ] Revoke: confirm
|
||||
- [ ] Revoked (list with the revoked row and the toast)
|
||||
|
||||
@@ -7,7 +7,7 @@ Feature: Integration screen (API reference)
|
||||
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 screen makes one call, GET /v1/image-variants, for owner and admin sessions only (features/integration_images.feature)
|
||||
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
|
||||
@@ -37,7 +37,7 @@ Feature: Integration screen (API reference)
|
||||
Scenario: Default, every card collapsed
|
||||
When the user navigates to /integration
|
||||
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 a "Base URL" label sits above a CodeSnippet with the single tab "Base URL" showing window.location.origin and 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
|
||||
|
||||
@@ -70,7 +70,7 @@ Feature: Integration screen (API reference)
|
||||
| Name | Type | Required | Rule |
|
||||
| title | string | Yes | 3 to 200 characters. Can't be changed later. |
|
||||
| body | string | No | No length limit. |
|
||||
And tabs fetch, curl and Response show, fetch active, with a CodeBlock labelled "JavaScript" and a ghost "Copy" button
|
||||
And one CodeSnippet with the tabs fetch, curl and Response shows, fetch active, with a "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
|
||||
@@ -114,7 +114,7 @@ Feature: Integration screen (API reference)
|
||||
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 its CodeSnippet 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."
|
||||
@@ -139,7 +139,7 @@ Feature: Integration screen (API reference)
|
||||
| 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 a CodeSnippet with the single tab "Error response · text/plain" 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
|
||||
@@ -169,15 +169,15 @@ Feature: Integration screen (API reference)
|
||||
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 shows icon check and "Copied", announced to assistive technology
|
||||
And the button, named "Copy code", shows icon check and "Copied", announced politely
|
||||
When 2 seconds elapse
|
||||
Then the button reverts to icon copy and "Copy"
|
||||
|
||||
Scenario: Copied, CodeBlock is independent of Base URL
|
||||
Scenario: Copied, CodeSnippet 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
|
||||
When the user activates that CodeSnippet's "Copy code" button
|
||||
Then the active tab's exact code string is written to the clipboard and only that button reads "Copied"
|
||||
And the Base URL button and other CodeSnippets 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
|
||||
@@ -189,5 +189,7 @@ Feature: Integration screen (API reference)
|
||||
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
|
||||
Scenario: Only the image variants calls
|
||||
Then the only /v1 request is GET /v1/image-variants, sent for owner and admin sessions on mount, on "Try again" and on window focus
|
||||
And expanding or collapsing any card makes no /v1 request
|
||||
And a session that is neither owner nor admin makes no /v1 request
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Mount IntegrationPage inside DashboardShell with a resolved verified session and a test router at
|
||||
/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
|
||||
`navigator.clipboard.writeText` per case. Mock only GET /v1/image-variants (see integration_images.qa.md). A spec fails if any other `/v1` call
|
||||
is made.
|
||||
|
||||
Catalogue spec (pure data, no mount, imports web/src/data/apiCatalogue.ts):
|
||||
@@ -44,12 +44,12 @@ Component specs, one per design state:
|
||||
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.
|
||||
active in one CodeSnippet, 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,
|
||||
4. **Copied: CodeSnippet**: expand a card; click "Copy code". Clipboard gets the active tab's 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
|
||||
5. **Authentication**: both cards, a CodeSnippet with 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
|
||||
@@ -61,6 +61,7 @@ Component specs, one per design state:
|
||||
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.
|
||||
11. **Base URL and error sample**: both are CodeSnippets (single tabs "Base URL" and "Error response · text/plain").
|
||||
|
||||
## Visual (`task visual:check -- integration`, desktop and mobile, light and dark)
|
||||
|
||||
@@ -78,7 +79,7 @@ diff, sets the smallest `maxPercent` that is sufficient, writes the reason, and
|
||||
- [ ] 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 %)
|
||||
- [ ] Copied: CodeSnippet (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)
|
||||
@@ -88,7 +89,7 @@ None of these states is clock-sensitive, so none is skipped.
|
||||
|
||||
## 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.
|
||||
CodeSnippet 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`.
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
Feature: Integration: images and variants, and CodeSnippet as the only code block
|
||||
|
||||
As a developer building a site on RenewCMS
|
||||
I want the Integration page to explain image variants and give me copyable, correct URLs
|
||||
So that I can request the right image size without reading the source
|
||||
|
||||
Background:
|
||||
Given the spec of record is docs/design/screens/integration.md ("Integration: images and ?v="),
|
||||
docs/design/screens/integration-api-keys.md, docs/design/components.md (CodeSnippet), the boards
|
||||
docs/design/source/IntegrationImages.dc.html and CodeSnippet.dc.html, and docs/design/routes.md
|
||||
And this slice is SPA only: no Go change, and the image API is the one of features/image-variants.feature
|
||||
And /integration is a verified route rendered as IntegrationPage inside DashboardShell, per
|
||||
features/integration.feature
|
||||
And the section "Images and variants" has the anchor id "images", sits where the board draws it (after the API groups and before "Session only" and "Errors"), and has an entry "Images and variants" in the section index and the "Jump to" select
|
||||
And the section index and the "Jump to" select list the sections in page order, so the "Images and variants" entry sits right before "Session only"
|
||||
And the board and its own index disagree on this position, which is logged for the design pass
|
||||
And the section title is an h2 "Images and variants" (18/26) with sub headings h3 (14/22)
|
||||
And the section intro reads "Every image in the media library can be requested as a named variant. Add the
|
||||
?v=<variant-name> parameter to the image URL. The original file is never changed, and a variant is generated the
|
||||
first time it is requested. Variants are managed in Settings, Media, Variants."
|
||||
And the section calls, with a relative URL through useApi only, GET /v1/image-variants, an owner/admin-only route
|
||||
And an item has the fields name, width, height, fit, format, quality, generate_on_upload, enabled,
|
||||
generated_files and cache_size, and items arrive in name order
|
||||
And only items with enabled true are "enabled variants"; the order of the API response is kept
|
||||
And "first variant" below means the first enabled variant, "second" and "third" the next ones, each falling back to the first when absent
|
||||
And {origin} is window.location.origin and {image} is the builder image, "north-wall.jpg", a fixed sample with no media lookup
|
||||
And a variant URL is "{origin}/media/{image}?v={name}"
|
||||
And every visible string goes through t(...), with the English text as the key and its French entry in
|
||||
web/src/i18n/messages.ts
|
||||
And the section uses Nuxt UI components and design tokens only, with no raw colour
|
||||
And the list is read through the useImageVariantsStore
|
||||
And the board sample data is {origin} "https://cms.renew.example", image "north-wall.jpg" and these variants, in this order:
|
||||
| name | width | height | format |
|
||||
| thumb | 400 | 300 | webp |
|
||||
| card | 800 | null | webp |
|
||||
| hero | 1200 | 630 | webp |
|
||||
| square | 600 | 600 | jpeg |
|
||||
| og | 1200 | 630 | jpeg |
|
||||
| banner | 1600 | 400 | webp |
|
||||
| teaser | 640 | 360 | webp |
|
||||
| sidebar | 320 | null | webp |
|
||||
| gallery | 1000 | 750 | webp |
|
||||
| avatar | 96 | 96 | webp |
|
||||
| wide | 1920 | 800 | webp |
|
||||
| mini | 120 | 90 | webp |
|
||||
|
||||
Rule: CodeSnippet is the only code block
|
||||
|
||||
Scenario: CodeSnippet anatomy
|
||||
Given a CodeSnippet with the tabs curl, HTML and JavaScript
|
||||
Then a header row at least 44 px high holds the tab list (aria-label "Language") on the left and the Copy button on the right
|
||||
And the selected tab has text --ui-text-highlighted, weight 500 and a 2 px --ui-primary underline
|
||||
And unselected tabs have --ui-text-toned and weight 400
|
||||
And below it a pre with tabindex 0 shows the active tab's code in IBM Plex Mono 12/19, scrolls horizontally and has no syntax colouring
|
||||
And the background is --ui-bg-elevated with a --ui-border border and no corner radius
|
||||
And the Copy button is at least 44 px wide, reads "Copy" with icon copy and has the accessible name "Copy code"
|
||||
And each tab and the Copy button show a 2 px --ui-primary focus ring when focused with the keyboard
|
||||
|
||||
Scenario: A CodeSnippet with one tab shows a plain label
|
||||
Given a CodeSnippet with a single tab "Base URL"
|
||||
Then the label is plain --ui-text-muted text, not a tab, with no underline
|
||||
|
||||
Scenario: Copy writes the active tab and reads Copied for 2 s
|
||||
Given a CodeSnippet with the tabs curl, HTML and JavaScript and the JavaScript tab active
|
||||
When the user activates "Copy"
|
||||
Then the JavaScript code is written to the clipboard
|
||||
And the button shows icon check and "Copied" in --ui-success
|
||||
And the text lives in an aria-live "polite" region, so "Copied" is announced politely
|
||||
When 2 seconds elapse
|
||||
Then the button reads "Copy" with icon copy again
|
||||
|
||||
Scenario: Two CodeSnippets copy independently
|
||||
Given the page shows several CodeSnippets
|
||||
When the user activates "Copy" on one of them
|
||||
Then only that one reads "Copied"
|
||||
|
||||
Scenario Outline: Every code block of the product is a CodeSnippet
|
||||
Then <place> renders as a CodeSnippet with <tabs>
|
||||
And no CodeBlock, no shiki highlighted block and no hand-rolled copy button remains there
|
||||
Examples:
|
||||
| place | tabs |
|
||||
| Base URL on /integration (label "Base URL" above it) | one tab "Base URL" with {origin} |
|
||||
| the Authentication "API key" example | tabs curl and fetch, GET /v1/auth/me |
|
||||
| an expanded EndpointCard | tabs fetch, curl and Response, fetch active |
|
||||
| the Errors section sample | one tab "Error response · text/plain" |
|
||||
| the "Request an image" snippet | tabs curl, HTML and JavaScript |
|
||||
| the URL builder output | one tab "URL" |
|
||||
| the one-time key of ApiKeyReveal on /integration/api-keys | one tab "API key" |
|
||||
|
||||
Scenario: The old component is removed
|
||||
Then web/src/components/CodeBlock.vue no longer exists and nothing imports it, including EndpointCard.vue and IntegrationView.vue
|
||||
And web/src/components/CodeText.vue stays, because it renders inline code in prose and not a code block
|
||||
And the shiki highlighter helpers and styles used only by CodeBlock are removed when nothing else uses them
|
||||
And the hand-rolled Base URL box, the error sample pre and the key box of ApiKeyReveal are gone
|
||||
|
||||
Scenario: The one-time key reveal keeps its behaviour
|
||||
Given the ApiKeyReveal dialog opens after POST /v1/api-key answers 201
|
||||
Then the key is shown in a CodeSnippet with one tab "API key" and "Copy"
|
||||
And the Copy button receives focus when the dialog opens, as before
|
||||
And activating it writes the full key to the clipboard and reads "Copied" for 2 s, announced politely
|
||||
And the warning "You won't be able to see this key again." and the text "Closing this window discards the key from the screen. Copy it first." stay unchanged
|
||||
|
||||
Rule: The variants list is reloaded
|
||||
|
||||
Scenario: Reload triggers
|
||||
Given the session role is "owner" or "admin"
|
||||
Then GET /v1/image-variants is sent on mount
|
||||
And it is sent again on "Try again"
|
||||
And it is sent again on every window focus event while /integration is shown
|
||||
And a reload keeps the active snippet tab and the builder selection when the selected variant is still enabled
|
||||
|
||||
Scenario: A non-admin never calls the API
|
||||
Given the session role is neither "owner" nor "admin"
|
||||
Then no request to GET /v1/image-variants is sent on mount, on focus or on any other trigger
|
||||
|
||||
Rule: Section content
|
||||
|
||||
Background:
|
||||
Given the session role is "owner"
|
||||
|
||||
Scenario: I1. Many variants
|
||||
Given GET /v1/image-variants answers 200 with the twelve sample variants enabled and "print" (disabled)
|
||||
When the user opens /integration#images
|
||||
Then the h3 "Request an image" shows a CodeSnippet with tabs "curl", "HTML", "JavaScript", with HTML active
|
||||
And the h3 "Variants" shows "12 enabled variants"
|
||||
And the table has the columns Name, Dimensions, Format and Example URL and 12 rows, in API order
|
||||
And the "thumb" row reads "thumb", "400×300", "WebP", "/media/north-wall.jpg?v=thumb"
|
||||
And the "card" row reads "800 × auto"
|
||||
And the table body scrolls vertically at 264 px, because there are more than 6 rows
|
||||
And the note reads "Disabled variants are not served. A request for one returns 400 "Unknown variant"."
|
||||
And "print" is not a row
|
||||
And the h3 "URL builder" shows the Image, Variant (default "thumb"), URL (CodeSnippet) and Preview
|
||||
And the h3 "Responses" shows the responses table
|
||||
|
||||
Scenario: The disabled-variants note matches the backend
|
||||
Given a variant "print" is disabled
|
||||
When /media/north-wall.jpg?v=print is requested
|
||||
Then the backend answers 400 application/json with {"status":400,"error":"unknown_variant","message":"Unknown variant","detail":"print"} and does not fall back to the original
|
||||
And the note is shown only when the response contains at least one disabled variant
|
||||
|
||||
Scenario: The snippets use the first variants
|
||||
Given the sample variants
|
||||
Then the curl tab reads: curl -O "https://cms.renew.example/media/north-wall.jpg?v=thumb"
|
||||
And the HTML tab reads:
|
||||
"""
|
||||
<img
|
||||
src="https://cms.renew.example/media/north-wall.jpg?v=card"
|
||||
srcset="https://cms.renew.example/media/north-wall.jpg?v=thumb 400w,
|
||||
https://cms.renew.example/media/north-wall.jpg?v=card 800w,
|
||||
https://cms.renew.example/media/north-wall.jpg?v=hero 1200w"
|
||||
sizes="(min-width: 800px) 800px, 100vw"
|
||||
alt="North wall">
|
||||
"""
|
||||
And the JavaScript tab reads:
|
||||
"""
|
||||
const res = await fetch("https://cms.renew.example/media/north-wall.jpg?v=thumb")
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
const blob = await res.blob()
|
||||
"""
|
||||
And the srcset widths 400w, 800w and 1200w are sample values, not read from the variants
|
||||
|
||||
Scenario: Tables and snippet scroll on narrow screens
|
||||
Given a viewport of 390 px
|
||||
Then the variants table keeps a minimum width of 520 px and scrolls horizontally inside its border
|
||||
And the Responses table keeps a minimum width of 560 px and scrolls horizontally
|
||||
And the CodeSnippet body scrolls horizontally and the page itself does not
|
||||
|
||||
Scenario: I2. One variant
|
||||
Given GET /v1/image-variants answers 200 with only "thumb" enabled
|
||||
Then the Variants heading reads "1 enabled variant" and the table has one row without a vertical scroll limit
|
||||
And the three snippets use "thumb" for the first, second and third variant
|
||||
And the "Disabled variants are not served" note is not shown
|
||||
|
||||
Scenario: I3. No variants, admin
|
||||
Given the session role is "admin"
|
||||
And GET /v1/image-variants answers 200 with {"items": []} or only disabled variants
|
||||
Then an empty block with the layers icon shows "No image variants yet"
|
||||
And its text reads "Until you add a variant, any request with the ?v= parameter returns status 400 unknown_variant. Requests without it return the original image."
|
||||
And a primary button "Go to image variants" navigates to /settings/media/variants
|
||||
And the snippets use the placeholder <variant-name>
|
||||
And no table and no URL builder are drawn
|
||||
And the Responses table is drawn
|
||||
|
||||
Scenario: I4. No variants, not an admin
|
||||
Given the session role is neither "owner" nor "admin"
|
||||
Then no request to GET /v1/image-variants is sent
|
||||
And the empty block shows "No image variants yet"
|
||||
And its text reads "Ask an owner or admin to add one. Until you do, any request with the ?v= parameter returns status 400 unknown_variant. Requests without it return the original image."
|
||||
And there is no button and no link
|
||||
And the snippets use the placeholder <variant-name>
|
||||
|
||||
Scenario: The empty-state copy is corrected against the backend
|
||||
Then the I3 and I4 texts above replace the design copy "the ?v= parameter is not accepted and every request returns the original image", which is false
|
||||
And the backend returns 400 unknown_variant for any ?v= value when no variants exist, and serves the original only without ?v
|
||||
And the board copy is logged as wrong for the design pass, and docs/design is not edited
|
||||
|
||||
Scenario: I5. Variant removed
|
||||
Given the URL builder has "hero" selected
|
||||
And a reload of the list returns no enabled "hero"
|
||||
Then the Variant field reads "hero (removed)" and has a --ui-warning border
|
||||
And the URL snippet reads "https://cms.renew.example/media/north-wall.jpg?v=hero"
|
||||
And a warning alert (role alert, triangle-alert icon) reads the title "The variant "hero" no longer exists"
|
||||
And its text reads "There is no fallback to the original. Requests with ?v=hero return status 400 with the body { "status": 400, "error": "unknown_variant", "message": "Unknown variant", "detail": "hero" }. Remove the parameter to get the original image, or pick another variant."
|
||||
And the Preview is dimmed to 50 %, 4 / 3, and reads "No preview: unknown variant"
|
||||
|
||||
Scenario: A reload on window focus can trigger I5
|
||||
Given the builder has "hero" selected and the list was loaded with "hero" enabled
|
||||
When "hero" is disabled or deleted elsewhere and the window receives a focus event
|
||||
Then GET /v1/image-variants is sent again and I5 is shown
|
||||
|
||||
Scenario: I6. Loading
|
||||
Given GET /v1/image-variants has not answered
|
||||
Then three skeleton blocks of 140, 200 and 120 px (--ui-bg-accented) fill a region with aria-busy "true"
|
||||
And the "Request an image" snippet, the variants table, the URL builder and the Responses table are not drawn
|
||||
And the intro paragraph is drawn
|
||||
|
||||
Scenario Outline: I7. Error loading
|
||||
Given GET /v1/image-variants fails with <failure>
|
||||
Then an error alert (role alert, circle-x icon) reads "Couldn't load image variants"
|
||||
And its text reads "The snippets below still work. The variant list and the URL builder need a connection."
|
||||
And a button "Try again" is shown
|
||||
And the "Request an image" snippet is drawn with the placeholder <variant-name>
|
||||
And no table, no URL builder and no Responses table are drawn
|
||||
When the user activates "Try again"
|
||||
Then GET /v1/image-variants is sent again
|
||||
Examples:
|
||||
| failure |
|
||||
| status 500 |
|
||||
| a network failure |
|
||||
|
||||
Scenario: A 403 on the list is handled as not an admin
|
||||
Given the session role is "owner" but GET /v1/image-variants answers 403
|
||||
Then I4 is shown
|
||||
And a 401 is left to the global handler of features/api_client_global_error_handling.feature
|
||||
|
||||
Scenario: I8. Snippet copied
|
||||
Given the sample variants and the HTML tab active
|
||||
When the user activates "Copy" on the "Request an image" snippet
|
||||
Then the HTML code is written to the clipboard
|
||||
And the button reads "Copied" with icon check for 2 s, announced politely
|
||||
And the URL builder's Copy button still reads "Copy"
|
||||
|
||||
Scenario: The URL builder
|
||||
Given the sample variants
|
||||
Then the Image field is a select showing "north-wall.jpg" and the Variant field is a select listing the enabled variants
|
||||
When the user picks "square" in Variant
|
||||
Then the URL snippet reads "https://cms.renew.example/media/north-wall.jpg?v=square"
|
||||
And the Preview is a framed placeholder with aspect-ratio width / height of the variant, and 4 / 3 when the width or the height is null, shows the label "square · 600×600 · JPEG" and is announced with role img and the same aria-label
|
||||
And the Preview is at most 240 px high and full width
|
||||
And no image is requested from the server
|
||||
|
||||
Scenario: I9. URL builder copied
|
||||
Given the URL builder shows "https://cms.renew.example/media/north-wall.jpg?v=thumb"
|
||||
When the user activates "Copy" on the URL snippet
|
||||
Then that exact URL is written to the clipboard
|
||||
And the button reads "Copied" for 2 s, announced politely
|
||||
And the "Request an image" snippet's button still reads "Copy"
|
||||
|
||||
Scenario: The Responses table
|
||||
Given any state except Loading and Error loading
|
||||
Then the table has the columns Request, Status and Response and these rows:
|
||||
| request | status | response | body |
|
||||
| /media/photo.png?v=thumb | 200 | The variant image. Generated on first request. | |
|
||||
| /media/photo.png?v=unknown | 400 | Unknown variant | { "status": 400, "error": "unknown_variant", "message": "Unknown variant", "detail": "unknown" } |
|
||||
| /media/photo.png?width=400 | 400 | Unsupported parameter | { "status": 400, "error": "unsupported_parameter", "message": "Unsupported parameter", "detail": "width" } |
|
||||
| /media/missing.png?v=thumb | 404 | Not found | 404 page not found |
|
||||
| /media/huge.png?v=thumb | 413 | Source image too large | The source image is too large to generate this variant. |
|
||||
| /media/photo.png?v=thumb | 500 | Unable to read the file | Unable to read the file |
|
||||
And the body of the 400 rows is application/json and monospace --ui-text-muted below the response text
|
||||
And the 404 body is text/plain "404 page not found"
|
||||
And the 413 and 500 bodies are text/plain
|
||||
And the table has no 409 and no 422 row
|
||||
And the 413 and 500 rows are added although the board lacks them, because the handler returns them, and this is logged for the design pass
|
||||
|
||||
Scenario: The error sample tab names its content type
|
||||
Then the Errors section sample keeps its single tab "Error response · text/plain"
|
||||
And the image JSON errors are covered only by the Responses table, where their bodies are application/json
|
||||
And no snippet labelled text/plain shows an unknown_variant or unsupported_parameter body
|
||||
|
||||
Scenario: The page calls only the image variants route
|
||||
When the user opens /integration as an owner or admin
|
||||
Then GET /v1/image-variants is sent once on mount, plus once per "Try again" and once per window focus event
|
||||
And no other /v1 call is made
|
||||
And for any other role no /v1 call is made
|
||||
|
||||
Rule: The existing Integration surfaces use CodeSnippet
|
||||
|
||||
Scenario: Authentication example
|
||||
Then the "API key" card shows one CodeSnippet with tabs curl and fetch for GET /v1/auth/me, curl active, using {origin}
|
||||
And there is no outer UTabs wrapper and no label "Shell" or "JavaScript"
|
||||
|
||||
Scenario: Endpoint card examples
|
||||
When the user expands a card
|
||||
Then one CodeSnippet holds the tabs fetch, curl and Response, fetch active
|
||||
And the tab list uses the underline style, not the UTabs link style
|
||||
And the Copy button copies the active tab and its accessible name is "Copy code"
|
||||
And the "Errors" list below it is unchanged
|
||||
|
||||
Scenario: Base URL and error sample
|
||||
Then "Base URL" is a label above a CodeSnippet whose only tab is "Base URL" and whose code is {origin}
|
||||
And the Errors sample is a CodeSnippet reading "HTTP/1.1 403 Forbidden", "Content-Type: text/plain", a blank line, "This action needs a real session."
|
||||
|
||||
Scenario: Copy buttons of the Integration page are independent
|
||||
When the user copies the Base URL
|
||||
Then only the Base URL button reads "Copied"
|
||||
@@ -0,0 +1,114 @@
|
||||
# QA: Integration images and variants, and CodeSnippet as the only code block
|
||||
|
||||
SPA only. No Go test, no live server, no SMTP, no real SQLite.
|
||||
|
||||
## Automated (Vitest + Vue Test Utils, `task test:web`)
|
||||
|
||||
Common setup: mount IntegrationPage inside DashboardShell at /integration with a test router, a resolved
|
||||
verified session whose role is set per case, and `window.location.origin` stubbed to
|
||||
`https://cms.renew.example`. Mock `/v1` through the same `useApi` seam as `variantsTestKit.ts`.
|
||||
Stub `navigator.clipboard.writeText` per case and use fake timers. A spec fails on any `/v1` call
|
||||
that is not listed for the case.
|
||||
|
||||
Variants fixture: the twelve board variants of the feature plus "print" (enabled false), in the board order, with
|
||||
full item fields (fit, quality, generate_on_upload, generated_files, cache_size).
|
||||
|
||||
CodeSnippet component spec (no router, no `/v1`):
|
||||
1. Three tabs: header with tablist aria-label "Language", the first tab selected with an underline class,
|
||||
the Copy button named "Copy code".
|
||||
2. A click on a tab switches the pre content. The Copy button writes the active tab's code.
|
||||
3. One tab renders a muted label, not a role tab.
|
||||
4. Copy: clipboard called with the code, "Copied" and icon check inside an aria-live "polite" element,
|
||||
back to "Copy" after 2000 ms (advance 1999 and 2000). Two instances copy independently.
|
||||
5. The pre has tabindex "0" and no syntax-highlight markup.
|
||||
|
||||
Integration images specs, one per design state, each with the mocked `/v1` call:
|
||||
1. **Many variants** (owner): GET /v1/image-variants -> 200 with the fixture. Assert the section order (Media
|
||||
group, Images and variants, Session only, Errors), anchor "images", the section index entry, "12 enabled variants",
|
||||
12 table rows with "print" absent, the 264 px max-height class on the table body, the disabled note, exact curl,
|
||||
HTML and JavaScript strings, HTML tab active, the Responses rows and bodies.
|
||||
2. **One variant**: 200 with only "thumb" enabled. "1 enabled variant", one row, no max-height limit, the three
|
||||
snippet slots all use "thumb", no disabled note.
|
||||
3. **No variants, admin**: role admin, 200 with {"items": []}. Empty block, text, the "Go to image variants" link with
|
||||
href /settings/media/variants, placeholder <variant-name> in the snippets, no table, no builder, Responses present.
|
||||
Repeat with a fixture containing only disabled variants.
|
||||
4. **No variants, not an admin**: role "member" (or no role). Assert no GET /v1/image-variants call on mount or on a
|
||||
window focus event, the non-admin text, no link and no button. Add: role owner with a 403 answer renders the same state.
|
||||
5. **Variant removed**: load the fixture, select "hero", make the mock return a list without "hero", then dispatch
|
||||
`window.dispatchEvent(new Event('focus'))`. Assert a second GET, "hero (removed)", the warning alert title and text
|
||||
with the exact 400 body, the URL snippet ends with ?v=hero, the preview label "No preview: unknown variant", 4 / 3
|
||||
aspect and 50 % opacity. Repeat with the reload triggered by "Try again" after a failed first load.
|
||||
6. **Loading**: GET /v1/image-variants pending. Assert three skeletons inside an aria-busy region, and no snippet,
|
||||
no table, no builder, no Responses table.
|
||||
7. **Error loading**: 500, then a rejected fetch. Assert the alert copy, the snippet present with
|
||||
<variant-name>, no table and no builder, no Responses table. Click "Try again" and assert a second call
|
||||
and the success state replaces the alert.
|
||||
8. **Snippet copied**: click Copy on the "Request an image" snippet. Clipboard receives the active (HTML) code, button
|
||||
reads "Copied" in an aria-live polite region, reverts after 2 s, and the builder's button is unaffected.
|
||||
9. **URL builder copied**: the clipboard receives "https://cms.renew.example/media/north-wall.jpg?v=thumb".
|
||||
Pick "square" first and assert the URL, preview label "square · 600×600 · JPEG" and the 600 / 600 aspect ratio.
|
||||
|
||||
Behaviour specs:
|
||||
10. **Reload triggers**: mount sends one GET; each focus event sends one more; "Try again" sends one more; a reload with
|
||||
the selected variant still enabled keeps the active tab and the selection. A non-admin sends none on any trigger.
|
||||
11. **Responses table**: the two 400 bodies are the exact JSON strings of the feature; the 404 body is
|
||||
"404 page not found"; the 413 body is "The source image is too large to generate this variant." and the 500 body is
|
||||
"Unable to read the file"; no 409 or 422 row.
|
||||
12. **Error sample label**: the Errors sample tab reads "Error response · text/plain" and no snippet in the images
|
||||
section carries a text/plain label.
|
||||
|
||||
Migration specs (update or replace the existing assertions of `IntegrationView.spec.ts`, the EndpointCard spec
|
||||
and the ApiKeys specs):
|
||||
13. Base URL is a CodeSnippet with the single tab "Base URL", origin in the code, Copy -> "Copied" -> "Copy" after 2 s.
|
||||
14. Authentication example: tabs curl and fetch for GET /v1/auth/me, no "Shell" or "JavaScript" label.
|
||||
15. Expanded EndpointCard (POST /v1/article/): tabs fetch, curl, Response in one CodeSnippet, fetch active,
|
||||
Copy copies the active tab, only that button reads "Copied". The Errors list is unchanged.
|
||||
16. Errors sample is a CodeSnippet containing the four sample lines.
|
||||
17. ApiKeyReveal: the key is in a CodeSnippet with the single tab "API key", the Copy button is focused when the
|
||||
dialog opens, and Copy writes the full key and reads "Copied" for 2 s.
|
||||
18. Source guard: nothing imports `@/components/CodeBlock.vue` (EndpointCard.vue and IntegrationView.vue included),
|
||||
the file is deleted, and `CodeText.vue` still exists.
|
||||
19. The Integration page makes the calls listed in the feature for owner and admin, and none for other roles. The old
|
||||
"page stays static" assertions in `IntegrationView.spec.ts` are changed accordingly.
|
||||
20. Every new string has a French entry (the existing i18n completeness spec must pass).
|
||||
|
||||
## Visual (`task visual:check -- integration`, desktop and mobile, light and dark)
|
||||
|
||||
Every capture must be within 10 %. Group "Integration: images" (board `IntegrationImages.dc.html` through AppFrame),
|
||||
setups in `web/visual/states.ts`, with the board's sample data: origin `https://cms.renew.example`, image
|
||||
`north-wall.jpg`, the twelve variants of the feature (plus disabled "print" so the note shows in Many), role admin
|
||||
unless stated.
|
||||
|
||||
| State | Mock |
|
||||
|---|---|
|
||||
| Many variants | GET /v1/image-variants -> 12 enabled + "print" disabled, HTML tab active, builder on "thumb" |
|
||||
| One variant | -> only "thumb" |
|
||||
| No variants, admin | -> {"items": []}, role admin |
|
||||
| No variants, not an admin | role without owner/admin, no call |
|
||||
| Variant removed | builder on "hero", list without "hero" after the reload (first 5 as the board) |
|
||||
| Loading | request never resolves |
|
||||
| Error loading | status 500 |
|
||||
| Snippet copied | Many with the snippet in the Copied state (clipboard stubbed) |
|
||||
| URL builder copied | Many with the URL snippet in the Copied state |
|
||||
|
||||
Approved visual exceptions (user, own turn), each with `approvedBy` "Florian Sylvain":
|
||||
- I2 One variant, desktop light, `maxPercent` just above the measured value (about 16 %), reason "board draws the striped preview offset by 32 px".
|
||||
- I8 Snippet copied, desktop light, `maxPercent` just above the measured value (about 12 %), same reason.
|
||||
- Errors, mobile, `maxPercent` just above the measured value (about 18.5 %), reason "the board changed; already 17.33 % at HEAD". Listed as a design-pass item.
|
||||
- The `integration-api-keys` mobile failures (14 to 24 %) are NOT excepted. They predate this slice (floating inset dialog versus the board's flush bottom sheet, an overlay-pattern slice). They stay red and documented, and this slice's gate is scoped to the Integration screen.
|
||||
|
||||
Also re-run the existing groups affected by the migration and keep them within 10 %: Integration ("1. Default",
|
||||
"2. Group scrolled, one card expanded", "3a. Copied: base URL", "3b. Copied: CodeSnippet", "4. Authentication",
|
||||
"6. Errors") and API keys ("One-time reveal", "One-time reveal: copied"). The board names stay as the design gives
|
||||
them. Existing pre-approved exceptions stay as recorded. Skipped: none are clock-sensitive.
|
||||
|
||||
## Manual checks (not coverable by screenshots)
|
||||
- Keyboard: tab order inside a CodeSnippet is tabs, then Copy, then the `pre` (tabindex 0). Arrow keys move
|
||||
between tabs. Enter and Space activate Copy. The focus ring is 2 px primary.
|
||||
- A screen reader announces "Copied" politely after Copy, and not again on "Copy" returning.
|
||||
- Hover and motion: Copy and tabs hover states, no motion on copy other than the label swap.
|
||||
- Find-in-page and the index entry "Images and variants" scroll to `#images`; opening `/integration#images` lands on the section.
|
||||
- The Variants table scrolls inside its 264 px body with the mouse wheel and keyboard, and the header stays visible.
|
||||
- Real clipboard in a browser (secure context) copies the exact snippet text.
|
||||
- Against a running server: `?v=<disabled>`, `?v=<unknown>` and `?width=400` answer 400 application/json with the
|
||||
documented bodies, and a missing image answers 404 text/plain "404 page not found".
|
||||
Reference in new issue
Block a user