From 27dadbbfa691c2aac79a769057c7842f347edcd8 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Tue, 29 Sep 2026 23:31:04 +0200 Subject: [PATCH] fix: account menu matches the redesigned board: System, Light, Dark stay in the menu and the sheet, and the visual-check exception on profile "Account menu" in web/visual/states.ts is removed --- features/profile_page.feature | 18 +++++++++++--- features/profile_page.qa.md | 17 ++++++++----- web/src/components/UserMenu.vue | 6 ++++- .../__tests__/DashboardShell.spec.ts | 24 +++++++++++++++++++ web/src/components/__tests__/UserMenu.spec.ts | 18 ++++++++++---- web/visual/states.ts | 5 ---- 6 files changed, 68 insertions(+), 20 deletions(-) diff --git a/features/profile_page.feature b/features/profile_page.feature index 7551f09..143f0f7 100644 --- a/features/profile_page.feature +++ b/features/profile_page.feature @@ -92,7 +92,7 @@ Feature: Profile page, display name and the account menu checked (kept from the shipped menu), a separator, and "Log out" (icon log-out); items are 32 px on desktop and 48 px at 15 px text on mobile And activating "Profile" navigates to /profile and closes the menu (when already on /profile - it only closes); activating a theme item sets the preference and closes the menu; + it only closes); activating a theme item sets the preference and, in the desktop popover, keeps the menu open (the mobile sheet closes); activating "Log out" behaves as today: POST /v1/auth/logout in a try/finally, then session.clear() and a navigation to /login And avatar initials are the uppercased first Unicode character of each of the first two words @@ -296,7 +296,10 @@ Feature: Profile page, display name and the account menu And its header shows "marta" in semibold and "marta@renew.example" in muted 12 px And below it are "Profile" (circle-user), a separator, "System", "Light", "Dark" (the current preference checked), a separator, and "Log out" (log-out) + And every item is 32 px high And "Profile" is not highlighted + And "System", "Light" and "Dark" form one group where choosing one applies the theme at once + and keeps the menu open Scenario: Account menu — opened from the drawer on mobile Given the user is on /home on a viewport below lg and the navigation drawer is open @@ -305,8 +308,17 @@ Feature: Profile page, display name and the account menu with the Home page behind it And its header shows a 36 px avatar "MA", the name "marta" at 15 px semibold and the email "marta@renew.example" muted - And its items are 48 px high: "Profile", a separator, "System", "Light", "Dark", a separator, - "Log out" + And its items are 48 px high at 15 px text: "Profile", a separator, "System", "Light", "Dark", + a separator, "Log out" + And the sheet has the same items in the same order as the desktop popover, so it is no taller + than the board's sheet + + Scenario: Account menu matches the board with no visual exception + Given web/visual/states.ts has no exception on the profile state "Account menu" + When `task visual:check -- profile "Account menu"` runs at desktop and mobile, light and dark + Then every capture is within 10 % + And System, Light and Dark are present in the popover and in the sheet + And item heights are 32 px on desktop and 48 px on mobile Scenario Outline: Closing the menu Given the account menu is open diff --git a/features/profile_page.qa.md b/features/profile_page.qa.md index d55759c..0f65f69 100644 --- a/features/profile_page.qa.md +++ b/features/profile_page.qa.md @@ -102,12 +102,17 @@ capture must be within 10 %. Base session mock: GET /v1/auth/me 200 with id 1, u "marta", email "marta@renew.example", is_verified true, display_name "" (state-specific overrides below). -- [ ] Account menu: /home with `now: boardNow` and the home Default fixtures, session as above, - menu opened by clicking the chip (on mobile: open the drawer, then the account row). Name - "marta", initials "MA", email "marta@renew.example". The theme items kept by user decision - are not drawn on the board, so the mobile capture is expected to exceed 10 %. The user has - approved a per-state `exception` for it, to be added with `approvedBy` only after the - real difference is measured and reported to the user with its cause. +- [ ] Account menu: /home with `now: boardNow` and the home Default fixtures, session as above + (id 1, "marta", "marta@renew.example", display_name ""), menu opened by clicking the chip + (on mobile: open the drawer, then the account row). Name "marta", initials "MA", email + "marta@renew.example". The board now draws System, Light and Dark, with Light checked in + its sample. The `exception` block is removed from web/visual/states.ts, so the default 10 % + gate applies. `task visual:check -- profile "Account menu"` must PASS on all four captures + (desktop and mobile, light and dark). If the only remaining diff is the board's Profile + highlight on Home (the app highlights Profile only on /profile), do NOT re-add an exception + and do NOT highlight Profile on Home. Stop and ask the user. +- [ ] Manual: item heights measured 32 px (desktop popover) and 48 px (mobile sheet); arrow keys move + between items; Escape and outside click close; choosing a theme keeps the menu open. - [ ] Loading: /profile with GET /v1/auth/me hanging; three skeleton cards, 16 px apart. - [ ] Default, no display name: empty field, "0 / 60", the username footer, Save at 40 %. - [ ] Editing: type "Marta Alves"; field focused; "11 / 60"; "Unsaved changes"; Save enabled. diff --git a/web/src/components/UserMenu.vue b/web/src/components/UserMenu.vue index 316988e..a0fb278 100644 --- a/web/src/components/UserMenu.vue +++ b/web/src/components/UserMenu.vue @@ -4,6 +4,7 @@ import { useMediaQuery } from '@vueuse/core' import { colorModeOptions, useAccountMenu } from '@/composables/useAccountMenu' const DESKTOP_QUERY = '(min-width: 64rem)' +const ITEM_CLASS = 'h-8 py-0' const props = defineProps<{ username: string @@ -60,7 +61,7 @@ const items = computed(() => [ { label: 'Profile', icon: 'i-lucide-circle-user', - class: onProfile.value ? 'bg-elevated' : undefined, + class: onProfile.value ? `${ITEM_CLASS} bg-elevated` : ITEM_CLASS, onSelect: goToProfile, }, ], @@ -68,6 +69,8 @@ const items = computed(() => [ type: 'checkbox' as const, label: option.label, checked: colorMode.preference === option.value, + class: ITEM_CLASS, + onSelect: (event: Event) => event.preventDefault(), onUpdateChecked: (checked: boolean) => { if (checked) { colorMode.preference = option.value @@ -78,6 +81,7 @@ const items = computed(() => [ { label: 'Log out', icon: 'i-lucide-log-out', + class: ITEM_CLASS, onSelect: () => emit('logout'), }, ], diff --git a/web/src/components/__tests__/DashboardShell.spec.ts b/web/src/components/__tests__/DashboardShell.spec.ts index d83c95a..b924e98 100644 --- a/web/src/components/__tests__/DashboardShell.spec.ts +++ b/web/src/components/__tests__/DashboardShell.spec.ts @@ -4,6 +4,7 @@ import { createPinia, setActivePinia } from 'pinia' import uiPlugin from '@nuxt/ui/vue-plugin' import router from '@/router' import { useApi } from '@/composables/useApi' +import { useColorMode } from '@/composables/useColorMode' import DashboardShell from '../DashboardShell.vue' import { DESKTOP_WIDTH, MOBILE_WIDTH, setViewportWidth } from './viewport' @@ -272,6 +273,29 @@ describe('Account menu: opened from the drawer on mobile', () => { expect(document.body.querySelector('[role="menu"]')).toBeNull() }) + it('applies a theme choice and closes the sheet', async () => { + const colorMode = useColorMode() + colorMode.preference = 'system' + await mountShell('/home', { width: MOBILE_WIDTH }) + ;(document.body.querySelector('[data-slot="toggle"]') as HTMLButtonElement).click() + await flushPromises() + ;( + document.body.querySelector( + '[role="dialog"] button[aria-haspopup]', + ) as HTMLButtonElement + ).click() + await flushPromises() + + const dark = Array.from( + document.body.querySelectorAll('[role="menu"] [role="menuitemcheckbox"]'), + ).find((item) => item.textContent?.trim() === 'Dark')! + dark.click() + await flushPromises() + + expect(colorMode.preference).toBe('dark') + expect(document.body.querySelector('[role="menu"]')).toBeNull() + }) + it('logs out from the sheet', async () => { await mountShell('/home', { width: MOBILE_WIDTH }) ;(document.body.querySelector('[data-slot="toggle"]') as HTMLButtonElement).click() diff --git a/web/src/components/__tests__/UserMenu.spec.ts b/web/src/components/__tests__/UserMenu.spec.ts index 80554dd..6af1ec0 100644 --- a/web/src/components/__tests__/UserMenu.spec.ts +++ b/web/src/components/__tests__/UserMenu.spec.ts @@ -236,7 +236,7 @@ describe('Menu items', () => { }) describe('Theme choice stays in the menu', () => { - it('checks the current preference, sets Dark, closes, and shows Dark checked on reopening', async () => { + it('checks the current preference, sets Dark at once and keeps the menu open', async () => { const colorMode = useColorMode() colorMode.preference = 'system' await mountMenu() @@ -248,15 +248,23 @@ describe('Theme choice stays in the menu', () => { await flushPromises() expect(colorMode.preference).toBe('dark') - expect(menu()).toBeNull() - - await openMenu() - + expect(menu()).not.toBeNull() expect(findItem('Dark').getAttribute('aria-checked')).toBe('true') expect(findItem('System').getAttribute('aria-checked')).toBe('false') }) }) +describe('Item heights', () => { + it('gives every desktop item 32 px', async () => { + await mountMenu() + await openMenu() + + for (const item of itemsIn(menu()!)) { + expect(item.classList.contains('h-8')).toBe(true) + } + }) +}) + describe('Avatar initials', () => { it.each([ ['marta', '', 'MA', 'marta'], diff --git a/web/visual/states.ts b/web/visual/states.ts index 9e1fb0c..824672e 100644 --- a/web/visual/states.ts +++ b/web/visual/states.ts @@ -779,11 +779,6 @@ export const visualStates: Record> = { 'GET /v1/article/': { status: 200, body: sixArticlesFixture }, }, steps: openAccountMenu, - exception: { - maxPercent: 26, - reason: 'Mobile sheet is ~153px taller than the board because the System/Light/Dark items are kept by user decision, and the board draws Profile highlighted on /home while the feature highlights it only on /profile', - approvedBy: 'Florian Sylvain, 2026-09-29', - }, }, Loading: { path: '/profile',