- theme: light and dark ignore prefers-color-scheme, choices and guard - i18n: ?locale= only with a preference, availableLocales from meta - usePreferences: cache validation, cycle order and PUT bodies, revert with a danger toast, profile apply, setLocale save, cache and reload - rail theme button, user menu language group, --shell-offset and the form save bar offset; fixture strings and resetState theme reset
192 lines
8.7 KiB
TypeScript
192 lines
8.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
import type { AdminProfile } from '../../src/api/types'
|
|
import UserMenu from '../../src/components/shell/UserMenu.vue'
|
|
import { loadStrings } from '../../src/app/i18n'
|
|
import { currentUser } from '../../src/state/useAuth'
|
|
import { page } from '../../src/state/usePreferences'
|
|
import { API, mockApi, profile, requestsTo, resetState, routerAt, signIn, wait } from '../helpers'
|
|
import { langFixture } from '../fixtures/typed'
|
|
|
|
async function mountMenu(user: Partial<AdminProfile> | null) {
|
|
let calls: Request[] = []
|
|
if (user) {
|
|
calls = await signIn({
|
|
[`GET ${API}/auth/me`]: { body: { data: { ...profile.data, ...user }, meta: {} } },
|
|
[`POST ${API}/auth/logout`]: { body: { data: { status: 'logged_out' }, meta: {} } },
|
|
})
|
|
}
|
|
const router = await routerAt('/acme/demo/widgets')
|
|
const wrapper = mount(UserMenu, { global: { plugins: [router] }, attachTo: document.body })
|
|
await flushPromises()
|
|
return { wrapper, router, calls }
|
|
}
|
|
|
|
async function open(wrapper: ReturnType<typeof mount>): Promise<HTMLElement> {
|
|
const trigger = wrapper.find('[data-user-menu]')
|
|
await trigger.trigger('keydown', { key: 'Enter' })
|
|
await flushPromises()
|
|
const content = document.querySelector<HTMLElement>('[data-user-menu-content]')
|
|
expect(content).not.toBeNull()
|
|
return content!
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetState()
|
|
})
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = ''
|
|
})
|
|
|
|
enableAutoUnmount(afterEach)
|
|
|
|
describe('user menu', () => {
|
|
it('labels the trigger with the name and shows initials, name and role', async () => {
|
|
const { wrapper } = await mountMenu({})
|
|
const trigger = wrapper.find('[data-user-menu]')
|
|
expect(trigger.attributes('aria-label')).toBe('Menu użytkownika: Dana Dev')
|
|
expect(trigger.attributes('aria-haspopup')).toBe('menu')
|
|
expect(trigger.attributes('disabled')).toBeUndefined()
|
|
expect(wrapper.find('[data-user-initials]').text()).toBe('DD')
|
|
expect(wrapper.find('[data-user-initials]').attributes('aria-hidden')).toBe('true')
|
|
expect(wrapper.find('[data-user-role]').text()).toBe('Developer')
|
|
})
|
|
|
|
it('falls back to the login for the name and initials and hides a missing role', async () => {
|
|
const { wrapper } = await mountMenu({ first_name: ' ', last_name: '', login: 'root', role: undefined })
|
|
expect(wrapper.find('[data-user-identity]').text()).toBe('root')
|
|
expect(wrapper.find('[data-user-initials]').text()).toBe('RO')
|
|
expect(wrapper.find('[data-user-role]').exists()).toBe(false)
|
|
})
|
|
|
|
it('uses a single initial for a single name', async () => {
|
|
const { wrapper } = await mountMenu({ first_name: 'ada', last_name: '' })
|
|
expect(wrapper.find('[data-user-initials]').text()).toBe('A')
|
|
})
|
|
|
|
it('is disabled without a signed-in admin', async () => {
|
|
const { wrapper } = await mountMenu(null)
|
|
expect(currentUser.value).toBeNull()
|
|
expect(wrapper.find('[data-user-menu]').attributes('disabled')).toBeDefined()
|
|
expect(wrapper.find('[data-user-initials]').text()).toBe('')
|
|
})
|
|
|
|
it('opens My account from the menu', async () => {
|
|
const { wrapper, router } = await mountMenu({})
|
|
const content = await open(wrapper)
|
|
const items = [...content.querySelectorAll<HTMLElement>('[role="menuitem"]')].map((item) => item.dataset.action)
|
|
expect(items).toEqual(['account', 'logout'])
|
|
const item = content.querySelector<HTMLElement>('[data-action="account"]')!
|
|
expect(item.getAttribute('role')).toBe('menuitem')
|
|
expect(item.textContent?.trim()).toBe('Moje konto')
|
|
|
|
item.click()
|
|
await flushPromises()
|
|
await wait(0)
|
|
await flushPromises()
|
|
expect(router.currentRoute.value.name).toBe('account')
|
|
expect(router.currentRoute.value.fullPath).toBe('/account')
|
|
expect(currentUser.value).not.toBeNull()
|
|
})
|
|
|
|
it('shows the avatar thumbnail in place of the initials in the trigger and the menu', async () => {
|
|
const avatar = { url: '/storage/uploads/a/b/c/abc.png', thumb_url: '/storage/uploads/a/b/c/thumb_1_250_250_0_0_crop.png' }
|
|
const { wrapper } = await mountMenu({ avatar })
|
|
const image = wrapper.find('[data-user-avatar]')
|
|
expect(image.attributes('src')).toBe(avatar.thumb_url)
|
|
expect(image.attributes('alt')).toBe('')
|
|
expect(image.classes()).toEqual(expect.arrayContaining(['rounded-full', 'object-cover', 'size-[34px]']))
|
|
expect(wrapper.find('[data-user-initials]').exists()).toBe(false)
|
|
const content = await open(wrapper)
|
|
const header = content.querySelector<HTMLImageElement>('img[data-user-avatar]')!
|
|
expect(header.getAttribute('src')).toBe(avatar.thumb_url)
|
|
expect(header.classList.contains('size-10')).toBe(true)
|
|
})
|
|
|
|
it('falls back to the full image without a thumbnail, and to the initials without an avatar', async () => {
|
|
const { wrapper } = await mountMenu({ avatar: { url: '/storage/uploads/a/b/c/abc.png' } })
|
|
expect(wrapper.find('[data-user-avatar]').attributes('src')).toBe('/storage/uploads/a/b/c/abc.png')
|
|
wrapper.unmount()
|
|
resetState()
|
|
const plain = await mountMenu({})
|
|
expect(plain.wrapper.find('[data-user-avatar]').exists()).toBe(false)
|
|
expect(plain.wrapper.find('[data-user-initials]').text()).toBe('DD')
|
|
})
|
|
|
|
it('opens a menu with the e-mail and logs out to the login route', async () => {
|
|
const { wrapper, router, calls } = await mountMenu({})
|
|
const content = await open(wrapper)
|
|
expect(content.getAttribute('role')).toBe('menu')
|
|
expect(content.textContent).toContain('dev@example.test')
|
|
const item = content.querySelector<HTMLElement>('[data-action="logout"]')!
|
|
expect(item.getAttribute('role')).toBe('menuitem')
|
|
expect(item.textContent?.trim()).toBe('Wyloguj')
|
|
|
|
item.click()
|
|
await flushPromises()
|
|
await wait(0)
|
|
await flushPromises()
|
|
expect(requestsTo(calls, 'POST', `${API}/auth/logout`)).toHaveLength(1)
|
|
expect(currentUser.value).toBeNull()
|
|
expect(router.currentRoute.value.name).toBe('login')
|
|
})
|
|
|
|
describe('language', () => {
|
|
/** Loads the bundle with the given admin locales, then mounts the menu. */
|
|
async function mountWithLocales(locales: string[], preference = '') {
|
|
mockApi({
|
|
[`GET ${API}/lang`]: { body: { data: langFixture.data, meta: { locale: 'pl', locales } } },
|
|
})
|
|
await loadStrings()
|
|
vi.restoreAllMocks()
|
|
return mountMenu({ preferences: { theme: 'system', locale: preference } })
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('lists every admin locale named in its own language, the current one checked', async () => {
|
|
const { wrapper } = await mountWithLocales(['en', 'pl'])
|
|
const content = await open(wrapper)
|
|
expect(content.querySelector('[data-language-label]')?.textContent?.trim()).toBe('Język')
|
|
const options = [...content.querySelectorAll<HTMLElement>('[data-locale-option]')]
|
|
expect(options.map((option) => option.dataset.localeOption)).toEqual(['en', 'pl'])
|
|
expect(options.map((option) => option.textContent?.trim())).toEqual(['English', 'Polski'])
|
|
expect(options.map((option) => option.getAttribute('role'))).toEqual(['menuitemradio', 'menuitemradio'])
|
|
expect(options.map((option) => option.getAttribute('aria-checked'))).toEqual(['false', 'true'])
|
|
})
|
|
|
|
it('checks the stored preference over the bundle locale', async () => {
|
|
const { wrapper } = await mountWithLocales(['en', 'pl'], 'en')
|
|
const content = await open(wrapper)
|
|
const en = content.querySelector<HTMLElement>('[data-locale-option="en"]')!
|
|
expect(en.getAttribute('aria-checked')).toBe('true')
|
|
})
|
|
|
|
it('saves the chosen language and reloads the page', async () => {
|
|
const { wrapper } = await mountWithLocales(['en', 'pl'])
|
|
const reload = vi.spyOn(page, 'reload').mockImplementation(() => undefined)
|
|
const calls = mockApi({
|
|
[`PUT ${API}/account/preferences`]: { body: { data: { theme: 'system', locale: 'en' }, meta: {} } },
|
|
})
|
|
const content = await open(wrapper)
|
|
content.querySelector<HTMLElement>('[data-locale-option="en"]')!.click()
|
|
await flushPromises()
|
|
const puts = requestsTo(calls, 'PUT', `${API}/account/preferences`)
|
|
expect(puts).toHaveLength(1)
|
|
expect(JSON.parse(await puts[0]!.clone().text())).toEqual({ locale: 'en' })
|
|
expect(window.localStorage.getItem('summer-admin.locale')).toBe('en')
|
|
expect(reload).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('has no language group with a single admin locale', async () => {
|
|
const { wrapper } = await mountWithLocales(['pl'])
|
|
const content = await open(wrapper)
|
|
expect(content.querySelector('[data-locale-option]')).toBeNull()
|
|
expect(content.querySelector('[data-language-label]')).toBeNull()
|
|
})
|
|
})
|
|
})
|