Skip to content

Commit 6cb62c6

Browse files
howwohmmclaudeautofix-ci[bot]
authored
test(ui): add tests for mobile menu default closed state (#2195)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 7083d6a commit 6cb62c6

File tree

1 file changed

+118
-0
lines changed

1 file changed

+118
-0
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { describe, it, expect, vi } from 'vitest'
2+
import { mockNuxtImport, mountSuspended } from '@nuxt/test-utils/runtime'
3+
import { computed, nextTick } from 'vue'
4+
import { HeaderMobileMenu } from '#components'
5+
6+
// Mock useConnector
7+
mockNuxtImport('useConnector', () => () => ({
8+
isConnected: computed(() => false),
9+
npmUser: computed(() => null),
10+
avatar: computed(() => null),
11+
}))
12+
13+
// Mock useAtproto
14+
mockNuxtImport('useAtproto', () => () => ({
15+
user: computed(() => null),
16+
}))
17+
18+
// Mock useFocusTrap (from @vueuse/integrations)
19+
vi.mock('@vueuse/integrations/useFocusTrap', () => ({
20+
useFocusTrap: () => ({
21+
activate: vi.fn(),
22+
deactivate: vi.fn(),
23+
}),
24+
}))
25+
26+
describe('MobileMenu', () => {
27+
async function mountMenu(open = false) {
28+
return mountSuspended(HeaderMobileMenu, {
29+
props: {
30+
open,
31+
links: [
32+
{
33+
type: 'group' as const,
34+
name: 'main',
35+
label: 'Navigation',
36+
items: [
37+
{
38+
type: 'link' as const,
39+
name: 'home',
40+
label: 'Home',
41+
to: '/',
42+
iconClass: 'i-lucide:home',
43+
},
44+
],
45+
},
46+
],
47+
},
48+
attachTo: document.body,
49+
})
50+
}
51+
52+
it('is closed by default', async () => {
53+
const wrapper = await mountMenu(false)
54+
try {
55+
// Menu content is behind v-if="isOpen" inside a Teleport
56+
expect(document.querySelector('[role="dialog"]')).toBeNull()
57+
} finally {
58+
wrapper.unmount()
59+
}
60+
})
61+
62+
it('opens when the open prop is set to true', async () => {
63+
const wrapper = await mountMenu(true)
64+
try {
65+
await nextTick()
66+
const dialog = document.querySelector('[role="dialog"]')
67+
expect(dialog).not.toBeNull()
68+
expect(dialog?.getAttribute('aria-modal')).toBe('true')
69+
} finally {
70+
wrapper.unmount()
71+
}
72+
})
73+
74+
it('closes when open prop changes from true to false', async () => {
75+
const wrapper = await mountMenu(true)
76+
try {
77+
await nextTick()
78+
expect(document.querySelector('[role="dialog"]')).not.toBeNull()
79+
80+
await wrapper.setProps({ open: false })
81+
await nextTick()
82+
expect(document.querySelector('[role="dialog"]')).toBeNull()
83+
} finally {
84+
wrapper.unmount()
85+
}
86+
})
87+
88+
it('emits update:open false when backdrop is clicked', async () => {
89+
const wrapper = await mountMenu(true)
90+
try {
91+
await nextTick()
92+
const backdrop = document.querySelector('[role="dialog"] > button')
93+
expect(backdrop).not.toBeNull()
94+
backdrop?.dispatchEvent(new Event('click', { bubbles: true }))
95+
await nextTick()
96+
expect(wrapper.emitted('update:open')).toBeTruthy()
97+
expect(wrapper.emitted('update:open')![0]).toEqual([false])
98+
} finally {
99+
wrapper.unmount()
100+
}
101+
})
102+
103+
it('emits update:open false when close button is clicked', async () => {
104+
const wrapper = await mountMenu(true)
105+
try {
106+
await nextTick()
107+
// Close button has aria-label matching $t('common.close') — find it inside nav
108+
const closeBtn = document.querySelector('nav button[aria-label]')
109+
expect(closeBtn).not.toBeNull()
110+
closeBtn?.dispatchEvent(new Event('click', { bubbles: true }))
111+
await nextTick()
112+
expect(wrapper.emitted('update:open')).toBeTruthy()
113+
expect(wrapper.emitted('update:open')![0]).toEqual([false])
114+
} finally {
115+
wrapper.unmount()
116+
}
117+
})
118+
})

0 commit comments

Comments
 (0)