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((r) => globalThis.setTimeout(r, ms)) function makeDeps(over: Partial = {}): 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>((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') })