Files
admin-web/src/core/navigation/adminNavigation.test.ts
T

286 lines
13 KiB
TypeScript
Raw Normal View History

2026-07-30 17:59:43 +08:00
import { describe, expect, it } from 'vitest'
import {
adminNavigation,
filterAdminNavigation,
getFirstNavigationLeaf,
getNavigationLeaves,
type AdminNavigationNode,
} from './adminNavigation'
describe('adminNavigation', () => {
it('offers the common container menu to overseas-only operators', () => {
const visible = filterAdminNavigation(adminNavigation, new Set(['platform:firstleg:terminal-drayage:list']))
expect(getNavigationLeaves(visible).map(item => item.path)).toEqual(['/dashboard', '/container-loading/domestic'])
})
it('matches the latest prototype sidebar using only implemented menu entries', () => {
// 新版.html sidebar (1404–1539), ordered by its data-menu CSS rules (45–56).
const allPermissions = new Set(getNavigationLeaves(adminNavigation).flatMap(node => node.permission ? [node.permission] : []))
const visible = filterAdminNavigation(adminNavigation, allPermissions)
expect(visible.map(node => ({ title: node.title, children: node.children?.map(child => child.title) ?? [] }))).toEqual([
{ title: '首页数据看板', children: [] },
{ title: '客户管理', children: ['客户列表', '海外仓阶梯费用'] },
{ title: '国内头程', children: ['订单列表', '订单测量', '测量数据审核', '库位管理', '托盘管理', '整柜业务', '船期管理'] },
{ title: '货柜管理', children: ['订舱列表', '拖车列表', '货柜列表', '装柜清单', '预装清单'] },
{ title: '海外尾程', children: ['尾程订单', '入库单', '废弃单', '出库单', '换标单', '派送单', '托盘管理', '库位管理', '库存管理', '退货库存管理', '退货箱号管理'] },
{ title: '车辆管理', children: ['排车列表', '车辆列表', '司机管理', '车次管理', '拼车需求池', '出车记录'] },
{ title: '海外业务', children: ['采购订单', '本土卡车', '本土散板'] },
{ title: '客户服务', children: ['异常订单'] },
{ title: '财务管理', children: ['客户账户', '客户应收应付', '应收款统计表', '客户对账单', '结算单应收', '结算单应付'] },
{ title: '通用', children: ['费用项目', '汇率管理', '仓库管理', '海外仓报价', '车队报价', '船期价格表', '下载中心'] },
{ title: '系统管理', children: ['用户管理', '角色权限', '菜单权限'] },
2026-08-04 21:01:39 +08:00
])
expect(visible).toHaveLength(11)
expect(visible.flatMap(node => node.children ?? [])).toHaveLength(51)
2026-08-13 19:00:37 +08:00
expect(adminNavigation.find((item) => item.key === 'notifications')).toBeUndefined()
2026-07-30 17:59:43 +08:00
})
2026-08-11 18:06:35 +08:00
it('assigns every page a unique path and only shares permissions backed by the API contract', () => {
2026-07-30 17:59:43 +08:00
const leaves = getNavigationLeaves(adminNavigation)
const paths = leaves.map((item) => item.path)
2026-08-03 11:45:45 +08:00
const permissions = leaves.flatMap((item) => item.permission ? [item.permission] : [])
2026-07-30 17:59:43 +08:00
2026-08-05 10:46:42 +08:00
expect(paths).toContain('/dashboard')
2026-08-06 13:56:04 +08:00
expect(paths).toContain('/orders')
2026-08-05 15:54:59 +08:00
expect(paths).not.toContain('/orders/first-leg')
2026-08-06 13:56:04 +08:00
expect(paths).not.toContain('/inbounds')
2026-08-05 15:54:59 +08:00
expect(paths).not.toContain('/first-leg-pricing/customer-rules')
2026-08-05 10:46:42 +08:00
expect(paths).toContain('/downloads')
2026-08-13 19:00:37 +08:00
expect(paths).not.toEqual(expect.arrayContaining(['/notifications/messages', '/notifications/templates', '/notifications/mine']))
2026-08-10 15:36:53 +08:00
expect(paths).toContain('/master-data/fee-items')
expect(paths).toContain('/container-loading/domestic')
expect(paths).toContain('/container-loading/overseas')
2026-08-14 19:02:34 +08:00
expect(paths).not.toContain('/container-loading/smart-loading')
2026-08-10 15:36:53 +08:00
expect(paths).toContain('/loading-plans')
expect(paths).toContain('/loading-lists')
2026-08-12 20:36:17 +08:00
expect(paths).not.toContain('/port-schedules')
expect(paths).not.toContain('/customs')
expect(paths).not.toContain('/outbound-dispatches')
2026-08-10 15:36:53 +08:00
expect(paths).not.toContain('/prototype/container-management/container-list')
expect(paths).not.toContain('/prototype/container-management/preload-list')
expect(paths).not.toContain('/prototype/container-management/manifest')
expect(paths).not.toContain('/prototype/general/fee-items')
2026-08-11 18:06:35 +08:00
expect(paths).toEqual(expect.arrayContaining([
'/finance/customer-accounts',
'/finance/customer-arap',
2026-08-19 10:00:50 +08:00
'/finance/receivable-statistics',
2026-08-11 18:06:35 +08:00
'/finance/customer-statements',
'/finance/settlement-receivables',
'/finance/settlement-payables',
]))
expect(paths).not.toEqual(expect.arrayContaining(['/finance/bills', '/finance/receipts', '/finance/costs']))
2026-08-05 10:46:42 +08:00
expect(paths).toContain('/prototype/system-management/brand-settings')
2026-08-03 11:45:45 +08:00
expect(paths).toContain('/system/permissions')
2026-07-30 17:59:43 +08:00
expect(new Set(paths).size).toBe(paths.length)
2026-08-11 18:06:35 +08:00
const duplicatedPermissions = permissions.filter((permission, index) => permissions.indexOf(permission) !== index)
2026-08-13 11:57:44 +08:00
expect(new Set(duplicatedPermissions)).toEqual(new Set())
2026-07-30 17:59:43 +08:00
expect(leaves.every((item) => Boolean(item.icon))).toBe(true)
})
2026-08-19 10:00:50 +08:00
it('shows the five formal finance pages plus receivable statistics with real backend permissions', () => {
2026-08-11 18:06:35 +08:00
const filtered = filterAdminNavigation(adminNavigation, new Set([
2026-08-13 11:57:44 +08:00
'platform:finance:customer-account:list',
'platform:finance:customer-arap:list',
2026-08-19 10:00:50 +08:00
'platform:finance:receivable-statistics:list',
2026-08-13 11:57:44 +08:00
'platform:finance:customer-statement:list',
'platform:finance:settlement-ar:list',
'platform:finance:settlement-ap:list',
2026-08-11 18:06:35 +08:00
]))
expect(getNavigationLeaves(filtered).map((item) => item.path).filter((path) => path.startsWith('/finance/'))).toEqual([
'/finance/customer-accounts',
'/finance/customer-arap',
2026-08-19 10:00:50 +08:00
'/finance/receivable-statistics',
2026-08-11 18:06:35 +08:00
'/finance/customer-statements',
'/finance/settlement-receivables',
'/finance/settlement-payables',
])
})
2026-08-19 16:45:51 +08:00
it('shows only the confirmed phase-four pages with their list permissions', () => {
const filtered = filterAdminNavigation(adminNavigation, new Set([
2026-08-21 11:20:09 +08:00
'platform:overseas-warehouse:last-mile-order:list',
'platform:overseas-warehouse:inbound-order:list',
'platform:overseas-warehouse:dispatch:list',
2026-08-26 15:48:34 +08:00
'platform:overseas-warehouse:outbound:list',
'platform:overseas-warehouse:delivery:list',
'platform:overseas-warehouse:exception-order:list',
'platform:overseas-warehouse:customer-visit:list',
2026-08-21 11:20:09 +08:00
'platform:overseas-warehouse:relabel-order:list',
'platform:overseas-warehouse:pallet:list',
'platform:overseas-warehouse:inventory:list',
2026-08-19 16:45:51 +08:00
'platform:overseas-business:purchase:list',
'platform:overseas-business:truck:list',
'platform:overseas-business:looseboard:list',
'platform:overseas-master-data:exchange-rate:list',
'platform:overseas-master-data:quote:list',
'platform:overseas-master-data:vehicle:list',
]))
const paths = getNavigationLeaves(filtered).map((item) => item.path)
expect(paths).toEqual(expect.arrayContaining([
'/overseas-warehouse/last-mile-orders',
'/overseas-warehouse/inbound-orders',
2026-08-21 11:20:09 +08:00
'/overseas-warehouse/dispatches',
2026-08-26 15:48:34 +08:00
'/overseas-warehouse/outbounds',
'/overseas-warehouse/delivery-orders',
'/overseas-warehouse/exception-orders',
2026-08-19 16:45:51 +08:00
'/overseas-warehouse/relabel-orders',
'/overseas-warehouse/pallets',
'/overseas-warehouse/inventory',
'/overseas-business/purchases',
'/overseas-business/trucks',
'/overseas-business/looseboards',
'/overseas-master-data/exchange-rates',
'/overseas-master-data/quotes',
'/overseas-master-data/vehicles',
]))
2026-08-20 18:02:52 +08:00
expect(paths).not.toContain('/overseas-warehouse/cartons')
expect(paths).not.toContain('/overseas-warehouse/products')
2026-08-26 15:48:34 +08:00
expect(paths).toEqual(expect.arrayContaining(['/overseas-warehouse/outbounds', '/overseas-warehouse/delivery-orders']))
2026-08-19 16:45:51 +08:00
})
2026-08-03 11:45:45 +08:00
it('filters protected pages without mutating the source tree', () => {
2026-07-30 17:59:43 +08:00
const sourceSnapshot = JSON.stringify(adminNavigation, (key, value) => key === 'icon' ? '[icon]' : value)
const filtered = filterAdminNavigation(adminNavigation, new Set(['platform:permission:list']))
2026-08-03 11:45:45 +08:00
const paths = getNavigationLeaves(filtered).map((item) => item.path)
expect(paths).toContain('/system/permissions')
expect(paths).not.toContain('/prototype/finance/profit-report')
2026-08-12 18:50:03 +08:00
expect(paths).not.toContain('/notifications/messages')
2026-08-03 11:45:45 +08:00
expect(paths).not.toContain('/customers')
2026-08-05 15:54:59 +08:00
expect(filtered.map((item) => item.title)).toEqual(['首页数据看板', '系统管理'])
2026-07-30 17:59:43 +08:00
expect(filtered).not.toBe(adminNavigation)
2026-08-05 15:54:59 +08:00
expect(filtered[0]).not.toBe(adminNavigation.find((item) => item.key === 'system'))
2026-07-30 17:59:43 +08:00
expect(JSON.stringify(adminNavigation, (key, value) => key === 'icon' ? '[icon]' : value)).toBe(sourceSnapshot)
})
2026-08-05 10:46:42 +08:00
it('keeps hidden prototype nodes out of the live menu while preserving implemented placements', () => {
const filtered = filterAdminNavigation(adminNavigation, new Set([
'platform:firstleg:inbound:list',
'platform:express:list',
'platform:counterparty:list',
'platform:warehouse:list',
]))
const paths = getNavigationLeaves(filtered).map((item) => item.path)
expect(filtered.map((item) => item.title)).toEqual(['首页数据看板', '国内头程', '通用'])
2026-08-05 10:46:42 +08:00
expect(paths).toEqual([
2026-08-05 15:54:59 +08:00
'/dashboard',
2026-08-06 13:56:04 +08:00
'/orders',
2026-08-05 10:46:42 +08:00
'/warehouses',
])
2026-08-05 15:54:59 +08:00
expect(paths).toContain('/dashboard')
2026-08-05 10:46:42 +08:00
expect(paths).not.toContain('/orders/first-leg')
expect(paths).not.toContain('/prototype/finance/profit-report')
})
2026-08-03 20:50:18 +08:00
it('keeps the merged customer entry for existing review-only roles', () => {
const filtered = filterAdminNavigation(adminNavigation, new Set(['platform:customer:review']))
expect(getNavigationLeaves(filtered).map((item) => item.path)).toContain('/customers')
})
2026-08-10 15:36:53 +08:00
it('shows the formal fee item page only with its view permission', () => {
const filtered = filterAdminNavigation(adminNavigation, new Set(['firstleg:fee-item:view']))
expect(getNavigationLeaves(filtered).map((item) => item.path)).toEqual(['/dashboard', '/master-data/fee-items'])
})
it('retains the supplier route permission while omitting its entry from the latest sidebar', () => {
2026-08-13 11:57:44 +08:00
const visible = filterAdminNavigation(adminNavigation, new Set(['platform:supplier:list']))
const missing = filterAdminNavigation(adminNavigation, new Set())
expect(getNavigationLeaves(adminNavigation).find(item => item.path === '/suppliers')?.permission).toBe('platform:supplier:list')
expect(getNavigationLeaves(visible).map((item) => item.path)).toEqual(['/dashboard'])
2026-08-13 11:57:44 +08:00
expect(getNavigationLeaves(missing).map((item) => item.path)).toEqual(['/dashboard'])
})
it('shows split container workbenches without a standalone smart-loading menu', () => {
2026-08-10 15:36:53 +08:00
const filtered = filterAdminNavigation(adminNavigation, new Set([
'platform:firstleg:container:list',
'platform:firstleg:preload:list',
'platform:firstleg:loading-list:list',
'platform:firstleg:terminal-drayage:list',
2026-08-10 15:36:53 +08:00
]))
expect(getNavigationLeaves(filtered).map((item) => item.path)).toEqual([
'/dashboard',
'/container-loading/domestic',
'/loading-lists',
'/loading-plans',
2026-08-10 15:36:53 +08:00
])
})
2026-08-12 20:36:17 +08:00
it('shows only the prototype-backed phase-three service center entries', () => {
const filtered = filterAdminNavigation(adminNavigation, new Set([
'platform:firstleg:ocean-booking:list',
'platform:firstleg:port-schedule:list',
'platform:firstleg:truck-appointment:list',
'platform:firstleg:customs:list',
'platform:firstleg:outbound-dispatch:list',
]))
expect(getNavigationLeaves(filtered).map((item) => item.path)).toEqual([
'/dashboard',
'/ocean-bookings',
'/trucking',
])
})
2026-08-03 11:45:45 +08:00
it('supports manually hiding a module without removing its route definition', () => {
const filtered = filterAdminNavigation([{ key: 'hidden', title: '隐藏模块', icon: adminNavigation[0].icon, path: '/hidden', hidden: true }], new Set())
expect(filtered).toEqual([])
})
2026-07-30 17:59:43 +08:00
it('keeps directories based on accessible descendants instead of a directory permission', () => {
const nodes: AdminNavigationNode[] = [{
key: 'operations',
title: '运营管理',
icon: adminNavigation[0].icon,
permission: 'directory:denied',
children: [{
key: 'tracking',
title: '轨迹管理',
icon: adminNavigation[0].icon,
path: '/tracking',
permission: 'tracking:list',
}],
}]
const filtered = filterAdminNavigation(nodes, new Set(['tracking:list']))
expect(filtered).toHaveLength(1)
expect(filtered[0].children?.[0].path).toBe('/tracking')
})
it('selects the first fallback leaf in strict depth-first configuration order', () => {
const nodes: AdminNavigationNode[] = [{
key: 'level-one',
title: '一级目录',
icon: adminNavigation[0].icon,
children: [{
key: 'level-two',
title: '二级目录',
icon: adminNavigation[0].icon,
children: [{
key: 'tracking',
title: '轨迹管理',
icon: adminNavigation[0].icon,
path: '/tracking',
permission: 'tracking:list',
}],
}, {
key: 'reports',
title: '报表管理',
icon: adminNavigation[0].icon,
path: '/reports',
permission: 'reports:list',
}],
}]
expect(getNavigationLeaves(nodes).map((item) => item.path)).toEqual(['/tracking', '/reports'])
expect(getFirstNavigationLeaf(nodes)?.path).toBe('/tracking')
})
})