470 lines
32 KiB
Gherkin
470 lines
32 KiB
Gherkin
Feature: Entry editor of a custom content type
|
||
As a verified user
|
||
I want to create, edit and delete an entry of a custom content type
|
||
So that the content follows the fields of its type
|
||
|
||
Background:
|
||
Given the SPA calls the API with relative URLs under /v1 and the "jwt" HttpOnly cookie
|
||
And API errors are plain text
|
||
And the routes are /content/:key/new and /content/:key/:id with meta auth "verified" and title "Content", per docs/design/screens/entry.md group "6 Entry editor", states 1 to 13
|
||
And the structural prep of this slice is done first: ContentEntries is presentational with one owner of the list-view state, useContentTypes is split into entry and user-lookup parts, and one field-type registry (lib/fieldTypes) serves the schema builder, the list cells and this form
|
||
And the page requests GET /v1/content-type/{key} (the type) and, on /content/:key/:id, GET /v1/content-type/{key}/entry/{id} at the same time
|
||
And an entry is {id, content_type, data, created_by, updated_by, created_at, updated_at}, with content_type the key string
|
||
And POST /v1/content-type/{key}/entry answers 201 with the entry, PUT .../entry/{id} answers 200 with the entry and DELETE answers 204 with no body
|
||
And POST and PUT take {"data": {...}}; PUT replaces the whole entry and keeps created_by and created_at
|
||
And the API refuses with 404 content_type_not_found, 404 entry_not_found, 400 for an id that is not a uint32, 401, 403, 409 "invalid_entry: <key>: unique" and 422 "invalid_entry: <key>: <required|type|min|max|options|unknown_field>", and the body names only one field per answer
|
||
And any verified user may read and write entries; there is no owner or admin gate in the SPA
|
||
And a "/content/:key/:id" whose id is not made of digits or does not fit a uint32 shows "Entry not found" with no entry request, and "new" is the literal route
|
||
And the form is generated from the type's fields in schema order through the fieldTypes registry, with the six types text, richtext, number, boolean, date and select
|
||
And the data sent holds exactly one key per field of the loaded type, with the value or null for an empty optional field, so no default is applied by the server to a field the user cleared and no unknown key is ever sent
|
||
And a text or richtext value is counted in characters (code points) like the server, and is not trimmed
|
||
And a required field is empty when it is null, undefined or an empty string; false and 0 are values
|
||
And validation is done by the client against the loaded type before any request (required, type, min, max, options), one rule per field at a time in that order, and the server's single answer is mapped to its field when it gets past the client
|
||
And "Created by" and "Updated by" names come from GET /v1/user/lookup?ids=<distinct created_by and updated_by> sent once after the entry answers, as in features/content_entry_list.feature; a missing name reads "–" with the date kept
|
||
And dates in the footer are "12 Oct 2026, 09:14" (local time zone, 24 h)
|
||
And the leave guard is useUnsavedChangesGuard, the same "Leave without saving?" dialog as features/schema_builder_edit.feature
|
||
And the status chip is the builder's: "Saved", "Saved at HH:MM" (never "N min ago"), "Unsaved changes", "Saving…", "Save failed", plus "New entry" and "Not saved"
|
||
And the SPA adds no beforeunload prompt, as nothing else in web/src does
|
||
And Markdown is rendered with markdown-it with raw HTML disabled and only http, https and mailto links active
|
||
And every string goes through t() and has a French entry in i18n/messages.ts
|
||
And a copy tagged "(proposed copy)" is not in the design and is the coder's single point of change
|
||
|
||
Scenario: 1 New entry
|
||
Given GET /v1/content-type/events resolves 200 with the 7 fields Title, Starts on, Ends on, Online, Capacity, Category, Description
|
||
When the user navigates to /content/events/new
|
||
Then the breadcrumb reads "Content", "Events", "New entry" and the title row reads "New entry" with the subtitle "Events · Fields marked * are required."
|
||
And no entry request is sent
|
||
And the chip reads "New entry", "Cancel" and "Save" are shown, and "Delete" is not
|
||
And "Save" is enabled although nothing was typed
|
||
And each required label carries " *"
|
||
And the control of Online is Off and carries the tag "Default"
|
||
And the help under Title reads "Each entry needs a different title.", under Capacity "Between 1 and 5000.", the placeholders read "Choose an option" (Category) and "Pick a date" (Starts on, Ends on)
|
||
And the footer reads "Entry" "Not saved yet", "Created by" "–", "Updated by" "–"
|
||
And on mobile the topbar reads "New entry" with an arrow-left and no action, the chip sits under the title row, and the bottom bar holds "Cancel" and "Save"
|
||
|
||
Scenario Outline: Defaults on a new entry
|
||
Given the type has a field <field> with default <default>
|
||
When the user opens /content/events/new
|
||
Then the control holds <shown> and the tag "Default" shows next to the label
|
||
When the user changes the value
|
||
Then the tag goes
|
||
And when the user restores the default value the tag shows again
|
||
|
||
Examples:
|
||
| field | default | shown |
|
||
| Online (boolean) | false | Off |
|
||
| Capacity (number) | 100 | 100 |
|
||
| Category (select) | Talk | Talk |
|
||
| Starts on (date) | 2026-11-21 | 21 Nov 2026 |
|
||
| Title (text) | Untitled | Untitled |
|
||
| Description (richtext) | none | an empty textarea, no tag |
|
||
|
||
Scenario: Defaults are shown only on create
|
||
When the user opens /content/events/42
|
||
Then no field carries the tag "Default" and the values are those of the entry
|
||
|
||
Scenario: 2 Editing
|
||
Given GET .../entry/42 resolves 200 with the entry "Lime plaster workshop" created by 3 on 2026-10-12 09:14 and updated by 5 on 2026-10-15 11:02, and the lookup names 3 "Inès Duarte" and 5 "Marta Alves"
|
||
When the user navigates to /content/events/42
|
||
Then the breadcrumb reads "Content", "Events", "Lime plaster workshop" and the title reads "Lime plaster workshop" with the subtitle "Events · Entry 42"
|
||
And the chip reads "Saved"
|
||
And "Delete", "Cancel" and "Save" are shown, "Save" is disabled
|
||
And the controls hold the entry's values
|
||
And the footer reads "Entry" "events / 42" (mono), "Created by" "Inès Duarte · 12 Oct 2026, 09:14", "Updated by" "Marta Alves · 15 Oct 2026, 11:02"
|
||
And on mobile the topbar reads the title with an arrow-left and a trash-2 action, the chip sits under the title row, and the bottom bar holds "Cancel" and "Save" (disabled)
|
||
|
||
Scenario Outline: The page title
|
||
Given the entry 42 whose first field <first> holds <value>
|
||
When the user opens /content/events/42
|
||
Then the title and the last breadcrumb read <title>
|
||
|
||
Examples:
|
||
| first | value | title |
|
||
| Title (text) | "Lime plaster workshop" | Lime plaster workshop |
|
||
| Title (text) | null | Entry 42 |
|
||
| Body (richtext) | "First line\n\nSecond" | First line |
|
||
| Seats (number) | 18 | 18 |
|
||
|
||
Scenario: The title does not change while typing
|
||
Given /content/events/42
|
||
When the user types in Title
|
||
Then the title row and the breadcrumb keep "Lime plaster workshop" until a save answers
|
||
|
||
Scenario Outline: Form controls per field type
|
||
Given a field of type <type>
|
||
Then the control is <control> with <detail>
|
||
|
||
Examples:
|
||
| type | control | detail |
|
||
| text | UInput | a counter "n / max" when max is set |
|
||
| richtext | MarkdownField | tabs "Write" and "Preview", the toolbar bold, italic, heading, link, list, the placeholder "Write in Markdown", the help "Markdown. Switch to Preview to check the result." and a counter when max is set |
|
||
| number | UInputNumber | the placeholder "Enter a number" and minus and plus buttons; decimals are accepted |
|
||
| boolean | USwitch | the text "On" or "Off" next to it |
|
||
| date | UInputDate | the placeholder "Pick a date" and a calendar; the value is "YYYY-MM-DD" with no time-zone shift |
|
||
| select | USelect | the placeholder "Choose an option", the choices in schema order, and for an optional field a first item "None" (proposed copy) that clears it |
|
||
|
||
Scenario Outline: Help lines
|
||
Given a field <field>
|
||
Then the help under it reads <help>, and is replaced by the error when there is one
|
||
|
||
Examples:
|
||
| field | help |
|
||
| text, unique | Each entry needs a different title. (the label in lower case) |
|
||
| number, min 1, max 5000 | Between 1 and 5000. |
|
||
| number, min 1 | At least 1. (proposed copy) |
|
||
| number, max 5000 | At most 5000. (proposed copy) |
|
||
| date, min 2026-01-01, max 2027-12-31 | From 1 Jan 2026 until 31 Dec 2027. (proposed copy) |
|
||
| richtext | Markdown. Switch to Preview to check the result. |
|
||
| text, no unique, no number or date bounds | no help line |
|
||
|
||
Scenario: Layout of the generated form
|
||
Given the 7 fields of Events on a 1440 px viewport
|
||
Then Title is full width, Starts on and Ends on share a row, Online and Capacity share a row, Category and Description are each full width, in a column of at most 760 px
|
||
And a date, number or boolean field pairs with the next one if that one is also a date, number or boolean, text, richtext and select are always full width
|
||
And below md every field is full width
|
||
|
||
Scenario: 3 Unsaved changes and the counter
|
||
Given /content/events/42 with Title "Lime plaster workshop" (21 characters, max 80)
|
||
When the user types ", two days" in Title
|
||
Then the counter beside the label reads "32 / 80" in mono, muted
|
||
And the chip reads "Unsaved changes" and "Save" is enabled
|
||
And a counter over the limit reads "91 / 80" in the error colour, and typing is not blocked
|
||
And a counter shows only for a text or richtext field with a max
|
||
|
||
Scenario: An edit that is reverted is not an unsaved change
|
||
Given the user changed Capacity from 18 to 20
|
||
When the user sets it back to 18
|
||
Then the chip reads "Saved", "Save" is disabled and the leave guard does not prompt
|
||
|
||
Scenario: Cancel
|
||
Given /content/events/42, opened from the list /content/events?page=2&sort=id:asc
|
||
When the user activates "Cancel"
|
||
Then the router returns to /content/events?page=2&sort=id:asc when the previous history entry is the list of this type, else it navigates to /content/events
|
||
And the leave guard runs first when there are unsaved changes
|
||
|
||
Scenario: 4 Markdown preview
|
||
Given a richtext field holding "Two days of hands-on work with **lime putty** and old stone.\n\n- Bring work clothes\n- Lunch is included"
|
||
When the user activates the "Preview" tab
|
||
Then the textarea and the toolbar are replaced by the rendered text with a bold "lime putty" and a bulleted list of two items, in a box at least 96 px high (120 px on mobile)
|
||
And raw HTML in the value is shown as text, never rendered, and only http, https and mailto links are active
|
||
And the tab state is not an edit, and switching back to "Write" restores the textarea with its value, caret kept
|
||
And an empty value in Preview reads "Nothing to preview yet." (proposed copy)
|
||
|
||
Scenario Outline: The Markdown toolbar
|
||
Given the Write tab with the selection <selection>
|
||
When the user activates <button>
|
||
Then the textarea holds <result> and keeps the focus
|
||
|
||
Examples:
|
||
| selection | button | result |
|
||
| "lime" | Bold | **lime** |
|
||
| "lime" | Italic | _lime_ |
|
||
| "lime" | Heading | ## lime |
|
||
| "lime" | Link | [lime](url) |
|
||
| "lime" | List | - lime |
|
||
|
||
Scenario: Payload of a create
|
||
Given /content/events/new with Title "Open day", Starts on 2026-11-21, Online left at its default false, Ends on, Capacity, Category and Description left empty
|
||
When the user activates "Save"
|
||
Then POST /v1/content-type/events/entry is sent once with {"data": {"title": "Open day", "starts_on": "2026-11-21", "ends_on": null, "online": false, "capacity": null, "category": null, "description": null}}
|
||
And a number is sent as a JSON number, a boolean as true or false, a date as "YYYY-MM-DD", a select as the choice text
|
||
|
||
Scenario: Payload of an edit
|
||
Given /content/events/42 with only Capacity changed from 18 to 20
|
||
When the user activates "Save"
|
||
Then PUT /v1/content-type/events/entry/42 is sent once with every field of the type, the unchanged ones with their loaded values
|
||
And a key that the entry holds but the type no longer has is not sent
|
||
|
||
Scenario Outline: A boolean that was never set
|
||
Given a boolean field <kind> and the user never touched it
|
||
When the entry is saved
|
||
Then the data holds <sent> for it
|
||
|
||
Examples:
|
||
| kind | sent |
|
||
| optional, no default | null |
|
||
| optional, loaded as null | null |
|
||
| required, no default | false |
|
||
| optional, default true | true |
|
||
|
||
Scenario: 5 Saving
|
||
Given the PUT has not answered
|
||
Then the chip reads "Saving…" with a loader, "Save" reads "Saving…" with a loader and is disabled, "Cancel" and "Delete" are disabled
|
||
And the form is dimmed to 70 % and every control is disabled
|
||
And a second activation of "Save", or Enter in a field, sends no second request
|
||
|
||
Scenario: 6 Saved (edit)
|
||
Given /content/events/42 with Title changed to "Lime plaster workshop, two days"
|
||
When the PUT answers 200 with updated_by 5 and updated_at 2026-10-19 11:02 local
|
||
Then a success toast "Entry saved" with "\"Lime plaster workshop, two days\" is up to date." is shown
|
||
And the chip reads "Saved at 11:02", "Save" is disabled and the controls hold the answered values
|
||
And the title, the breadcrumb and the footer "Updated by" read the answered entry ("Marta Alves · 19 Oct 2026, 11:02")
|
||
And no "Leave without saving?" prompt is shown afterwards
|
||
And a later edit returns the chip to "Unsaved changes"
|
||
|
||
Scenario: Saved (create)
|
||
Given /content/events/new with the required fields filled
|
||
When the POST answers 201 with id 43
|
||
Then the router replaces the URL with /content/events/43 and no entry request is made for it, the answered entry is used
|
||
And the same toast, the chip "Saved at HH:MM" and the footer ("Entry" "events / 43", "Created by" and "Updated by" the current user) show
|
||
And "Delete" now shows and a Back from there goes to the page before the new form
|
||
And no "Leave without saving?" prompt is shown
|
||
|
||
Scenario: Where "Saved at" comes from
|
||
Given the answered entry has updated_at 2026-10-19T11:02:00 in the local time zone
|
||
Then the chip text is "Saved at 11:02" built from that timestamp, never from the clock
|
||
And a Saved chip never turns into "Saved N min ago"
|
||
And a test builds its expected text with the same local formatter from the mocked updated_at and pins the time zone; no test freezes the system date
|
||
|
||
Scenario Outline: 7 Save failed
|
||
Given the save <failure>
|
||
Then an error alert titled "Couldn't save the entry" shows "Something went wrong on our side. Your changes are still here. Try again in a moment." with a "Try again" action
|
||
And the chip reads "Save failed" in the error colour, the values stay, "Save" is enabled and the form is not dimmed
|
||
When the user activates "Try again"
|
||
Then the same body is sent again and the alert clears while it is in flight
|
||
|
||
Examples:
|
||
| failure |
|
||
| answers 500 |
|
||
| never reaches the server |
|
||
| answers 400 |
|
||
| answers 422 "invalid_entry" with no field |
|
||
|
||
Scenario: 8 Required fields empty
|
||
Given /content/events/new with Title and Starts on empty
|
||
When the user activates "Save"
|
||
Then no request is sent
|
||
And "Fill in Title." shows under Title and "Pick a date for Starts on." under Starts on, each with the error colour on the control
|
||
And the chip reads "Not saved" in the error colour with a circle-alert icon
|
||
And focus moves to the Title control, the first field in schema order with an error, and the page scrolls to it
|
||
And the controls carry aria-invalid and the message is tied to them by aria-describedby
|
||
|
||
Scenario Outline: Required messages
|
||
Given an empty required field of type <type> labelled <label>
|
||
When the user saves
|
||
Then the message under it reads <message>
|
||
|
||
Examples:
|
||
| type | label | message |
|
||
| text | Title | Fill in Title. |
|
||
| richtext | Notes | Fill in Notes. (proposed copy) |
|
||
| number | Capacity | Fill in Capacity. (proposed copy)|
|
||
| date | Starts on | Pick a date for Starts on. |
|
||
| select | Category | Choose an option for Category. |
|
||
|
||
Scenario: A required boolean refused by the server
|
||
Given /content/events/42 where Online is a required boolean the user never touched
|
||
When the server answers 422 "invalid_entry: online: required"
|
||
Then the message under Online reads "Choose yes or no for Online."
|
||
And a required boolean is never refused on the client: an untouched one is sent as false
|
||
|
||
Scenario: 9 Rule errors
|
||
Given /content/events/42 with Title of 91 characters, Ends on holding a value the server refuses as a date, Capacity 6000 and Category "Seminar" which is not a choice
|
||
When the user activates "Save"
|
||
Then only one message shows per field: "Title can be up to 80 characters." with the counter "91 / 80", "Ends on must be a date, like 2026-11-21.", "Capacity must be at most 5000.", "Choose one of: Talk, Workshop or Meetup."
|
||
And the chip reads "Not saved" and focus is on Title
|
||
|
||
Scenario Outline: Messages per rule and type
|
||
Given a field <field> and the value <value>
|
||
When the entry is saved
|
||
Then the message under it reads <message>
|
||
|
||
Examples:
|
||
| field | value | message |
|
||
| Title, text, min 3 | "ab" | Title needs at least 3 characters. |
|
||
| Title, text, max 80 | 91 characters | Title can be up to 80 characters. |
|
||
| Title, text | 5 (server type) | Title must be text. |
|
||
| Capacity, number, min 1 | 0 | Capacity must be at least 1. |
|
||
| Capacity, number, max 5000 | 6000 | Capacity must be at most 5000. |
|
||
| Capacity, number | "4" (server type) | Capacity must be a number. |
|
||
| Online, boolean | "yes" (server type) | Online must be yes or no. |
|
||
| Starts on, date, min 2026-01-01 | 2025-12-31 | Starts on can't be before 1 Jan 2026. |
|
||
| Starts on, date, max 2027-12-31 | 2028-01-01 | Starts on can't be after 31 Dec 2027. |
|
||
| Starts on, date | 2026-02-30 (server type)| Starts on must be a date, like 2026-11-21. |
|
||
| Category, select | "Seminar" | Choose one of: Talk, Workshop or Meetup. |
|
||
|
||
Scenario Outline: A server answer that gets past the client
|
||
Given the client found no error and the request is sent
|
||
When the server answers <status> "invalid_entry: <key>: <rule>" for a key of the loaded type
|
||
Then the message of the table above shows under the field of <key>, the chip reads "Not saved", focus moves to that field and the values stay
|
||
|
||
Examples:
|
||
| status | key | rule |
|
||
| 422 | title | min |
|
||
| 422 | capacity | max |
|
||
| 422 | ends_on | type |
|
||
| 422 | category | options |
|
||
| 422 | title | required|
|
||
|
||
Scenario: Errors clear field by field
|
||
Given the errors of state 8 are shown
|
||
When the user edits Title
|
||
Then the message under Title goes and the chip reads "Unsaved changes"
|
||
And the message under Starts on stays until Starts on is edited or the next save
|
||
And nothing is validated on blur
|
||
|
||
Scenario: 10 Duplicate unique value (409)
|
||
Given /content/events/new with Title "Heat pumps: what the numbers say" and another entry holds it
|
||
When the POST answers 409 "invalid_entry: title: unique"
|
||
Then an error alert titled "Not saved" shows "Another entry already uses this title. Change it and save again."
|
||
And under Title "Another entry already uses this Title. Enter a different one." shows, with the counter "n / 80", and the chip reads "Not saved"
|
||
And focus moves to Title and the values stay
|
||
And the alert goes when Title is edited
|
||
|
||
Scenario Outline: The unique message per type
|
||
Given a unique <type> field labelled <label>
|
||
When the server answers 409 "invalid_entry: <key>: unique"
|
||
Then the message under it reads <message> and the alert reads <alert>
|
||
|
||
Examples:
|
||
| type | label | key | message | alert |
|
||
| text | Title | title | Another entry already uses this Title. Enter a different one. | Another entry already uses this title. Change it and save again. |
|
||
| select | Category | category | Another entry already uses this choice for Category. | Another entry already uses this choice. Change it and save again. (proposed copy) |
|
||
| number | Capacity | capacity | Another entry already uses this Capacity. Enter a different one. (proposed copy) | Another entry already uses this capacity. Change it and save again. (proposed copy) |
|
||
|
||
Scenario Outline: 11 Entry not found
|
||
Given <where>
|
||
Then a centered state with the file-x icon reads "Entry not found" with "It may have been deleted by someone else." and a primary "Back to Events"
|
||
And the breadcrumb reads "Content" then "Events" and the mobile title is "Events"
|
||
And "Back to Events" navigates to /content/events
|
||
And the leave guard does not prompt on that navigation
|
||
|
||
Examples:
|
||
| where |
|
||
| GET .../entry/999 answers 404 entry_not_found |
|
||
| a PUT answers 404 entry_not_found (edits discarded with the page) |
|
||
| the URL is /content/events/abc or /content/events/4294967296 (no request) |
|
||
|
||
Scenario Outline: Other answers while loading or saving
|
||
Given any request of the editor
|
||
When it answers <status> <body>
|
||
Then <result>
|
||
|
||
Examples:
|
||
| status | body | result |
|
||
| 404 | content_type_not_found | the page "Content type not found" of features/content_entry_list.feature state 9 |
|
||
| 403 | any | the page "You don't have access to Events" of features/content_entry_list.feature state 8 |
|
||
| 401 | any | the existing redirect to /login with the expired reason |
|
||
|
||
Scenario Outline: Loading the entry fails (proposed, not drawn)
|
||
Given GET .../entry/42 or the type <failure>
|
||
Then an error alert reads "Couldn't load the entry" (proposed copy) with "GET /v1/content-type/events/entry/42 failed. Check your connection and try again." and a "Try again" action
|
||
And no form shows and "Try again" sends only the failed request again
|
||
|
||
Examples:
|
||
| failure |
|
||
| answers 500 |
|
||
| never reaches the server |
|
||
|
||
Scenario: Loading (proposed, not drawn)
|
||
Given the type and the entry have not resolved
|
||
Then the title row shows a skeleton, the form is skeleton rows with aria-busy="true", and "Save" and "Delete" are not shown
|
||
|
||
Scenario: 12 Schema changed since load
|
||
Given /content/events/new was opened with the 7 fields, then Room (required) was added and Description removed
|
||
When the POST answers 422 "invalid_entry: room: required"
|
||
Then a warning alert titled "The fields of Events changed" shows "A field was removed and a new required field, Room, was added after you opened this form. Reload the form to continue. What you typed here can't be saved against the old fields." with a "Reload form" action
|
||
And every control is disabled, the form is dimmed, "Save" is disabled
|
||
And the values stay visible
|
||
And the chip reads "Not saved" (the board draws "Saved" here) and "Cancel" stays enabled
|
||
|
||
Scenario Outline: What counts as a schema change
|
||
Given the editor holds the type as loaded
|
||
When the server answers 422 "invalid_entry: <key>: <rule>"
|
||
Then <result>
|
||
|
||
Examples:
|
||
| key | rule | result |
|
||
| room | required | the key is not a field of the loaded type: state 12 |
|
||
| description | unknown_field | state 12 |
|
||
| colour | unknown_field | state 12 |
|
||
| title | required | state 8 on Title (the key is known) |
|
||
| room | unique | state 12 (409) |
|
||
|
||
Scenario: The alert names what changed only when the SPA can tell
|
||
Given the type is fetched again by "Reload form"
|
||
Then the alert copy of the design is used as is; the SPA never builds a diff of fields, since the design text names Room and the removed field only as a sample
|
||
|
||
Scenario: Reload form
|
||
Given state 12
|
||
When the user activates "Reload form"
|
||
Then GET /v1/content-type/events is sent again and, on /content/:key/:id, GET .../entry/42 too
|
||
And the form is rebuilt from the new type with defaults on a new entry, or the stored values on an edit; what was typed is dropped
|
||
And the alert goes, the chip reads "New entry" or "Saved", and the URL is unchanged
|
||
And a failure of the reload shows the load error of this slice
|
||
|
||
Scenario: 13 Delete entry: confirm
|
||
Given /content/events/42 with no unsaved changes
|
||
When the user activates "Delete" (the trash-2 action on mobile)
|
||
Then no request is sent
|
||
And the dialog "Delete this entry?" opens with "\"Lime plaster workshop\" will be deleted from Events. This can't be undone." (a bottom sheet on mobile)
|
||
And the actions are "Cancel" and "Delete entry" (error colour, never the initial focus)
|
||
And Cancel, the close icon and Esc close it with nothing sent and focus back on "Delete"
|
||
|
||
Scenario: Deleting
|
||
Given the dialog is open
|
||
When the user activates "Delete entry"
|
||
Then DELETE /v1/content-type/events/entry/42 is sent once
|
||
And the button reads "Deleting…" with a loader, Cancel is disabled, Esc, the close icon and the overlay do not dismiss, and a second activation sends nothing
|
||
|
||
Scenario: Deleted
|
||
Given DELETE answers 204
|
||
Then the dialog closes and the router replaces the URL with /content/events with no "Leave without saving?" prompt
|
||
And a success toast "Entry deleted" with "\"Lime plaster workshop\" was deleted from Events." (proposed copy) is shown
|
||
And the list loads its own data, so the entry is gone from it
|
||
|
||
Scenario Outline: Other answers of DELETE
|
||
Given the dialog is open and DELETE <outcome>
|
||
Then <result>
|
||
|
||
Examples:
|
||
| outcome | result |
|
||
| answers 404 entry_not_found | the dialog closes and the page "Entry not found" shows |
|
||
| answers 404 content_type_not_found| the dialog closes and the page "Content type not found" shows |
|
||
| answers 403 | the dialog closes and the page "You don't have access to Events" shows |
|
||
| answers 401 | the existing redirect to /login with the expired reason |
|
||
| answers 500 or fails (network) | the dialog stays with an error alert "Couldn't delete the entry" (proposed copy) and "Something went wrong on our side. Nothing was deleted. Try again in a moment."; the buttons are enabled again |
|
||
|
||
Scenario: Delete with unsaved edits
|
||
Given /content/events/42 with unsaved edits
|
||
When the user opens the delete dialog
|
||
Then its copy uses the saved title, it does not save or discard the edits, and Cancel leaves them exactly as they were
|
||
And on 204 the router leaves with no leave prompt
|
||
|
||
Scenario Outline: Leaving with unsaved edits
|
||
Given <state>
|
||
When the user leaves through the breadcrumb, the mobile arrow, "Cancel", the sidebar or the browser back
|
||
Then <result>
|
||
|
||
Examples:
|
||
| state | result |
|
||
| no edit | the router navigates with no prompt |
|
||
| an edit on /content/events/42 | the dialog "Leave without saving?" with "Keep editing" and "Discard changes" |
|
||
| any typed value on /content/events/new | the same dialog |
|
||
| /content/events/new untouched, defaults only | no prompt |
|
||
| after Save answered 200 or 201 | no prompt |
|
||
| after Delete answered 204 | no prompt |
|
||
| state 12 with typed values | the same dialog |
|
||
|
||
Scenario: Switching to another entry or type reuses nothing
|
||
Given the user edited /content/events/42 and discarded the edits to leave
|
||
When the user opens /content/events/43 or /content/recipes/new
|
||
Then the form is built from that route alone, with no value, error or chip carried over
|
||
|
||
Scenario: Stale answers are ignored
|
||
Given a GET for /content/events/42 is in flight
|
||
When the user navigates to /content/events/43 and the first answer arrives later
|
||
Then it is dropped
|
||
|
||
Scenario: Reaching the editor from the list
|
||
Given the list of features/content_entry_list.feature
|
||
When the user activates "New entry" or a row
|
||
Then the editor opens at /content/events/new or /content/events/42, no longer the catch-all 404
|
||
|
||
Scenario: Accessibility
|
||
Then every control has a visible label tied to it, the required mark " *" is hidden from assistive technology and the control carries aria-required
|
||
And the chip is a polite live region
|
||
And the Preview tabs are a tablist with arrow-key movement, and the toolbar buttons have accessible names (Bold, Italic, Heading, Link, List)
|
||
And dialogs trap focus, open with focus on the title and return it to the opener on close; the destructive button is never the initial focus
|
||
And controls are 44 px high on mobile and focus is a 2 px --ui-primary outline with a 2 px offset
|