import { test } from 'node:test' import assert from 'node:assert/strict' import { createLeaveGuard, judgeEntryRow, resultCellIssue } from '../pages/entryGuards' // 用户实机反馈:只写「缺少成绩」会让人去改本来就对的成绩格式, // 文案必须带上那一格的实际内容,并区分「空的 / 秒位越界 / 写法不认识」。 // 现场分批比赛后,空格子只是「还没录」,归为提示、不阻塞发布。 test('空格子:文案说明还没录并带出「(空)」,且不阻塞发布', () => { const issue = resultCellIssue('第 3 行 张三', ' ', 's') assert.equal(issue?.kind, 'empty') assert.equal(issue?.blocking, false) assert.equal(issue?.message, '第 3 行 张三:还没录成绩(当前「(空)」),本次发布不计入这一行') }) test('写错的成绩阻塞发布:秒位越界与写法不认识都是 blocking', () => { assert.equal(resultCellIssue('第 3 行 张三', '1:70', 's')?.blocking, true) assert.equal(resultCellIssue('第 3 行 张三', '约1分06秒', 's')?.blocking, true) }) test('秒位 ≥ 60:文案带出原内容和可用的写法', () => { const issue = resultCellIssue('第 3 行 张三', '1:70', 's') assert.equal(issue?.kind, 'seconds-overflow') assert.equal(issue?.message, '第 3 行 张三:秒位不能 ≥ 60(当前「1:70」),请写成 1:06 或 66 这类') }) test('写法不认识:文案带出原内容和可填写法', () => { const issue = resultCellIssue('第 3 行 张三', '约1分06秒', 's') assert.equal(issue?.kind, 'unrecognized') assert.equal( issue?.message, '第 3 行 张三:成绩写法无法识别(当前「约1分06秒」),可用 66、66.33、1:06、1:06.33、1分06秒', ) }) test('三种失败原因的文案互不相同,且都能定位到那一格', () => { const empty = resultCellIssue('第 3 行 张三', '', 's') const overflow = resultCellIssue('第 3 行 张三', '1:70', 's') const unknown = resultCellIssue('第 3 行 张三', '快点跑', 's') const messages = [empty, overflow, unknown].map((issue) => issue?.message ?? '') assert.equal(new Set(messages).size, 3) for (const message of messages) { assert.ok(message.startsWith('第 3 行 张三:'), message) } assert.ok(messages[1]?.includes('「1:70」')) assert.ok(messages[2]?.includes('「快点跑」')) assert.ok(messages[0]?.includes('「(空)」')) }) test('容错后的写法不再报错', () => { for (const text of ['1:06', '1:06', '1分06秒', '1分6秒', '1分06', '66', '66.33']) { assert.equal(resultCellIssue('第 1 行 李四', text, 's'), null, text) } }) test('距离 / 计数项目的文案不套用时间格式提示', () => { const issue = resultCellIssue('第 2 行 王五', '1.2.3', 'm') assert.equal(issue?.kind, 'unrecognized') assert.ok(issue?.message.includes('(当前「1.2.3」)')) assert.ok(issue?.message.includes('米')) assert.ok(!issue?.message.includes('1:06'), '不该拿时间格式去教距离项目') }) // 三类行徽标:已填 / 未录 / 有错。空成绩不再算错误,但要有提示。 test('行状态三类互斥:空成绩=未录(提示),写错=有错(阻塞)', () => { const missing = judgeEntryRow({ label: '第 22 行 赵六', resultText: ' ', unit: 's', status: 'ok', rank: null, needsRank: false, }) assert.equal(missing.state, 'missing') assert.equal(missing.blocker, null) assert.ok(missing.warning?.includes('还没录成绩'), missing.warning ?? '') const error = judgeEntryRow({ label: '第 3 行 张三', resultText: '1:70', unit: 's', status: 'ok', rank: null, needsRank: false, }) assert.equal(error.state, 'error') assert.ok(error.blocker?.includes('秒位不能 ≥ 60'), error.blocker ?? '') const filled = judgeEntryRow({ label: '第 1 行 李四', resultText: '66.33', unit: 's', status: 'ok', rank: null, needsRank: false, }) assert.equal(filled.state, 'filled') assert.equal(filled.blocker, null) }) test('非 ok 状态(DNS/DQ/DNF/退赛)没有数值也算已填', () => { for (const status of ['dns', 'dq', 'dnf', 'withdrawn']) { const row = judgeEntryRow({ label: '第 2 行 王五', resultText: '', unit: 's', status, rank: null, needsRank: false, }) assert.equal(row.state, 'filled', status) assert.equal(row.warning, null) } }) test('手动名次项目:只填名次即可发布,缺名次才阻塞', () => { const rankOnly = judgeEntryRow({ label: '第 4 行 篮球', resultText: '', unit: '', status: 'ok', rank: 1, needsRank: true, }) assert.equal(rankOnly.state, 'filled') assert.equal(rankOnly.blocker, null) const noRank = judgeEntryRow({ label: '第 4 行 篮球', resultText: '', unit: '', status: 'ok', rank: null, needsRank: true, }) assert.equal(noRank.state, 'error') assert.ok(noRank.blocker?.includes('缺少名次'), noRank.blocker ?? '') }) test('成绩 ≤ 0 只提示不挡:状态仍是已填', () => { const row = judgeEntryRow({ label: '第 5 行 田径', resultText: '0', unit: 's', status: 'ok', rank: null, needsRank: false, }) assert.equal(row.state, 'filled') assert.equal(row.blocker, null) assert.ok(row.warning?.includes('≤ 0'), row.warning ?? '') }) // 路由离开守卫:dirty 为 true 时必须拦下,确认后重放那一次必须放行(否则死循环)。 test('未保存时离开被拦下,确认后重放放行', () => { let dirty = true const guard = createLeaveGuard(() => dirty) assert.equal(guard.beforeLeave('/dashboard'), false) assert.equal(guard.confirm(), '/dashboard') assert.equal(guard.beforeLeave('/dashboard'), true) // 用户取消后仍然拦下。 assert.equal(guard.beforeLeave('/review'), false) guard.cancel() assert.equal(guard.confirm(), null) assert.equal(guard.beforeLeave('/review'), false) // 保存后就不再拦。 dirty = false assert.equal(guard.beforeLeave('/review'), true) }) test('重放没走到守卫时,重新跳转结束仍要重新确认', () => { const guard = createLeaveGuard(() => true) assert.equal(guard.beforeLeave('/dashboard'), false) assert.equal(guard.confirm(), '/dashboard') // 这次跳转被别处直接拦下:reset 必须在下次离开前收回「已放行」。 guard.reset() assert.equal(guard.beforeLeave('/review'), false) }) test('没有未保存修改时直接放行,且不产生待确认的离开', () => { const guard = createLeaveGuard(() => false) assert.equal(guard.beforeLeave('/standings'), true) assert.equal(guard.confirm(), null) })