Files
RenewCMS/features/localization_editor_navigation.feature
T

363 lines
26 KiB
Gherkin

Feature: Editor language navigation, title, header actions and permission states
The article editor at /article/:id/edit opens one language version of an article. A language menu switches between the readable versions, the title is an editable field, the header shows one primary action chosen from the version's capabilities, and every control renders from capabilities[] on GET /v1/article/{id}. Unsaved work survives a session expiry and is restored for the same user and article only. SPA only: the backend contract of features/article_story_lifecycle.feature, features/article_edit_revision.feature and features/site_languages.feature is unchanged.
Background:
Given /article/:id/edit is a verified route rendered by EditorView inside DashboardShell, per docs/design/routes.md
And the visual spec is docs/design/screens/localization-editor.md (groups L5, L5b, L8) and docs/design/screens/localization-conflicts-moves.md (loc.l11.session, loc.l11.restored), with the copy drawn in docs/design/source/LocaleFrame.dc.html
And error responses are plain text from http.Error, never JSON, and the SPA never shows an HTTP status name, a revision number or an id
And GET /v1/article/{id} carries lang, story_id, capabilities[] and translations[] as in features/article_story_lifecycle.feature
And capabilities[] holds any of "read", "edit", "publish", "delete", "create_translation" and is the only input for showing or enabling an action
And translations[] lists readable versions only, default language first then by tag, each with id, lang, title, is_online, language_enabled and capabilities, and includes the version being read
And GET /v1/site/{key}/languages (readable by any signed-in user) gives the site's enabled languages and the default, and supplies the languages that have no version yet
And the UI says "article", never "story", and names the language in action copy only when the site has more than one enabled language
And site "journal" has enabled languages en (default), fr, de, ar and disabled language es
And story 5 holds article 10 (en, online, "Renovation costs"), article 12 (fr, offline, "Couts de renovation"), article 11 (de, offline, not readable to the caller) and article 13 (ar, online, "Takalif")
And "owner" holds capabilities ["read","edit","publish","delete","create_translation"] on every version
And PATCH /v1/article/{id} is sent without "if_revision" in this slice (last write wins, as today); the revision conflict dialog belongs to localization_translation_flows
Rule: The language menu
Scenario: The menu lists readable versions in API order with their state
Given the owner is on /article/12/edit
When the owner activates the language control named "Language: French. Switch version"
Then a menu opens, not a listbox, with the rows English, Arabic and French and no German version row
And German appears only as an "Add translation" row, because the owner holds create_translation
And each row shows its language name, its code and a publication badge ("Published" or "Draft")
And the row for French is marked current
And the order is English (default), Arabic, French, following translations[]
Scenario Outline: Row tags and absent rows follow each sibling's capabilities
Given the caller reads article 12 and the sibling <sibling> has capabilities <caps>
When the language menu opens
Then <result>
Examples:
| sibling | caps | result |
| Arabic (13) | ["read"] | the Arabic row is tagged "Read only" and opens that version |
| German (11) | not in translations[] | no German version row, no count, no placeholder; for a caller without create_translation no German row at all, for a caller with it only an "Add translation" row |
| English (10) | ["read","edit"] | the English row has no "Read only" tag |
| French disabled | language_enabled false | the row stays listed and opens, marked as a disabled language |
Scenario Outline: Missing languages offer Add translation only to a caller who can create
Given the site enables en, fr, de and ar and article 20 exists only in en
And the caller's capabilities on article 20 are <caps>
When the language menu opens
Then <rows>
Examples:
| caps | rows |
| ["read","edit","create_translation"] | missing languages, including languages whose version exists but is unreadable to the caller, appear as rows with an "Add translation" action |
| ["read","edit"] | the missing languages appear nowhere and nothing is offered |
Scenario Outline: Menu size
Given the site enables <count> languages and the caller reads <readable> versions
When the language menu opens
Then <layout>
Examples:
| count | readable | layout |
| 4 | 4 | all four rows are visible without a filter |
| 2 | 2 | the two rows are visible without a filter |
| many | many | the list scrolls inside the viewport and a filter field narrows it by language name |
Scenario: The menu is keyboard operable
Given the language menu is closed with focus on its control
When the user presses Enter or ArrowDown
Then the menu opens with focus on the current row
When the user presses ArrowDown and Enter
Then the next row is chosen
When the menu is open and the user presses Escape
Then it closes and focus returns to the control
Scenario: A single-language site shows no language control
Given site "archive" enables one language and article 30 belongs to it
When the owner opens /article/30/edit
Then no language control, language badge or language name appears in the header or the action labels
And the primary action reads "Publish", not "Publish French"
Rule: Switching versions opens another article id
Scenario: Switching from a clean editor
Given the owner is on /article/12/edit with no unsaved changes
When the owner chooses English in the language menu
Then GET /v1/article/10 is requested before the route changes
And on 200 the route becomes /article/10/edit, the title shows "Renovation costs" and the control reads English
And a polite announcement reads "Editing English version"
Scenario: A slow switch shows a loading state
Given GET /v1/article/10 has not resolved 150 ms after the choice
Then the editor shows the Loading version state (loc.l5.loading) and the current version stays on screen until the response arrives
When GET /v1/article/10 resolves in under 150 ms
Then no loading state is ever shown
Scenario: A failed switch keeps the current version
Given the owner is on /article/12/edit
When the owner chooses English and GET /v1/article/10 answers 500 or never reaches the server
Then a toast reads "Couldn't open the English version" with "You're still editing French. Try again."
And the route is still /article/12/edit and the title and unsaved state are untouched
Scenario: The editor opened directly on an unavailable version
Given GET /v1/article/9999 answers 404 "Article not found"
When the owner navigates to /article/9999/edit
Then the editor shows only the Version unavailable state with the title "This version isn't available" and the text "It may have been deleted, or you may not have access."
And one action "Back to Articles" navigates to /article
And no header, SaveStatus, Save, primary action, language control or editor surface renders
Scenario Outline: Choosing another version with unsaved changes asks first
Given the owner is on /article/12/edit with unsaved changes to the French title
When the owner chooses English in the language menu
Then a dialog titled "Leave without saving?" reads "Your changes to the French version aren't saved."
And it offers "Keep editing", "Discard and open English" and "Save and open English" in that order
When the owner activates <choice>
Then <outcome>
Examples:
| choice | outcome |
| Keep editing | the dialog closes, the route stays /article/12/edit, the unsaved title is intact, no request is sent |
| Discard and open English | the local French draft is removed, then the route becomes /article/10/edit and nothing is saved |
| Save and open English | PATCH /v1/article/12 is sent first and on 200 the route becomes /article/10/edit |
Scenario: Save and open English stops when the save fails
Given the "Leave without saving?" dialog is open for the French version
When the owner activates "Save and open English" and PATCH /v1/article/12 answers 500
Then the route stays /article/12/edit, the edits stay unsaved and the Save failed state of the header shows
And no request for article 10 is sent
Rule: The title is an editable field
Scenario: The title replaces the heading and the old tooltip
Given the owner opens /article/12/edit
Then the title is an input in heading size labelled "Title (French)" holding "Couts de renovation"
And the tooltip "The title can't be changed after creation." no longer exists anywhere
And on a single-language site the label is "Title"
Scenario Outline: Title field states
Given the owner is on /article/12/edit
When <action>
Then the field shows <look>
And the helper text is <helper>
And Save is <save>
Examples:
| action | look | helper | save |
| the page loads, title untouched | neutral border | none | disabled |
| the field gets focus, nothing changed | primary outline | "Changing the title doesn't change the address." | disabled |
| the user types "Couts reels" | primary border, counter "11 / 200" | "Changing the title doesn't change the address." | enabled |
| the user changes it and focus leaves | neutral border and Unsaved changes | "Changing the title doesn't change the address." | enabled |
| the user clears the field | error border and icon, aria-invalid | "Enter a title" as an alert | blocked |
| the user types "ab" | error border, counter "2 / 200" | "Use at least 3 characters" | blocked |
| the user types 203 characters | error border, counter "203 / 200" | "Use 200 characters or fewer" | blocked |
Scenario: Saving the title sends it with the other edits
Given the owner changed the title to "Couts reels" and the body
When the owner activates Save
Then PATCH /v1/article/12 is sent with the title, the body and any changed search fields in one request
And the field shows a spinner and "Saving title…" while in flight
And on 200 the header title, the breadcrumb and the language menu row all read "Couts reels"
And the slug is not touched and no slug field is sent
Scenario: A failed title save keeps the text
Given the owner changed the title to "Couts reels"
When PATCH /v1/article/12 answers 500
Then the field keeps "Couts reels" with an error border and icon
And the helper reads "Couldn't save the title. Your text is kept." with a "Try again" action
And the state is still unsaved
When the owner activates "Try again"
Then the same PATCH is sent again and focus returns to the field
Scenario: The server rejects a title the client allowed
Given the owner types a title the server answers 400 with apperr.ArticleTitleErrorMessage
Then the field shows the invalid state with that plain-text message and the edit is kept
Rule: One primary action in the header
Scenario Outline: Header priority and action matrix
Given the version is <state> and the caller's capabilities are <caps>
Then the filled primary button is <primary>
And the secondary buttons are <secondary>
And <note>
Examples:
| state | caps | primary | secondary | note |
| clean draft | edit, publish | "Publish French" | Save (disabled) | loc.l5.default |
| clean draft | edit | none | Save (disabled) | the hint "Publishing needs the publish permission" shows |
| dirty draft | edit | "Save" | none | loc.l5.dirty; the hint "Saving keeps it as a draft." shows |
| dirty draft | edit, publish | "Save and publish French" | Save | loc.l5.dirtyPublish |
| clean online | edit, publish | none | "Unpublish French", Save (disabled) | the alert "This version is online. Saving updates the live page right away." shows |
| dirty online | edit, publish | "Save" | "Unpublish French" | the same online alert shows |
| saving | any | "Saving…" busy | none | aria-busy is set on the button |
| saved | edit, publish | "Publish French" | Save (disabled) | loc.l5.saved |
| save failed | edit | "Try saving again" | none | nothing was published |
| publish failed after save | edit, publish | "Retry publish" | Save (disabled) | edits show as saved |
| publish without edit | publish | "Publish French" | none | fields are read-only |
| read-only | read | none | none | the reference action would stay; it is release 2 and absent here |
Scenario: At most one filled button
Given any combination of state and capabilities in the matrix
Then at most one button in the header is filled
Scenario: Saving an online version is allowed without publish permission
Given the version is online and the caller holds ["read","edit"]
Then Save is available, no Publish or Unpublish button renders and the online alert shows
Scenario Outline: Collapse order by width
Given the owner is on the clean online French version with edit and publish capabilities
When the viewport is <width> px wide
Then <layout>
Examples:
| width | layout |
| 1440 | title, language, publication badge, status badge, Unpublish French, Save and More actions are all visible with their labels |
| 1024 | Unpublish moves into the More actions menu; the status badge and the other badges keep their words |
| 390 | title, language, publication and dirty badges stay at the top; Save, the primary and an overflow button sit in a fixed bottom bar; Unpublish, Edit slug, Move and Delete go to the overflow sheet; every touch target is at least 44 px high |
Scenario: The overflow menu follows capabilities
Given the owner opens More actions on the French version
Then the menu holds Edit slug, Move article and Delete items, and Unpublish when the width collapsed it
And on mobile it opens as a sheet
When the caller lacks the capability for an item
Then the item is shown disabled with the reason as visible text and aria-disabled
And Edit slug, Move article and Delete article are shown disabled with the reason "Not available yet." until the slice that wires them
Scenario: Cannot delete the whole article (P6)
Given the caller can create and modify only their own versions and holds ["read","edit","publish","delete","create_translation"] on French but not on a sibling
When the caller opens More actions
Then "Delete article" and "Move article" are disabled with the reason "You can't delete this article."
And "Delete French version" is enabled
Rule: Save and publish
Scenario: Save and publish saves first, then publishes only that version
Given the owner is on the dirty French draft with edit and publish capabilities
When the owner activates "Save and publish French"
Then a dialog titled "Save and publish French?" reads "Your unsaved changes to the French version are saved first, then only the French version goes online. English and German aren't affected."
When the owner confirms
Then PATCH /v1/article/12 is sent first
And only after it answers 200 is PATCH /v1/article/12/publish sent
And no request touches articles 10, 11 or 13
And a toast reads "French published" with "Other versions weren't changed."
Scenario: A failed save publishes nothing
Given the owner confirmed "Save and publish French"
When PATCH /v1/article/12 answers 500
Then PATCH /v1/article/12/publish is never sent
And a toast reads "Couldn't save French" with "Nothing was published. Your changes are kept. Try again."
And the banner reads "Couldn't save French" with "Your changes are kept here. Nothing was published. Try saving again." and the primary reads "Try saving again"
And the edits stay unsaved
Scenario: A failed publish after a good save is not a failed save
Given PATCH /v1/article/12 answers 200
When PATCH /v1/article/12/publish answers 500
Then the banner reads "Saved as a draft. Publishing failed. Try publishing again." with "Your edits are saved. Only the publishing step failed."
And the state reads Saved, not Save failed, and the primary reads "Retry publish"
When the owner activates "Retry publish"
Then only PATCH /v1/article/12/publish is sent
Scenario: Publishing is blocked in a disabled language
Given French is disabled on journal
When PATCH /v1/article/12/publish answers 422 "language_disabled"
Then a warning reads "French is disabled on this site. Re-enable it to publish." with "You can still edit and save this version. Ask an owner to re-enable French."
And Save keeps working
Scenario Outline: Publishing a clean version
Given the owner is on the clean <version> version
When the owner activates "<button>"
Then a dialog titled "<title>" reads "<text>"
When the owner confirms
Then <request> is sent and only that version changes
Examples:
| version | button | title | text | request |
| French draft | Publish French | Publish French? | Only the French version goes online. English and German aren't affected. | PATCH /v1/article/12/publish |
| French online | Unpublish French | Unpublish French? | The French version goes back to draft and stops being public. English stays online. | PATCH /v1/article/12/unpublish |
Scenario: Unpublish never saves local edits
Given the owner is on the dirty online French version
When the owner activates "Unpublish French"
Then the dialog titled "Unpublish French?" reads "The saved French version goes offline and becomes a draft. English stays online." and an alert "Your edits stay in the editor" explains "Your unsaved changes are kept here. Nothing is saved when you unpublish. Save them yourself when you are ready."
When the owner confirms
Then only PATCH /v1/article/12/unpublish is sent, with no PATCH /v1/article/12
And the editor still holds the unsaved edits and the state is still unsaved
Scenario: Publish fails without a save
When a clean publish answers 500
Then a toast reads "Couldn't publish French" with "Nothing changed. Try again."
Rule: Permission-driven rendering
Scenario Outline: A version without edit renders read-only
Given the caller's capabilities on article 12 are <caps>
When the caller opens /article/12/edit
Then <result>
Examples:
| caps | result |
| ["read"] | a banner reads "You can read this version. You can't edit it." with "The source reference and the versions you can read stay available."; the title and body are read-only (aria-readonly); the formatting toolbar is hidden; no Save, Publish, Unpublish or Delete renders; keyboard Ctrl/⌘ S does nothing |
| ["read","publish"] | a notice reads "You can publish this version but not edit it" with "Fields are read-only. Publish stays available."; the title and body are read-only and the toolbar is hidden; "Publish French" (or Unpublish) is available |
| ["read","edit"] | a notice reads "Publishing needs the publish permission" with "Saving keeps it as a draft."; Save shows and no Publish control renders |
Scenario: The read-only menu has no Add translation row
Given the caller holds ["read"] on article 12 and cannot create on the site
When the language menu opens
Then no "Add translation" row is offered for any missing language
Scenario: Server checks still win
Given the caller's capabilities say ["read","edit"] but PATCH /v1/article/12 answers 403 "missing_permission"
Then a toast reports the failure, the edit is kept and the state is Save failed
Rule: Session expiry and local draft recovery
Scenario: A 401 on save mirrors the draft for this user and article
Given user 7 is on /article/12/edit with an unsaved title "Couts reels", body and search fields
When the owner activates Save and PATCH /v1/article/12 answers 401
Then the title, the body and the search fields are stored in this browser under user 7 and article 12 with the time of the mirror
And the app navigates to /login?redirect=/article/12/edit&reason=expired
Scenario: Restoring after login
Given user 7 stored a draft for article 12 at 14:18
When user 7 logs in again and the editor loads article 12 (GET answers 200 with older content)
Then the editor shows the stored title, body and search fields, not the loaded ones
And an info alert reads "Restored unsaved changes from 14:18" with "You logged in again. Your French title and body were kept in this browser."
And the state is unsaved
Scenario: Another account never sees the draft
Given user 7 stored a draft for article 12
When user 8 opens /article/12/edit
Then the loaded article is shown unchanged with no alert and user 7's draft is not read
Scenario: A draft is for one article
Given user 7 stored a draft for article 12
When user 7 opens /article/10/edit
Then nothing is restored
Scenario Outline: When the local draft is deleted
Given user 7 stored a draft for article 12
When <event>
Then the stored draft for user 7 and article 12 is removed
Examples:
| event |
| a save of article 12 answers 200 |
| the owner activates "Discard and open English" or "Discard changes" |
| user 7 logs out explicitly |
| GET /v1/article/12 answers 404 |
| GET /v1/article/12 returns capabilities without "edit" |
Scenario: A session expiry keeps the draft
Given a 401 redirects to /login with reason=expired
Then the stored draft is not removed by the redirect
Scenario: A stored draft grants nothing
Given a stored draft exists for article 12 and the caller can no longer read it
Then the editor shows the Version unavailable state and the draft is removed, never rendered
Rule: Existing editor behaviour that stays
Scenario: Skeleton, preview, offline and the leave guard are unchanged
Then features/editor_core.feature keeps its skeleton after 150 ms, Preview, Waiting for connection, OfflineBanner and "Leave without saving?" (Discard changes / Keep editing) when navigating away, except where this feature replaces them
Scenario: Replaced behaviour
Then "The title is never editable" in features/editor_core.feature is superseded by the title field above
And the session draft key of features/editor_core.feature scenario "Session expired mid-save" is superseded by the user and article scoped key above
And the 404-on-load copy "This article doesn't exist." is superseded by the Version unavailable copy above