Files
RenewCMS/web/src/components/__tests__/AccountSheetFocus.spec.ts
T

135 lines
3.5 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { mount, flushPromises, type VueWrapper } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import uiPlugin from '@nuxt/ui/vue-plugin'
import router from '@/router'
import { useApi } from '@/composables/useApi'
import DashboardShell from '../DashboardShell.vue'
import { MOBILE_WIDTH, setViewportWidth } from './viewport'
vi.mock('@/composables/useApi', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/composables/useApi')>()
return {
...actual,
useApi: vi.fn(),
}
})
let mountedShell: VueWrapper | null = null
async function mountMobileShell() {
localStorage.clear()
setViewportWidth(MOBILE_WIDTH)
setActivePinia(createPinia())
vi.mocked(useApi).mockReturnValue({
getMe: vi.fn().mockResolvedValue({
verified: true,
username: 'marta',
email: 'marta@renew.example',
displayName: '',
}),
request: vi.fn().mockResolvedValue(undefined),
})
await router.push('/home')
mountedShell = mount(DashboardShell, {
attachTo: document.body,
global: { plugins: [router, uiPlugin] },
})
await flushPromises()
}
function drawerChip() {
return document.body.querySelector<HTMLButtonElement>('[role="dialog"] button[aria-haspopup]')
}
function sheet() {
return document.body.querySelector('[role="menu"]')
}
function menuItem(label: string) {
return Array.from(
document.body.querySelectorAll<HTMLElement>('[role="menu"] [role="menuitem"]'),
).find((item) => item.textContent?.trim() === label)!
}
async function openDrawer() {
document.body.querySelector<HTMLButtonElement>('[data-slot="toggle"]')!.click()
await flushPromises()
}
async function openSheetFromChip() {
await openDrawer()
drawerChip()!.click()
await flushPromises()
}
const closingActions = [
{
name: 'pressing Escape',
close: async () => {
document.activeElement?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
)
},
},
{
name: 'tapping the backdrop',
close: async () => {
const overlay = document.body.querySelector<HTMLElement>('[data-slot="overlay"]')!
overlay.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }))
overlay.dispatchEvent(new MouseEvent('pointerup', { bubbles: true }))
overlay.click()
},
},
{
name: 'activating "Profile"',
close: async () => {
menuItem('Profile').click()
},
},
]
afterEach(async () => {
mountedShell?.unmount()
mountedShell = null
await router.push('/login')
document.body.innerHTML = ''
})
describe('Mobile account sheet focus', () => {
it.each(closingActions)(
'returns focus to the navbar toggle when the sheet closes by $name',
async ({ close }) => {
await mountMobileShell()
await openSheetFromChip()
expect(sheet()).not.toBeNull()
await close()
await flushPromises()
expect(sheet()).toBeNull()
expect(document.activeElement).toBe(document.body.querySelector('[data-slot="toggle"]'))
},
)
it('logs out from the sheet without throwing', async () => {
const unhandled = vi.fn()
process.on('unhandledRejection', unhandled)
await mountMobileShell()
await openSheetFromChip()
menuItem('Log out').click()
await flushPromises()
expect(router.currentRoute.value.path).toBe('/login')
expect(unhandled).not.toHaveBeenCalled()
process.off('unhandledRejection', unhandled)
})
it('does not put focus on the chip or the toggle when the sheet was never opened', async () => {
await mountMobileShell()
expect(document.activeElement).toBe(document.body)
})
})