fa5a59e5cd
Build Backend JAR / build (push) Has been cancelled
- 后台管理页(admin)所有面板的新增/导入表单改为弹窗操作,原有字段 ID 全部保留、提交逻辑不变;导入删除入口不再触发二次确认拦截 - 分组管理升级为独立菜单(V102 + schema initializer),移除 5 个面板内的管理分组按钮;分组列表改为蓝白主题、增加权限分组横幅 - Python 侧 group-manage 权限守卫(_ensure_backend_menu_access 补充 group-manage) - 引入 V101(biz_task_file_job 复合索引)+ 新增 TaskProgressLight/TaskFileJob 轻量端点与进度聚合作 - 前端 progress-light / page-separated-loads / dispatch-guard 共享模块及单元测试
244 lines
8.7 KiB
TypeScript
244 lines
8.7 KiB
TypeScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import {
|
|
createPageSeparatedLoads,
|
|
type PageSeparatedLoadsDeps,
|
|
} from '../src/shared/page-separated-loads.ts'
|
|
|
|
const tick = (ms = 0) => new Promise<void>((r) => globalThis.setTimeout(r, ms))
|
|
|
|
function makeDeps(over: Partial<PageSeparatedLoadsDeps> = {}): PageSeparatedLoadsDeps & {
|
|
calls: string[]
|
|
} {
|
|
const calls: string[] = []
|
|
return {
|
|
calls,
|
|
statsLoader: async () => {
|
|
calls.push('stats')
|
|
return { pendingTaskCount: 1 }
|
|
},
|
|
summaryLoader: async () => {
|
|
calls.push('summary')
|
|
return { items: [] }
|
|
},
|
|
lightLoader: async (ids) => {
|
|
calls.push(`light:${ids.join(',')}`)
|
|
return { items: ids.map((id) => ({ taskId: id, status: 'RUNNING' })) }
|
|
},
|
|
detailLoader: async (taskId) => {
|
|
calls.push(`detail:${taskId}`)
|
|
return { taskId, fileName: `file-${taskId}.xlsx` }
|
|
},
|
|
resultLoader: async (taskId) => {
|
|
calls.push(`result:${taskId}`)
|
|
return { taskId, url: `/api/result/${taskId}` }
|
|
},
|
|
...over,
|
|
}
|
|
}
|
|
|
|
const ticks = (ms = 0) => globalThis.setTimeout as unknown as typeof setTimeout
|
|
void ticks
|
|
|
|
test('test_page_dashboard_only_stats', async () => {
|
|
// dashboard 通道只调统计接口:不触发 history/light/detail/result 任何调用
|
|
const deps = makeDeps()
|
|
const page = createPageSeparatedLoads(deps)
|
|
await page.loadDashboard()
|
|
assert.deepEqual(deps.calls, ['stats'], 'dashboard 通道只调 statsLoader')
|
|
assert.deepEqual(
|
|
page.requestLog().map((e) => e.phase),
|
|
['stats'],
|
|
'请求日志只有 stats 通道',
|
|
)
|
|
})
|
|
|
|
test('test_page_history_only_summary', async () => {
|
|
// history 通道只调摘要接口:不拉任何任务明细
|
|
const deps = makeDeps()
|
|
const page = createPageSeparatedLoads(deps)
|
|
await page.loadHistory()
|
|
assert.deepEqual(deps.calls, ['summary'], 'history 通道只调 summaryLoader')
|
|
assert.ok(!deps.calls.some((c) => c.startsWith('detail:')), 'history 不拉明细')
|
|
assert.ok(!deps.calls.some((c) => c.startsWith('result:')), 'history 不拉结果')
|
|
assert.equal(deps.calls.filter((c) => c === 'summary').length, 1, '摘要仅一次')
|
|
assert.deepEqual(
|
|
page.requestLog().map((e) => e.phase),
|
|
['summary'],
|
|
)
|
|
})
|
|
|
|
test('test_page_progress_only_light', async () => {
|
|
// progress 通道只调轻量进度:请求路径为 light,不触碰 detail/result 通道
|
|
const deps = makeDeps()
|
|
const page = createPageSeparatedLoads(deps)
|
|
await page.loadProgress()
|
|
assert.ok(deps.calls.some((c) => c.startsWith('light:')), 'progress 调用 lightLoader')
|
|
assert.ok(!deps.calls.some((c) => c.startsWith('detail:')), '不调明细')
|
|
assert.ok(!deps.calls.some((c) => c.startsWith('result:')), '不调结果')
|
|
const light = page.requestLog().find((e) => e.phase === 'light')
|
|
assert.ok(light, '请求日志记录 light 通道')
|
|
})
|
|
|
|
test('test_page_no_full_detail_on_load', async () => {
|
|
// 首次打开不加载全部明细:初载只有三类轻量请求,无 detail/result
|
|
const deps = makeDeps({
|
|
lightLoader: async () => ({ items: [1, 2, 3].map((taskId) => ({ taskId, status: 'RUNNING' })) }),
|
|
})
|
|
const page = createPageSeparatedLoads(deps)
|
|
await page.initialLoad()
|
|
assert.equal(
|
|
deps.calls.filter((c) => c.startsWith('detail:')).length,
|
|
0,
|
|
'首开不拉任何明细',
|
|
)
|
|
assert.equal(
|
|
deps.calls.filter((c) => c.startsWith('result:')).length,
|
|
0,
|
|
'首开不拉任何结果',
|
|
)
|
|
assert.deepEqual(
|
|
page.requestLog().map((e) => e.phase),
|
|
['stats', 'summary', 'light'],
|
|
'初载仅 stats/summary/light 三通道',
|
|
)
|
|
})
|
|
|
|
test('test_page_items_on_open', async () => {
|
|
// 打开明细才请求:expand 后仅请求对应任务,未展开的任务不请求
|
|
const deps = makeDeps({
|
|
lightLoader: async () => ({ items: [10, 20].map((taskId) => ({ taskId, status: 'RUNNING' })) }),
|
|
})
|
|
const page = createPageSeparatedLoads(deps)
|
|
await page.initialLoad()
|
|
const detail = await page.openDetail(10)
|
|
assert.ok(deps.calls.includes('detail:10'), '展开 10 才请求其明细')
|
|
assert.ok(!deps.calls.includes('detail:20'), '未展开 20 不请求明细')
|
|
assert.deepEqual(detail, { taskId: 10, fileName: 'file-10.xlsx' })
|
|
assert.equal(page.requestLog().filter((e) => e.phase === 'detail').length, 1)
|
|
})
|
|
|
|
test('test_page_result_on_demand', async () => {
|
|
// 结果按需:用户点取结果才请求结果文件,初载与展开明细都不触发
|
|
const deps = makeDeps({
|
|
lightLoader: async () => ({ items: [7].map((taskId) => ({ taskId, status: 'SUCCESS' })) }),
|
|
})
|
|
const page = createPageSeparatedLoads(deps)
|
|
await page.initialLoad()
|
|
await page.openDetail(7)
|
|
assert.equal(deps.calls.filter((c) => c.startsWith('result:')).length, 0, '取结果前不请求')
|
|
const result = await page.openResult(7)
|
|
assert.ok(deps.calls.includes('result:7'), '点取结果才请求')
|
|
assert.deepEqual(result, { taskId: 7, url: '/api/result/7' })
|
|
})
|
|
|
|
test('test_page_request_count_bounded', async () => {
|
|
// 请求数有界:初载恒为 3 个并发轻量请求,每次按需展开仅 +1,不因任务数放大
|
|
const deps = makeDeps({
|
|
lightLoader: async () => ({ items: [1, 2, 3, 4, 5].map((taskId) => ({ taskId, status: 'RUNNING' })) }),
|
|
})
|
|
const page = createPageSeparatedLoads(deps)
|
|
await page.initialLoad()
|
|
await Promise.all([1, 2, 3, 4, 5].map((id) => page.openDetail(id)))
|
|
await tick()
|
|
const counts = page.requestLog().reduce<Record<string, number>>((acc, e) => {
|
|
acc[e.phase] = (acc[e.phase] ?? 0) + 1
|
|
return acc
|
|
}, {})
|
|
assert.equal(counts.stats, 1, '统计仅 1 次')
|
|
assert.equal(counts.summary, 1, '摘要仅 1 次')
|
|
assert.equal(counts.light, 1, '轻量进度仅 1 次(批内合并)')
|
|
assert.equal(counts.detail ?? 0, 5, '5 个任务各展开 1 次明细')
|
|
assert.ok(Object.keys(counts).length <= 4, '不产生任何其它通道请求')
|
|
assert.ok(page.requestLog().length <= 3 + 5, '总请求数 = 初载 3 + 按需 5,有界')
|
|
})
|
|
|
|
test('test_page_snapshot', async () => {
|
|
// 请求快照:完整回放页面生命周期内的请求顺序与 taskIds
|
|
const calls: string[] = []
|
|
const deps: PageSeparatedLoadsDeps = {
|
|
statsLoader: async () => {
|
|
calls.push('stats')
|
|
return {}
|
|
},
|
|
summaryLoader: async () => {
|
|
calls.push('summary')
|
|
return { items: [] }
|
|
},
|
|
lightLoader: async (ids) => {
|
|
calls.push(`light:${ids.join(',')}`)
|
|
return { items: [1, 2].map((taskId) => ({ taskId, status: 'RUNNING' })) }
|
|
},
|
|
detailLoader: async (taskId) => {
|
|
calls.push(`detail:${taskId}`)
|
|
return { taskId }
|
|
},
|
|
resultLoader: async (taskId) => {
|
|
calls.push(`result:${taskId}`)
|
|
return { taskId }
|
|
},
|
|
}
|
|
const page = createPageSeparatedLoads(deps)
|
|
await page.initialLoad()
|
|
await page.openDetail(1)
|
|
await page.openResult(2)
|
|
assert.deepEqual(
|
|
page.requestLog().map((e) => [e.phase, e.taskIds]),
|
|
[
|
|
['stats', []],
|
|
['summary', []],
|
|
['light', []],
|
|
['detail', [1]],
|
|
['result', [2]],
|
|
],
|
|
'快照按请求发生顺序记录阶段与任务',
|
|
)
|
|
assert.deepEqual(
|
|
calls,
|
|
['stats', 'summary', 'light:', 'detail:1', 'result:2'],
|
|
'loader 调用序列与快照一致',
|
|
)
|
|
})
|
|
|
|
test('test_page_open_unknown_task_no_request', async () => {
|
|
// 轻量通道未就绪的任务展开/取结果不发起任何请求(不存在任务静默返回)
|
|
const deps = makeDeps({ lightLoader: async () => ({ items: [] }) })
|
|
const page = createPageSeparatedLoads(deps)
|
|
await page.initialLoad()
|
|
const detail = await page.openDetail(999)
|
|
const result = await page.openResult(999)
|
|
assert.equal(detail, undefined)
|
|
assert.equal(result, undefined)
|
|
assert.equal(deps.calls.filter((c) => c.startsWith('detail:')).length, 0)
|
|
assert.equal(deps.calls.filter((c) => c.startsWith('result:')).length, 0)
|
|
})
|
|
|
|
test('test_page_deps_validation', async () => {
|
|
await assert.rejects(async () => {
|
|
createPageSeparatedLoads(null as never).initialLoad()
|
|
}, /statsLoader/)
|
|
await assert.rejects(async () => {
|
|
createPageSeparatedLoads(
|
|
{
|
|
statsLoader: async () => ({}),
|
|
summaryLoader: async () => ({}),
|
|
lightLoader: async () => ({}),
|
|
detailLoader: async () => ({}),
|
|
resultLoader: async () => ({}),
|
|
} as never,
|
|
{ maxConcurrentRequests: 0 },
|
|
).initialLoad()
|
|
}, /maxConcurrentRequests/)
|
|
})
|
|
|
|
test('test_page_light_items_seeded', async () => {
|
|
// light 响应带 taskId 时,初载后这些任务即视为就绪,可直接按需展开
|
|
const deps = makeDeps({
|
|
lightLoader: async () => ({ items: [42].map((taskId) => ({ taskId, status: 'RUNNING' })) }),
|
|
})
|
|
const page = createPageSeparatedLoads(deps)
|
|
await page.initialLoad()
|
|
assert.equal((await page.openDetail(42)).fileName, 'file-42.xlsx')
|
|
assert.equal((await page.openResult(42)).url, '/api/result/42')
|
|
})
|