- account_internal_test: metadata subtitle reader and writer (NULL, blank, null, [], invalid JSON, non-objects, key preservation, NULL on the last key), the attachOne contract, avatar cap, unique-violation mapping, English fallbacks, and 401 for non-backend principals on all three routes - account_test (Postgres): profile payload incl. copied WinterCMS metadata and avatar rows, trimmed profile saves and metadata keys, the forbidden-key allow-list with unchanged rows, email rules, password rules, the failed attempt log, Bearer and cookie session effects, CSRF, avatar upload, refusals (SVG, HTML as PNG, size, part shape, 413, no bucket) with no row or blob left, replace and remove with blob cleanup - lagoon: TestBackendAdminMetadataColumn covers type, Winter row round trip, rollback of only the column and re-migrate - SPA: AccountView, the user menu account item and avatar image, setCurrentUser and profileInitials; lang fixture gains myaccount keys
132 lines
5.8 KiB
TypeScript
132 lines
5.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } 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 { currentUser } from '../../src/state/useAuth'
|
|
import { API, profile, requestsTo, resetState, routerAt, signIn, wait } from '../helpers'
|
|
|
|
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')
|
|
})
|
|
})
|