import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import type { RecordBreak } from '@/shared' import { IGNORE_BREAK_REASON, breakBonusText, breakKey, breakNote, handledBreakKeys, RECORD_LEVEL_LABELS, RECORD_LEVEL_OPTIONS, recordSourceLabel, } from '../pages/recordBreaks' /** * 破纪录的驳回 / 作废(P2 A41)。 * * 以前只有「登记 + 批准」两条路:误判的纪录在系统内无法纠正,「忽略」还只存在浏览器内存里 * (刷新就回来)。现在驳回与作废都落服务端登记表,「已处理」的键用来抑制前端推导出来的疑似列表。 * * 项目没有组件测试工具,纯逻辑直接测,界面接线读源码断言。 */ const RECORDS_SOURCE = readFileSync(new URL('../pages/RecordsPage.vue', import.meta.url), 'utf8') function section(source: string, start: string, end: string): string { const from = source.indexOf(start) const to = source.indexOf(end, from) assert.ok(from >= 0 && to > from, `源码里找不到 ${start} … ${end} 这一段`) return source.slice(from, to) } function makeBreak(overrides: Partial): RecordBreak { return { id: 1, eventId: 101, entryId: 7001, level: 'meet', resultValue: 11.2, resultText: '11.20', bonusPoints: 5, state: 'pending', approvedBy: null, approvedAt: null, ...overrides, } } // ── 已处理登记的键(抑制前端推导出的疑似列表) ─────────────────────────────── test('疑似条目的键与服务端登记行的键必须能对上(对不上就抑制不掉)', () => { assert.equal(breakKey(101, 7001, 'meet'), '101-7001-meet') }) test('approved / rejected 的登记会被抑制,pending 的不会', () => { const keys = handledBreakKeys([ makeBreak({ id: 1, state: 'approved' }), makeBreak({ id: 2, state: 'rejected', entryId: 7002 }), makeBreak({ id: 3, state: 'pending', entryId: 7003 }), ]) assert.equal(keys.has(breakKey(101, 7001, 'meet')), true) assert.equal(keys.has(breakKey(101, 7002, 'meet')), true) assert.equal(keys.has(breakKey(101, 7003, 'meet')), false) }) test('同一报名项的不同档位是两条独立的登记', () => { const keys = handledBreakKeys([makeBreak({ state: 'approved', level: 'province' })]) assert.equal(keys.has(breakKey(101, 7001, 'province')), true) assert.equal(keys.has(breakKey(101, 7001, 'meet')), false) }) test('待确认列表按三层过滤(本机标记 + 服务端已处理)', () => { const pending = section(RECORDS_SOURCE, 'const pendingBreaks = computed(', 'function levelRank(') assert.match(pending, /!rejectedKeys\.value\.includes\(item\.key\)/) assert.match(pending, /!approvedKeys\.value\.includes\(item\.key\)/) assert.match(pending, /!handledKeys\.value\.has\(item\.key\)/) }) test('推导疑似条目时用的就是 breakKey,两边不会各算一套', () => { assert.match(RECORDS_SOURCE, /key: breakKey\(event\.id, entry\.id, best\.level\)/) }) // ── 字段容错 ──────────────────────────────────────────────────────────────── test('来源:history / current 各有文案,缺字段显示 —(不冒充「历史」)', () => { assert.equal(recordSourceLabel('history'), '历史') assert.equal(recordSourceLabel('current'), '本届') assert.equal(recordSourceLabel(null), '—') assert.equal(recordSourceLabel(undefined), '—') }) test('登记说明缺字段时不渲染', () => { assert.equal(breakNote({ note: '现场判定不是破纪录' }), '现场判定不是破纪录') assert.equal(breakNote({ note: ' 驳回 ' }), '驳回') assert.equal(breakNote({ note: '' }), null) assert.equal(breakNote({ note: null }), null) assert.equal(breakNote({}), null) }) test('加分缺字段时显示 —,不渲染成「+—」', () => { assert.equal(breakBonusText({ bonusPoints: 5 }), '+5') assert.equal(breakBonusText({ bonusPoints: 2.5 }), '+2.5') assert.equal(breakBonusText({ bonusPoints: null }), '—') assert.equal(breakBonusText({}), '—') }) test('四个档位的文案与下拉选项齐全', () => { assert.deepEqual(RECORD_LEVEL_OPTIONS.map((option) => option.value), [ 'meet', 'city', 'province', 'national', ]) assert.equal(RECORD_LEVEL_LABELS.meet, '赛会') assert.equal(RECORD_LEVEL_LABELS.national, '全国') }) // ── 已批准的破纪录卡片 ────────────────────────────────────────────────────── test('卡片数据来自 GET /api/record-breaks?state=approved', () => { const loadBreaks = section(RECORDS_SOURCE, 'async function loadBreaks(', 'async function loadSuspected(') assert.match(loadBreaks, /adminApi\.recordBreaks\('approved'\)/) assert.match(loadBreaks, /adminApi\.recordBreaks\('rejected'\)/) // 两态分开取、任一失败都只影响这张卡片:用 allSettled,不用 Promise.all(一个挂全挂)。 assert.match(loadBreaks, /Promise\.allSettled\(\[/) assert.match(loadBreaks, /breaksUnavailable\.value = failure !== undefined/) assert.match(RECORDS_SOURCE, /已批准的破纪录<\/span>/) }) test('接口读不到时卡片显示空态说明,不让整页报错', () => { assert.match(RECORDS_SOURCE, /v-else-if="breaksUnavailable"/) assert.match(RECORDS_SOURCE, /读不到破纪录登记/) assert.match(RECORDS_SOURCE, /示例数据模式或后端版本较旧时没有这个接口/) // 空态在「没有已批准的破纪录」之前判断:读不到不能被说成「没有」。 assert.ok( RECORDS_SOURCE.indexOf('v-else-if="breaksUnavailable"') < RECORDS_SOURCE.indexOf('approvedBreaks.length === 0'), ) }) test('访客 403 与后端太旧分开说:权限问题不能显示成「接口不可用」', () => { assert.match(RECORDS_SOURCE, /breaksForbidden\.value = failures\.some\(\(item\) => isForbidden\(item\.reason\)\)/) assert.match(RECORDS_SOURCE, /访客只读:看不到破纪录登记/) assert.match(RECORDS_SOURCE, /要求录入员及以上权限/) }) test('每条登记一个「作废」按钮,调用 reject 并带上必填原因', () => { assert.match(RECORDS_SOURCE, />\s*作废\s*<\/button>/) const revoke = section(RECORDS_SOURCE, 'async function confirmRevoke(', 'function cancelRevoke(') assert.match(revoke, /await adminApi\.rejectBreak\(item\.id, reason\)/) assert.match(RECORDS_SOURCE, /:reason-required="true"/) assert.match(RECORDS_SOURCE, /@confirm="confirmRevoke"/) }) test('作废成功后刷新纪录库与榜单,并提示加分已撤回', () => { const revoke = section(RECORDS_SOURCE, 'async function confirmRevoke(', 'function cancelRevoke(') assert.match(revoke, /await Promise\.all\(\[loadRecords\(\), data\.refreshStandings\(\)\]\)/) assert.match(revoke, /'加分已撤回,该级别纪录已恢复上一条'/) assert.ok( revoke.indexOf('rejectBreak') < revoke.indexOf('loadRecords()'), '先作废再刷新,顺序反了会把旧数据当新数据', ) }) test('访客只读时「作废」按钮置灰', () => { const card = section(RECORDS_SOURCE, '已批准的破纪录', '') assert.match(card, /:disabled="!canWrite \|\| revoking"/) }) // ── 「忽略」改成持久 ──────────────────────────────────────────────────────── test('忽略先登记再驳回,原因是固定的「现场判定不是破纪录」', () => { const reject = section(RECORDS_SOURCE, 'async function confirmReject(', 'function blockSampleData(') assert.match(reject, /adminApi\.client\.records\.createBreak\(/) assert.match(reject, /await adminApi\.rejectBreak\(created\.id, IGNORE_BREAK_REASON\)/) assert.equal(IGNORE_BREAK_REASON, '现场判定不是破纪录') assert.ok( reject.indexOf('createBreak') < reject.indexOf('rejectBreak(created.id'), '必须先拿到 pending 行才能驳回', ) }) test('写不进服务端时才退回本机标记,并如实说明刷新后会回来', () => { const reject = section(RECORDS_SOURCE, 'async function confirmReject(', 'function blockSampleData(') assert.match(reject, /toast\.warning\('已在本机忽略(未连接服务端)', '没有写入服务端,刷新页面后该条会重新出现'\)/) // 不能出现「只在本地隐藏」这种旧说法:现在默认是落服务端的。 assert.doesNotMatch(RECORDS_SOURCE, /接口未提供拒绝动作/) }) test('忽略确认框的正文按模式说清后果(示例数据下不能承诺「不会再出现」)', () => { const message = section(RECORDS_SOURCE, 'const rejectMessage = computed(', 'async function confirmReject(') assert.match(message, /示例数据模式没有后端可写:只在本机隐藏该条,刷新页面后它会重新出现。/) assert.match(message, /会在服务端登记为一条破纪录记录并立即驳回/) assert.match(RECORDS_SOURCE, /:message="rejectMessage"/) })