task-116: 后台管理新增/导入表单弹窗化 + 分组管理独立菜单与UI优化 + 轻量进度端点
Build Backend JAR / build (push) Has been cancelled
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 共享模块及单元测试
This commit is contained in:
@@ -139,6 +139,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
fileActivate: '/api/publish/tasks/{taskId}/files/{fileId}/activate',
|
||||
items: '/api/publish/tasks/{taskId}/items',
|
||||
progressBatch: '/api/publish/tasks/progress/batch',
|
||||
progressLight: '/api/publish/tasks/progress/light',
|
||||
result: '/api/publish/tasks/{taskId}/result',
|
||||
dashboard: '/api/publish/dashboard',
|
||||
history: '/api/publish/history',
|
||||
@@ -154,6 +155,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
resultDownload: '/api/delete-brand/results/{resultId}/download',
|
||||
resultSubmit: '/api/delete-brand/tasks/{taskId}/result',
|
||||
progressBatch: '/api/delete-brand/tasks/progress/batch',
|
||||
progressLight: '/api/delete-brand/tasks/progress/light',
|
||||
},
|
||||
productRisk: {
|
||||
candidates: '/api/product-risk-resolve/candidates',
|
||||
@@ -168,6 +170,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
taskDelete: '/api/product-risk-resolve/tasks/{taskId}',
|
||||
taskBatch: '/api/product-risk-resolve/tasks/batch',
|
||||
progressBatch: '/api/product-risk-resolve/tasks/progress/batch',
|
||||
progressLight: '/api/product-risk-resolve/tasks/progress/light',
|
||||
pendingShopResult: '/api/product-risk-resolve/pending-shop-result',
|
||||
resultDownload: '/api/product-risk-resolve/results/{resultId}/download',
|
||||
},
|
||||
@@ -186,6 +189,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
stageFinished: '/api/shop-match/tasks/{taskId}/stage-finished',
|
||||
taskBatch: '/api/shop-match/tasks/batch',
|
||||
progressBatch: '/api/shop-match/tasks/progress/batch',
|
||||
progressLight: '/api/shop-match/tasks/progress/light',
|
||||
skipAsins: '/api/shop-match/tasks/{taskId}/skip-asins/paginated',
|
||||
resultDownload: '/api/shop-match/results/{resultId}/download',
|
||||
},
|
||||
@@ -201,6 +205,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
history: '/api/patrol-delete/history',
|
||||
historyDelete: '/api/patrol-delete/history/{resultId}',
|
||||
progressBatch: '/api/patrol-delete/tasks/progress/batch',
|
||||
progressLight: '/api/patrol-delete/tasks/progress/light',
|
||||
taskCreate: '/api/patrol-delete/tasks',
|
||||
taskDelete: '/api/patrol-delete/tasks/{taskId}',
|
||||
taskResult: '/api/patrol-delete/tasks/{taskId}/result',
|
||||
@@ -215,6 +220,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
history: '/api/query-asin/history',
|
||||
historyDelete: '/api/query-asin/history/{resultId}',
|
||||
progressBatch: '/api/query-asin/tasks/progress/batch',
|
||||
progressLight: '/api/query-asin/tasks/progress/light',
|
||||
taskCreate: '/api/query-asin/tasks',
|
||||
taskDelete: '/api/query-asin/tasks/{taskId}',
|
||||
taskResult: '/api/query-asin/tasks/{taskId}/result',
|
||||
@@ -230,6 +236,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
history: '/api/shop-data-crawl/history',
|
||||
historyDelete: '/api/shop-data-crawl/history/{resultId}',
|
||||
progressBatch: '/api/shop-data-crawl/tasks/progress/batch',
|
||||
progressLight: '/api/shop-data-crawl/tasks/progress/light',
|
||||
taskCreate: '/api/shop-data-crawl/tasks',
|
||||
taskDelete: '/api/shop-data-crawl/tasks/{taskId}',
|
||||
resultDownload: '/api/shop-data-crawl/results/{resultId}/download',
|
||||
@@ -244,6 +251,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
history: '/api/withdraw/history',
|
||||
historyDelete: '/api/withdraw/history/{resultId}',
|
||||
progressBatch: '/api/withdraw/tasks/progress/batch',
|
||||
progressLight: '/api/withdraw/tasks/progress/light',
|
||||
taskCreate: '/api/withdraw/tasks',
|
||||
taskDelete: '/api/withdraw/tasks/{taskId}',
|
||||
taskResult: '/api/withdraw/tasks/{taskId}/result',
|
||||
@@ -259,6 +267,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
taskDelete: '/api/price-track/tasks/{taskId}',
|
||||
tasksBatch: '/api/price-track/tasks/batch',
|
||||
progressBatch: '/api/price-track/tasks/progress/batch',
|
||||
progressLight: '/api/price-track/tasks/progress/light',
|
||||
loopRuns: '/api/price-track/loop-runs',
|
||||
loopRunDetail: '/api/price-track/loop-runs/{loopRunId}',
|
||||
loopRunStop: '/api/price-track/loop-runs/{loopRunId}/stop',
|
||||
@@ -279,6 +288,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
history: '/api/similar-asin/history',
|
||||
historyDelete: '/api/similar-asin/history/{resultId}',
|
||||
progressBatch: '/api/similar-asin/tasks/progress/batch',
|
||||
progressLight: '/api/similar-asin/tasks/progress/light',
|
||||
taskActivate: '/api/similar-asin/tasks/{taskId}/activate',
|
||||
taskDelete: '/api/similar-asin/tasks/{taskId}',
|
||||
resultDownload: '/api/similar-asin/results/{resultId}/download',
|
||||
@@ -291,6 +301,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
history: '/api/appearance-patent/history',
|
||||
historyDelete: '/api/appearance-patent/history/{resultId}',
|
||||
progressBatch: '/api/appearance-patent/tasks/progress/batch',
|
||||
progressLight: '/api/appearance-patent/tasks/progress/light',
|
||||
taskActivate: '/api/appearance-patent/tasks/{taskId}/activate',
|
||||
taskDelete: '/api/appearance-patent/tasks/{taskId}',
|
||||
resultDownload: '/api/appearance-patent/results/{resultId}/download',
|
||||
@@ -302,6 +313,7 @@ test('test_endpoints_frozen_snapshot', () => {
|
||||
history: '/api/collect-data/history',
|
||||
historyDelete: '/api/collect-data/history/{resultId}',
|
||||
progressBatch: '/api/collect-data/tasks/progress/batch',
|
||||
progressLight: '/api/collect-data/tasks/progress/light',
|
||||
taskActivate: '/api/collect-data/tasks/{taskId}/activate',
|
||||
taskDelete: '/api/collect-data/tasks/{taskId}',
|
||||
fail: '/api/collect-data/tasks/{taskId}/fail',
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
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')
|
||||
})
|
||||
@@ -0,0 +1,166 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { http } from '../src/shared/api/http.ts'
|
||||
import { API_ENDPOINTS } from '../src/shared/api/endpoints.ts'
|
||||
import {
|
||||
getModuleProgressLight,
|
||||
type ProgressLightModule,
|
||||
type TaskProgressLightItem,
|
||||
} from '../src/shared/api/progress-light.ts'
|
||||
import type { TaskProgressBatchVo } from '../src/shared/api/types/task.ts'
|
||||
|
||||
function setupWindow() {
|
||||
;(globalThis as Record<string, unknown>).window = {
|
||||
localStorage: { getItem: () => '42' },
|
||||
location: { origin: 'http://localhost' },
|
||||
}
|
||||
}
|
||||
|
||||
function mockRequest(
|
||||
t: Parameters<typeof test>[1] extends (t: infer T) => unknown ? T : never,
|
||||
impl: (config: { url?: string; method?: string; params?: Record<string, unknown>; data?: unknown; timeout?: number }) => Promise<unknown>,
|
||||
) {
|
||||
t.mock.method(http, 'request', impl as never)
|
||||
}
|
||||
|
||||
const okResponse = (data: unknown) => Promise.resolve({ data: { success: true, message: 'ok', data } })
|
||||
|
||||
const LIGHT_MODULES: ProgressLightModule[] = [
|
||||
'publish', 'deleteBrand', 'productRisk', 'shopMatch', 'patrolDelete',
|
||||
'queryAsin', 'shopDataCrawl', 'withdraw', 'priceTrack', 'similarAsin',
|
||||
'appearancePatent', 'collectData',
|
||||
]
|
||||
|
||||
test('test_light_url_module', async (t) => {
|
||||
setupWindow()
|
||||
const seen: { url?: string }[] = []
|
||||
mockRequest(t, (config) => {
|
||||
seen.push(config)
|
||||
return okResponse({ items: [], missingTaskIds: [] })
|
||||
})
|
||||
for (const module of LIGHT_MODULES) {
|
||||
const endpoint = (API_ENDPOINTS as Record<string, Record<string, string>>)[module].progressLight
|
||||
assert.ok(endpoint && endpoint.endsWith('/tasks/progress/light'), `${module} 应有 progressLight 端点: ${endpoint}`)
|
||||
await getModuleProgressLight(module, [1])
|
||||
}
|
||||
assert.equal(seen.length, LIGHT_MODULES.length)
|
||||
assert.deepEqual(seen.map((c) => c.url), LIGHT_MODULES.map((m) => {
|
||||
const raw = (API_ENDPOINTS as Record<string, Record<string, string>>)[m].progressLight
|
||||
return `/newApi${raw}`
|
||||
}), '每个模块的 light URL 与端点常量一一对应')
|
||||
})
|
||||
|
||||
test('test_light_payload', async (t) => {
|
||||
setupWindow()
|
||||
let captured: { url?: string; method?: string; data?: unknown } = {}
|
||||
mockRequest(t, (config) => {
|
||||
captured = config
|
||||
return okResponse({ items: [], missingTaskIds: [] })
|
||||
})
|
||||
await getModuleProgressLight('similarAsin', [5, 3, 5])
|
||||
assert.equal(captured.url, '/newApi/api/similar-asin/tasks/progress/light')
|
||||
assert.equal(captured.method, 'POST')
|
||||
assert.deepEqual(captured.data, { taskIds: [3, 5] }, 'taskIds 去重排序后进 body')
|
||||
|
||||
const empty = await getModuleProgressLight('similarAsin', [])
|
||||
assert.deepEqual(empty, { items: [], missingTaskIds: [] }, '空 taskIds 不发请求直接返回空结构')
|
||||
})
|
||||
|
||||
test('test_light_response_shape', async (t) => {
|
||||
setupWindow()
|
||||
mockRequest(t, () => okResponse({
|
||||
items: [
|
||||
{ taskId: 1, status: 'RUNNING', statusCode: null, fileStatus: null, fileError: null, fileReady: false, updatedAt: '2026-09-01T10:00:00' },
|
||||
{ taskId: 2, status: 'SUCCESS', statusCode: null, fileStatus: 'SUCCESS', fileError: null, fileReady: true, updatedAt: '2026-09-01T10:05:00' },
|
||||
],
|
||||
missingTaskIds: [],
|
||||
}))
|
||||
const result = await getModuleProgressLight('publish', [1, 2])
|
||||
assert.equal(result.items.length, 2, 'items 数组保留')
|
||||
assert.equal(result.items[0].taskId, 1)
|
||||
assert.equal(result.items[0].status, 'RUNNING')
|
||||
assert.equal(result.items[0].fileReady, false)
|
||||
assert.equal(result.items[1].fileStatus, 'SUCCESS')
|
||||
assert.equal(result.items[1].fileReady, true)
|
||||
assert.equal(result.items[1].updatedAt, '2026-09-01T10:05:00')
|
||||
assert.deepEqual(result.missingTaskIds, [])
|
||||
})
|
||||
|
||||
test('test_light_fallback_flag', async (t) => {
|
||||
setupWindow()
|
||||
let called = 0
|
||||
mockRequest(t, () => {
|
||||
called += 1
|
||||
return okResponse({ items: [], missingTaskIds: [] })
|
||||
})
|
||||
const result = await getModuleProgressLight('withdraw', [7], { useLight: false })
|
||||
assert.equal(called, 0, 'useLight=false 时不发起 light 请求(调用方自行回退旧端点)')
|
||||
assert.deepEqual(result, { items: [], missingTaskIds: [] })
|
||||
|
||||
const on = await getModuleProgressLight('withdraw', [7], { useLight: true })
|
||||
assert.equal(called, 1, 'useLight=true(默认)时发起 light 请求')
|
||||
assert.deepEqual(on, { items: [], missingTaskIds: [] })
|
||||
})
|
||||
|
||||
test('test_light_missing_ids', async (t) => {
|
||||
setupWindow()
|
||||
mockRequest(t, () => okResponse({
|
||||
items: [{ taskId: 1, status: 'RUNNING' }],
|
||||
missingTaskIds: [9, 10],
|
||||
}))
|
||||
const result = await getModuleProgressLight('collectData', [1, 9, 10])
|
||||
assert.deepEqual(result.missingTaskIds, [9, 10], 'missingTaskIds 映射为 number[]')
|
||||
assert.equal(result.items.length, 1)
|
||||
|
||||
mockRequest(t, () => okResponse({ items: [{ taskId: 1, status: 'RUNNING' }] }))
|
||||
const noKey = await getModuleProgressLight('collectData', [1], { force: true })
|
||||
assert.deepEqual(noKey.missingTaskIds, [], '后端缺 missingTaskIds 字段时默认空数组')
|
||||
})
|
||||
|
||||
test('test_light_timeout', async (t) => {
|
||||
setupWindow()
|
||||
let captured: { timeout?: number } = {}
|
||||
mockRequest(t, (config) => {
|
||||
captured = config
|
||||
return okResponse({ items: [], missingTaskIds: [] })
|
||||
})
|
||||
await getModuleProgressLight('priceTrack', [1], { force: true })
|
||||
assert.equal(captured.timeout, 10000, '进度类请求默认 10s 超时')
|
||||
})
|
||||
|
||||
test('test_light_type_compat', async (t) => {
|
||||
setupWindow()
|
||||
mockRequest(t, () => okResponse({
|
||||
items: [{ taskId: 1, status: 'PENDING' }],
|
||||
missingTaskIds: [],
|
||||
}))
|
||||
const result = await getModuleProgressLight('shopMatch', [1], { force: true })
|
||||
const batch: TaskProgressBatchVo<TaskProgressLightItem> = result
|
||||
assert.ok('items' in batch && 'missingTaskIds' in batch, '结构与现有 TaskProgressBatchVo 兼容')
|
||||
assert.equal(batch.items[0].taskId, 1)
|
||||
assert.equal(batch.items[0].status, 'PENDING')
|
||||
})
|
||||
|
||||
test('test_light_snapshot', async (t) => {
|
||||
setupWindow()
|
||||
const calls: Array<{ url?: string; method?: string; params?: Record<string, unknown>; data?: unknown }> = []
|
||||
mockRequest(t, (config) => {
|
||||
calls.push(config)
|
||||
return okResponse({ items: [{ taskId: 1, status: 'RUNNING', fileReady: false }], missingTaskIds: [] })
|
||||
})
|
||||
await getModuleProgressLight('appearancePatent', [1], { force: true })
|
||||
await getModuleProgressLight('deleteBrand', [2], { force: true })
|
||||
assert.equal(calls[0].url, '/newApi/api/appearance-patent/tasks/progress/light')
|
||||
assert.equal(calls[0].method, 'POST')
|
||||
assert.deepEqual(calls[0].data, { taskIds: [1] })
|
||||
assert.equal(calls[1].url, '/newApi/api/delete-brand/tasks/progress/light')
|
||||
assert.deepEqual(calls[1].data, { taskIds: [2] })
|
||||
})
|
||||
|
||||
test('test_light_reexport', async () => {
|
||||
setupWindow()
|
||||
const javaModules = await import('../src/shared/api/java-modules.ts')
|
||||
const direct = await import('../src/shared/api/progress-light.ts')
|
||||
assert.equal(javaModules.getModuleProgressLight, direct.getModuleProgressLight, 'java-modules 应 re-export getModuleProgressLight')
|
||||
assert.equal(javaModules.TaskProgressLightItem, direct.TaskProgressLightItem, '类型应同一引用')
|
||||
})
|
||||
Reference in New Issue
Block a user