import { mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' import { createI18n } from 'vue-i18n' import { createMemoryHistory, createRouter } from 'vue-router' import { beforeEach, describe, expect, it, vi } from 'vitest' import ReservationAppShell from '@/layouts/ReservationAppShell.vue' import enUS from '@/i18n/locales/en-US' import thTH from '@/i18n/locales/th-TH' import zhCN from '@/i18n/locales/zh-CN' import { useAuthStore } from '@/stores/authStore' import type { AuthMenuResult } from '@/types/auth' vi.mock('@/services/healthService', () => ({ fetchBackendHealth: vi.fn().mockResolvedValue({ status: 'UP', service: 'test', }), })) function createAuthPayload(menus: AuthMenuResult[]) { return { access_token: 'session-token', token_type: 'Bearer', expires_at: '2026-07-09T12:00:00Z', user: { id: '1900000000000000001', username: 'admin', display_name: '系统管理员', super_admin: true, }, default_hotel_id: 'HOTEL-TEST', hotels: [ { hotel_id: 'HOTEL-TEST', hotel_name: '测试酒店', time_zone: 'Asia/Bangkok', default_hotel: true, }, ], permissions: ['RESERVATION_ORDER_READ', 'RESERVATION_TASK_READ'], menus, } } function createReservationOrdersMenu(): AuthMenuResult { return { menu_code: 'RESERVATION_ORDERS', menu_name: '订单列表', route_path: '/reservation/orders', component_key: 'ReservationOrders', icon_key: 'pi pi-list', permission_code: 'RESERVATION_ORDER_READ', sort_order: 10, } } function createReservationTasksMenu(): AuthMenuResult { return { menu_code: 'RESERVATION_TASKS', menu_name: '任务队列', route_path: '/reservation/tasks', component_key: 'ReservationTasks', icon_key: 'pi pi-check-square', permission_code: 'RESERVATION_TASK_READ', sort_order: 20, } } function createDebugMenu(): AuthMenuResult { return { menu_code: 'DEBUG_EML_SUPERAGENT', menu_name: 'Debug EML', route_path: '/debug/eml-superagent', component_key: 'DebugEmlSuperAgent', icon_key: 'pi pi-upload', permission_code: 'SYSTEM_DEBUG_EML_RUN', sort_order: 30, } } function createManualInvoiceMenu(): AuthMenuResult { return { menu_code: 'RESERVATION_MANUAL_INVOICE', menu_name: '手工开票', route_path: '/reservation/invoices/new', component_key: 'ReservationManualInvoice', icon_key: 'pi pi-file-pdf', permission_code: 'RESERVATION_INVOICE_GENERATE', sort_order: 25, } } function createRoomingListMenu(): AuthMenuResult { return { menu_code: 'RESERVATION_ROOMING_LIST', menu_name: 'Rooming List', route_path: '/reservation/rooming-lists/new', component_key: 'ReservationRoomingList', icon_key: 'pi pi-file-excel', permission_code: 'RESERVATION_ROOMING_LIST_GENERATE', sort_order: 26, } } function createSystemMenu(): AuthMenuResult { return { menu_code: 'SYSTEM_SETTINGS', menu_name: '系统设置', route_path: '/system', component_key: 'SystemSettings', icon_key: 'pi pi-cog', permission_code: 'SYSTEM_ADMIN_CONSOLE_ACCESS', sort_order: 40, } } async function mountShell(menus = [createReservationOrdersMenu(), createReservationTasksMenu()]) { const i18n = createI18n({ legacy: false, locale: 'zh-CN', fallbackLocale: 'zh-CN', messages: { 'zh-CN': zhCN, 'en-US': enUS, 'th-TH': thTH, }, }) const router = createRouter({ history: createMemoryHistory(), routes: [ { path: '/reservation/orders', component: { template: '
' }, meta: { titleKey: 'nav.orders' }, }, { path: '/reservation/tasks', component: { template: '' }, meta: { titleKey: 'nav.taskQueue' }, }, { path: '/debug/eml-superagent', component: { template: '' }, meta: { titleKey: 'debugEml.title' }, }, { path: '/reservation/invoices/new', component: { template: '' }, meta: { titleKey: 'nav.manualInvoice' }, }, { path: '/reservation/rooming-lists/new', component: { template: '' }, meta: { titleKey: 'nav.roomingList' }, }, { path: '/system', component: { template: '' }, meta: { titleKey: 'nav.systemSettings' }, }, ], }) const pinia = createPinia() setActivePinia(pinia) useAuthStore().applyLoginResult(createAuthPayload(menus)) await router.push('/reservation/orders') await router.isReady() return mount(ReservationAppShell, { global: { plugins: [i18n, router, pinia], }, slots: { default: '', }, }) } describe('ReservationAppShell', () => { beforeEach(() => { sessionStorage.clear() }) it('switches between Chinese, English, and Thai labels', async () => { const wrapper = await mountShell() expect(wrapper.text()).toContain('订单列表') await wrapper.findAll('button').find((button) => button.text() === 'ไทย')?.trigger('click') expect(wrapper.text()).toContain('รายการออเดอร์') await wrapper.findAll('button').find((button) => button.text() === 'EN')?.trigger('click') expect(wrapper.text()).toContain('Order list') }) it('renders navigation from backend menus and hides Debug EML when the menu is absent', async () => { const wrapper = await mountShell() expect(wrapper.find('a[href="/reservation/orders"]').exists()).toBe(true) expect(wrapper.find('a[href="/reservation/tasks"]').exists()).toBe(true) expect(wrapper.text()).toContain('预订事项') expect(wrapper.text()).not.toContain('任务队列') expect(wrapper.find('a[href="/debug/eml-superagent"]').exists()).toBe(false) }) it('shows Debug EML only when backend menus include it', async () => { const wrapper = await mountShell([createReservationOrdersMenu(), createDebugMenu()]) expect(wrapper.find('a[href="/debug/eml-superagent"]').exists()).toBe(true) expect(wrapper.text()).toContain('Debug EML') }) it('shows Manual Invoice only when backend menus include it', async () => { const wrapper = await mountShell([createReservationOrdersMenu(), createManualInvoiceMenu()]) expect(wrapper.find('a[href="/reservation/invoices/new"]').exists()).toBe(true) expect(wrapper.text()).toContain('手工开票') }) it('shows Rooming List only when backend menus include it', async () => { const wrapper = await mountShell([createReservationOrdersMenu(), createRoomingListMenu()]) expect(wrapper.find('a[href="/reservation/rooming-lists/new"]').exists()).toBe(true) expect(wrapper.text()).toContain('房表生成') }) it('shows System Settings only when backend menus include it', async () => { const wrapper = await mountShell([createReservationOrdersMenu(), createSystemMenu()]) expect(wrapper.find('a[href="/system"]').exists()).toBe(true) expect(wrapper.text()).toContain('系统设置') }) it('shows current user and hotel context in the top bar', async () => { const wrapper = await mountShell() expect(wrapper.text()).toContain('系统管理员') expect(wrapper.text()).toContain('测试酒店') }) })