chore: initial commit

This commit is contained in:
2026-07-30 17:59:42 +08:00
commit 155f06846d
62 changed files with 7852 additions and 0 deletions
+326
View File
@@ -0,0 +1,326 @@
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 {
Building2,
CircleUserRound,
ClipboardList,
KeyRound,
LayoutGrid,
Settings,
ShieldCheck,
Truck,
Users,
} from '@lucide/vue'
import {
clientNavigation,
filterNavigationByPermissions,
getFirstAccessiblePath,
getNavigationLeaves,
getNavigationParentKeys,
openNavigationParentKeys,
} from './clientNavigation.ts'
function collectNavigationNodes(nodes) {
return nodes.flatMap((node) => [
node,
...(node.children ? collectNavigationNodes(node.children) : []),
])
}
test('keeps the confirmed personal entries at the top level', () => {
assert.deepEqual(
clientNavigation.filter((node) => node.path).map(({ title, path, permission }) => ({
title,
path,
permission,
})),
[
{ title: '业务总览', path: '/', permission: 'dashboard:view' },
{ title: '我的信息', path: '/account/profile', permission: 'account:profile:view' },
],
)
})
test('keeps the confirmed navigation groups and leaf order', () => {
assert.deepEqual(
clientNavigation.map((node) => ({
title: node.title,
children: node.children?.map(({ title, path, permission }) => ({
title,
path,
permission,
})),
})),
[
{ title: '业务总览', children: undefined },
{ title: '我的信息', children: undefined },
{
title: '头程业务',
children: [
{
title: '头程订单',
path: '/orders/first-leg',
permission: 'order:first-leg:list',
},
],
},
{
title: '企业管理',
children: [
{
title: '企业资料',
path: '/company/profile',
permission: 'company:profile:view',
},
{
title: '企业成员',
path: '/organization/members',
permission: 'company:member:list',
},
{
title: '角色管理',
path: '/organization/roles',
permission: 'company:role:list',
},
{
title: '菜单权限',
path: '/organization/permissions',
permission: 'company:permission:list',
},
],
},
],
)
})
test('provides confirmed icons and unique keys recursively', () => {
const expectedIcons = new Map([
['dashboard', LayoutGrid],
['account-profile', CircleUserRound],
['first-leg', Truck],
['first-leg-orders', ClipboardList],
['company', Settings],
['company-profile', Building2],
['company-members', Users],
['company-roles', ShieldCheck],
['company-permissions', KeyRound],
])
const nodes = collectNavigationNodes(clientNavigation)
for (const node of nodes) {
assert.equal(node.icon, expectedIcons.get(node.key), `${node.key} has the confirmed icon`)
}
assert.equal(new Set(nodes.map((node) => node.key)).size, nodes.length)
})
test('flattens leaves with unique paths and non-empty permissions', () => {
const leaves = getNavigationLeaves(clientNavigation)
const paths = leaves.map((leaf) => leaf.path)
assert.deepEqual(paths, [
'/',
'/account/profile',
'/orders/first-leg',
'/company/profile',
'/organization/members',
'/organization/roles',
'/organization/permissions',
])
assert.equal(new Set(paths).size, paths.length)
assert.ok(leaves.every((leaf) => leaf.permission.trim().length > 0))
assert.equal(new Set(leaves.map((leaf) => leaf.permission)).size, leaves.length)
})
test('filters inaccessible leaves and empty navigation groups', () => {
const visible = filterNavigationByPermissions(clientNavigation, ['account:profile:view', 'order:first-leg:list'])
assert.deepEqual(getNavigationLeaves(visible).map((item) => item.path), ['/account/profile', '/orders/first-leg'])
assert.deepEqual(visible.map((item) => item.title), ['我的信息', '头程业务'])
assert.equal(getFirstAccessiblePath(['account:profile:view']), '/account/profile')
assert.equal(getFirstAccessiblePath([]), '/forbidden')
})
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'), ['first-leg'])
assert.deepEqual(getNavigationParentKeys(clientNavigation, '/organization/members'), ['company'])
assert.deepEqual(getNavigationParentKeys(syntheticTree, '/nested/leaf'), ['level-one', 'level-two'])
assert.deepEqual(getNavigationParentKeys(clientNavigation, '/not-found'), [])
})
test('opens the company parent group for a company route', () => {
const openedKeys = []
openNavigationParentKeys('/organization/members', clientNavigation, (key) => openedKeys.push(key))
assert.deepEqual(openedKeys, ['company'])
})
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 '../clientNavigation.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('../router/index.ts', import.meta.url), 'utf8')
const dashboardRoute = routerSource.match(/\{ path: '', name: 'dashboard'.*?\}/)?.[0]
assert.ok(dashboardRoute)
assert.match(dashboardRoute, /permission: 'dashboard:view'/)
})