import assert from 'node:assert/strict' import { spawnSync } from 'node:child_process' import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { join } from 'node:path' import test from 'node:test' import { ClipboardList, LayoutGrid, Settings, Truck, } from '@lucide/vue' import { clientNavigation, filterNavigationByPermissions, getFirstAccessiblePath, getNavigationLeaves, getNavigationParentKeys, getRouteBreadcrumbs, openNavigationParentKeys, } from './clientNavigation.ts' function collectNavigationNodes(nodes) { return nodes.flatMap((node) => [ node, ...(node.children ? collectNavigationNodes(node.children) : []), ]) } test('keeps account and notification routes as fixed-header utilities', () => { assert.deepEqual( clientNavigation.filter((node) => node.hidden).map(({ title, path, permission }) => ({ title, path, permission, })), [ { title: '我的信息', path: '/account/profile', permission: 'account:profile:view' }, { title: '我的消息', path: '/notifications', permission: 'merchant:notification:view' }, ], ) }) test('keeps standalone business pages at the client sidebar root', () => { assert.deepEqual( clientNavigation.filter((node) => !node.hidden).map((node) => ({ title: node.title, children: node.children?.map(({ title }) => title), })), [ { title: '首页', children: undefined }, { title: '订单中心', children: ['头程订单', '入库订单', '派送订单', '海外采购', '美国本土卡', '美本土散板'], }, { title: '报关资料', children: undefined }, { title: '船期价格', children: undefined }, { title: '海外仓', children: ['贴标申请', '仓储转运', '货物废弃', '退货换标'], }, { title: '费用与账单', children: ['资金账户', '客户账单'], }, { title: '企业设置', children: ['企业资料', '企业成员', '角色管理', '菜单权限'], }, ], ) }) test('provides icons and unique keys recursively for every visible item', () => { const nodes = collectNavigationNodes(clientNavigation) for (const node of nodes) { assert.ok(node.icon, `${node.key} has an icon`) } assert.equal(new Set(nodes.map((node) => node.key)).size, nodes.length) }) test('flattens visible leaves with unique paths', () => { const leaves = getNavigationLeaves(clientNavigation) const paths = leaves.map((leaf) => leaf.path) assert.deepEqual(paths, [ '/', '/orders/first-leg', '/orders/inbound', '/orders/delivery', '/cross-border/procurement', '/cross-border/domestic-trucking', '/cross-border/pallet-shipping', '/first-leg/customs-documents', '/first-leg/sailing-prices', '/overseas-warehouse/labeling', '/overseas-warehouse/storage-transfer', '/overseas-warehouse/cargo-disposal', '/overseas-warehouse/return-relabel', '/finance/account', '/finance/bills', '/company/profile', '/organization/members', '/organization/roles', '/organization/permissions', '/account/profile', '/notifications', ]) assert.equal(new Set(paths).size, paths.length) assert.ok(leaves.every((leaf) => leaf.permission.trim().length > 0)) }) test('filters inaccessible leaves and empty navigation groups', () => { const visible = filterNavigationByPermissions(clientNavigation, ['account:profile:view', 'order:first-leg:list', 'order:first-leg:customs-list', 'order:first-leg:pricing-list']) assert.deepEqual(getNavigationLeaves(visible).map((item) => item.path), ['/orders/first-leg', '/first-leg/customs-documents', '/first-leg/sailing-prices']) assert.deepEqual(visible.map((item) => item.title), ['订单中心', '报关资料', '船期价格']) assert.equal(getFirstAccessiblePath(['account:profile:view']), '/account/profile') assert.equal(getFirstAccessiblePath([]), '/forbidden') }) test('exposes active inbound and delivery queries only with their existing read permissions', () => { const deliveryOnly = ['merchant:warehouse:delivery:list'] const inboundOnly = ['merchant:warehouse:inbound:list'] assert.deepEqual(getNavigationLeaves(filterNavigationByPermissions(clientNavigation, deliveryOnly)) .map(({ title, path }) => ({ title, path })), [{ title: '派送订单', path: '/orders/delivery' }]) assert.deepEqual(getNavigationLeaves(filterNavigationByPermissions(clientNavigation, inboundOnly)) .map(({ title, path }) => ({ title, path })), [{ title: '入库订单', path: '/orders/inbound' }]) assert.equal(getFirstAccessiblePath(deliveryOnly), '/orders/delivery') assert.equal(getFirstAccessiblePath(inboundOnly), '/orders/inbound') assert.deepEqual(getNavigationLeaves(filterNavigationByPermissions(clientNavigation, ['merchant:warehouse:delivery:create'])), []) assert.deepEqual(getRouteBreadcrumbs('/orders/delivery/DL-1', '派送单详情'), [ { title: '首页', path: '/' }, { title: '订单中心' }, { title: '派送订单', path: '/orders/delivery' }, { title: '派送单详情' }, ]) assert.deepEqual(getNavigationParentKeys(clientNavigation, '/orders/delivery'), ['order-center']) }) test('excludes entries marked for manual hiding without affecting other visible items', () => { const visible = filterNavigationByPermissions([ { key: 'shown', title: '显示入口', icon: LayoutGrid, path: '/shown', permission: 'shown:view' }, { key: 'hidden', title: '隐藏入口', icon: LayoutGrid, path: '/hidden', permission: 'hidden:view', hidden: true }, ], ['shown:view', 'hidden:view']) assert.deepEqual(getNavigationLeaves(visible).map((item) => item.path), ['/shown']) }) test('puts enterprise settings behind the prototype settings divider', () => { const company = clientNavigation.find((node) => node.key === 'company-center') assert.ok(company) assert.equal(company.dividerBefore, true) }) test('flattens a synthetic three-level navigation tree recursively', () => { const nestedLeaf = { key: 'nested-leaf', title: '嵌套叶子', icon: ClipboardList, path: '/nested/leaf', permission: 'nested:leaf:view', } const syntheticTree = [ { key: 'level-one', title: '一级分组', icon: Settings, children: [ { key: 'level-two', title: '二级分组', icon: Truck, children: [nestedLeaf], }, ], }, ] assert.deepEqual(getNavigationLeaves(syntheticTree), [nestedLeaf]) }) test('returns navigation parent keys from outermost to innermost', () => { const syntheticTree = [ { key: 'level-one', title: '一级分组', icon: Settings, children: [ { key: 'level-two', title: '二级分组', icon: Truck, children: [ { key: 'nested-leaf', title: '嵌套叶子', icon: ClipboardList, path: '/nested/leaf', permission: 'nested:leaf:view', }, ], }, ], }, ] assert.deepEqual(getNavigationParentKeys(clientNavigation, '/'), []) assert.deepEqual(getNavigationParentKeys(clientNavigation, '/orders/first-leg'), ['order-center']) assert.deepEqual(getNavigationParentKeys(clientNavigation, '/organization/members'), ['company-center']) assert.deepEqual(getNavigationParentKeys(syntheticTree, '/nested/leaf'), ['level-one', 'level-two']) assert.deepEqual(getNavigationParentKeys(clientNavigation, '/not-found'), []) }) test('builds complete breadcrumbs without repeating the current navigation page', () => { assert.deepEqual(getRouteBreadcrumbs('/', '业务总览'), [{ title: '首页' }]) assert.deepEqual(getRouteBreadcrumbs('/orders/first-leg', '订单列表'), [ { title: '首页', path: '/' }, { title: '订单中心' }, { title: '订单列表' }, ]) assert.deepEqual(getRouteBreadcrumbs('/orders/first-leg/create', '创建订单'), [ { title: '首页', path: '/' }, { title: '订单中心' }, { title: '头程订单', path: '/orders/first-leg' }, { title: '创建订单' }, ]) assert.deepEqual(getRouteBreadcrumbs('/notifications/NOTICE-001', '消息详情'), [ { title: '首页', path: '/' }, { title: '我的消息', path: '/notifications' }, { title: '消息详情' }, ]) assert.deepEqual(getRouteBreadcrumbs('/forbidden', '无权访问'), [ { title: '首页', path: '/' }, { title: '无权访问' }, ]) }) test('opens the enterprise settings parent group for a company route', () => { const openedKeys = [] openNavigationParentKeys('/organization/members', clientNavigation, (key) => openedKeys.push(key)) assert.deepEqual(openedKeys, ['company-center']) }) test('opens nested parent groups from outermost to innermost', () => { const syntheticTree = [ { key: 'level-one', title: '一级分组', icon: Settings, children: [ { key: 'level-two', title: '二级分组', icon: Truck, children: [ { key: 'nested-leaf', title: '嵌套叶子', icon: ClipboardList, path: '/nested/leaf', permission: 'nested:leaf:view', }, ], }, ], }, ] const openedKeys = [] openNavigationParentKeys('/nested/leaf', syntheticTree, (key) => openedKeys.push(key)) assert.deepEqual(openedKeys, ['level-one', 'level-two']) }) test('does not open a group when the navigation path does not exist', () => { const openedKeys = [] openNavigationParentKeys('/not-found', clientNavigation, (key) => openedKeys.push(key)) assert.deepEqual(openedKeys, []) }) test('enforces mutually exclusive leaf and non-empty group node types', () => { const testDirectory = mkdtempSync(join(fileURLToPath(new URL('.', import.meta.url)), '.client-navigation-types-')) const fixturePath = join(testDirectory, 'fixture.ts') const compilerPath = fileURLToPath(new URL('../../node_modules/typescript/bin/tsc', import.meta.url)) try { writeFileSync(fixturePath, ` import type { Component } from 'vue' import { getNavigationLeaves, type ClientNavigationGroup, type ClientNavigationLeaf, type ClientNavigationNode, } from '../tree.ts' declare const icon: Component const leaf: ClientNavigationLeaf = { key: 'leaf', title: 'Leaf', icon, path: '/leaf', permission: 'leaf:view', } const group: ClientNavigationGroup = { key: 'group', title: 'Group', icon, children: [leaf], } const nodes: readonly ClientNavigationNode[] = [leaf, group] getNavigationLeaves(nodes) // @ts-expect-error groups require at least one child const emptyGroup: ClientNavigationGroup = { key: 'empty', title: 'Empty', icon, children: [] } // @ts-expect-error groups cannot be route leaves const linkedGroup: ClientNavigationGroup = { key: 'linked', title: 'Linked', icon, children: [leaf], path: '/linked' } // @ts-expect-error leaves cannot contain child nodes const parentLeaf: ClientNavigationLeaf = { key: 'parent', title: 'Parent', icon, path: '/parent', permission: 'parent:view', children: [leaf] } void emptyGroup void linkedGroup void parentLeaf `) const result = spawnSync(process.execPath, [ compilerPath, '--noEmit', '--strict', '--skipLibCheck', '--target', 'ES2022', '--module', 'ESNext', '--moduleResolution', 'Bundler', '--allowImportingTsExtensions', fixturePath, ], { encoding: 'utf8' }) assert.equal(result.status, 0, result.stdout || result.stderr) } finally { rmSync(testDirectory, { recursive: true, force: true }) } }) test('keeps the dashboard route permission synchronized with navigation', () => { const routerSource = readFileSync(new URL('../modules/dashboard/routes.ts', import.meta.url), 'utf8') const dashboardRoute = routerSource.match(/path: ''[\s\S]*?navigation: \{ key: 'dashboard'[\s\S]*?\}/)?.[0] assert.ok(dashboardRoute) assert.match(dashboardRoute, /permission: 'dashboard:view'/) }) test('exposes customer business lists and keeps utility routes outside navigation', () => { const routerSource = [ 'first-leg', 'cross-border', 'overseas-warehouse', 'order-management', ].map((moduleName) => readFileSync(new URL(`../modules/${moduleName}/routes.ts`, import.meta.url), 'utf8')).join('\n') const routes = [ ['first-leg/quick-order', 'QuickOrderView.vue'], ['cross-border/procurement', 'ProcurementView.vue'], ['cross-border/domestic-trucking', 'DomesticTruckingView.vue'], ['cross-border/pallet-shipping', 'PalletShippingView.vue'], ['overseas-warehouse/labeling', 'WarehouseServiceView.vue'], ['overseas-warehouse/storage-transfer', 'WarehouseServiceView.vue'], ['overseas-warehouse/cargo-disposal', 'WarehouseServiceView.vue'], ['overseas-warehouse/return-relabel', 'ReturnOrdersView.vue'], ['overseas-warehouse/return-inventory', 'ReturnInventoryView.vue'], ['overseas-warehouse/return-cartons', 'ReturnInventoryView.vue'], ['orders/my', 'MyOrdersView.vue'], ['orders/inbound', 'InboundOrdersView.vue'], ['orders/delivery', 'DeliveryOrdersView.vue'], ] const hiddenActivePaths = new Set([ 'first-leg/quick-order', 'overseas-warehouse/return-inventory', 'overseas-warehouse/return-cartons', ]) for (const [path, view] of routes) { if (path !== 'orders/my') { assert.match(routerSource, new RegExp(`path: '${path}'[\\s\\S]*?\\./views/${view.replace('.', '\\.')}[\\s\\S]*?release: 'active'[\\s\\S]*?permission:`)) const visible = !hiddenActivePaths.has(path) assert.equal(getNavigationLeaves(clientNavigation).some((item) => item.path === `/${path}`), visible) continue } assert.match(routerSource, new RegExp(`path: '${path}'[\\s\\S]*?\\./views/${view.replace('.', '\\.')}[\\s\\S]*?release: 'prototype'[\\s\\S]*?permission:`)) assert.equal(getNavigationLeaves(clientNavigation).some((item) => item.path === `/${path}`), false) } assert.doesNotMatch(routerSource, /first-leg\/import|BatchImportView|merchant:first-leg:import/) })