123 lines
6.9 KiB
TypeScript
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')
|
|
}
|
|
})
|
|
})
|