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() 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(''), ) }) }) 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') }) })