import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import DashboardView from './DashboardView.vue' import { getTodayNewOrderCount, getActiveCustomerCount, getDashboardOperationalMetrics } from '../api/dashboard' const access = vi.hoisted(() => ({ permissions: new Set() })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: (permission: string) => access.permissions.has(permission) }) })) vi.mock('../api/dashboard', () => ({ getTodayNewOrderCount: vi.fn().mockResolvedValue(12), getActiveCustomerCount: vi.fn().mockResolvedValue(8), getDashboardOperationalMetrics: vi.fn().mockResolvedValue({ inTransitContainerCount: 3, inTransitTeu: 4.25, unknownContainerTypeCount: 1, pendingCustomsOrderCount: 7, asOf: '2026-09-11T12:00:00', }), })) enableAutoUnmount(afterEach) describe('DashboardView', () => { beforeEach(() => { vi.clearAllMocks() access.permissions = new Set(['platform:order:list', 'platform:customer:list', 'platform:firstleg:container:list', 'platform:firstleg:customs:list']) }) it('makes no metric requests for a minimal pool sales account and gives an authorized-menu hint', async () => { access.permissions = new Set(['platform:fleet:capacity-offer:list', 'platform:fleet:capacity-offer:inquiry-create', 'platform:fleet:capacity-offer:inquiry-edit', 'platform:fleet:capacity-offer:inquiry-cancel']) const wrapper = mount(DashboardView); await flushPromises() expect(getTodayNewOrderCount).not.toHaveBeenCalled(); expect(getActiveCustomerCount).not.toHaveBeenCalled(); expect(getDashboardOperationalMetrics).not.toHaveBeenCalled() expect(wrapper.findAll('.dashboard-metric')).toHaveLength(0) expect(wrapper.text()).toContain('请从左侧菜单进入已授权业务') expect(wrapper.text()).not.toContain('指标加载失败'); expect(wrapper.text()).not.toContain('刷新数据') }) it('requests only granted metric sources and requires both container and customs permissions', async () => { access.permissions = new Set(['platform:customer:list', 'platform:firstleg:container:list']) const wrapper = mount(DashboardView); await flushPromises() expect(getActiveCustomerCount).toHaveBeenCalledOnce(); expect(getTodayNewOrderCount).not.toHaveBeenCalled(); expect(getDashboardOperationalMetrics).not.toHaveBeenCalled() expect(wrapper.findAll('.dashboard-metric p').map(node => node.text())).toEqual(['活跃客户']) await wrapper.findAll('button').find(button => button.text().trim() === '刷新数据')!.trigger('click'); await flushPromises() expect(getActiveCustomerCount).toHaveBeenCalledTimes(2); expect(getDashboardOperationalMetrics).not.toHaveBeenCalled() }) it('renders only the four live metrics', async () => { const wrapper = mount(DashboardView) await flushPromises() expect(wrapper.text()).toContain('核心指标来自实时业务数据') expect(wrapper.text()).toContain('数据更新时间:2026-09-11 12:00') expect(wrapper.findAll('.dashboard-metric')).toHaveLength(4) expect(wrapper.findAll('.dashboard-metric p').map(label => label.text())).toEqual(['今日新运单', '在途柜量', '活跃客户', '待清关票数']) expect(wrapper.find('.density-fields').exists()).toBe(false) expect(wrapper.findAll('.dashboard-metric-mark')).toHaveLength(4) expect(wrapper.text()).toContain('12 票') expect(wrapper.text()).toContain('4.25 TEU') expect(wrapper.text()).toContain('8 家') expect(wrapper.text()).toContain('7 票') expect(wrapper.text()).toContain('1 柜箱型未知') expect(wrapper.text()).not.toContain('近 30 天运单量趋势') expect(wrapper.text()).not.toContain('运单状态分布') expect(wrapper.text()).not.toContain('TOP10 航线货量') expect(wrapper.text()).not.toContain('收入构成') expect(wrapper.text()).not.toContain('待办事项') expect(wrapper.text()).not.toContain('静态演示数据') }) it('keeps successful metrics when one request fails and refreshes through the visible control', async () => { vi.mocked(getTodayNewOrderCount).mockRejectedValueOnce(new Error('指标加载失败')) const wrapper = mount(DashboardView) await flushPromises() expect(wrapper.text()).toContain('部分核心指标加载失败') expect(wrapper.get('.dashboard-metric strong').text()).toContain('—') expect(wrapper.text()).toContain('4.25 TEU') await wrapper.findAll('button').find(button => button.text().trim() === '刷新数据')!.trigger('click') await flushPromises() expect(getTodayNewOrderCount).toHaveBeenCalledTimes(2) expect(wrapper.text()).toContain('核心指标来自实时业务数据') expect(wrapper.text()).toContain('12 票') }) it('uses the same visualization root when entering and exiting fullscreen', async () => { const wrapper = mount(DashboardView) await flushPromises() let fullscreen: Element | null = null const previousElement = Object.getOwnPropertyDescriptor(document, 'fullscreenElement') const previousExit = Object.getOwnPropertyDescriptor(document, 'exitFullscreen') const enter = vi.fn(async () => { fullscreen = wrapper.element; document.dispatchEvent(new Event('fullscreenchange')) }) const exit = vi.fn(async () => { fullscreen = null; document.dispatchEvent(new Event('fullscreenchange')) }) Object.defineProperty(wrapper.element, 'requestFullscreen', { configurable: true, value: enter }) Object.defineProperty(document, 'fullscreenElement', { configurable: true, get: () => fullscreen }) Object.defineProperty(document, 'exitFullscreen', { configurable: true, value: exit }) try { expect(wrapper.classes()).not.toContain('admin-density') const button = wrapper.get('button[aria-pressed]') await button.trigger('click') await flushPromises() expect(enter).toHaveBeenCalledOnce() expect(button.text()).toBe('退出全屏') expect(button.attributes('aria-pressed')).toBe('true') await button.trigger('click') await flushPromises() expect(exit).toHaveBeenCalledOnce() expect(button.text()).toBe('全屏展示') expect(button.attributes('aria-pressed')).toBe('false') } finally { if (previousElement) Object.defineProperty(document, 'fullscreenElement', previousElement) else Reflect.deleteProperty(document, 'fullscreenElement') if (previousExit) Object.defineProperty(document, 'exitFullscreen', previousExit) else Reflect.deleteProperty(document, 'exitFullscreen') } }) })