513 lines
31 KiB
Gherkin
513 lines
31 KiB
Gherkin
Feature: SEO round, slice 4: Sites defaults, Articles search status, Integration "Search and sharing" and suggestion dismissal
|
||
|
||
As the owner of a RenewCMS install, and as anyone who edits articles or builds a frontend on the API
|
||
I want to set the site-wide SEO defaults, see which articles need attention, dismiss suggestions I do not want, and read how the SEO routes work
|
||
So that every article resolves good SEO data and a developer can find the routes that serve it
|
||
|
||
Background:
|
||
Given the screens are SitesView (/settings/sites), ArticleView (/article, also reached as /articles), IntegrationView (/integration) and the Editor SEO panel (/article/:id/edit), inside DashboardShell, per docs/design/routes.md
|
||
And the visual spec of record is docs/design/screens/sites.md ("Sites: SEO defaults", 5 states), articles.md ("Articles: search status", 3 states), integration.md ("Search and sharing"), editor.md (SuggestionList), docs/design/components.md (SeoStatusChip, SuggestionList), docs/design/seo-slices.md and docs/design/states.md ("SEO round"). Exact copy is read from those files and is not repeated here unless a scenario needs it
|
||
And the board frames of docs/design/source/DirectoryFrame.dc.html (Sites form, SuggestionList), AppFrame.dc.html (Articles rows, cards and toolbar) and IntegrationPage.dc.html (group and index) are the layout tiebreaker
|
||
And this slice is SPA only and uses the API of features/seo_data_backend.feature and features/seo_output_backend.feature, plus counts.suggestions added by features/articles_pagination_api.feature. No other Go file changes
|
||
And the SPA calls, with relative URLs through useApi only: GET /v1/site/, POST /v1/site/ and PATCH /v1/site/{id} (site_name, title_template, default_description), GET /v1/article/ with the optional seo=suggestions parameter, whose response also carries counts.suggestions, and PATCH /v1/article/{id} with seo_dismissed. The Integration page makes no request
|
||
And an article list item carries seo_status ("ready", "suggestions" or "hidden") and seo_suggestions (a count)
|
||
And the SPA never re-implements a suggestion rule or an SEO status. It draws the status and the count the list returned
|
||
And the API refuses a title_template without the exact, case-sensitive token %title% with 400 invalid_title_template, and refuses site_name above 100, title_template above 200 and default_description above 320 characters
|
||
And a new site that omits the three fields gets site_name "", title_template "%title% | %site%" and default_description ""
|
||
And the API refuses a seo_dismissed code outside the 11 known codes with 400, replaces the saved list with the list sent, collapses duplicates, and never reports or counts a dismissed code
|
||
And the sample site is Renew (id 1, default, renew.example), the sample article title is "How we price a bathroom renovation", and the sample rows are those of the board frame "Articles: search status"
|
||
And the roles are those of features/sites_authors_integration.feature: only the owner reaches the Sites form
|
||
And every visible string goes through t(...) with the English text as the key and a French entry in web/src/i18n/messages.ts
|
||
And the screens use Nuxt UI components and design tokens only, with no raw colors, and no comment is added anywhere
|
||
And a scenario tagged @not-in-design uses copy or behaviour that docs/design does not define: it takes the proposal of this feature, has a French entry, and is listed in the .qa.md to be fed back to the design
|
||
|
||
Rule: Sites form: Site name, Title template and Default description (owner only)
|
||
|
||
Scenario: Field order in the form, as the board draws it
|
||
Given the owner opens "Edit Renew"
|
||
Then the fields appear in this order: Name, Key, Base URL, Default social image, Site name, Title template (with its Example), Default description, Default robots policy, Locale, the default switch
|
||
And under the fields come the "Canonical URL preview" block and then the "How articles use these defaults" block
|
||
And the slide-over keeps its existing width, header, "Cancel" and "Save changes"
|
||
|
||
Scenario: Edit: SEO defaults
|
||
Given the site Renew has site_name "Renew", title_template "%title% | %site%" and default_description ""
|
||
When the owner opens "Edit Renew"
|
||
Then "Site name" shows "Renew" with "Shown in titles through the template, and sent as the Open Graph site name."
|
||
And "Site name" has no "(optional)" label and an empty value is allowed
|
||
And "Title template" shows "%title% | %site%" in the mono face with "Use %title% for the article title and %site% for the site name."
|
||
And the "Example" row reads "How we price a bathroom renovation | Renew"
|
||
And "Default description" is marked optional, shows the placeholder "One or two sentences about the site." and the counter "0 / 320"
|
||
And the helper reads "Used when an article has no description, excerpt or body text."
|
||
And the block "How articles use these defaults" lists Title "Article title, then the template.", Description "Used last, after the article's description, excerpt and body.", Image "Used when an article has no cover image." and Robots "Used unless an article is hidden from search engines."
|
||
|
||
Scenario: Add: defaults
|
||
When the owner opens "Add site"
|
||
Then "Site name" is empty with the placeholder "e.g. Renew"
|
||
And "Title template" holds "%title% | %site%"
|
||
And "Default description" is empty with the placeholder "One or two sentences about the site."
|
||
And the "How articles use these defaults" block is shown
|
||
|
||
Scenario Outline: The live example follows the typing
|
||
Given the form is open and "Site name" holds "<site_name>" and "Name" holds "<name>"
|
||
When the owner types "<template>" in "Title template"
|
||
Then the "Example" row reads "<example>"
|
||
And no request is sent
|
||
|
||
Examples:
|
||
| name | site_name | template | example |
|
||
| Journal | Journal | %title% – %site% | How we price a bathroom renovation – Journal |
|
||
| Journal | | %title% – %site% | How we price a bathroom renovation – Journal |
|
||
| Journal | Le Journal | %site%: %title% | Le Journal: How we price a bathroom renovation |
|
||
| Journal | Journal | %title% | How we price a bathroom renovation |
|
||
| Journal | Journal | %site% / %site% | Journal / Journal |
|
||
|
||
@not-in-design
|
||
Scenario: The example uses the site name, then the name, then the placeholder
|
||
Given "Site name" and "Name" are both empty
|
||
Then the Example row uses "Renew" as the site name
|
||
And when "Site name" is empty and "Name" is "Journal", it uses "Journal", as the API does
|
||
|
||
Scenario Outline: Template: not valid
|
||
Given the owner typed "<template>" in "Title template"
|
||
When the field loses focus or the owner activates "Save changes"
|
||
Then the field shows the error "The template must contain %title%." and the Example row is not shown
|
||
And nothing is sent, the slide-over stays open and focus goes to the first invalid field
|
||
|
||
Examples:
|
||
| template |
|
||
| just text |
|
||
| %site% |
|
||
| %TITLE% |
|
||
| |
|
||
|
||
Scenario: A valid template clears the error
|
||
Given "Title template" shows "The template must contain %title%."
|
||
When the owner types "%title%"
|
||
Then the error is gone and the Example row is shown again
|
||
|
||
Scenario: The API refuses a template the form accepted
|
||
When PATCH /v1/site/1 answers 400 "invalid_title_template"
|
||
Then "Title template" shows "The template must contain %title%." and the values are kept
|
||
|
||
Scenario Outline: The inputs stop at the API limits
|
||
Given the owner types a long text in <field>
|
||
Then the input stops accepting characters at <limit>, so the API 400 for those fields is never produced by typing
|
||
|
||
Examples:
|
||
| field | limit |
|
||
| Site name | 100 |
|
||
| Title template | 200 |
|
||
| Default description | 320 |
|
||
|
||
Scenario Outline: The description counter
|
||
When the owner types <n> characters in "Default description"
|
||
Then the counter reads "<n> / 320" and nothing is blocked
|
||
|
||
Examples:
|
||
| n |
|
||
| 0 |
|
||
| 45 |
|
||
| 320 |
|
||
|
||
Scenario: Saving only sends what changed
|
||
Given the owner opened "Edit Renew" and changed "Title template" to "%title% – %site%" and nothing else
|
||
When the owner activates "Save changes"
|
||
Then exactly one PATCH /v1/site/1 is sent containing title_template "%title% – %site%" only
|
||
And on 200 the slide-over closes and the existing saved behaviour of the Sites list applies
|
||
|
||
Scenario: Several SEO fields at once
|
||
Given the owner changed "Site name" to "Renew Journal", "Default description" to "Notes on renewal work." and "Default robots policy" to "Noindex"
|
||
When the owner activates "Save changes"
|
||
Then exactly one PATCH /v1/site/1 is sent containing site_name, default_description and default_robots
|
||
And values are compared and sent trimmed, as the existing Name field is
|
||
|
||
Scenario: Creating a site with the defaults untouched
|
||
Given the owner filled Name, Key and Base URL and left the three SEO fields as they were
|
||
When the owner activates "Add site"
|
||
Then POST /v1/site/ is sent without site_name, title_template or default_description
|
||
And the created site has the API defaults
|
||
|
||
Scenario: Creating a site with SEO values
|
||
Given the owner typed "Renew Shop" as site name, "%title% - %site%" as template and "A shop about renewal." as description
|
||
When the owner activates "Add site"
|
||
Then POST /v1/site/ holds site_name, title_template and default_description with those values
|
||
|
||
Scenario: A site without the new fields
|
||
Given GET /v1/site/ returned a site with no site_name, title_template or default_description
|
||
When the owner opens its edit form
|
||
Then "Site name" and "Default description" are empty and "Title template" holds "%title% | %site%"
|
||
And "Save changes" with no other change sends no request
|
||
|
||
Scenario: The new fields count as changes
|
||
Given the owner typed in any of the three fields
|
||
When the owner closes the slide-over
|
||
Then the existing "Discard changes?" dialog is shown
|
||
|
||
Scenario Outline: Save failed
|
||
When the save fails with <failure>
|
||
Then the slide-over stays open with the notice "Couldn't save the site. Your changes are still here. Try again in a moment."
|
||
And the values of the three fields are kept
|
||
|
||
Examples:
|
||
| failure |
|
||
| status 500 |
|
||
| a network error |
|
||
|
||
Scenario: Owner only, as on the rest of Sites
|
||
Given the session role is "admin" or "verified"
|
||
Then there is no Sites entry and no form
|
||
When the user opens /settings/sites or /settings/sites?edit=renew from a bookmark
|
||
Then the existing 403 page "You don't have access to Sites" is shown, with nothing about SEO added
|
||
|
||
Scenario: A 403 owner_only on save is handled like any Sites save
|
||
Given the owner's role changed while the form was open
|
||
When PATCH /v1/site/1 answers 403 owner_only
|
||
Then the existing action toast "Only the owner can manage sites" with "Ask the owner of this site." is shown, as for the other Sites fields
|
||
|
||
Scenario: A 401 sends the user to login
|
||
When PATCH /v1/site/1 answers 401
|
||
Then the user lands on /login?redirect=/settings/sites&reason=expired, as on the rest of Sites
|
||
|
||
Rule: Articles list: the search status chip (SeoStatusChip)
|
||
|
||
Scenario Outline: The chip follows the status the list returns
|
||
Given an article item with seo_status "<status>" and seo_suggestions <count>
|
||
Then the Status cell shows <cell>
|
||
|
||
Examples:
|
||
| status | count | cell |
|
||
| ready | 0 | its Online or Draft badge and no SEO chip |
|
||
| suggestions | 2 | its badge and a warning-soft chip with a lightbulb icon and "2" |
|
||
| suggestions | 1 | its badge and a warning-soft chip with a lightbulb icon and "1" |
|
||
| hidden | 0 | its badge and an outline chip with an eye-off icon and "Hidden" |
|
||
| hidden | 2 | its badge and an outline chip with an eye-off icon and "Hidden", no count |
|
||
|
||
Scenario: Chip placement on desktop
|
||
Then the chip sits in the Status cell, in the same row as the Online or Draft badge, 6 px after it, 22 px high, with a 12 px icon
|
||
And the Status cell is wide enough that the chip never wraps below the badge in the board's 1440 px frame
|
||
|
||
Scenario: Chip placement on mobile cards
|
||
Given the viewport is 390 px wide
|
||
Then the chip sits in the card's meta row under the title, after the Online or Draft badge and before "Updated …", 20 px high
|
||
And the card has no other new element
|
||
|
||
Scenario Outline: Tooltips and accessible names
|
||
Given a chip with <status> and <count>
|
||
Then its tooltip and its accessible name read "<text>"
|
||
|
||
Examples:
|
||
| status | count | text |
|
||
| suggestions | 1 | 1 suggestion |
|
||
| suggestions | 2 | 2 suggestions |
|
||
| suggestions | 3 | 3 suggestions |
|
||
| hidden | 0 | Hidden from search engines |
|
||
|
||
@not-in-design
|
||
Scenario: A touch device has no hover, so the name is read
|
||
Given the viewport is 390 px wide
|
||
Then the chip carries its text for assistive technology, and the visible mobile chip is the same as on desktop
|
||
|
||
@not-in-design
|
||
Scenario Outline: Items the chip cannot draw show nothing
|
||
Given an article item <item>
|
||
Then no SEO chip is shown and nothing breaks
|
||
|
||
Examples:
|
||
| item |
|
||
| without seo_status |
|
||
| with seo_status "suggestions" and seo_suggestions 0 |
|
||
| with an unknown seo_status |
|
||
|
||
@not-in-design
|
||
Scenario: A row action keeps the chip
|
||
Given a row with a chip
|
||
When the user publishes or unpublishes it and the PATCH answers 200
|
||
Then the row updates in place and the chip is unchanged
|
||
And the chip is refreshed by the next list load
|
||
|
||
Scenario: The chip changes nothing else in the row
|
||
Then the sort, the row menu, the Authors and Site columns, the busy state and the delete flow behave as before
|
||
|
||
Rule: Articles list: the "Needs attention" toggle
|
||
|
||
Scenario: The toggle sits next to the search field
|
||
Then on desktop a 32 px "Needs attention" switch with a lightbulb icon and a count sits directly left of the search field, on the toolbar baseline
|
||
And on mobile a 44 px "Needs attention" switch with the same content is part of the toolbar stack, above the Site filter when it exists
|
||
And off, it has the default background, an accented border and the toned text colour
|
||
And on, it uses --rc-sand as fill and border, --rc-on-sand as text and weight 500
|
||
And it is a switch: role "switch" with aria-checked, reachable by Tab, toggled by Space and Enter
|
||
|
||
Scenario: Turning it on
|
||
Given the URL is /articles?page=3&size=50&q=plaster&status=online&sort=title:asc&site=journal
|
||
When the user turns "Needs attention" on
|
||
Then the URL becomes /articles?size=50&q=plaster&status=online&sort=title:asc&site=journal&seo=suggestions with page omitted
|
||
And GET /v1/article/ is requested with seo=suggestions, size 50, q "plaster", status online, sort title:asc, site journal and page 1
|
||
And the tab counts are unchanged
|
||
|
||
Scenario: Turning it off
|
||
Given the URL holds ?seo=suggestions&page=2
|
||
When the user turns the switch off
|
||
Then seo leaves the URL, the page resets to 1, and q, status, sort, site and size are kept
|
||
And GET /v1/article/ is requested without seo
|
||
|
||
Scenario Outline: Other changes keep the filter
|
||
Given the URL holds ?seo=suggestions
|
||
When the user <action>
|
||
Then ?seo=suggestions stays in the URL
|
||
|
||
Examples:
|
||
| action |
|
||
| searches for "plaster" |
|
||
| changes the tab |
|
||
| changes the sort |
|
||
| chooses a site |
|
||
| goes to page 2 |
|
||
| changes the page size |
|
||
|
||
Scenario Outline: Changes made while the filter is on reset the page, as everywhere else
|
||
Given the URL holds ?seo=suggestions&page=2
|
||
When the user <action>
|
||
Then the page resets to 1 and ?seo=suggestions stays
|
||
|
||
Examples:
|
||
| action |
|
||
| searches for "plaster" |
|
||
| changes the tab |
|
||
| changes the sort |
|
||
| chooses a site |
|
||
| changes the page size |
|
||
|
||
Scenario: Opening a URL with the filter
|
||
When the user opens /articles?seo=suggestions&page=2
|
||
Then the switch is on and GET /v1/article/?seo=suggestions&page=2 is requested
|
||
|
||
Scenario Outline: Unknown seo values are dropped silently
|
||
Given the URL holds <query>
|
||
Then seo is removed from the URL with no notice, the switch is off and the unfiltered list is requested
|
||
|
||
Examples:
|
||
| query |
|
||
| ?seo=maybe |
|
||
| ?seo= |
|
||
| ?seo=READY |
|
||
| ?seo=ready |
|
||
| ?seo=hidden |
|
||
| ?seo=suggestions,x |
|
||
|
||
Scenario: Needs attention on
|
||
Given the filter is on and the response holds the three articles with suggestions
|
||
Then the list shows only those three articles, each with its chip
|
||
And hidden-only articles are not in the list, because the API does not return them for seo=suggestions
|
||
And the tab counts still show the full counts of the unfiltered list
|
||
|
||
Scenario: Same behaviour on a single-site install
|
||
Given GET /v1/site/ returned one site
|
||
Then the Site column and the Site filter are not rendered
|
||
And the switch, the chips and ?seo=suggestions behave exactly as with several sites
|
||
|
||
Scenario Outline: The count on the switch
|
||
Given the switch is on or off
|
||
When the list response <answer>
|
||
Then the switch reads <label>
|
||
|
||
Examples:
|
||
| answer | label |
|
||
| holds counts.suggestions 3 | "Needs attention" with the count "3" in the mono face |
|
||
| holds counts.suggestions 0 | "Needs attention" with the count "0" |
|
||
| has no counts.suggestions | "Needs attention" with no count |
|
||
| has not arrived yet (first load) | "Needs attention" with no count |
|
||
| failed with status 500 or a network error | "Needs attention" with no count and no extra notice |
|
||
|
||
@not-in-design
|
||
Scenario: Where the count comes from
|
||
Given the switch is on or off
|
||
Then the count is counts.suggestions of the latest successful GET /v1/article/ response
|
||
And no request with seo=suggestions and size=5 is sent for the count
|
||
And the count does not change when only q, tab, site, sort, page or size change, because counts.suggestions is global like the tab counts
|
||
|
||
@not-in-design
|
||
Scenario: The count survives a page or search change that is still loading
|
||
Given the count "3" was shown from an earlier response
|
||
When a new list request is in flight
|
||
Then the switch keeps showing "3"
|
||
|
||
Scenario: Needs attention: none
|
||
Given ?seo=suggestions, no search, the tab "All", and the response is empty
|
||
Then the state shows "Nothing needs attention", "Every article has a clean check. Suggestions are optional, so this is just a bonus." and "Show all articles"
|
||
And the paginator and summary are hidden
|
||
When the user activates "Show all articles"
|
||
Then seo leaves the URL, the page resets to 1, the other query values are kept and the list is requested without seo
|
||
|
||
@not-in-design
|
||
Scenario Outline: Zero results with the filter and a search or another tab
|
||
Given ?seo=suggestions and <condition>
|
||
And the response is empty
|
||
Then the existing <state> is shown and the filter stays on
|
||
|
||
Examples:
|
||
| condition | state |
|
||
| a search for "tiles" | "No results" state |
|
||
| the tab "Draft" | "No drafts" state |
|
||
| the tab "Online" | "Nothing is online yet" state |
|
||
|
||
@not-in-design
|
||
Scenario: Zero results with the filter and a site
|
||
Given ?seo=suggestions&site=archive, no search, the tab "All" and the response is empty
|
||
Then "Nothing needs attention" is shown rather than "No articles on "Archive""
|
||
|
||
Scenario: Existing states keep working with the filter
|
||
Then Loading, Empty, row actions, delete, the Site filter and pagination behave as in docs/design/screens/articles.md with the filter applied
|
||
And an error loading a page keeps the previous rows and shows its Retry notice as before
|
||
|
||
Rule: Editor SEO panel: dismissing a suggestion (deferred from slice 3)
|
||
|
||
Scenario: The dismiss column and the footnote, as the board draws them
|
||
Given the SEO panel is open and the check answers title_long and cover_alt
|
||
Then each suggestion row ends with a dismiss "×" button, as in the board frame
|
||
And under the list the footnote reads "Suggestions never block saving or publishing. Dismiss one and it stays hidden for this article."
|
||
And the compact suggestion list of the open section keeps no dismiss button and no footnote, as drawn
|
||
|
||
Scenario: Accessible name and keyboard
|
||
Given the dismiss button of the row "The meta title is 74 characters. Search engines may cut it off after about 60."
|
||
Then its accessible name reads "Dismiss suggestion: The meta title is 74 characters. Search engines may cut it off after about 60." and it has a visible 2 px focus ring
|
||
And Tab reaches it after the row's "Go to meta title" link, and Enter or Space activates it
|
||
And the name has a French entry with the message interpolated
|
||
|
||
Scenario: Dismiss succeeds
|
||
Given the article saved seo_dismissed is ["no_au"] and the check answers title_long, cover_alt and no_au was already filtered out
|
||
When the user activates the dismiss button of title_long
|
||
Then exactly one PATCH /v1/article/12 is sent containing seo_dismissed ["no_au", "title_long"] and no other field
|
||
And the row stays and the button is disabled with a busy state until the PATCH answers
|
||
And the unsaved draft, the panel values and the editor status are not touched by the request
|
||
|
||
Scenario: Row removal and count update
|
||
When the PATCH answers 200
|
||
Then the title_long row leaves the list and the remaining rows keep the order of the check
|
||
And one new POST /v1/article/12/seo/check is sent so that the list, the section summary line and the article's status come from the server
|
||
And the summary reads "1 suggestion" when one suggestion remains, and "Ready" with "All good" when none remains
|
||
And the "Articles" chip count of this article is the one of the next list load
|
||
|
||
Scenario: Dismissing the last suggestion
|
||
Given one suggestion is left
|
||
When it is dismissed and the PATCH answers 200
|
||
Then "All good" is shown with "{n} dismissed suggestions are hidden." (the board's copy, with the French plural forms) and the link "Show dismissed"
|
||
And focus moves to the "Show dismissed" link
|
||
|
||
Scenario Outline: Focus after a row leaves
|
||
Given the list holds three rows and the user dismisses the <position> one
|
||
Then focus moves to the dismiss button of <target>
|
||
|
||
Examples:
|
||
| position | target |
|
||
| first | the new first row |
|
||
| second | the row that took its place |
|
||
| last | the new last row |
|
||
|
||
Scenario Outline: Dismiss failed
|
||
When the PATCH fails with <failure>
|
||
Then the row stays, the button is enabled again and an error toast shows "Couldn't dismiss the suggestion" with "Try again in a moment."
|
||
And the panel, the draft and the editor status are unchanged and no check is sent
|
||
|
||
Examples:
|
||
| failure |
|
||
| status 500 |
|
||
| a network error |
|
||
|
||
Scenario Outline: A session problem during the dismissal follows the existing rules
|
||
When the PATCH answers <status>
|
||
Then <result>
|
||
|
||
Examples:
|
||
| status | result |
|
||
| 401 | the user lands on /login?redirect=/article/12/edit&reason=expired, the draft is kept |
|
||
| 404 | the "Not found" editor state is shown |
|
||
|
||
@not-in-design
|
||
Scenario: Show dismissed
|
||
Given the article has seo_dismissed ["no_au", "short"]
|
||
Then "All good" or the list footer shows "2 dismissed suggestions are hidden." and "Show dismissed"
|
||
When the user activates "Show dismissed"
|
||
Then exactly one PATCH /v1/article/12 is sent with seo_dismissed [] and, on 200, a new check is sent and the restored suggestions appear
|
||
|
||
Scenario: Dismissal is per article and never blocks anything
|
||
Then a dismissed code stays hidden for this article only and for every later check of it
|
||
And Save, "Save and publish", Publish and Ctrl or Cmd S stay enabled whatever the dismissed list is
|
||
|
||
Scenario: Who can dismiss
|
||
Given the session role is "owner", "admin" or "verified"
|
||
Then the dismiss button is available, since anyone who can edit the article edits its SEO fields
|
||
|
||
Rule: Integration: the "Search and sharing" group
|
||
|
||
Scenario: The group sits after Articles
|
||
Then web/src/data/apiCatalogue.ts has a group "Search and sharing" (key "seo") placed directly after "Articles" in API_GROUPS
|
||
And the group description reads "RenewCMS doesn't render pages or add meta tags. These endpoints give your frontend the resolved SEO data and the list of published URLs."
|
||
And the group shows "2 routes"
|
||
|
||
Scenario: The two cards
|
||
Then the group holds GET /v1/article/{id}/seo and GET /v1/site/{key}/sitemap, both with the badge "Public" and "API key OK", collapsed by default
|
||
And GET /v1/article/{id}/seo is removed from the "Articles" group and GET /v1/site/{key}/sitemap is removed from the "Sites" group, so no route appears twice
|
||
And POST /v1/article/{id}/seo/check stays in "Articles"
|
||
|
||
Scenario: Article SEO card
|
||
When the user expands GET /v1/article/{id}/seo
|
||
Then the card describes the resolved SEO object: title with the site template applied, description, canonical_url, robots, og{type,title,description,url,site_name,image,image_alt,locale}, twitter{card,title,description,image,image_alt}, image{url,alt,source}, authors[{name,url,same_as}], published_at, modified_at, fallbacks and json_ld
|
||
And it says that drafts answer 404 to anonymous callers
|
||
And it shows "No request body.", the fetch, curl and Response tabs in one CodeSnippet with Copy, and the error rows 400, 401 and 404 of the real API
|
||
|
||
Scenario: Sitemap card
|
||
When the user expands GET /v1/site/{key}/sitemap
|
||
Then the card shows the query rows page ("A whole number from 1. Default 1.") and size ("25, 50, 100 or 500. Default 100."), the item shape { url, modified_at, noindex } and that the total is in the X-Total-Count header
|
||
And the path in its examples uses the sample site key
|
||
And it shows the error rows of the real API: 400 with the body-mismatch message, 401, and 404 "site_not_found"
|
||
|
||
Scenario: Sidebar index and Jump to select
|
||
Then the sticky section index lists, in this order, Authentication, Articles, Search and sharing, Media, Users and invites, Settings, Sites, Authors, Images and variants, Session only, Errors, Notes
|
||
And the mobile "Jump to" select lists the same options in the same order
|
||
When the user activates "Search and sharing" in the index
|
||
Then the page scrolls to the group and the item has aria-current while it is in view
|
||
And the group has an anchor and a deep link #seo
|
||
|
||
Scenario: Counts stay in sync
|
||
Then the cards per group are Articles 8, Search and sharing 2, Media 10, Users and invites 6, Settings 1
|
||
And the group "Sites" shows "1 route" and its description no longer mentions the sitemap
|
||
And the total number of endpoint cards in API_GROUPS is unchanged
|
||
And web/src/data/__tests__/apiCatalogue.spec.ts and web/src/views/__tests__/IntegrationView.spec.ts use these numbers and the new section list
|
||
|
||
Scenario: The PATCH reference lists the new fields
|
||
Then PATCH /v1/article/{id} lists the optional fields slug, excerpt, seo_title, seo_description, cover_image_id, cover_image_alt, social_title, social_description, social_image_id, canonical_url, noindex and seo_dismissed, with the rules of the API
|
||
And that is asserted by apiCatalogue.spec.ts and no field is added twice
|
||
|
||
Scenario: The 403 row of the status table
|
||
Then the 403 row of the Errors table also states, in a note under its messages, that site SEO defaults are owner only and that article SEO fields need only the right to edit the article
|
||
And the plain-text messages of that row are unchanged
|
||
|
||
Scenario: Nothing changes for the other groups
|
||
Then the Session only group, the Errors table, the Images and variants section, the Notes and the sample error response are as before, apart from the 403 note
|
||
|
||
Scenario: The page still makes no request
|
||
Then opening and expanding the new cards sends no /v1 request
|
||
|
||
Rule: French, layering and visual conformance
|
||
|
||
Scenario: French
|
||
Given the language is "fr"
|
||
Then every new string of the four screens, including "Needs attention", "Hidden", the tooltips, "Nothing needs attention", the Sites form copy, the dismiss button name, footnote and toast, the new group title, description, card descriptions and query rows, the 403 note, and the @not-in-design copy, is the French message
|
||
And a missing French entry fails the i18n spec
|
||
|
||
Scenario: French plurals of the tooltip
|
||
Given the language is "fr"
|
||
Then a chip with 1 suggestion has the tooltip "1 suggestion" and with 2 or more "{n} suggestions" in French
|
||
|
||
Scenario: Every call goes through useApi
|
||
Then no component calls fetch directly and every URL is relative
|
||
|
||
Scenario: Slice 3 leftovers
|
||
Then a grep of web/src for hard-coded English in templates and for t(...) keys without a French entry finds none, or every finding is reported in the qa report with its file and line
|
||
|
||
Scenario: Visual conformance
|
||
Then `task visual:check -- articles`, `-- sites`, `-- integration`, `-- editor`, `-- settings`, `-- users`, `-- profile` and `-- home` pass every frame at 1440x900 and 390x844, in light and dark
|
||
And that includes the 5 new "Sites: SEO defaults" frames, the 3 new "Articles: search status" frames and every existing frame of those screens
|
||
And the slice 3 mobile exceptions for the dismiss column are removed from web/visual/states.ts and the Editor panel frames pass without any exception
|
||
And no per-state exception is added; a failure is reported to the user instead
|