feat: settings page, sidebar and drawer destination "Settings" (icon settings, after Integration), route /settings (verified), showing only the running build's Version
This commit is contained in:
1 parent
b146dfead7
commit
162fea4afb
26 files changed
+805
-45
No files matched your search
@@ -17,7 +17,7 @@ Feature: DashboardShell wordmark, sidebar collapse and the UserMenu
|
||||
AuthShell uses) followed by the "RenewCMS" wordmark text, not the plain
|
||||
<span class="font-semibold">RenewCMS</span> the shell renders today
|
||||
And the sidebar's #default slot is a UNavigationMenu orientation="vertical" :collapsed bound
|
||||
to the sidebar's own collapsed state, with items Home, Articles, Users and Integration, in that order
|
||||
to the sidebar's own collapsed state, with items Home, Articles, Users, Integration and Settings, in that order
|
||||
And the sidebar's #footer slot renders a UserMenu component, not the plain
|
||||
<span>Log out</span> text the shell renders today
|
||||
And UserMenu is the account menu specified in features/profile_page.feature, anchored to a
|
||||
|
||||
@@ -130,14 +130,14 @@ Feature: Nuxt UI shell foundation and router guards
|
||||
Then the alert above the title contains "Your session expired."
|
||||
And the footer area outside the card contains a "Forgot password?" link
|
||||
|
||||
Scenario: DashboardShell renders the four navigation destinations
|
||||
Scenario: DashboardShell renders the five navigation destinations
|
||||
Given the current route is /article
|
||||
When DashboardShell mounts
|
||||
Then the sidebar navigation lists exactly four destinations in this order: "Home" linking to
|
||||
/home, "Articles" linking to /article, "Users" linking to /users, and "Integration" linking
|
||||
to /integration
|
||||
Then the sidebar navigation lists exactly five destinations in this order: "Home" linking to
|
||||
/home, "Articles" linking to /article, "Users" linking to /users, "Integration" linking
|
||||
to /integration, and "Settings" linking to /settings
|
||||
And the "Articles" destination has aria-current="page"
|
||||
And none of "Home", "Users" and "Integration" has aria-current set
|
||||
And none of "Home", "Users", "Integration" and "Settings" has aria-current set
|
||||
|
||||
Scenario: DashboardShell shows the offline banner when the browser reports offline
|
||||
Given navigator.onLine is false
|
||||
|
||||
@@ -41,7 +41,7 @@ Mock `useApi` / the `/v1/auth/me` call per case; no real backend, no httptest se
|
||||
13. **AuthShell notice/footer slots** — mount AuthShell with notice and footer slot content; assert
|
||||
both render outside/above the card content as specified.
|
||||
14. **DashboardShell nav + aria-current** — mount DashboardShell with current route /article; assert
|
||||
4 links in order (Home→/home, Articles→/article, Users→/users, Integration→/integration), and only the Articles link
|
||||
5 links in order (Home→/home, Articles→/article, Users→/users, Integration→/integration, Settings→/settings), and only the Articles link
|
||||
has aria-current="page".
|
||||
15. **DashboardShell offline banner** — mock navigator.onLine false, mount DashboardShell; assert
|
||||
the offline banner is visible; set navigator.onLine true and dispatch the 'online' event; assert
|
||||
@@ -57,7 +57,7 @@ plus the components.md AuthShell/DashboardShell sections.
|
||||
- [ ] AuthShell below sm: card loses border, 20px padding fills screen, fields/buttons use xl size,
|
||||
light and dark.
|
||||
- [ ] AuthShell footer: UColorModeSelect size="sm" plus footer links render outside the card.
|
||||
- [ ] DashboardShell desktop: sidebar with RenewCMS logo header, 3 nav items, UserMenu footer;
|
||||
- [ ] DashboardShell desktop: sidebar with RenewCMS logo header, 5 nav items, UserMenu footer;
|
||||
collapsing the sidebar leaves a 56px icon rail with tooltips on hover/focus.
|
||||
- [ ] DashboardShell mobile (below lg): UDashboardSidebarToggle appears, sidebar becomes a
|
||||
slideover, closes after navigating.
|
||||
|
||||
@@ -18,8 +18,8 @@ Feature: Profile page, display name and the account menu
|
||||
display_name in a response is read as ""; the session store gains an action that replaces
|
||||
the session's displayName from the display_name of a PATCH /v1/user/me response
|
||||
And the PageHeader shows breadcrumb "Profile" (mobile title "Profile"), the page title
|
||||
"Profile" and the subtitle "Your account on this RenewCMS workspace."; none of the four
|
||||
navigation items (Home, Articles, Users, Integration) has aria-current="page" on /profile
|
||||
"Profile" and the subtitle "Your account on this RenewCMS workspace."; none of the five
|
||||
navigation items (Home, Articles, Users, Integration, Settings) has aria-current="page" on /profile
|
||||
And the page is at most 720 px wide with three ProfileCards in this order: Display name,
|
||||
Account, Password; cards are separated by 20 px, have 20 px padding (16 px on mobile) and
|
||||
inputs are 40 px high (44 px on mobile, 16 px text)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
Feature: Settings page and GET /v1/settings
|
||||
|
||||
As a verified RenewCMS user in the shared workspace
|
||||
I want a Settings page that shows the version of the RenewCMS build I am running
|
||||
So that I can tell which build is deployed when I report a problem or check an upgrade
|
||||
|
||||
Background:
|
||||
Given GET /v1/settings is mounted under /v1 in api/router.go in its own router, behind
|
||||
jwtauth.Verifier, jwtauth.Authenticator, middleware.RequireCurrentToken and
|
||||
middleware.RequireVerifiedUser, the same chain as NewUserRouter
|
||||
And the response of a verified caller is 200 with the JSON body {"version": "<value>"}, one
|
||||
snake_case field and nothing else, built from the domain entity settings.Settings in
|
||||
internal/domain/settings
|
||||
And GetSettingsUseCase (internal/infrastructure/useCases) is built with the version string and
|
||||
depends only on the domain package; the handler in api/handlers/settings receives it
|
||||
through NewHandler and never reads config, os or the environment
|
||||
And config.Settings gains a Version field; cmd/server/main.go sets it from its own
|
||||
main.version variable ("dev" unless set with -ldflags -X main.version) right after
|
||||
config.NewSettingsFromEnv, and api/dependecyInjection.go passes settings.Version to the use
|
||||
case; no os.Getenv or os.LookupEnv for the version appears outside cmd/server/main.go and
|
||||
internal/infrastructure/config, and the version is captured once at startup
|
||||
And /settings is a verified route (meta.auth "verified", meta.title "Settings") rendered as
|
||||
SettingsPage inside DashboardShell, per docs/design/routes.md and docs/design/screens/settings.md
|
||||
And SettingsPage calls GET /v1/settings once per navigation to it
|
||||
And the PageHeader shows breadcrumb "Settings" (mobile title "Settings"), the page title
|
||||
"Settings" and the subtitle "Workspace information."
|
||||
And the page is at most 720 px wide, with padding 32 px on desktop and 20px 16px 88px on
|
||||
mobile, and 20 px between the header and the card
|
||||
And the page holds one card, --ui-bg with a 1 px --ui-border, 20 px padding on desktop and
|
||||
16 px on mobile, containing a single row laid out as the board frame draws it: the label at
|
||||
the left, the value at the right, vertically centred, 16 px apart
|
||||
And the label is "Version" (15 px semibold, --ui-text-highlighted) and the value is the
|
||||
server's version string in IBM Plex Mono 14 px, --ui-text-toned, with no "v" added by the
|
||||
SPA
|
||||
And the page has no action, toggle, link or input; every verified user sees it, owner and
|
||||
admin alike
|
||||
And DashboardShell's navigation gains "Settings" (icon settings, i-lucide-settings, linking to
|
||||
/settings) after "Integration", in the sidebar and in the mobile drawer, so it lists five
|
||||
destinations in this order: Home, Articles, Users, Integration, Settings
|
||||
And skeleton placeholders show only after 150 ms of loading, so a fast response never flashes
|
||||
them
|
||||
And the Loading and Error states are not drawn in docs/design; they are decided in this spec
|
||||
and covered by Vitest only (no visual state)
|
||||
|
||||
Scenario Outline: A verified caller gets the running build's version
|
||||
Given the process was started with a config.Settings whose Version is "<version>"
|
||||
And a verified user is logged in with a valid jwt cookie
|
||||
When the client sends GET /v1/settings
|
||||
Then the response status is 200
|
||||
And the response body is exactly the JSON object {"version": "<version>"}
|
||||
|
||||
Examples:
|
||||
| version |
|
||||
| v2.2.0-beta |
|
||||
| dev |
|
||||
|
||||
Scenario: The use case returns the version it was built with
|
||||
Given GetSettingsUseCase is built with the version "v2.2.0-beta"
|
||||
When GetSettings is called
|
||||
Then it returns a settings.Settings whose Version is "v2.2.0-beta"
|
||||
|
||||
Scenario: A request without a jwt cookie is refused
|
||||
When the client sends GET /v1/settings with no jwt cookie
|
||||
Then the response status is 401
|
||||
And the use case is not consulted for the response
|
||||
|
||||
Scenario: A token whose version no longer matches the account is refused
|
||||
Given user id 7 exists with token version 1
|
||||
And the jwt cookie was minted for user 7 with token version 0
|
||||
When the client sends GET /v1/settings with that cookie
|
||||
Then the response status is 401
|
||||
And the response body is apperr.UnauthorizedErrorMessage
|
||||
|
||||
Scenario: An unverified account is refused
|
||||
Given user id 8 exists with is_verified false and a valid jwt cookie
|
||||
When the client sends GET /v1/settings with that cookie
|
||||
Then the response status is 403
|
||||
And the response body is apperr.VerificationRequiredErrorMessage
|
||||
And the response does not contain the version
|
||||
|
||||
Scenario: Default — the Version row shows the server's version
|
||||
Given GET /v1/settings resolves 200 with {"version": "v2.2.0-beta"}
|
||||
When the user navigates to /settings
|
||||
Then the breadcrumb reads "Settings" (mobile title "Settings")
|
||||
And the header shows the title "Settings" and the subtitle "Workspace information."
|
||||
And the card shows one row: the label "Version" and the value "v2.2.0-beta"
|
||||
And the "Settings" nav item has aria-current="page" and no other nav item does
|
||||
|
||||
Scenario: The value is shown exactly as reported, with no prefix added
|
||||
Given GET /v1/settings resolves 200 with {"version": "dev"}
|
||||
When the user navigates to /settings
|
||||
Then the value reads "dev"
|
||||
|
||||
Scenario: Navigation — "Settings" sits after Integration
|
||||
Given the user is on /home
|
||||
When DashboardShell mounts
|
||||
Then the sidebar and the mobile drawer list Home, Articles, Users, Integration, Settings in
|
||||
that order
|
||||
And "Settings" has the settings icon and links to /settings
|
||||
|
||||
Scenario: Loading — the value becomes a skeleton after 150 ms while the request is in flight
|
||||
Given GET /v1/settings has not yet resolved
|
||||
When the user navigates to /settings
|
||||
Then the label "Version" is visible at once, and nothing stands in for the value during the
|
||||
first 150 ms
|
||||
And after 150 ms a skeleton replaces the value only, and the card is aria-busy="true"
|
||||
When GET /v1/settings then resolves 200 with {"version": "v2.2.0-beta"}
|
||||
Then the skeleton is replaced by "v2.2.0-beta" and aria-busy is no longer set
|
||||
|
||||
Scenario Outline: Error — a 500 or a network failure replaces the row with an inline alert and a retry
|
||||
Given GET /v1/settings <failure>
|
||||
When the user navigates to /settings
|
||||
Then the card shows an error alert with no close button and no title, reading "Couldn't reach
|
||||
RenewCMS. Check your connection and try again.", with an action "Try again" (icon rotate-cw)
|
||||
And the "Version" row is not shown
|
||||
And the header (title and subtitle) is still shown
|
||||
When the user activates "Try again"
|
||||
Then the Loading state shows and GET /v1/settings is sent again
|
||||
When it then resolves 200 with {"version": "v2.2.0-beta"}
|
||||
Then the Default state's row replaces the alert
|
||||
|
||||
Examples:
|
||||
| failure |
|
||||
| resolves with a 500 response |
|
||||
| never reaches the server |
|
||||
|
||||
Scenario: A 401 or an unverified 403 is handled globally, not by the page
|
||||
Given GET /v1/settings resolves 401
|
||||
When the user navigates to /settings
|
||||
Then useApi's global handling redirects to "/login?redirect=/settings&reason=expired"
|
||||
And SettingsPage shows no error alert of its own
|
||||
@@ -0,0 +1,92 @@
|
||||
# QA: Settings page and GET /v1/settings
|
||||
|
||||
## Automated Go (`task test`, package `test`, black-box)
|
||||
|
||||
Files: `test/settings_test.go` (use case) and `test/settings_http_test.go` (HTTP). Fakes: reuse
|
||||
`mockUserRepo` from `test/user_test.go`; no settings repository exists, so none is needed. Name any
|
||||
new fake uniquely, since all fakes share one package.
|
||||
|
||||
Setup shared by the HTTP cases:
|
||||
1. Call `validator.InitValidator()` once.
|
||||
2. Build `token := jwtauth.New("HS256", []byte("test-secret"), nil)`, `getUser :=
|
||||
useCases.NewGetUserUseCase(userRepo)` and `settingsHandler :=
|
||||
settings.NewHandler(useCases.NewGetSettingsUseCase(version))`.
|
||||
3. Mount on a fresh `chi.NewRouter()` mirroring `NewSettingsRouter`:
|
||||
`r.Route("/v1/settings", ...)` with `jwtauth.Verifier(token)`, `jwtauth.Authenticator(token)`,
|
||||
`middleware.RequireCurrentToken(getUser)`, `middleware.RequireVerifiedUser(getUser)`, then
|
||||
`r.Get("/", settingsHandler.GetSettings)`. No live server, no SQLite, no SMTP.
|
||||
4. Mint cookies with `token.Encode(map[string]any{"user_id": uint32(id), "token_version":
|
||||
uint32(v)})`, as in `test/stale_token_invalidation_http_test.go`.
|
||||
|
||||
Cases:
|
||||
5. Use case, table-driven over versions "v2.2.0-beta" and "dev": `NewGetSettingsUseCase(v)
|
||||
.GetSettings().Version == v`. The rule that `internal/domain/settings` and the use case do not
|
||||
import `config`, `os`, `net/http` or `gorm` is a review item, not a Go test.
|
||||
6. Verified user id 7 (`IsVerified: true`, `TokenVersion: 0`), matching cookie, version
|
||||
"v2.2.0-beta": `GET /v1/settings` returns 200 and the body decodes to exactly one key
|
||||
`version` equal to "v2.2.0-beta" (decode into `map[string]any` and assert `len == 1`). Repeat
|
||||
with version "dev".
|
||||
7. No cookie: 401. Do not assert an exact body (it is produced by `jwtauth.Authenticator`).
|
||||
8. User 7 with `TokenVersion: 1`, cookie encoding `token_version: 0`: 401, body exactly
|
||||
`apperr.UnauthorizedErrorMessage`.
|
||||
9. User 8 with `IsVerified: false`, matching cookie: 403, body exactly
|
||||
`apperr.VerificationRequiredErrorMessage`, and the body does not contain the version string.
|
||||
10. After building the router, call `t.Setenv("ENVIRONMENT", "development")` and re-request as in
|
||||
case 6: the response is unchanged (nothing is read from the environment at request time).
|
||||
|
||||
## Automated SPA (Vitest + Vue Test Utils, `task test:web`)
|
||||
|
||||
Mock `useApi`'s `request` for `GET /v1/settings`. Mount SettingsPage inside DashboardShell with a
|
||||
resolved verified session and a test router at /settings. Use fake timers for the 150 ms cases. No
|
||||
test freezes the system date.
|
||||
|
||||
1. **Default** — resolve `{ version: 'v2.2.0-beta' }`; assert breadcrumb "Settings", title
|
||||
"Settings", subtitle "Workspace information.", one row with the label "Version" and the value
|
||||
exactly "v2.2.0-beta", `request` called once with `/v1/settings`, and no button, link, switch or
|
||||
input inside the card.
|
||||
2. **No prefix** — resolve `{ version: 'dev' }`; assert the value text is exactly "dev".
|
||||
3. **Navigation** — assert the sidebar and the drawer list Home, Articles, Users, Integration,
|
||||
Settings in that order with the settings icon on the last; on /settings only "Settings" carries
|
||||
`aria-current="page"`.
|
||||
4. **Loading** — unresolved request; at 149 ms assert no skeleton and that "Version" is present; at
|
||||
150 ms assert a skeleton in place of the value and `aria-busy="true"` on the card; resolve and
|
||||
assert the value and no `aria-busy`.
|
||||
5. **Error, Examples-driven** — reject with `ApiError(500)`, then `NetworkError`; assert the alert
|
||||
text "Couldn't reach RenewCMS. Check your connection and try again.", the "Try again" action, no
|
||||
close button, no title and no "Version" row, with the header still shown. Click "Try again";
|
||||
assert Loading and a second `request`; resolve and assert the Default row.
|
||||
6. **401** — reject with `ApiError(401)`; assert the page shows no alert of its own (the redirect
|
||||
itself is covered by `useApi`'s existing tests).
|
||||
7. **Router meta** — assert the `/settings` route has `meta.auth === 'verified'` and
|
||||
`meta.title === 'Settings'`.
|
||||
8. **Amended shell specs** — update the existing Vitest for `nuxt_ui_shell_foundation` case 14,
|
||||
`dashboard_shell_branding`, users_screen and profile_page to five nav items, in the same change.
|
||||
|
||||
## Visual (`task visual:check -- settings`, desktop and mobile, light and dark)
|
||||
|
||||
Reference: `docs/design/screens/settings.md`; board group "Settings" in `docs/design/source/RenewCMS
|
||||
Screens Back-office.dc.html`; exact values from the `isSettings` block of
|
||||
`docs/design/source/AppFrame.dc.html`. Add the mock to `web/visual/states.ts`. The gate is hard:
|
||||
every capture within 10 %.
|
||||
|
||||
- [ ] Default: mock `GET /v1/settings` returning `{ "version": "v2.2.0-beta" }` (the board's
|
||||
sample). Layout as the frame draws it: the header is a 20/28 semibold title with the muted
|
||||
subtitle 2 px below; the card is the header's next sibling, 20 px below; the card is
|
||||
`--ui-bg` with a 1 px `--ui-border` and no drawn radius; the row is a flex row with
|
||||
`justify-content: space-between`, `align-items: center` and a 16 px gap, the label at the
|
||||
left and the mono value at the right; the whole page is at most 720 px wide.
|
||||
- [ ] Not drawn, no visual state: Loading and Error. Do not add them to `web/visual/states.ts`;
|
||||
Vitest cases 4 and 5 cover them.
|
||||
- [ ] Not covered by screenshots: keyboard order (Tab reaches the "Settings" nav item; on Error,
|
||||
Tab reaches "Try again", and Enter and Space activate it); focus after a navigation; the
|
||||
skeleton pulse under reduced motion.
|
||||
|
||||
## Manual verification
|
||||
|
||||
- With a build made through `task build` or `task release` (`-ldflags -X main.version=...`),
|
||||
open /settings and confirm the value equals the injected version. A build without the flag must
|
||||
show `dev`.
|
||||
- Confirm against a running `task dev` API that an unverified account is sent to /register/pending
|
||||
and a logged-out browser to /login?redirect=/settings&reason=expired.
|
||||
- Confirm the embedded SPA (built without `-tags nofrontend`) serves /settings on a hard refresh,
|
||||
since Vitest never exercises the embedded frontend.
|
||||
@@ -18,8 +18,8 @@ Feature: Users screen and InviteUserModal
|
||||
and rows are keyed by kind and id, because a user and an invite can share a numeric id
|
||||
And DashboardShell's navigation gains "Users" (icon users, linking to /users) between
|
||||
"Articles" and "Integration", in the sidebar and in the mobile drawer, so it lists Home,
|
||||
Articles, Users, Integration in that order; "Users" has aria-current="page" on /users; the
|
||||
"four navigation destinations" scenario of nuxt_ui_shell_foundation.feature and the items of
|
||||
Articles, Users, Integration, Settings in that order; "Users" has aria-current="page" on /users; the
|
||||
"five navigation destinations" scenario of nuxt_ui_shell_foundation.feature and the items of
|
||||
dashboard_shell_branding.feature are amended accordingly
|
||||
And the PageHeader shows breadcrumb "Users" (mobile title "Users")
|
||||
And the page header is the title "Users" with the subtitle "Everyone who can sign in and work
|
||||
@@ -108,7 +108,7 @@ Feature: Users screen and InviteUserModal
|
||||
Scenario: Navigation — "Users" sits between Articles and Integration
|
||||
Given the user is on /users
|
||||
When DashboardShell mounts
|
||||
Then the sidebar and the mobile drawer list Home, Articles, Users, Integration in that order
|
||||
Then the sidebar and the mobile drawer list Home, Articles, Users, Integration, Settings in that order
|
||||
And "Users" has the users icon, links to /users and has aria-current="page"
|
||||
|
||||
Scenario: Loading — skeleton rows after 150 ms while the first request, or a retry, is in flight
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
The invitations API is already built and covered by features/invitations.qa.md: this slice has no
|
||||
Go change, so it has no HTTP-level Go section.
|
||||
|
||||
The shell specs were amended to four destinations (Home, Articles, Users, Integration):
|
||||
The shell specs were amended to five destinations (Home, Articles, Users, Integration, Settings):
|
||||
nuxt_ui_shell_foundation.feature / .qa.md (case 14) and dashboard_shell_branding.feature. Their
|
||||
Vitest tests must be updated in the same change.
|
||||
|
||||
@@ -25,8 +25,8 @@ part). Run each layout case at a desktop and a below-md width (mock `matchMedia`
|
||||
1. **Default** — assert breadcrumb "Users", title, subtitle, "Invite user", rows in fixture order,
|
||||
"Inès Duarte" with "ines" beside it, the "you" tag only on the "marta" row, badge words and
|
||||
the fixture's absolute dates; assert no accepted row shows an email.
|
||||
2. **Navigation** — assert the sidebar and the drawer list Home, Articles, Users, Integration in
|
||||
that order, and "Users" carries aria-current="page" on /users.
|
||||
2. **Navigation** — assert the sidebar and the drawer list Home, Articles, Users, Integration,
|
||||
Settings in that order, and "Users" carries aria-current="page" on /users.
|
||||
3. **Loading** — unresolved `GET`; with fake timers assert nothing is drawn at 149 ms; at 150 ms
|
||||
assert 5 skeleton rows and `aria-busy="true"`; resolve and assert the Default rows.
|
||||
4. **Only you** — a single "marta" item; assert the row, the hint text and "Invite user".
|
||||
|
||||
Reference in new issue
Block a user