Files
RenewCMS/web/src/components/__tests__/DashboardShell.spec.ts
T
2026-09-27 02:11:38 +02:00

179 lines
5.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { mount, flushPromises } 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'
vi.mock('@/composables/useApi', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/composables/useApi')>()
return {
...actual,
useApi: vi.fn(),
}
})
async function mountShell(path: string, username = 'marta') {
setActivePinia(createPinia())
vi.mocked(useApi).mockReturnValue({
getMe: vi.fn().mockResolvedValue({ verified: true, username }),
request: vi.fn().mockResolvedValue(undefined),
})
await router.push(path)
const wrapper = mount(DashboardShell, {
attachTo: document.body,
global: {
plugins: [router, uiPlugin],
},
})
await flushPromises()
return wrapper
}
afterEach(async () => {
await router.push('/login')
document.body.innerHTML = ''
})
describe('DashboardShell renders the three navigation destinations', () => {
it('lists Home, Articles and Integration, with only Articles carrying aria-current', async () => {
const wrapper = await mountShell('/article')
const home = wrapper.find('a[href="/home"]')
const articles = wrapper.find('a[href="/article"]')
const integration = wrapper.find('a[href="/integration"]')
expect(home.exists()).toBe(true)
expect(articles.exists()).toBe(true)
expect(integration.exists()).toBe(true)
expect(articles.attributes('aria-current')).toBe('page')
expect(home.attributes('aria-current')).toBeUndefined()
expect(integration.attributes('aria-current')).toBeUndefined()
})
})
describe('DashboardShell shows the offline banner when the browser reports offline', () => {
it('shows the banner while offline and hides it once back online', async () => {
const onLineSpy = vi
.spyOn(navigator, 'onLine', 'get')
.mockReturnValue(false)
const wrapper = await mountShell('/article')
expect(wrapper.text()).toContain("You're offline")
onLineSpy.mockReturnValue(true)
window.dispatchEvent(new Event('online'))
await flushPromises()
expect(wrapper.text()).not.toContain("You're offline")
onLineSpy.mockRestore()
})
})
describe('The sidebar header renders AppLogo and the "RenewCMS" wordmark', () => {
it('renders the logo before the wordmark text, neither as a link', async () => {
const wrapper = await mountShell('/home')
const header = wrapper.find('[data-slot="header"]')
expect(
header.find('svg[role="img"][aria-label="RenewCMS"]').exists(),
).toBe(true)
expect(header.text()).toContain('RenewCMS')
expect(header.find('a').exists()).toBe(false)
const headerHtml = header.html()
expect(headerHtml.indexOf('<svg')).toBeLessThan(
headerHtml.indexOf('RenewCMS</span>'),
)
})
})
describe('The sidebar collapse trigger toggles the rail and exposes aria-expanded', () => {
it('flips the rail and aria-expanded on repeated activation', async () => {
await mountShell('/home')
const collapseButton = () =>
document.body.querySelector(
'[data-testid="sidebar-collapse"]',
) as HTMLButtonElement
expect(collapseButton().getAttribute('aria-expanded')).toBe('true')
collapseButton().click()
await flushPromises()
expect(collapseButton().getAttribute('aria-expanded')).toBe('false')
collapseButton().click()
await flushPromises()
expect(collapseButton().getAttribute('aria-expanded')).toBe('true')
})
})
describe('A successful logout clears the session and navigates to /login', () => {
it('calls POST /v1/auth/logout, clears the session and navigates to /login', async () => {
await mountShell('/home')
const logOutItem = () =>
Array.from(
document.body.querySelectorAll('[role="menuitem"]'),
).find((el) => el.textContent?.trim() === 'Log out') as HTMLElement
const trigger = document.body.querySelector(
'button[aria-haspopup]',
) as HTMLButtonElement
trigger.click()
await flushPromises()
logOutItem().click()
await flushPromises()
expect(vi.mocked(useApi)().request).toHaveBeenCalledWith(
'/v1/auth/logout',
expect.objectContaining({ method: 'POST' }),
)
expect(router.currentRoute.value.path).toBe('/login')
})
})
describe('Collapsed sidebar hides the wordmark and username text but keeps an accessible name', () => {
it('hides visible text but keeps accessible names for the logo and the user menu', async () => {
await mountShell('/home')
const collapseButton = document.body.querySelector(
'[data-testid="sidebar-collapse"]',
) as HTMLButtonElement
collapseButton.click()
await flushPromises()
const header = document.body.querySelector(
'[data-slot="header"]',
) as HTMLElement
expect(
header.querySelector('svg[role="img"][aria-label="RenewCMS"]'),
).toBeTruthy()
expect(header.textContent).not.toContain('RenewCMS')
expect(header.querySelector('span[aria-hidden="true"]')).toBeNull()
const footer = document.body.querySelector(
'[data-slot="footer"]',
) as HTMLElement
expect(footer.textContent).not.toContain('marta')
const userMenuTrigger = footer.querySelector(
'button[aria-haspopup]',
) as HTMLButtonElement
expect(userMenuTrigger.getAttribute('aria-label')).toBe(
'marta — account menu',
)
expect(
footer.querySelector('[data-slot="fallback"]')?.textContent,
).toBe('MA')
})
})