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('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') }) })