fix(gui-v2): mount Drawer only on mobile (v-if) + shared Tailwind breakpoint

CRITICAL: replace `lg:hidden` on PrimeVue Drawer with `v-if="isMobile"` so the
teleported portal/overlay is never created on desktop viewports regardless of
mobileOpen state. Replace useMediaQuery raw string in SidebarHeader with
useBreakpoints(breakpointsTailwind).smaller('lg') shared by both components.
Add desktop/mobile comments; adapt tests to useBreakpoints mock; add
Drawer-absent-on-desktop and aside w-16/w-64 width-class assertions (21 tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 20:41:50 +02:00
parent f0f9cb7e36
commit 23e1262f9c
4 changed files with 141 additions and 19 deletions

View File

@@ -7,18 +7,44 @@
* 2. Passes `groups` prop to SidebarNav.
* 3. Mobile Drawer v-model:visible wires to shell.mobileOpen (get path).
* 4. Drawer close (v-model:visible = false) calls shell.setMobileOpen(false).
* 5. Drawer is NOT rendered when isMobile=false (desktop); IS rendered when isMobile=true.
* 6. Desktop <aside> applies correct width class based on sidebarCollapsed.
*
* Stubs: Drawer is stubbed with a simple slot passthrough so we can inspect
* whether its `visible` prop is correctly bound to the store.
*
* @vueuse/core is mocked so we can control isMobile per test context.
*/
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 AppSidebar from '@/components-v2/layout/AppSidebar.vue'
import type { V2NavGroup } from '@/types/v2/nav'
// ---------------------------------------------------------------------------
// Mock @vueuse/core so we can control `isMobile` per test.
// AppSidebar uses useBreakpoints(breakpointsTailwind).smaller('lg').
// We return an object whose .smaller() method returns a controllable ref.
// ---------------------------------------------------------------------------
const mockIsMobileRef = ref(false)
vi.mock('@vueuse/core', () => ({
breakpointsTailwind: {},
useBreakpoints: () => ({
smaller: (_bp: string) => mockIsMobileRef,
}),
}))
// ---------------------------------------------------------------------------
// Import component AFTER mock is set up
// ---------------------------------------------------------------------------
// eslint-disable-next-line import/first -- intentional: mock must be declared first
import AppSidebar from '@/components-v2/layout/AppSidebar.vue'
// ---------------------------------------------------------------------------
// A minimal nav group fixture
// ---------------------------------------------------------------------------
@@ -75,7 +101,12 @@ function mountSidebar(groups: V2NavGroup[] = testGroups) {
}
describe('AppSidebar', () => {
beforeEach(() => setActivePinia(createPinia()))
beforeEach(() => {
setActivePinia(createPinia())
// Default to desktop (isMobile=false) so most tests exercise the stable path
mockIsMobileRef.value = false
})
// -------------------------------------------------------------------------
// Child composition
@@ -108,10 +139,67 @@ describe('AppSidebar', () => {
})
// -------------------------------------------------------------------------
// Mobile Drawer wiring
// Drawer v-if: mount/unmount based on isMobile
// -------------------------------------------------------------------------
it('Drawer visible is true when shell.mobileOpen is true', async () => {
it('desktop (isMobile=false): Drawer is NOT rendered', () => {
mockIsMobileRef.value = false
const wrapper = mountSidebar()
// v-if="isMobile" means the Drawer stub must be absent on desktop
expect(wrapper.find('.drawer-stub').exists()).toBe(false)
})
it('mobile (isMobile=true): Drawer IS rendered', async () => {
mockIsMobileRef.value = true
const wrapper = mountSidebar()
await wrapper.vm.$nextTick()
expect(wrapper.find('.drawer-stub').exists()).toBe(true)
})
// -------------------------------------------------------------------------
// Desktop <aside> width class based on sidebarCollapsed
// -------------------------------------------------------------------------
it('desktop: aside has w-64 class when sidebar is expanded', async () => {
mockIsMobileRef.value = false
const wrapper = mountSidebar()
const shell = useShellUiStore()
shell.sidebarCollapsed = false
await wrapper.vm.$nextTick()
expect(wrapper.find('aside').classes()).toContain('w-64')
expect(wrapper.find('aside').classes()).not.toContain('w-16')
})
it('desktop: aside has w-16 class when sidebar is collapsed', async () => {
mockIsMobileRef.value = false
const wrapper = mountSidebar()
const shell = useShellUiStore()
shell.sidebarCollapsed = true
await wrapper.vm.$nextTick()
expect(wrapper.find('aside').classes()).toContain('w-16')
expect(wrapper.find('aside').classes()).not.toContain('w-64')
})
// -------------------------------------------------------------------------
// Mobile Drawer wiring (only exercised when isMobile=true)
// -------------------------------------------------------------------------
it('mobile: Drawer visible is true when shell.mobileOpen is true', async () => {
mockIsMobileRef.value = true
const wrapper = mountSidebar()
const shell = useShellUiStore()
@@ -125,7 +213,9 @@ describe('AppSidebar', () => {
expect(drawer.attributes('data-visible')).toBe('true')
})
it('Drawer visible is false when shell.mobileOpen is false', async () => {
it('mobile: Drawer visible is false when shell.mobileOpen is false', async () => {
mockIsMobileRef.value = true
const wrapper = mountSidebar()
const shell = useShellUiStore()
@@ -138,7 +228,9 @@ describe('AppSidebar', () => {
expect(drawer.attributes('data-visible')).toBe('false')
})
it('emitting update:visible false from Drawer calls shell.setMobileOpen(false)', async () => {
it('mobile: emitting update:visible false from Drawer calls shell.setMobileOpen(false)', async () => {
mockIsMobileRef.value = true
const wrapper = mountSidebar()
const shell = useShellUiStore()
@@ -152,7 +244,9 @@ describe('AppSidebar', () => {
expect(setMobileOpenSpy).toHaveBeenCalledWith(false)
})
it('emitting update:visible true from Drawer calls shell.setMobileOpen(true)', async () => {
it('mobile: emitting update:visible true from Drawer calls shell.setMobileOpen(true)', async () => {
mockIsMobileRef.value = true
const wrapper = mountSidebar()
const shell = useShellUiStore()