Skip to content

Commit 400ad78

Browse files
howwohmmclaude
andcommitted
test(ui): add tests for mobile menu default state and open/close behavior
Covers issue requirements: - Menu is closed by default (dialog not in DOM) - Opens when prop is set to true - Closes when prop changes back to false - Emits update:open on backdrop click - Emits update:open on close button click Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f2fc1a commit 400ad78

File tree

1 file changed

+112
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)