Vitest hoists vi.mock()/vi.hoisted() above all imports, so the component import can sit with the other imports (import/first satisfied) without the eslint-disable-next-line directives — the mock factories only deref their refs at mount time. Honors the no-eslint-disable rule. 28/28 affected specs green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
197 lines
6.0 KiB
TypeScript
197 lines
6.0 KiB
TypeScript
/**
|
|
* SidebarHeader.spec.ts — unit tests for the non-trivial logic in SidebarHeader.
|
|
*
|
|
* Strategy: mount with @vue/test-utils + createPinia. Child components (Icon)
|
|
* and @vueuse/core's useBreakpoints are stubbed so we test only:
|
|
* 1. Collapse-button desktop path → shell.toggleSidebar() called.
|
|
* 2. Collapse-button mobile path → shell.setMobileOpen(false) called.
|
|
* 3. Collapsed state hides wordmark + pill; keep collapse button.
|
|
*
|
|
* The unit project (src/ __tests__ glob) runs in happy-dom with globals: true
|
|
* so `describe/it/expect/vi` are available without explicit imports.
|
|
*/
|
|
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
import { mount } from '@vue/test-utils'
|
|
import { ref } from 'vue'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { useShellUiStore } from '@/stores/useShellUiStore'
|
|
import SidebarHeader from '@/components-v2/layout/SidebarHeader.vue'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock @vueuse/core so we can control `isMobile` per test.
|
|
// The component uses useBreakpoints(breakpointsTailwind).smaller('lg').
|
|
// Vitest hoists vi.mock() above all imports, so the mock is registered
|
|
// before SidebarHeader's transitive @vueuse/core import resolves; the
|
|
// factory only dereferences mockIsMobileRef inside .smaller(), invoked
|
|
// at component-mount time (well after this const initialises).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const mockIsMobileRef = ref(false)
|
|
|
|
vi.mock('@vueuse/core', () => ({
|
|
breakpointsTailwind: {},
|
|
useBreakpoints: () => ({
|
|
smaller: (_: string) => mockIsMobileRef,
|
|
}),
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Stubs — keep tests fast and focused on logic, not child rendering
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const globalStubs = {
|
|
// Icon renders nothing; we just need the collapse button to be present
|
|
Icon: { template: '<span class="icon-stub" />' },
|
|
}
|
|
|
|
function mountHeader() {
|
|
return mount(SidebarHeader, {
|
|
global: {
|
|
plugins: [createPinia()],
|
|
stubs: globalStubs,
|
|
},
|
|
})
|
|
}
|
|
|
|
describe('SidebarHeader', () => {
|
|
beforeEach(() => {
|
|
setActivePinia(createPinia())
|
|
|
|
// Reset mobile mock to desktop default before each test
|
|
mockIsMobileRef.value = false
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Collapse button — desktop path
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('desktop: collapse button calls toggleSidebar', async () => {
|
|
const wrapper = mountHeader()
|
|
const shell = useShellUiStore()
|
|
const toggleSpy = vi.spyOn(shell, 'toggleSidebar')
|
|
|
|
mockIsMobileRef.value = false
|
|
|
|
const btn = wrapper.find('button[aria-label]')
|
|
|
|
await btn.trigger('click')
|
|
|
|
expect(toggleSpy).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('desktop: collapse button does NOT call setMobileOpen', async () => {
|
|
const wrapper = mountHeader()
|
|
const shell = useShellUiStore()
|
|
const setMobileOpenSpy = vi.spyOn(shell, 'setMobileOpen')
|
|
|
|
mockIsMobileRef.value = false
|
|
|
|
const btn = wrapper.find('button[aria-label]')
|
|
|
|
await btn.trigger('click')
|
|
|
|
expect(setMobileOpenSpy).not.toHaveBeenCalled()
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Collapse button — mobile path
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('mobile: collapse button calls setMobileOpen(false)', async () => {
|
|
mockIsMobileRef.value = true
|
|
|
|
const wrapper = mountHeader()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.mobileOpen = true // simulate open drawer
|
|
|
|
const setMobileOpenSpy = vi.spyOn(shell, 'setMobileOpen')
|
|
|
|
const btn = wrapper.find('button[aria-label]')
|
|
|
|
await btn.trigger('click')
|
|
|
|
expect(setMobileOpenSpy).toHaveBeenCalledWith(false)
|
|
})
|
|
|
|
it('mobile: collapse button does NOT call toggleSidebar', async () => {
|
|
mockIsMobileRef.value = true
|
|
|
|
const wrapper = mountHeader()
|
|
const shell = useShellUiStore()
|
|
const toggleSpy = vi.spyOn(shell, 'toggleSidebar')
|
|
|
|
const btn = wrapper.find('button[aria-label]')
|
|
|
|
await btn.trigger('click')
|
|
|
|
expect(toggleSpy).not.toHaveBeenCalled()
|
|
})
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Collapsed state hides wordmark + pill
|
|
// -------------------------------------------------------------------------
|
|
|
|
it('expanded: wordmark and pill are visible', () => {
|
|
const wrapper = mountHeader()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.sidebarCollapsed = false
|
|
|
|
// The wordmark text appears directly in a <span>
|
|
expect(wrapper.text()).toContain('Crewli')
|
|
expect(wrapper.text()).toContain('Beta')
|
|
})
|
|
|
|
it('collapsed: wordmark and pill are hidden', async () => {
|
|
const wrapper = mountHeader()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.sidebarCollapsed = true
|
|
|
|
// Give Vue a tick to re-render after the store mutation
|
|
await wrapper.vm.$nextTick()
|
|
|
|
expect(wrapper.text()).not.toContain('Crewli')
|
|
expect(wrapper.text()).not.toContain('Beta')
|
|
})
|
|
|
|
it('collapsed: collapse button is still rendered', async () => {
|
|
const wrapper = mountHeader()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.sidebarCollapsed = true
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const btn = wrapper.find('button[aria-label]')
|
|
|
|
expect(btn.exists()).toBe(true)
|
|
})
|
|
|
|
it('collapsed: collapse button has aria-label "Expand sidebar"', async () => {
|
|
const wrapper = mountHeader()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.sidebarCollapsed = true
|
|
|
|
await wrapper.vm.$nextTick()
|
|
|
|
const btn = wrapper.find('button[aria-label]')
|
|
|
|
expect(btn.attributes('aria-label')).toBe('Expand sidebar')
|
|
})
|
|
|
|
it('expanded: collapse button has aria-label "Collapse sidebar"', () => {
|
|
const wrapper = mountHeader()
|
|
const shell = useShellUiStore()
|
|
|
|
shell.sidebarCollapsed = false
|
|
|
|
const btn = wrapper.find('button[aria-label]')
|
|
|
|
expect(btn.attributes('aria-label')).toBe('Collapse sidebar')
|
|
})
|
|
})
|