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

This commit is contained in:
fsylvain committed 2026-09-29 23:31:04 +02:00
1 parent 6dc47789ef
commit 27dadbbfa6
6 files changed
+68 -20

No files matched your search

+15 -3
View File
@@ -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
+11 -6
View File
@@ -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.
+5 -1
View File
@@ -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'),
},
],
@@ -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<HTMLElement>('[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()
+13 -5
View File
@@ -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'],
-5
View File
@@ -779,11 +779,6 @@ export const visualStates: Record<string, Record<string, VisualState>> = {
'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',