import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { mount, flushPromises } from '@vue/test-utils' import uiPlugin from '@nuxt/ui/vue-plugin' import UserMenu from '../UserMenu.vue' import { useColorMode } from '@/composables/useColorMode' function mountMenu(props: { username: string; collapsed: boolean }) { return mount(UserMenu, { attachTo: document.body, props, global: { plugins: [uiPlugin], }, }) } function findTriggerButton() { return document.body.querySelector('button') } async function openMenu() { findTriggerButton()?.click() await flushPromises() } function findMenuItemsByRole(role: string) { return Array.from(document.body.querySelectorAll(`[role="${role}"]`)) } function findCheckboxItem(label: string) { return findMenuItemsByRole('menuitemcheckbox').find( (el) => el.textContent?.trim() === label, ) } beforeEach(() => { localStorage.clear() document.documentElement.classList.remove('dark', 'light') }) afterEach(() => { document.body.innerHTML = '' }) describe("UserMenu's dropdown lists the account label, color-mode checkboxes, then Log out", () => { it('shows avatar initials MA, the username, and the three ordered groups', async () => { const wrapper = mountMenu({ username: 'marta', collapsed: false }) const avatar = document.body.querySelector('[data-slot="fallback"]') expect(avatar?.textContent).toBe('MA') expect(avatar?.getAttribute('aria-hidden')).toBe('true') expect(wrapper.text()).toContain('marta') await openMenu() const groups = findMenuItemsByRole('group') expect(groups.length).toBe(3) expect(groups[0].textContent).toContain('marta') const checkboxLabels = findMenuItemsByRole('menuitemcheckbox').map( (el) => el.textContent?.trim(), ) expect(checkboxLabels).toEqual(['System', 'Light', 'Dark']) expect(groups[2].textContent?.trim()).toBe('Log out') wrapper.unmount() }) it('derives initials from the first letter of the first two words for a multi-word username', () => { const wrapper = mountMenu({ username: 'jane doe', collapsed: false }) const avatar = document.body.querySelector('[data-slot="fallback"]') expect(avatar?.textContent).toBe('JD') wrapper.unmount() }) it('checks exactly the item matching the current color-mode preference', async () => { const colorMode = useColorMode() colorMode.preference = 'dark' const wrapper = mountMenu({ username: 'marta', collapsed: false }) await openMenu() expect(findCheckboxItem('Dark')?.getAttribute('aria-checked')).toBe( 'true', ) expect(findCheckboxItem('Light')?.getAttribute('aria-checked')).toBe( 'false', ) expect(findCheckboxItem('System')?.getAttribute('aria-checked')).toBe( 'false', ) wrapper.unmount() }) }) describe('Selecting a color-mode item updates the preference and its checked state', () => { it.each([ ['system', 'light'], ['light', 'dark'], ['dark', 'system'], ])('moves from %s to %s', async (from, to) => { const colorMode = useColorMode() colorMode.preference = from as 'system' | 'light' | 'dark' const wrapper = mountMenu({ username: 'marta', collapsed: false }) await openMenu() const label = to.charAt(0).toUpperCase() + to.slice(1) const fromLabel = from.charAt(0).toUpperCase() + from.slice(1) findCheckboxItem(label)?.dispatchEvent( new MouseEvent('pointerup', { bubbles: true }), ) findCheckboxItem(label)?.dispatchEvent( new MouseEvent('click', { bubbles: true }), ) await flushPromises() expect(colorMode.preference).toBe(to) await openMenu() expect(findCheckboxItem(label)?.getAttribute('aria-checked')).toBe( 'true', ) expect(findCheckboxItem(fromLabel)?.getAttribute('aria-checked')).toBe( 'false', ) wrapper.unmount() }) }) describe('A successful logout clears the session and navigates to /login', () => { it('emits logout when the user activates Log out', async () => { const wrapper = mountMenu({ username: 'marta', collapsed: false }) await openMenu() const logOutItem = Array.from( document.body.querySelectorAll('[role="menuitem"]'), ).find((el) => el.textContent?.trim() === 'Log out') ;(logOutItem as HTMLElement)?.click() await flushPromises() expect(wrapper.emitted('logout')).toBeTruthy() wrapper.unmount() }) }) describe('Collapsed sidebar hides the wordmark and username text but keeps an accessible name', () => { it('hides the visible username but keeps it in the accessible name, and opens the same dropdown', async () => { const wrapper = mountMenu({ username: 'marta', collapsed: true }) expect(wrapper.text()).not.toContain('marta') expect(findTriggerButton()?.getAttribute('aria-label')).toBe( 'marta — account menu', ) expect( document.body.querySelector('[data-slot="fallback"]')?.textContent, ).toBe('MA') await openMenu() const groups = findMenuItemsByRole('group') expect(groups.length).toBe(3) expect(groups[0].textContent).toContain('marta') wrapper.unmount() }) })