306 lines
20 KiB
Gherkin
306 lines
20 KiB
Gherkin
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"
|