Files
admin-web/src/modules/dashboard/pages/DashboardView.test.ts
T
2026-10-20 23:34:11 +08:00

123 lines
6.9 KiB
TypeScript

import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { ElButton, ElTooltip } from 'element-plus'
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<string>() }))
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)
function mountDashboard() {
return mount(DashboardView, { global: { components: { ElButton, ElTooltip } } })
}
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 = mountDashboard(); 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 = mountDashboard(); await flushPromises()
expect(getActiveCustomerCount).toHaveBeenCalledOnce(); expect(getTodayNewOrderCount).not.toHaveBeenCalled(); expect(getDashboardOperationalMetrics).not.toHaveBeenCalled()
expect(wrapper.findAll('.workbench-metric-label').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 = mountDashboard()
await flushPromises()
expect(wrapper.find('.dashboard-context > p').exists()).toBe(false)
expect(wrapper.classes()).toContain('admin-workbench')
expect(wrapper.text()).toContain('数据更新时间:2026-09-11 12:00')
expect(wrapper.findAll('.dashboard-metric')).toHaveLength(4)
expect(wrapper.findAll('.workbench-metric-label').map(label => label.text())).toEqual(['今日新运单', '在途柜量', '活跃客户', '待清关票数'])
expect(wrapper.find('.density-fields').exists()).toBe(false)
expect(wrapper.findAll('.dashboard-metric.workbench-metric .workbench-icon')).toHaveLength(4)
expect(wrapper.findAll('button[aria-label^="查看"]')).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.get('.dashboard-metric .workbench-badge[data-tone="amber"]').text()).toBe('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 = mountDashboard()
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.find('.dashboard-context > p').exists()).toBe(false)
expect(wrapper.text()).toContain('12 票')
})
it('uses the same visualization root when entering and exiting fullscreen', async () => {
const wrapper = mountDashboard()
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')
}
})
})