317 lines
13 KiB
TypeScript
317 lines
13 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
adminNavigation,
|
|
filterAdminNavigation,
|
|
getFirstNavigationLeaf,
|
|
getNavigationLeaves,
|
|
type AdminNavigationNode,
|
|
} from './adminNavigation'
|
|
|
|
describe('adminNavigation', () => {
|
|
it('keeps the latest prototype menu skeleton in source order and groups additions under general', () => {
|
|
expect(adminNavigation.map((item) => item.title)).toEqual([
|
|
'首页数据看板',
|
|
'客户管理',
|
|
'国内仓',
|
|
'海外派送',
|
|
'供应商中心',
|
|
'货柜管理',
|
|
'海外业务',
|
|
'海外仓',
|
|
'财务管理',
|
|
'销售管理',
|
|
'通用',
|
|
'系统管理',
|
|
])
|
|
expect(adminNavigation.find((item) => item.key === 'customers')?.children?.map((item) => item.title)).toEqual([
|
|
'客户列表',
|
|
'海外仓阶梯费用',
|
|
])
|
|
expect(adminNavigation.find((item) => item.key === 'domestic-warehouse')?.children?.map((item) => item.title)).toEqual([
|
|
'订单列表',
|
|
'订单测量',
|
|
'测量数据审核',
|
|
'库位管理',
|
|
'托盘管理',
|
|
'整柜业务',
|
|
])
|
|
expect(adminNavigation.find((item) => item.key === 'service-center')).toBeUndefined()
|
|
expect(adminNavigation.find((item) => item.key === 'supplier-center')?.children?.map((item) => item.title)).toEqual([
|
|
'供应商列表',
|
|
'快递公司',
|
|
'往来单位',
|
|
])
|
|
expect(adminNavigation.find((item) => item.key === 'container-management')?.children?.map((item) => item.title)).toEqual([
|
|
'订舱列表',
|
|
'拖车列表',
|
|
'货柜列表',
|
|
'预装清单',
|
|
'装柜清单',
|
|
])
|
|
expect(adminNavigation.find((item) => item.key === 'overseas-inventory')?.children?.map((item) => item.title)).toEqual([
|
|
'尾程订单',
|
|
'入库单',
|
|
'换标单',
|
|
'托盘管理',
|
|
'库存管理',
|
|
])
|
|
expect(adminNavigation.find((item) => item.key === 'general')?.children?.map((item) => item.title)).toEqual([
|
|
'产品信息',
|
|
'费用项目',
|
|
'汇率管理',
|
|
'仓库管理',
|
|
'海外仓报价',
|
|
'船期价格表',
|
|
'车辆列表',
|
|
'下载中心',
|
|
])
|
|
expect(adminNavigation.find((item) => item.key === 'notifications')).toBeUndefined()
|
|
expect(adminNavigation.find((item) => item.key === 'system')?.children?.map((item) => item.title)).toEqual([
|
|
'用户管理',
|
|
'角色权限',
|
|
'租户管理',
|
|
'品牌设置',
|
|
'菜单权限',
|
|
])
|
|
})
|
|
|
|
it('assigns every page a unique path and only shares permissions backed by the API contract', () => {
|
|
const leaves = getNavigationLeaves(adminNavigation)
|
|
const paths = leaves.map((item) => item.path)
|
|
const permissions = leaves.flatMap((item) => item.permission ? [item.permission] : [])
|
|
|
|
expect(paths).toContain('/dashboard')
|
|
expect(paths).toContain('/orders')
|
|
expect(paths).not.toContain('/orders/first-leg')
|
|
expect(paths).not.toContain('/inbounds')
|
|
expect(paths).not.toContain('/first-leg-pricing/customer-rules')
|
|
expect(paths).toContain('/downloads')
|
|
expect(paths).not.toEqual(expect.arrayContaining(['/notifications/messages', '/notifications/templates', '/notifications/mine']))
|
|
expect(paths).toContain('/master-data/fee-items')
|
|
expect(paths).toContain('/container-loading')
|
|
expect(paths).not.toContain('/container-loading/smart-loading')
|
|
expect(paths).toContain('/loading-plans')
|
|
expect(paths).toContain('/loading-lists')
|
|
expect(paths).not.toContain('/port-schedules')
|
|
expect(paths).not.toContain('/customs')
|
|
expect(paths).not.toContain('/outbound-dispatches')
|
|
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')
|
|
expect(paths).toEqual(expect.arrayContaining([
|
|
'/finance/customer-accounts',
|
|
'/finance/customer-arap',
|
|
'/finance/receivable-statistics',
|
|
'/finance/customer-statements',
|
|
'/finance/settlement-receivables',
|
|
'/finance/settlement-payables',
|
|
]))
|
|
expect(paths).not.toEqual(expect.arrayContaining(['/finance/bills', '/finance/receipts', '/finance/costs']))
|
|
expect(paths).toContain('/prototype/system-management/brand-settings')
|
|
expect(paths).toContain('/system/permissions')
|
|
expect(new Set(paths).size).toBe(paths.length)
|
|
const duplicatedPermissions = permissions.filter((permission, index) => permissions.indexOf(permission) !== index)
|
|
expect(new Set(duplicatedPermissions)).toEqual(new Set())
|
|
expect(leaves.every((item) => Boolean(item.icon))).toBe(true)
|
|
})
|
|
|
|
it('shows the five formal finance pages plus receivable statistics with real backend permissions', () => {
|
|
const filtered = filterAdminNavigation(adminNavigation, new Set([
|
|
'platform:finance:customer-account:list',
|
|
'platform:finance:customer-arap:list',
|
|
'platform:finance:receivable-statistics:list',
|
|
'platform:finance:customer-statement:list',
|
|
'platform:finance:settlement-ar:list',
|
|
'platform:finance:settlement-ap:list',
|
|
]))
|
|
|
|
expect(getNavigationLeaves(filtered).map((item) => item.path).filter((path) => path.startsWith('/finance/'))).toEqual([
|
|
'/finance/customer-accounts',
|
|
'/finance/customer-arap',
|
|
'/finance/receivable-statistics',
|
|
'/finance/customer-statements',
|
|
'/finance/settlement-receivables',
|
|
'/finance/settlement-payables',
|
|
])
|
|
})
|
|
|
|
it('shows only the confirmed phase-four pages with their list permissions', () => {
|
|
const filtered = filterAdminNavigation(adminNavigation, new Set([
|
|
'platform:overseas-warehouse:last-mile-order:list',
|
|
'platform:overseas-warehouse:inbound-order:list',
|
|
'platform:overseas-warehouse:relabel-order:list',
|
|
'platform:overseas-warehouse:pallet:list',
|
|
'platform:overseas-warehouse:inventory:list',
|
|
'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',
|
|
'/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',
|
|
]))
|
|
expect(paths).not.toContain('/overseas-warehouse/cartons')
|
|
expect(paths).not.toContain('/overseas-warehouse/products')
|
|
expect(paths.some((path) => path.includes('outbound') || path.includes('delivery'))).toBe(false)
|
|
})
|
|
|
|
it('filters protected pages without mutating the source tree', () => {
|
|
const sourceSnapshot = JSON.stringify(adminNavigation, (key, value) => key === 'icon' ? '[icon]' : value)
|
|
const filtered = filterAdminNavigation(adminNavigation, new Set(['platform:permission:list']))
|
|
|
|
const paths = getNavigationLeaves(filtered).map((item) => item.path)
|
|
expect(paths).toContain('/system/permissions')
|
|
expect(paths).not.toContain('/prototype/finance/profit-report')
|
|
expect(paths).not.toContain('/notifications/messages')
|
|
expect(paths).not.toContain('/customers')
|
|
expect(filtered.map((item) => item.title)).toEqual(['首页数据看板', '系统管理'])
|
|
expect(filtered).not.toBe(adminNavigation)
|
|
expect(filtered[0]).not.toBe(adminNavigation.find((item) => item.key === 'system'))
|
|
expect(JSON.stringify(adminNavigation, (key, value) => key === 'icon' ? '[icon]' : value)).toBe(sourceSnapshot)
|
|
})
|
|
|
|
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(['首页数据看板', '国内仓', '供应商中心', '通用'])
|
|
expect(paths).toEqual([
|
|
'/dashboard',
|
|
'/orders',
|
|
'/master-data/express-companies',
|
|
'/master-data/counterparties',
|
|
'/warehouses',
|
|
])
|
|
expect(paths).toContain('/dashboard')
|
|
expect(paths).not.toContain('/orders/first-leg')
|
|
expect(paths).not.toContain('/prototype/finance/profit-report')
|
|
})
|
|
|
|
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')
|
|
})
|
|
|
|
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('shows the public supplier list only with the list permission', () => {
|
|
const visible = filterAdminNavigation(adminNavigation, new Set(['platform:supplier:list']))
|
|
const missing = filterAdminNavigation(adminNavigation, new Set())
|
|
|
|
expect(getNavigationLeaves(visible).map((item) => item.path)).toEqual(['/dashboard', '/suppliers'])
|
|
expect(getNavigationLeaves(missing).map((item) => item.path)).toEqual(['/dashboard'])
|
|
})
|
|
|
|
it('shows container list and downstream lists without a standalone smart-loading menu', () => {
|
|
const filtered = filterAdminNavigation(adminNavigation, new Set([
|
|
'platform:firstleg:container:list',
|
|
'platform:firstleg:preload:list',
|
|
'platform:firstleg:loading-list:list',
|
|
]))
|
|
|
|
expect(getNavigationLeaves(filtered).map((item) => item.path)).toEqual([
|
|
'/dashboard',
|
|
'/container-loading',
|
|
'/loading-plans',
|
|
'/loading-lists',
|
|
])
|
|
})
|
|
|
|
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',
|
|
])
|
|
})
|
|
|
|
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([])
|
|
})
|
|
|
|
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')
|
|
})
|
|
})
|