import { test } from 'node:test' import assert from 'node:assert/strict' import { MAX_ENTRY_BATCH, dedupeEntryIds, entryBatchOutcome, entryBatchSizeError, entryBatchSkipNote, planEntryBatch, type EntryBatchRow, } from '../pages/entryBatch' function row(overrides: Partial & { entryId: number }): EntryBatchRow { return { name: '张三', bib: '1042', team: '第一分公司', categoryId: 7, entryStatus: 'registered', ...overrides, } } test('dedupeEntryIds:去重、丢非法 id,保持勾选顺序', () => { assert.deepEqual(dedupeEntryIds([3, 1, 3, 0, -2, 1, 2.5, 2]), [3, 1, 2]) assert.deepEqual(dedupeEntryIds([]), []) }) test('entryBatchSizeError:空选与超上限都给出中文提示', () => { assert.equal(entryBatchSizeError(0), '请先勾选要操作的报名项') assert.equal(entryBatchSizeError(3), null) assert.equal(entryBatchSizeError(MAX_ENTRY_BATCH), null) assert.match(entryBatchSizeError(MAX_ENTRY_BATCH + 1) ?? '', /分批提交/) }) // dq / dns 是别人的判定:退赛不能覆盖,跳过时必须说清原因。 test('entryBatchSkipNote:只动 registered / scratched,其余跳过并说明', () => { assert.equal(entryBatchSkipNote('scratch', 'registered'), null) assert.equal(entryBatchSkipNote('scratch', 'scratched'), '已是退赛状态,跳过') assert.equal(entryBatchSkipNote('scratch', 'dq'), '已被判罚取消资格(dq),跳过,不覆盖原判定') assert.equal(entryBatchSkipNote('scratch', 'dns'), '已标记未出场(dns),跳过,不覆盖原判定') assert.equal(entryBatchSkipNote('restore', 'scratched'), null) assert.equal(entryBatchSkipNote('restore', 'registered'), '当前正常参赛,无需恢复') // 移出报名不看状态:报名项本身就是要被物理删除的。 assert.equal(entryBatchSkipNote('remove', 'dq'), null) }) test('planEntryBatch:退赛只改 registered,跳过的人在确认框里列出来', () => { const plan = planEntryBatch('scratch', [ row({ entryId: 1 }), row({ entryId: 2, entryStatus: 'dq', name: '李四' }), row({ entryId: 1 }), ]) // 请求仍带上全部勾选 id:跳过与否由服务端逐条回报,前端不替它过滤。 assert.deepEqual(plan.ids, [1, 2]) assert.equal(plan.changed, 1) assert.equal(plan.scopeCount, 1) assert.deepEqual(plan.skipped, [{ name: '李四', note: '已被判罚取消资格(dq),跳过,不覆盖原判定' }]) assert.ok(plan.details.some((line) => line.includes('李四'))) assert.match(plan.message, /0 分且不占名次/) }) test('planEntryBatch:恢复只改已退赛的人', () => { const plan = planEntryBatch('restore', [ row({ entryId: 1, entryStatus: 'scratched' }), row({ entryId: 2, entryStatus: 'registered', name: '王五' }), ]) assert.deepEqual(plan.ids, [1, 2]) assert.equal(plan.changed, 1) assert.deepEqual(plan.skipped, [{ name: '王五', note: '当前正常参赛,无需恢复' }]) }) // 移出报名必须让操作员看到「删谁」与「重算几个组别」,规模大时还要明说别人的名次会变。 test('planEntryBatch:移出报名列出被删的人与重算范围', () => { const plan = planEntryBatch('remove', [ row({ entryId: 1, name: '张三', bib: '1042', team: '第一分公司' }), row({ entryId: 2, name: '李四', bib: null, team: '第二分公司', categoryId: 8 }), ]) assert.deepEqual(plan.ids, [1, 2]) assert.equal(plan.changed, 2) assert.equal(plan.scopeCount, 2) assert.deepEqual(plan.skipped, []) assert.ok(plan.details.some((line) => line.includes('张三|号码 1042|第一分公司'))) assert.ok(plan.details.some((line) => line.includes('李四|号码 —|第二分公司'))) assert.ok(plan.details.some((line) => line.includes('会重算 1 个小项(当前项目)、2 个组别'))) assert.match(plan.message, /不可恢复/) }) test('planEntryBatch:超过 20 人时只列前 20 并追加「别人的名次会变」', () => { const rows = Array.from({ length: 25 }, (_, index) => row({ entryId: index + 1, name: `选手${index + 1}` })) const plan = planEntryBatch('remove', rows) assert.equal(plan.changed, 25) assert.ok(plan.details.some((line) => line.includes('还有 5 人未列出'))) assert.match(plan.message, /别人的名次会变/) }) test('entryBatchOutcome:汇总逐条成败与删除规模', () => { const text = entryBatchOutcome({ items: [], summary: { ok: 2, failed: 1, skipped: 1, changed: 1, removedEntries: 1, removedResults: 1, removedLedger: 2, removedBreaks: 0, recomputedEvents: [3], }, }) assert.match(text, /成功 2 条(跳过 1 条)/) assert.match(text, /失败 1 条/) assert.match(text, /删除报名 1 条/) assert.match(text, /删除流水 2 条/) assert.match(text, /重算 1 个小项/) })