Files
RenewCMS/features/schema_builder_create.feature
T

517 lines
32 KiB
Gherkin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Feature: Create a custom content type in the Schema builder
As an owner or admin
I want to define a new content type (name, key and ordered fields) on /schema/new
So that users can write entries of that 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 POST /v1/content-type takes {key, name, fields[{key, label, type, required, unique, default, min, max, options}]}
And the builder never sends an "icon" key
And POST answers 201 with the content type, 400 invalid_content_type_key, 409 content_type_key_taken, 422 invalid_field, 403 or 500
And the route /schema/new is declared before /schema/:key, has meta auth "verified" and title "Schema", and renders the builder
And a route meta flag, not a path pattern, tells the shell that /schema/new needs the custom list only
And only owners and admins can use Schema; the SPA decides from the session role, the API still enforces it
And the six field types are text "Text", richtext "Rich text", number "Number", boolean "Yes or no", date "Date" and select "Choice"
And the limits are a type name of up to 100 characters, a type key of 2 to 40, 1 to 50 fields, a field key of up to 40 characters and 1 to 50 choices
And the layout is: Name and Key with the status chip, Discard and Save on top; the FieldList on the left; the FieldInspector on the right; the ApiPreview at the bottom
And below 48rem the FieldList is the page, the FieldInspector is a 92% bottom sheet closed with "Done", the ApiPreview is a bottom sheet, and Discard and Save sit in a bottom bar
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: Reaching the builder
Given the Schema list is Populated or Empty
When the user activates "New content type" (header button, empty-state button or the mobile floating button)
Then the router navigates to /schema/new
And the builder is shown, not the "Content type not found" page
And /schema/events and /schema/unknown still show "Content type not found" for a key that is not built-in
Scenario: Roles and sessions
Given <who> opens /schema/new
Then <result>
Examples:
| who | result |
| a verified owner or admin | the builder |
| a verified user who is neither | "You don't have access to Schema" with "Go to Home", URL unchanged |
| an unverified user | the redirect to /register/pending |
| a signed-out visitor | the redirect to /login |
Scenario: The builder needs no built-in list
Given an owner opens /schema/new as the first page of the session
Then GET /v1/content-type is called once for the sidebar
And GET /v1/builtin-type is never called
And the builder is shown as soon as the session is ready, with no skeleton
Scenario: New type, desktop (design state 1)
Given an owner opens /schema/new on a 1440 px viewport
Then the breadcrumb is "Schema" (a link to /schema) then "New content type"
And "Name" is an empty text input with the placeholder "For example, Events", marked required, with maxlength 100
And "Key" is a read-only mono input with the placeholder "events" and the help "Used in URLs and the API. Lowercase letters, digits and hyphens. It can't change after you save."
And the list header reads "Fields" and "1 of 50"
And one row is shown with the label "Title" and the key "title" in mono, selected, with the badge "New"
And "Add field" is shown under the list
And the inspector title is "Field: Title" with the badge "New"
And "Label" holds "Title"
And "Key" holds "title" with the Auto badge and the help "The name used in the API. Starts with a letter. Lowercase letters, digits and underscores."
And the type tiles show Text selected
And "Required" is off with "An entry can't be saved with this field empty."
And "Unique values" is off
And "Default value" is empty with "Used when an entry is created and this field is left out."
And "Shortest" and "Longest" are empty with "Counted in characters. Entries show a counter."
And "Save" and "Discard" are disabled
And no status chip is shown
And no "entries use this content type" bar is shown
And the API preview bar is collapsed
Scenario: New type, mobile (design state 1)
Given the same page on a 390 px viewport
Then the topbar shows an arrow-left and the title "New content type"
And Name and Key are stacked above the list, then the rows, then the "API preview" row with a chevron
And no inspector is shown until a row is activated
And the bottom bar holds Discard and Save
Scenario Outline: The key follows the name until it is edited
Given an empty new type
When the user types the name "<name>"
Then the Key holds "<key>" with the Auto badge, read-only
Examples:
| name | key |
| Events | events |
| Press mentions | press-mentions |
| Café & Bar | cafe-bar |
| Job openings! | job-openings |
| 2026 Awards | 2026-awards |
| !!! | (empty, mode empty) |
| a name of 60 letters | the first 40 characters, without a trailing hyphen |
Scenario: Editing and resetting the key
Given the name "Events" and the Auto key "events"
When the user activates the pencil
Then the Key becomes editable, the Auto badge goes and "Reset to match the name" is shown
When the user types the key "agenda" then edits the name to "Events and talks"
Then the Key stays "agenda"
When the user activates "Reset to match the name"
Then the Key is "events-and-talks" with the Auto badge
Scenario Outline: Type-level rules (design states 6 and 14)
Given a new type with a valid field
When the user sets <input> to <value> and activates Save
Then <message> is shown under <input> with an alert icon
And POST /v1/content-type is not called
Examples:
| input | value | message |
| Name | (empty or spaces) | Give the content type a name. |
| Key | e | Use 2 to 40 lowercase letters, digits or hyphens. |
| Key | Events | Use 2 to 40 lowercase letters, digits or hyphens. |
| Key | my_type | Use 2 to 40 lowercase letters, digits or hyphens. |
| Key | -events | Use 2 to 40 lowercase letters, digits or hyphens. |
| Key | events--2 | Use 2 to 40 lowercase letters, digits or hyphens. |
| Key | new | The key "new" is reserved. Choose a different key. (proposed copy) |
Scenario Outline: Accepted type keys
Given a new type with a valid field and the name "X"
When the user types the key "<key>" and activates Save
Then POST /v1/content-type is called with that key
Examples:
| key |
| ab |
| press-mentions |
| 2026-awards |
| 40 lowercase letters |
Scenario: Type-level errors together (design state 6)
Given a new type with no name, the key "e" and no field
When the user activates Save
Then "Give the content type a name.", "Use 2 to 40 lowercase letters, digits or hyphens." and "A content type needs at least one field." are shown
And the empty list shows "No fields yet" with the message in the error colour
And the chip reads "3 problems to fix"
And Save is disabled until all three are fixed, and Discard stays enabled
And the inspector shows "Field settings" and "Add a field, then choose its type and rules here."
Scenario: When errors are shown
Given a new type whose name is empty
Then no error is shown while nothing was edited
When the user edits an input, an error for that input shows as they type
And when the user activates Save, every error of the form shows at once
And errors are tied to their input with aria-describedby and aria-invalid
And a row with an error shows an alert icon, not only the red border
Scenario Outline: The status chip
Given <situation>
Then the chip reads "<chip>"
Examples:
| situation | chip |
| a pristine new type | (no chip) |
| an edit with no error | Unsaved changes |
| three inputs in error | 3 problems to fix |
| one input in error | 1 problem to fix |
| the POST in flight | Saving… |
| the POST failed with 500 | Save failed |
| navigator.onLine is false | Waiting for connection |
Scenario Outline: Counting problems
Given <errors>
Then the chip counts <count>
Examples:
| errors | count |
| no name, a one-letter key, no field | 3 |
| a field with no label, a field with min above max, a choice list with a blank and a duplicate | 3 |
| one choice list with a blank row and a duplicate row | 1 |
| one field with a bad key | 1 |
Scenario: Field list rows
Given a type with the fields Title, Starts on, Online
Then each row shows a grip button labelled "Move <label>", the icon box of its type, the label and the key in mono
And an unselected row shows its badges "New", "Required", "Unique" and an alert icon when in error
And the selected row is on the sand background and shows Move up, Move down and Remove
And a field without a label reads "Untitled field" and a field without a key reads "–"
Scenario: Adding a field
Given the list has 1 field
When the user activates "Add field"
Then a row is appended, selected, with a Text type and no label, and the label input is focused
And the header reads "2 of 50"
And at 50 fields "Add field" is disabled and the header reads "50 of 50"
Scenario Outline: Field key from the label
Given a new field is selected
When the user types the label "<label>"
Then its Key holds "<key>" with the Auto badge
Examples:
| label | key |
| Title | title |
| Starts on | starts_on |
| Éléments | elements |
| Ends on (date) | ends_on_date |
| 2nd room | nd_room |
| Title (when "title" exists)| title_2 |
| a label of 60 letters | the first 40 characters |
Scenario Outline: Field key rules (design state 7)
Given a new field is selected with the Key in manual mode
When the user types the key "<key>"
Then <outcome>
Examples:
| key | outcome |
| Title | "Start with a letter. Use lowercase letters, digits and underscores." under Key |
| 1st | the same message |
| a-b | the same message |
| a b | the same message |
| (empty) | the same message |
| starts_on | no error |
| a | no error |
| a_40_characters | no error, and a 41st character cannot be typed |
Scenario: Duplicate field keys
Given two fields whose keys are both "title" in manual mode
Then "Another field already uses this key." (proposed copy) is shown under the Key of the second field
And the chip counts one problem for that field
Scenario: A single field key error (design state 7)
Given a new field with the key "start-date" and everything else valid
Then the Key shows the error with the alert icon and the chip reads "1 problem to fix"
And Save is disabled
Scenario Outline: The inspector follows the type
Given a new field of type "<type>"
Then the limits are labelled <limits>
And "Unique values" is <unique>
And the default control is <default>
Examples:
| type | limits | unique | default |
| text | "Shortest" and "Longest" with the suffix "characters" | available | a text input, placeholder "No default" |
| richtext | "Shortest" and "Longest" with the suffix "characters" | disabled with "Not available for rich text." | a textarea, placeholder "No default" |
| number | "Lowest value" and "Highest value" | available | a number input, placeholder "No default" |
| boolean | none; "Yes or no fields have no limits." | disabled with "Not available for yes or no fields." | the switch "On by default" with help "New entries start with this switch off when the field is left out." |
| date | "Earliest date" and "Latest date" | available | a date input, placeholder "No default" |
| select | none; "Limits come from the choices below." | available | a select of the choices, placeholder "None" |
Scenario: Type tiles
Given the inspector of a new field
Then six radio tiles are shown, each with its icon, name and description
| tile | icon | description |
| Text | type | Short text, like a title or a name |
| Rich text | pilcrow | Long text with formatting, written in Markdown|
| Number | hash | Whole or decimal numbers |
| Yes or no | toggle-left | An on or off switch |
| Date | calendar | A calendar day, without a time |
| Choice | list | One option from a list you define |
And the selected tile is on the sand background
And the help under "Longest" reads "Counted in characters. Entries show a counter." for Text and Rich text, and "Entries outside the range are refused." for Number and Date
Scenario: Changing the type of a new field
Given a Number field with min 1, max 5000, default 20 and unique on
When the user picks the tile "Yes or no"
Then the label, the key and Required are kept
And min, max, default, options and unique are reset
When the user picks "Choice"
Then the choice list editor is shown with no choice
Scenario Outline: Field rules
Given a new field of type "<type>" with <settings>
Then <message> is shown under <where>
Examples:
| type | settings | message | where |
| text | an empty label or spaces | Give the field a label. | Label |
| number | min 6000, max 5000 | Lowest value can't be above the highest value. | the limits |
| text | min 10, max 3 | Shortest can't be longer than longest. (proposed copy) | the limits |
| date | min 2027-12-31, max 2026-01-01 | Earliest date can't be after the latest date. (proposed copy) | the limits |
| text | min 2.5 | Use a whole number, 0 or more. (proposed copy) | the limit |
| text | min -1 | Use a whole number, 0 or more. (proposed copy) | the limit |
| number | min 1, max 5000, default 0 | The default value is outside the limits. (proposed copy) | Default value |
| text | max 3, default "abcd" | The default value is outside the limits. (proposed copy) | Default value |
| select | no choice | Add at least one choice. (proposed copy) | the choice list|
| select | a blank choice | Choices can't be blank. | that row |
| select | "Talk" twice | Two choices are the same. | the second row |
| number | min 1, max 5000 | no error | - |
| number | min 5, max 5 | no error | - |
| text | min 0 | no error | - |
Scenario: Unique and default
Given a Text field with the default "Draft"
When the user turns "Unique values" on
Then the Default value input is disabled with "A field with unique values can't have a default."
And the default is not sent
When the user turns "Unique values" off
Then the default "Draft" is back
Scenario: Choice list editor (design state 8)
Given a Choice field with the choices Talk, Workshop, Meetup
Then "Choices" is shown with the required marker and "3 of 50"
And each choice is a row with a grip, a text input, Remove, and the placeholder "Choice"
And an input "Add a choice" with the button "Add" is shown
And the help reads "Add 1 to 50 choices. Each one needs a different name. Removing a choice that entries use will be refused when you save."
When the user types "Panel" in "Add a choice" and activates Add (or presses Enter)
Then the choice is appended and the input is emptied and keeps the focus
And the Default value select lists the choices
When the selected default choice is removed or renamed
Then the default is cleared
Scenario: Choice list limits and errors
Given a Choice field with 5 choices, the fourth blank and the fifth equal to the first
Then "Choices can't be blank." is shown under the fourth row and "Two choices are the same." under the fifth, each with an alert icon
And the field counts as one problem
And at 50 choices the Add button is disabled
And choices are compared after trimming, case-sensitively
Scenario: Reordering choices
Given a Choice field with 3 choices
When the user uses the grip of the second choice
Then Move up and Move down work as for fields, with the same live region ("Moving Workshop. Position 2 of 3. ...")
Scenario: Removing a field
Given a list with 3 fields
When the user activates Remove on the selected one
Then it disappears with no undo row (an undo row only exists for saved fields, slice 4)
And the next row is selected, or the previous one at the end, or none if the list is empty
And the header counts one less
Scenario Outline: Moving fields with the buttons
Given a list Title, Starts on, Ends on with <selected> selected
When the user activates <button>
Then the order becomes <order>
And the selected row stays selected and keeps the focus on the same button when possible
Examples:
| selected | button | order |
| Starts on | Move up | Starts on, Title, Ends on |
| Starts on | Move down | Title, Ends on, Starts on |
Scenario Outline: Disabled ends
Given a list of 3 fields with <row> selected
Then <button> is disabled
Examples:
| row | button |
| Title | Move up |
| Ends on | Move down |
Scenario: Keyboard reorder (design state 9)
Given the list Title, Starts on, Ends on
When the user focuses the grip "Move Starts on" and presses Space
Then the row is picked up with the primary outline
And the polite live region reads "Moving Starts on. Position 2 of 3. Press Up or Down to move, Space to drop, Esc to cancel."
When the user presses Up
Then the row is first and the live region reads "Moving Starts on. Position 1 of 3. ..."
When the user presses Space
Then the new order is kept and the live region announces the drop
When the user picks it up again, moves it and presses Esc
Then the order returns to what it was before the pick-up
Scenario: No pointer drag in this slice
Then rows cannot be dragged with the pointer
Scenario: Selecting a field on mobile
Given a 390 px viewport
When the user activates a row
Then a bottom sheet titled "Field: <label>" opens with the inspector, Move up, Move down and Remove (proposed placement), and a "Done" button
And focus moves to the sheet title
When the user activates Done
Then the sheet closes and the focus returns to the row
Scenario: API preview (design state 10)
Given the type key "events" and the fields Title, Starts on, Online, Capacity, Category
When the user opens "API preview"
Then the panel shows "Where entries of this type live" with these five routes
| GET | /v1/content-type/events/entry |
| POST | /v1/content-type/events/entry |
| GET | /v1/content-type/events/entry/{id} |
| PUT | /v1/content-type/events/entry/{id} |
| DELETE | /v1/content-type/events/entry/{id} |
And "A sample entry. Values come from each field's type and default." with the JSON of a sample entry
And "Updates as you edit. Fields you add or remove here appear after you save."
And the collapsed bar shows "API preview", "GET /v1/content-type/events/entry" in mono and "Copy JSON"
And the key falls back to "events" while the Key is empty
Scenario Outline: Sample values in the preview
Given a field "f" of type "<type>" with <settings>
Then the sample entry data holds <value> for "f"
Examples:
| type | settings | value |
| text | no default | "Example text" |
| text | default "Draft" | "Draft" |
| richtext | no default | "Markdown text" |
| number | no default, no min | 0 |
| number | min 1 | 1 |
| number | min 1, default 20 | 20 |
| boolean | no default | false |
| boolean | on by default | true |
| date | no default | "2026-11-21" |
| select | choices Talk, Workshop | "Talk" |
| select | default "Workshop" | "Workshop" |
| select | no choice yet | null |
Scenario: The sample entry envelope and live updates
Then the sample is {"id": 42, "content_type": <key>, "data": {...}, "created_by": 3, "updated_by": 3, "created_at": "2026-10-15T11:02:00Z", "updated_at": "2026-10-15T11:02:00Z"} indented by 2 spaces
And the data keys follow the field order and skip fields without a valid key
And it updates on every edit, reorder and removal
And it contains no date from the clock
Scenario: Copy JSON
When the user activates "Copy JSON"
Then the sample entry JSON is written to the clipboard
And the button reads "Copied" for 2 seconds, then "Copy JSON" again
And the same happens in the mobile sheet
Scenario: API preview on mobile
Given a 390 px viewport
When the user activates the row "API preview"
Then a bottom sheet titled "API preview" shows "GET /v1/content-type/events/entry", the JSON and a full-width "Copy JSON" button
Scenario: Saving a valid type
Given the name "Events", the key "events" and these fields
| label | key | type | required | unique | default | min | max | options |
| Title | title | text | true | true | | | 80 | |
| Starts on | starts_on | date | true | false | | 2026-01-01 | 2027-12-31 | |
| Online | online | boolean | false | false | on | | | |
| Capacity | capacity | number | false | false | 20 | 1 | 5000 | |
| Category | category | select | false | false | Talk | | | Talk, Workshop, Meetup |
| Description | description | richtext | false | false | | | 5000 | |
When the user activates Save
Then POST /v1/content-type is called once with the key "events", the name "Events" and the fields in list order
And each field carries exactly key, label, type, required, unique, default, min, max, options
And unset default, min and max are null, and options are [] for a non-select field
And the boolean default is true, an off switch sends null, a date bound is "YYYY-MM-DD", the name is trimmed, choices are trimmed
And the body has no "icon" key
Scenario: Saving (design state 11, create)
Given the POST has not answered
Then the chip reads "Saving…" with a loader, Save shows a loader and is disabled, Discard is disabled
And Name, Key and the inspector are dimmed and not editable
And Add field, the grips, the Move and Remove buttons are disabled
And a second activation of Save sends no second request
Scenario: Saved (design state 12, create)
Given the POST answers 201 with the content type "events" named "Events" and 6 fields
Then a success toast "Content type saved" with "Events now has 6 fields." is shown
And the content types store holds "events" in name order when its list was loaded
And the sidebar Content group lists "Events"
And the router replaces the URL with /schema, where the Events row is listed
And no "Leave without saving?" prompt is shown
And with one field the toast reads "Events now has 1 field." (proposed copy)
Scenario: Key already taken (design state 14)
Given the name "Events", the key "events" and the fields Title (required) and Starts on (date)
When the POST answers 409 with the body "content_type_key_taken"
Then under Key: The key "events" is already used by another content type. Choose a different key.
And the Key is editable (manual mode), has the error and the focus, and the Auto badge is hidden
And the chip reads "1 problem to fix" and Save is disabled
And the fields and the name are untouched
When the user edits the key
Then the error goes and Save is enabled
Scenario: The server refuses a key or a field
Given the POST answers <status> with <body>
Then <outcome>
Examples:
| status | body | outcome |
| 400 | invalid_content_type_key | the key message under Key, as in the rules above |
| 422 | invalid_field | the banner "Check the fields marked in the list." (proposed title and placement), edits kept |
| 403 | Only ... | the page "You don't have access to Schema" |
| 401 | (any) | the existing redirect to /login with the expired reason |
Scenario Outline: Save failed (design state 13)
Given the POST <failure>
Then an error alert titled "Couldn't save the content type" shows "Something went wrong on our side. Your changes are still here. Try again in a moment."
And it has a "Try again" action and the chip reads "Save failed"
And every edit is kept and Save stays enabled
When the user activates "Try again"
Then the same body is sent again
Examples:
| failure |
| answers 500 |
| fails (network) |
Scenario: Offline (design state 16)
Given the user has edited the form and navigator.onLine becomes false
Then the banner "You're offline. Changes will be saved when you reconnect." with a wifi-off icon is shown
And the chip reads "Waiting for connection"
And Save is disabled and every edit is kept
When the connection returns
Then the banner goes, Save is enabled again and nothing is sent by itself
Scenario: Discard
Given the user has edited the form
When the user activates Discard
Then Name and Key are empty, the list holds the starter field Title, errors and the chip are cleared
And nothing is sent and the URL stays /schema/new
And Discard and Save are disabled again
Scenario Outline: Leaving with unsaved edits
Given <state>
When the user leaves through the breadcrumb, the mobile arrow, the sidebar or the browser back
Then <result>
Examples:
| state | result |
| a pristine form | the router navigates with no prompt |
| an edited name, key or field | the dialog "Leave without saving?" with "Keep editing" and "Discard changes" is shown |
| after Save answered 201 | the router navigates with no prompt |
Scenario: Keep editing or discard on leave
Given the leave dialog is open
When the user chooses "Keep editing" (or dismisses the dialog)
Then the user stays on /schema/new with every edit
When the user chooses "Discard changes"
Then the router navigates away
Scenario: Accessibility of the builder
Then every control has a visible label
And the type tiles are a radio group
And the field list is one tab stop and arrow keys move between rows
And controls are 44 px high on mobile
And focus is a 2 px ring in the primary colour