test: mobile drawer parity contract (close control, expanded header, height)
MOBILE-SHELL-PARITY. Adds 7 Vitest assertions for the three defects: - AppSidebar: header pt is '!hidden' (default close-X suppressed), content pt is a full-height flex column (flex-col/h-full/min-h-0), showCloseIcon is not forced false, and WorkspaceSwitcher renders inside the drawer. - SidebarHeader: the Icon stub now exposes data-icon; mobile brand-row control is an explicit close (aria-label 'Sluit menu', tabler-x), desktop stays the collapse chevron, and the header renders expanded on mobile even when sidebarCollapsed is true (logo parity). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,7 @@ vi.mock('@vueuse/core', () => ({
|
||||
*/
|
||||
const DrawerStub = {
|
||||
name: 'Drawer',
|
||||
props: ['visible', 'position', 'pt'],
|
||||
props: ['visible', 'position', 'pt', 'showCloseIcon'],
|
||||
emits: ['update:visible'],
|
||||
template: '<div class="drawer-stub" :data-visible="visible"><slot /></div>',
|
||||
}
|
||||
@@ -138,6 +138,70 @@ describe('AppSidebar', () => {
|
||||
expect(wrapper.find('.drawer-stub').exists()).toBe(true)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MOBILE-SHELL-PARITY — drawer chrome pt contract
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('mobile: drawer force-hides the PrimeVue default header (!hidden) so the default close-X cannot overlap', async () => {
|
||||
mockIsMobileRef.value = true
|
||||
|
||||
const wrapper = mountSidebar()
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const pt = wrapper.findComponent(DrawerStub).props('pt') as {
|
||||
header: { class: string }
|
||||
content: { class: string }
|
||||
}
|
||||
|
||||
// defect 2: PrimeVue's default header (which holds the default close-X)
|
||||
// is suppressed with the important variant so base styles cannot win.
|
||||
expect(pt.header.class).toContain('!hidden')
|
||||
})
|
||||
|
||||
it('mobile: drawer content is a full-height flex column (WorkspaceSwitcher bottom-anchor)', async () => {
|
||||
mockIsMobileRef.value = true
|
||||
|
||||
const wrapper = mountSidebar()
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const pt = wrapper.findComponent(DrawerStub).props('pt') as {
|
||||
content: { class: string }
|
||||
}
|
||||
|
||||
// defect 3: full-height flex column so SidebarNav claims the slack and
|
||||
// WorkspaceSwitcher (flex-shrink-0) anchors at the bottom.
|
||||
expect(pt.content.class).toContain('flex-col')
|
||||
expect(pt.content.class).toContain('h-full')
|
||||
expect(pt.content.class).toContain('min-h-0')
|
||||
})
|
||||
|
||||
it('mobile: drawer does NOT suppress the close affordance via showCloseIcon=false', async () => {
|
||||
mockIsMobileRef.value = true
|
||||
|
||||
const wrapper = mountSidebar()
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// The mobile close control lives in SidebarHeader's brand row; the Drawer
|
||||
// is left at its default showCloseIcon (never forced to false).
|
||||
expect(wrapper.findComponent(DrawerStub).props('showCloseIcon')).not.toBe(false)
|
||||
})
|
||||
|
||||
it('mobile: WorkspaceSwitcher renders inside the drawer', async () => {
|
||||
mockIsMobileRef.value = true
|
||||
|
||||
const wrapper = mountSidebar()
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const drawer = wrapper.find('.drawer-stub')
|
||||
|
||||
expect(drawer.exists()).toBe(true)
|
||||
expect(drawer.find('.workspace-switcher-stub').exists()).toBe(true)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Desktop <aside> width class based on sidebarCollapsed
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -41,8 +41,9 @@ vi.mock('@vueuse/core', () => ({
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const globalStubs = {
|
||||
// Icon renders nothing; we just need the collapse button to be present
|
||||
Icon: { template: '<span class="icon-stub" />' },
|
||||
// Icon stub exposes `name` via data-icon so tests can assert which glyph
|
||||
// renders (e.g. the mobile close X vs the desktop collapse chevron).
|
||||
Icon: { props: ['name', 'size'], template: '<span class="icon-stub" :data-icon="name" />' },
|
||||
}
|
||||
|
||||
function mountHeader() {
|
||||
@@ -194,6 +195,51 @@ describe('SidebarHeader', () => {
|
||||
expect(btn.attributes('aria-label')).toBe('Collapse sidebar')
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// MOBILE-SHELL-PARITY — mobile close control (defect 2) + always-expanded
|
||||
// header (defect 1)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('mobile: brand-row control is an explicit close (aria-label "Sluit menu", X icon)', () => {
|
||||
mockIsMobileRef.value = true
|
||||
|
||||
const wrapper = mountHeader()
|
||||
const btn = wrapper.find('button[aria-label]')
|
||||
|
||||
expect(btn.attributes('aria-label')).toBe('Sluit menu')
|
||||
expect(btn.find('.icon-stub').attributes('data-icon')).toBe('tabler-x')
|
||||
})
|
||||
|
||||
it('desktop: brand-row control stays the collapse chevron (aria-label "Collapse sidebar", chevron icon)', () => {
|
||||
mockIsMobileRef.value = false
|
||||
|
||||
const wrapper = mountHeader()
|
||||
const btn = wrapper.find('button[aria-label]')
|
||||
|
||||
expect(btn.attributes('aria-label')).toBe('Collapse sidebar')
|
||||
expect(btn.find('.icon-stub').attributes('data-icon')).toBe('tabler-chevron-left')
|
||||
})
|
||||
|
||||
it('mobile: header renders EXPANDED even when sidebarCollapsed is true (logo parity)', async () => {
|
||||
mockIsMobileRef.value = true
|
||||
|
||||
const wrapper = mountHeader()
|
||||
const shell = useShellUiStore()
|
||||
|
||||
shell.sidebarCollapsed = true
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Expanded brand row: wordmark present, exactly one control (the close X),
|
||||
// and NO collapsed-state expand-chevron row.
|
||||
expect(wrapper.find('.brand-name').exists()).toBe(true)
|
||||
expect(wrapper.text()).toContain('Crewli')
|
||||
|
||||
const buttons = wrapper.findAll('button[aria-label]')
|
||||
|
||||
expect(buttons).toHaveLength(1)
|
||||
expect(buttons[0].attributes('aria-label')).toBe('Sluit menu')
|
||||
})
|
||||
|
||||
// P6-styling-fix — the brand row is ALWAYS left-aligned with constant
|
||||
// px-4 padding (no justify-content switch on collapse). The logo's
|
||||
// horizontal position is therefore identical in expanded vs collapsed
|
||||
|
||||
Reference in New Issue
Block a user