Files
crawler-plugin/frontend-vue/tests/page-separated-loads.test.ts
T
huangzd1997 fa5a59e5cd
Build Backend JAR / build (push) Has been cancelled
task-116: 后台管理新增/导入表单弹窗化 + 分组管理独立菜单与UI优化 + 轻量进度端点
- 后台管理页(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 共享模块及单元测试
2026-09-01 12:54:13 +08:00

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