46 lines
2.9 KiB
TypeScript
46 lines
2.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { KbxNavigationResolvedEntry } from '@kbx/contracts'
|
|
import { buildKbxHomeQuickStart, buildKbxHomeWorkbench, matchKbxRoutePattern, resolveKbxSafeRecentPath, resolveKbxSafeWorkspacePath } from '@kbx/ui'
|
|
|
|
const entry=(screenId:string,path:string,homePriority?:number):KbxNavigationResolvedEntry=>({screenId,path,title:screenId,module:'ERP',section:'업무',permissions:[],menuVisible:true,favoriteAllowed:true,recentPolicy:'screen',homePriority})
|
|
|
|
describe('KBX navigation hardening',()=>{
|
|
it('deduplicates home quick start in favorite, tab, recent, priority order',()=>{
|
|
const a=entry('A','/a',30),b=entry('B','/b',10),c=entry('C','/c',20)
|
|
const result=buildKbxHomeQuickStart([a,b,c],[a],[{screenId:'B',path:'/b',title:'B',visitedAt:'2026-08-08T10:00:00Z'}],[{key:'C:/c',screenId:'C',title:'C',path:'/c',openedAt:'2026-08-08T09:00:00Z',lastActivatedAt:'2026-08-08T11:00:00Z'}])
|
|
expect(result.map(x=>x.screenId)).toEqual(['A','C','B'])
|
|
})
|
|
|
|
it('never trusts a persisted route unless route mode and path boundary are both valid',()=>{
|
|
const base={...entry('A','/erp/items'),recentPolicy:'route' as const}
|
|
expect(resolveKbxSafeRecentPath(base,'/erp/items/123?tab=stock')).toBe('/erp/items/123')
|
|
expect(resolveKbxSafeRecentPath(base,'/erp/items-evil/123')).toBe('/erp/items')
|
|
expect(resolveKbxSafeRecentPath(base,'https://evil.example/x')).toBe('/erp/items')
|
|
expect(resolveKbxSafeRecentPath({...base,recentPolicy:'screen'},'/erp/items/123')).toBe('/erp/items')
|
|
})
|
|
|
|
it('keeps dirty/open work ahead of favorites and avoids duplicate home launch rows',()=>{
|
|
const a=entry('A','/a',30),b=entry('B','/b',10)
|
|
const result=buildKbxHomeWorkbench([a,b],[a],[],[
|
|
{key:'A:/a',screenId:'A',title:'A',path:'/a',openedAt:'2026-08-08T09:00:00Z',lastActivatedAt:'2026-08-08T11:00:00Z',dirty:true},
|
|
])
|
|
expect(result.filter(x=>x.screenId==='A')).toHaveLength(1)
|
|
expect(result[0]?.source).toBe('dirty')
|
|
})
|
|
|
|
it('validates workspace deep links against the registered route capability',()=>{
|
|
expect(matchKbxRoutePattern('/oms/orders/:orderId/edit','/oms/orders/123/edit')).toBe(true)
|
|
expect(matchKbxRoutePattern('/oms/orders/:orderId/edit','/oms/orders/123/delete')).toBe(false)
|
|
expect(resolveKbxSafeWorkspacePath(['/oms/orders/:orderId/edit'],'/oms/orders/123/edit','/home')).toBe('/oms/orders/123/edit')
|
|
expect(resolveKbxSafeWorkspacePath(['/oms/orders/:orderId/edit'],'https://evil.example/oms/orders/123/edit','/home')).toBe('/home')
|
|
})
|
|
|
|
it('rejects oversized and control-character workspace paths',()=>{
|
|
const fallback='/home'
|
|
expect(resolveKbxSafeWorkspacePath(['/erp/items/:itemId'],`/erp/items/${'a'.repeat(2100)}`,fallback)).toBe(fallback)
|
|
expect(resolveKbxSafeWorkspacePath(['/erp/items/:itemId'],'/erp/items/abc\n123',fallback)).toBe(fallback)
|
|
expect(resolveKbxSafeWorkspacePath(['/erp/items/:itemId'],'/erp/items/%2e%2e',fallback)).toBe(fallback)
|
|
})
|
|
|
|
})
|