635 lines
34 KiB
Gherkin
635 lines
34 KiB
Gherkin
Feature: Sites and Authors screens
|
|
|
|
As a RenewCMS owner or admin
|
|
I want /settings/sites and /authors to list, add, edit and delete sites and authors
|
|
So that every article belongs to a frontend and credits the right bylines
|
|
|
|
Background:
|
|
Given /settings/sites is a verified route rendered as SitesView and /authors is a verified route
|
|
rendered as AuthorsView, both inside DashboardShell, per docs/design/routes.md
|
|
And both screens are built from the shared directory pieces, defined once and not redrawn:
|
|
DirectoryList, DirectoryPanel (the slide-over), ReassignDialog, SlugField and MediaField
|
|
And the screens call, with relative URLs through useApi only, GET, POST, PATCH and DELETE on
|
|
/v1/site/ and /v1/author/, PATCH /v1/site/{id}/default and GET /v1/user/
|
|
And POST /v1/author/me is not exposed in the UI by this slice
|
|
And a site has the fields id, name, key, base_url, default_social_image_id, default_robots
|
|
("index" or "noindex"), locale, is_default and article_count
|
|
And an author has the fields id, name, slug, bio, avatar_id, website, social_links, user_id,
|
|
is_linked, article_count and, for owner and admin only, user_email
|
|
And an error answered by the API is a plain-text code, and the SPA maps the code to the copy
|
|
of docs/design/states.md, never showing the code itself
|
|
And the sites fixture is, in list order:
|
|
| name | key | base_url | articles | default |
|
|
| Renew | renew | https://renew.example | 268 | yes |
|
|
| Journal | journal | https://journal.example | 38 | no |
|
|
| Projects | projects | https://projects.example | 6 | no |
|
|
| Archive | archive | https://archive.example | 0 | no |
|
|
And the authors fixture includes "Inès Duarte" (slug ines-duarte, linked, 9 articles),
|
|
"Camille Roche" (slug camille-roche, guest, 3 articles), "Léa Fontaine" (slug lea-fontaine,
|
|
guest, 0 articles) and one author with 1 article
|
|
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 screens use Nuxt UI components and design tokens only, with no raw colors
|
|
And the board frames of docs/design/source/DirectoryFrame.dc.html are the layout tiebreaker
|
|
|
|
Rule: Sites list
|
|
|
|
Scenario: Sites: default (owner)
|
|
Given the session role is "owner"
|
|
And GET /v1/site/ answers 200 with the sites fixture
|
|
When the user navigates to /settings/sites
|
|
Then the page shows "Sites", "The frontends that display your content." and a primary
|
|
"Add site" button
|
|
And the columns are "Site", "Base URL" and "Articles"
|
|
And each row shows the name with its key and the base URL
|
|
And the counts read "268 articles", "38 articles", "6 articles" and "No articles"
|
|
And "Renew" carries a "Default" badge
|
|
And a site whose default_robots is "noindex" carries a "Noindex" badge
|
|
And below md the rows are cards and a full-width "Add site" button is pinned to the bottom
|
|
|
|
Scenario: Sites: only the default site
|
|
Given GET /v1/site/ answers 200 with only the default site
|
|
When the owner navigates to /settings/sites
|
|
Then the row is listed and the hint "Add another site if your content is shown on more than one
|
|
frontend. Each article belongs to one site." appears under the list
|
|
And no empty-state wall is drawn
|
|
|
|
Scenario: Sites: loading
|
|
Given GET /v1/site/ is pending
|
|
When the owner navigates to /settings/sites
|
|
Then no skeleton shows before 150 ms
|
|
And after 150 ms skeleton rows are drawn and the list has aria-busy "true"
|
|
|
|
Scenario Outline: Sites: error on load
|
|
Given GET /v1/site/ fails with <failure>
|
|
When the owner navigates to /settings/sites
|
|
Then an alert shows "Couldn't load sites", "Check your connection and try again." and a
|
|
"Try again" action
|
|
And no rows are drawn
|
|
When the user activates "Try again"
|
|
Then GET /v1/site/ is requested again and, on 200, the list replaces the alert
|
|
|
|
Examples:
|
|
| failure |
|
|
| status 500 |
|
|
| a network error |
|
|
|
|
Scenario: Sites: row menu
|
|
Given the sites list is shown
|
|
When the user activates the ellipsis button of "Journal", or presses Enter on it
|
|
Then a menu offers "Edit", "Set as default" and "Delete…"
|
|
And below md the menu is a bottom sheet titled "Journal"
|
|
|
|
Scenario: Sites: delete unavailable on the default site
|
|
Given the sites list is shown
|
|
When the user opens the menu of "Renew"
|
|
Then "Delete…" has aria-disabled "true" and stays focusable
|
|
And a tooltip on desktop, and text in the sheet on touch, reads "The default site can't be
|
|
deleted. Make another site the default first."
|
|
And activating it opens no dialog and sends no request
|
|
|
|
Scenario: Sites: set as default, confirm and success
|
|
Given the sites list is shown
|
|
When the user chooses "Set as default" on "Journal"
|
|
Then a dialog titled "Make Journal the default site?" reads "New articles start on this site.
|
|
Articles that are already on other sites stay where they are." with "Cancel" and
|
|
"Make default"
|
|
When the user activates "Make default"
|
|
Then exactly one PATCH /v1/site/2/default is sent with no body
|
|
And the button reads "Saving…" while it runs
|
|
And on 200 the "Default" badge moves to "Journal" and a toast shows "Default site changed" and
|
|
"\"Journal\" is now the default site."
|
|
|
|
Scenario Outline: Sites: set as default, failures
|
|
Given the "Make Journal the default site?" dialog is open
|
|
When PATCH /v1/site/2/default answers <status>
|
|
Then <outcome>
|
|
|
|
Examples:
|
|
| status | outcome |
|
|
| 500 | the dialog stays open with the notice "Couldn't save the site. Your changes are still here. Try again in a moment." |
|
|
| 404 | the dialog closes, the row leaves the list and an info toast shows "That site no longer exists" and "It was deleted somewhere else, so we removed it from the list." |
|
|
| 403 | the dialog closes and an error toast shows "Only the owner can manage sites" and "Ask the owner of this site." |
|
|
|
|
Rule: Sites access
|
|
|
|
Scenario: Sites: hidden entry for an admin
|
|
Given the session role is "admin"
|
|
When the user opens /settings
|
|
Then the sidebar has no "Publishing" group and no "Sites" entry
|
|
And the page has no "Publishing" card and no "Manage sites" row
|
|
|
|
Scenario: Sites: the owner sees the entry
|
|
Given the session role is "owner"
|
|
When the user opens /settings
|
|
Then the sidebar has a "Publishing" group with a "Sites" entry linking to /settings/sites
|
|
And Settings has a "Publishing" card with a "Manage sites" row linking to /settings/sites
|
|
|
|
Scenario Outline: Sites: forbidden (403 page)
|
|
Given the session role is "<role>"
|
|
When the user opens <path>
|
|
Then the page shows "You don't have access to Sites", "Only the owner can manage sites. Ask the
|
|
owner of this site if you need a change." and a "Back to Settings" button
|
|
And the user is not redirected, no slide-over opens and the sidebar has no Sites entry
|
|
When the user activates "Back to Settings"
|
|
Then the user lands on /settings
|
|
|
|
Examples:
|
|
| role | path |
|
|
| admin | /settings/sites |
|
|
| member | /settings/sites |
|
|
| admin | /settings/sites?edit=journal |
|
|
|
|
Scenario: Sites: the API answers 403 where the SPA expected the owner
|
|
Given the session role is "owner" but the API answers 403 "Only the owner can do this."
|
|
When the answer is for a page load
|
|
Then the 403 page shows
|
|
When the answer is for an action
|
|
Then an error toast shows "Only the owner can manage sites" and "Ask the owner of this site."
|
|
|
|
Rule: Sites form
|
|
|
|
Scenario: Sites form: add, empty
|
|
Given the sites list is shown and the default site is "Renew"
|
|
When the user activates "Add site" (the page header button, or the pinned bottom button below md)
|
|
Then a slide-over titled "Add site" opens, a 92 % bottom sheet below md
|
|
And it has the fields Name (placeholder "e.g. Journal"), Key (placeholder "Generated from the
|
|
name"), Base URL (placeholder "https://blog.example.com", help "The public address of the
|
|
frontend. Use https and no path."), "Default social image (optional)" with "Choose image"
|
|
and "Used when an article has no image of its own.", "Default robots policy" with "Index"
|
|
and "Noindex" and "Search engines may list this site's pages.", "Locale (optional)"
|
|
(placeholder "e.g. en or fr-CH", help "A language tag. Leave empty if the site has no
|
|
single language."), and the switch "Make this the default site" with "New articles start
|
|
here. The current default is Renew."
|
|
And a "Canonical URL preview" is shown, plus "Cancel" and "Add site"
|
|
And Name has focus
|
|
|
|
Scenario: Sites form: key from the name
|
|
Given the Add site slide-over is open
|
|
When the user types "Journal" in Name
|
|
Then Key shows "journal" with an "Auto" badge and the help "Lowercase letters, numbers and
|
|
hyphens. Used in API paths. It can't be changed after the site is added."
|
|
When the user activates the pencil
|
|
Then Key becomes editable and no longer follows the name
|
|
|
|
Scenario Outline: Sites form: base URL not valid
|
|
Given the Add site slide-over is open with a valid name and key
|
|
When the user enters base URL "<value>" and leaves the field or submits
|
|
Then no request is sent
|
|
And the Base URL error reads "<message>"
|
|
|
|
Examples:
|
|
| value | message |
|
|
| http://blog.example.com | Use https. For example https://blog.example.com. |
|
|
| https://blog.example.com/en | Remove the path. Use only the host, like https://blog.example.com. |
|
|
| blog | Enter a full URL, like https://blog.example.com. |
|
|
|
|
Scenario: Sites form: base URL refused by the server
|
|
Given the form passed the client checks
|
|
When POST or PATCH answers 400 invalid_site_url
|
|
Then Base URL shows "Enter a full URL, like https://blog.example.com."
|
|
|
|
Scenario Outline: Sites form: loopback hosts accept http
|
|
Given the Add site slide-over is open
|
|
When the user enters base URL "<value>"
|
|
Then no base URL error is shown
|
|
|
|
Examples:
|
|
| value |
|
|
| http://localhost:5173 |
|
|
| http://127.0.0.1:8081 |
|
|
| http://[::1]:5173 |
|
|
|
|
Scenario: Sites form: add succeeds
|
|
Given the Add site slide-over is filled with name "Journal", key "journal", base URL
|
|
"https://journal.example", robots "Noindex", empty locale and the default switch off
|
|
When the user activates "Add site"
|
|
Then exactly one POST /v1/site/ is sent with name, key, base_url, default_robots "noindex",
|
|
no locale, no default_social_image_id and is_default false
|
|
And the button reads "Saving…" while the request runs and the fields are disabled
|
|
And on 201 the slide-over closes and the new site is listed
|
|
|
|
Scenario: Sites form: duplicate key
|
|
Given the Add site slide-over has key "journal" and a site with that key is in the loaded list
|
|
When the user leaves Key, or submits and POST /v1/site/ answers 409 site_key_taken
|
|
Then Key shows "A site with the key \"journal\" already exists. Choose another key." and a
|
|
suggestion "Use journal-2"
|
|
And activating the suggestion sets Key to "journal-2"
|
|
|
|
Scenario Outline: Sites form: save failed
|
|
Given the Add site slide-over is filled
|
|
When the save fails with <failure>
|
|
Then a notice in the panel reads "Couldn't save the site. Your changes are still here. Try again in a moment."
|
|
And all the values are kept and the form is enabled again
|
|
|
|
Examples:
|
|
| failure |
|
|
| status 500 |
|
|
| a network error |
|
|
|
|
Scenario: Sites form: invalid locale from the server
|
|
Given the form has locale "english"
|
|
When the save answers 400 invalid_locale
|
|
Then Locale shows "Use a language tag like en or fr-CH." and the panel stays open
|
|
|
|
Scenario: Sites form: edit the default site
|
|
When the owner chooses "Edit" on "Renew"
|
|
Then a slide-over titled "Edit Renew" opens with the fields filled
|
|
And Key shows "renew" read-only with a lock and "Used in API paths, so it can't change."
|
|
And "Default site" is on and locked with "New articles start on the default site. To change it,
|
|
make another site the default."
|
|
And the submit reads "Save changes"
|
|
|
|
Scenario: Sites form: edit another site, noindex
|
|
When the owner chooses "Edit" on "Projects" whose robots is "noindex"
|
|
Then the slide-over is titled "Edit Projects", Key is read-only and locked, "Noindex" is
|
|
selected with "Asks search engines not to list this site's pages." and the switch
|
|
"Make this the default site" is available
|
|
When the user changes the name and activates "Save changes"
|
|
Then exactly one PATCH /v1/site/3 is sent without the key field
|
|
|
|
Scenario: Sites form: make another site the default from the form
|
|
Given the "Projects" edit form has the default switch turned on
|
|
When the user activates "Save changes"
|
|
Then the SPA sends PATCH /v1/site/3 for the changed fields, then PATCH /v1/site/3/default
|
|
And is_default is never sent in the first PATCH
|
|
|
|
Scenario: Sites form: discard changes
|
|
Given a slide-over is open with a changed field
|
|
When the user closes it with "Cancel", Escape or the backdrop
|
|
Then the existing "Discard changes?" dialog is asked
|
|
And an unchanged form closes without it
|
|
|
|
Scenario: Sites form: deep link
|
|
Given the session role is "owner"
|
|
When the user opens /settings/sites?edit=projects
|
|
Then the "Edit Projects" slide-over opens
|
|
When the user opens /settings/sites?edit=nope
|
|
Then an info toast shows "That site no longer exists" and the slide-over stays closed
|
|
|
|
Rule: Sites delete
|
|
|
|
Scenario: Sites delete: site with articles
|
|
Given the sites list is shown
|
|
When the owner chooses "Delete…" on "Journal"
|
|
Then a dialog titled "Delete this site?" reads "\"Journal\" will be deleted. 38 articles belong
|
|
to it, so choose a site to move them to. This can't be undone."
|
|
And "Move articles to" defaults to the default site "Renew" shown with "renew.example"
|
|
And the text "38 articles will move to Renew. Their canonical host changes to renew.example."
|
|
is shown, with "Cancel" and "Move 38 articles and delete"
|
|
|
|
Scenario: Sites delete: choose destination
|
|
Given the "Delete this site?" dialog is open for "Journal"
|
|
When the user opens the select
|
|
Then "Journal" is not offered and each option shows the name and the host
|
|
|
|
Scenario: Sites delete: with articles succeeds
|
|
Given the dialog is open for "Journal" (id 2) with destination "Renew" (id 1)
|
|
When the user activates "Move 38 articles and delete"
|
|
Then exactly one DELETE /v1/site/2?reassign_to=1 is sent
|
|
And the button reads "Moving articles…" while it runs
|
|
And on 204 the dialog closes, the row leaves the list and a toast shows "Site deleted" and
|
|
"38 articles moved to Renew."
|
|
|
|
Scenario: Sites delete: no articles
|
|
When the owner chooses "Delete…" on "Archive"
|
|
Then a dialog titled "Delete this site?" reads "\"Archive\" will be deleted. No articles belong
|
|
to it. This can't be undone." with "Cancel" and "Delete site" and no select
|
|
When the user activates "Delete site"
|
|
Then DELETE /v1/site/4 is sent with no reassign_to and, on 204, the row leaves the list
|
|
|
|
Scenario Outline: Sites delete: failures
|
|
Given the delete dialog is open for "Journal"
|
|
When DELETE answers <status>
|
|
Then <outcome>
|
|
|
|
Examples:
|
|
| status | outcome |
|
|
| 500 or network | the dialog stays open with the notice titled "Couldn't delete the site" and text "Nothing was changed. Try again in a moment." |
|
|
| 409 site_has_articles | the dialog stays open and shows "Choose where the articles go. This site has 38 articles." |
|
|
| 403 default_site | the dialog closes and an error toast shows "The default site can't be deleted. Make another site the default first." |
|
|
| 404 | the dialog closes, the row leaves the list and the info toast "That site no longer exists" shows |
|
|
|
|
Rule: Authors list
|
|
|
|
Scenario Outline: Authors: default
|
|
Given the session role is "<role>"
|
|
And GET /v1/author/ answers 200 with the authors fixture
|
|
When the user navigates to /authors
|
|
Then the sidebar entry "Authors" sits between "Media" and "Users" and carries aria-current "page"
|
|
And the page shows "Authors", "The names readers see on your articles. An author doesn't need
|
|
an account.", a primary "Add author" button and the search field "Search by name or slug"
|
|
And the columns are "Author", "Slug", "Account" and "Articles"
|
|
And each row shows the avatar and name, the slug, a "Linked" or "Guest" badge, and
|
|
"9 articles", "1 article" or "No articles"
|
|
And a long name is truncated and shown in full on hover
|
|
And the first request is GET /v1/author/?page=1&size=25
|
|
|
|
Examples:
|
|
| role |
|
|
| owner |
|
|
| admin |
|
|
|
|
Scenario: Authors: loading
|
|
Given GET /v1/author/ is pending
|
|
When the user navigates to /authors
|
|
Then skeleton rows are drawn and the list has aria-busy "true"
|
|
|
|
Scenario Outline: Authors: error on load
|
|
Given GET /v1/author/ fails with <failure>
|
|
When the user navigates to /authors
|
|
Then an alert shows "Couldn't load authors", "Check your connection and try again." and "Try again"
|
|
When the user activates "Try again"
|
|
Then the list is requested again
|
|
|
|
Examples:
|
|
| failure |
|
|
| status 500 |
|
|
| a network error |
|
|
|
|
Scenario Outline: Authors: empty, first run
|
|
Given GET /v1/author/ answers 200 with total 0 and no search
|
|
And the session role is "<role>"
|
|
When the user navigates to /authors
|
|
Then the toolbar is hidden and the empty state shows "No authors yet" and "Authors are the names
|
|
readers see on articles. They don't need an account."
|
|
And <button>
|
|
|
|
Examples:
|
|
| role | button |
|
|
| owner | an "Add author" button is shown |
|
|
| admin | an "Add author" button is shown |
|
|
| member | no "Add author" button is shown |
|
|
|
|
Scenario: Authors: no results
|
|
Given the user has searched "zoe" and GET /v1/author/?q=zoe answers 200 with total 0
|
|
Then the state shows "No authors match \"zoe\"", "Check the spelling, or search for a shorter
|
|
part of the name." and "Clear search"
|
|
When the user activates "Clear search"
|
|
Then q leaves the URL and the full list is requested
|
|
|
|
Scenario: Authors: search and pagination in the URL
|
|
When the user types "cam" in the search field
|
|
Then after the debounce GET /v1/author/?q=cam&page=1&size=25 is sent and the URL holds q=cam
|
|
When the user opens /authors?q=camille&page=2&size=50
|
|
Then GET /v1/author/?q=camille&page=2&size=50 is sent
|
|
And the Articles paginator is shown only when total exceeds 25, with page sizes 5, 25, 50, 100
|
|
|
|
Scenario Outline: Authors: invalid page or size in the URL
|
|
When the user opens /authors?<query>
|
|
Then the invalid value is replaced by its default with no notice and the API 400 never reaches the user
|
|
|
|
Examples:
|
|
| query |
|
|
| size=30 |
|
|
| page=abc |
|
|
| page=0 |
|
|
|
|
Scenario: Authors: row menu (owner or admin)
|
|
When the user activates the ellipsis of "Camille Roche" (a guest)
|
|
Then a menu offers "Edit", "Link user" and "Delete…"
|
|
And for "Inès Duarte" (linked) the menu offers "Edit" and "Delete…"
|
|
And below md the menu is a bottom sheet
|
|
|
|
Scenario: Authors: read-only list (other roles)
|
|
Given the session role is "member"
|
|
When the user navigates to /authors
|
|
Then the description reads "The names readers see on your articles. Only an owner or admin can
|
|
change authors."
|
|
And there is no "Add author" button, no pinned bottom button, no row menu and no slide-over form
|
|
And search still works and each row shows a chevron
|
|
And the "Authors" sidebar entry is visible
|
|
|
|
Scenario: Authors: read-only detail
|
|
Given the session role is "member"
|
|
When the user opens the row "Inès Duarte"
|
|
Then a panel of the same width as the form shows "Inès Duarte", "Linked", "9 articles", "Bio",
|
|
"Website" and "Only an owner or admin can change this author."
|
|
And no field is editable, the account email is not shown and "Close" is the only action
|
|
When the user opens /authors?edit=ines-duarte
|
|
Then the same read-only detail opens
|
|
|
|
Rule: Authors form
|
|
|
|
Scenario: Authors form: add, empty
|
|
Given the session role is "owner" or "admin"
|
|
When the user activates "Add author"
|
|
Then a slide-over titled "Add author" opens, a 92 % bottom sheet below md
|
|
And it shows "Avatar (optional)" with "Choose image", Name (placeholder "e.g. Camille Roche"),
|
|
Slug (placeholder "Generated from the name"), "Bio (optional)" (placeholder "A sentence or two
|
|
about the author.") with the counter "0 / 280", "Website (optional)" (placeholder
|
|
"https://example.com"), "Social links (optional)" with "Add link" and "0 of 5", "Linked user
|
|
(optional)" showing "No linked user" with "Link a CMS user if this author has an account.
|
|
Guest writers need no link.", and a "Byline preview" ("Author name", "The bio appears here.")
|
|
And the buttons are "Cancel" and "Add author"
|
|
|
|
Scenario: Authors form: slug from the name
|
|
When the user types "Camille Roche" in Name
|
|
Then Slug shows "camille-roche" with an "Auto" badge, an "Edit slug" pencil and the help
|
|
"Generated from the name. Used in public author URLs and API paths. Unique across all authors."
|
|
And the byline preview shows the typed name
|
|
|
|
Scenario: Authors form: add succeeds
|
|
Given the form has name "Camille Roche", the automatic slug, a bio, a website and one link
|
|
When the user activates "Add author"
|
|
Then exactly one POST /v1/author/ is sent with name, slug "camille-roche", bio, avatar_id,
|
|
website, social_links [{type,url}] and user_id
|
|
And the button reads "Saving…" while it runs
|
|
And on 201 the slide-over closes and the author is listed
|
|
|
|
Scenario: Authors form: slug collision
|
|
Given the form has slug "camille-roche" and POST answers 409 author_slug_taken
|
|
Then Slug shows "The slug \"camille-roche\" is already used by another author.", a suggestion
|
|
"Use camille-roche-2" and "Reset to match the name"
|
|
And activating a suggestion rewrites the slug and keeps the other values
|
|
|
|
Scenario Outline: Authors form: validation
|
|
Given the Add author slide-over is open
|
|
When the user submits with <input>
|
|
Then no request is sent and <message> is shown under <field>
|
|
|
|
Examples:
|
|
| input | field | message |
|
|
| an empty name | Name | Enter a name. |
|
|
| a bio of 291 characters | Bio | Shorten the bio to 280 characters. with the counter "291 / 280" in error color |
|
|
| website "example.com" | Website | Enter a full URL starting with https://. |
|
|
| a social link with url "http://x" | its row | Enter a full URL starting with https://. |
|
|
|
|
Scenario Outline: Authors form: server validation codes
|
|
When the save answers <status> <code>
|
|
Then <message> is shown under <field>
|
|
|
|
Examples:
|
|
| status | code | field | message |
|
|
| 422 | bio_too_long | Bio | Shorten the bio to 280 characters. |
|
|
| 422 | invalid_url | the field | Enter a full URL starting with https://. |
|
|
| 409 | user_already_linked | Linked user | That user already has an author. Choose another user. |
|
|
|
|
Scenario: Authors form: social links
|
|
When the user activates "Add link"
|
|
Then a row with a type select (X, LinkedIn, GitHub, Mastodon, Custom) and a URL field is added
|
|
And the placeholders per type are "https://x.com/username", "https://www.linkedin.com/in/username",
|
|
"https://github.com/username", "https://mastodon.social/@username" and "https://example.com"
|
|
And an invalid link shows "Enter a full URL starting with https://."
|
|
And after the fifth row "Add link" is disabled and "You can add up to 5 links." is shown
|
|
And the links are sent as social_links with the lowercase types x, linkedin, github, mastodon, custom
|
|
|
|
Scenario: Authors form: linked user, choose
|
|
Given the options are the users of GET /v1/user/ minus every user_id found by paging
|
|
GET /v1/author/?size=100
|
|
When the user opens the Linked user select
|
|
Then under the title "Choose a user" the group "Users without an author" lists "Marta Alves"
|
|
with "marta@renew.example" and "Rui Santos" with "rui@renew.example"
|
|
And when no user is left it reads "Every user already has an author."
|
|
|
|
Scenario: Authors form: edit a linked author
|
|
When the user chooses "Edit" on "Inès Duarte"
|
|
Then the slide-over is titled "Edit Inès Duarte" with the fields filled
|
|
And Linked user shows "ines.duarte@renew.example" with "Unlink" and "New articles by this user
|
|
start with this author."
|
|
And the submit reads "Save changes"
|
|
And saving sends one PATCH /v1/author/{id} with only the changed fields
|
|
|
|
Scenario: Authors form: unlink
|
|
Given the edit form of "Inès Duarte" is open (owner or admin)
|
|
When the user activates "Unlink"
|
|
Then a dialog titled "Unlink Inès Duarte?" reads "Inès Duarte will stay on their articles. New
|
|
articles by ines.duarte@renew.example will no longer start with this author." with "Cancel"
|
|
and "Unlink user"
|
|
When the user activates "Unlink user"
|
|
Then exactly one PATCH /v1/author/{id} is sent with {"user_id": null}
|
|
And on 200 the field returns to "No linked user" and a toast shows "User unlinked" and
|
|
"Inès Duarte is now a guest author."
|
|
|
|
Scenario: Authors form: link a guest
|
|
When the user chooses "Link user" on "Camille Roche", in the row menu or the form
|
|
Then a dialog titled "Link Camille Roche to a user?" reads "Only users without an author are
|
|
listed." with the same user list, "Cancel" and "Link user"
|
|
When the user picks a user and activates "Link user"
|
|
Then exactly one PATCH /v1/author/{id} is sent with that user_id and the row shows "Linked"
|
|
|
|
Scenario: Authors form: avatar picker
|
|
When the user activates "Choose image"
|
|
Then the existing media picker opens titled "Choose an image" with "The avatar for Camille Roche."
|
|
and "Shown as a square. Choose an image with the face or logo near the middle." and "Use image"
|
|
When the user picks an image and activates "Use image"
|
|
Then the avatar shows as a square and avatar_id is sent on save
|
|
And the Sites default social image uses this same picker and sends default_social_image_id
|
|
|
|
Scenario Outline: Authors form: save failed
|
|
When the save fails with <failure>
|
|
Then a notice in the panel reads "Couldn't save the author. Your changes are still here. Try again in a moment."
|
|
And the values are kept
|
|
|
|
Examples:
|
|
| failure |
|
|
| status 500 |
|
|
| a network error |
|
|
|
|
Scenario: Authors form: role changed while open
|
|
When the save answers 403 author_rights
|
|
Then an error toast shows "Only an owner or admin can change authors" and "You can still edit
|
|
your own author profile."
|
|
|
|
Scenario: Authors form: deep link
|
|
When an owner or admin opens /authors?edit=camille-roche
|
|
Then the "Edit Camille Roche" slide-over opens
|
|
When the slug is unknown
|
|
Then an info toast shows "That author no longer exists" and nothing opens
|
|
|
|
Scenario: Authors form: own author, any role, by deep link only
|
|
Given the session role is "member" and GET /v1/auth/me carries an author with slug "ines-duarte"
|
|
When the user opens /authors?edit=ines-duarte
|
|
Then the form opens with the linked user locked, no "Unlink", no "Link user" and no delete action
|
|
And saving sends PATCH /v1/author/{id} with only the changed fields
|
|
When the same member opens /authors?edit=camille-roche
|
|
Then the read-only detail opens
|
|
|
|
Rule: Authors delete
|
|
|
|
Scenario: Authors delete: reassign (recommended)
|
|
When an owner or admin chooses "Delete…" on "Camille Roche" (id 7, 3 articles)
|
|
Then a dialog titled "Delete this author?" reads "\"Camille Roche\" will be deleted and is
|
|
credited on 3 articles. This can't be undone."
|
|
And "Credit these articles to" is preselected on the first other author, with "Where an article
|
|
has several authors, Camille Roche is removed and the others stay."
|
|
And the options come from GET /v1/author/?size=100 without the deleted author
|
|
And the buttons are "Cancel" and "Delete and reassign 3 articles"
|
|
And for a linked author the line "Linked to ines.duarte@renew.example. The user account isn't
|
|
affected." is added
|
|
|
|
Scenario: Authors delete: reassign succeeds
|
|
Given the dialog is open with destination "Inès Duarte" (id 2)
|
|
When the user activates "Delete and reassign 3 articles"
|
|
Then exactly one DELETE /v1/author/7?reassign_to=2 is sent
|
|
And the button reads "Reassigning…" while it runs
|
|
And on 204 the row leaves the list and a toast shows "Author deleted" and "3 articles now credit
|
|
Inès Duarte."
|
|
|
|
Scenario: Authors delete: choose who
|
|
When the user opens the select
|
|
Then the deleted author is not listed and the last option is "Nobody" with "Remove the byline"
|
|
|
|
Scenario: Authors delete: nobody
|
|
When the user chooses "Nobody"
|
|
Then the text "The articles keep any other authors. Articles with only this author will have no
|
|
byline." is shown
|
|
When the user confirms
|
|
Then DELETE /v1/author/7?reassign_to=none is sent
|
|
|
|
Scenario Outline: Authors delete: failed (notice shared by site and author delete)
|
|
Given an author delete dialog is open
|
|
When DELETE fails with <failure>
|
|
Then the dialog stays open with "Couldn't delete the author" and "Nothing was changed. Try again in a moment."
|
|
And the buttons are enabled again
|
|
|
|
Examples:
|
|
| failure |
|
|
| status 500 |
|
|
| a network error |
|
|
|
|
Scenario: Authors delete: server fallback (409)
|
|
When DELETE answers 409 author_has_articles
|
|
Then a dialog titled "Can't delete this author" reads "\"Camille Roche\" is credited on 3
|
|
articles. Choose another author to credit instead, or remove Camille Roche from those
|
|
articles first."
|
|
And its buttons are "Close" and "Reassign articles"
|
|
And "Reassign articles" reopens the reassign dialog
|
|
|
|
Scenario: Authors delete: no articles
|
|
When the user chooses "Delete…" on "Léa Fontaine"
|
|
Then a dialog titled "Delete this author?" reads "\"Léa Fontaine\" will be deleted. No articles
|
|
credit this author. This can't be undone." with "Cancel" and "Delete author" and no select
|
|
When the user confirms
|
|
Then DELETE /v1/author/{id} is sent with no reassign_to and the row leaves the list
|
|
|
|
Scenario: Authors delete: author already gone
|
|
When DELETE answers 404 author_not_found
|
|
Then the dialog closes, the row leaves the list and the info toast "That author no longer
|
|
exists" shows
|
|
|
|
Rule: Cross-cutting
|
|
|
|
Scenario Outline: Session problems use the app-wide handling
|
|
Given a load of <path> answers <status>
|
|
Then <outcome>
|
|
|
|
Examples:
|
|
| path | status | outcome |
|
|
| /authors | 401 | the user lands on /login?redirect=/authors&reason=expired |
|
|
| /settings/sites | 401 | the user lands on /login?redirect=/settings/sites&reason=expired |
|
|
| /authors | 403 (unverified) | the user lands on /register/pending |
|
|
|
|
Scenario: Slide-over accessibility
|
|
When a slide-over or dialog is open
|
|
Then focus moves into it, is trapped, and returns to the opening control on close
|
|
And Escape closes it unless a request is running
|
|
|
|
Scenario: Language: every string has a French entry
|
|
Given the language is "fr"
|
|
When the user opens each state of both screens
|
|
Then every visible string is the French message and none falls back to English
|
|
|
|
Scenario: Existing screens are unchanged
|
|
Then the Editor, Articles and Profile views and their specs are not modified by this slice
|