176 lines
4.9 KiB
TypeScript
176 lines
4.9 KiB
TypeScript
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()
|
|
})
|
|
})
|