Files
Scoreboard/web/src/admin/tests/channels-medals.test.ts
T
AskaEth 877ff15823 feat(0.5.1): 业务逻辑审查 9 条 P0 全修 + 自绘安装器 + 现场反馈四项
## 算分正确性(6 份只读审查 → 收敛成 5 个病根,均先复现失败再修)
- 「人工名次」与「自动名次回写」互斥:原来同一份成绩**发布两次分数不同**
  (团体报名项 dual 通道 10/8/6 → 25/18/14),现在连发多次逐字节一致
- 删小项时在同一事务作废旧流水并推榜:原来分数**永久残留**在总榜、任何重算都打不到
- 规则包导入整体事务化:原来中途失败会清空名次表且补不回(奖牌全丢);
  另加守卫「compare=none 的小项不得配 basis=result/aggregate」
- 末位归零只抹真正落在末位的那一档:原来跨末位的并列组会被整组抹 0
  (7 人三人并列第 5 时第 5、6 名的分也一起没了)
- 模板「分值表档位超出录取名次」现在拒绝保存:原来静默截断成 0 分

## 现场反馈(主人实测报的)
- 已发布成绩可改:行内「修改本条」+ 成绩格/状态格可点(退回草稿→改→重新发布本组),
  与「作废」语义分开;取消路径不产生任何副作用
- 单位积分明细补「运动员 / 来源」列,并显示「已修订 ×N(时间·原因)」:
  管理端、大屏覆盖层、xlsx 备注列三处同源
- 单位奖牌榜改为按奖牌独立排名(金→银→铜→单位编码),与积分榜解耦
- 历史孤儿积分流水清理(迁移 0022,只动 result/record_bonus,人工调整分不受影响)

## 新功能
- 团体项目聚合口径二选一:平均(默认)/ 总和(迁移 0023,通道编辑器可切)

## 安装器
- 自绘五页安装界面:无边框、自绘标题栏、DPI 自适应(4K@200% 实测)、
  安装/卸载逻辑复用 electron-builder 宏不变;默认「为所有用户安装」并自动放行防火墙 19540
- 构建期校验:安装器素材里的版本号必须与 VERSION 一致,否则构建失败

自检:服务端 14 包 ×2 / 前端 vue-tsc + build + 742 例 ×2 / Console 20 文件 + 68 例 ×2,全绿。
端到端对拍:修前 vs 修后,纯团体 / 双计 / 普通个人三形态**逐字节一致**,仅出 bug 的双通道形态按要求改变。
2026-09-13 15:20:18 +08:00

58 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
// 计分通道页的两个奖牌开关(设计 17 §八第 2 条)与小项页的「该项启用奖牌结算」。
// 项目没有组件测试工具,按仓库既有做法(avg-mode.test.ts)读源码断言接线。
const CHANNELS_SOURCE = readFileSync(
new URL('../components/baseData/ChannelsSection.vue', import.meta.url),
'utf8',
)
// 新增行的默认值(含「计入单位总分」按小项类型取值)在 channelRows.ts,见 channels-count-to-team.test.ts。
const CHANNEL_ROWS_SOURCE = readFileSync(
new URL('../components/baseData/channelRows.ts', import.meta.url),
'utf8',
)
const EVENTS_SOURCE = readFileSync(
new URL('../components/baseData/SportsEventsSection.vue', import.meta.url),
'utf8',
)
test('通道行带两个奖牌开关,缺字段按 true 归一', () => {
assert.match(CHANNELS_SOURCE, /countToMedals: normalizeCountToMedals\(channel\.countToMedals\)/)
assert.match(CHANNELS_SOURCE, /countToTeamMedals: normalizeCountToTeamMedals\(channel\.countToTeamMedals\)/)
assert.match(CHANNELS_SOURCE, /发奖牌/)
assert.match(CHANNELS_SOURCE, /个人奖牌并入单位金牌榜/)
})
test('两个开关一起序列化进 ScoreChannelInput,scope=team 的值不被清掉', () => {
// buildInputs 必须带上两个字段:PUT 是整体替换,漏掉等于把它们重置回默认(发牌)。
assert.match(CHANNELS_SOURCE, /countToMedals: row\.countToMedals/)
assert.match(CHANNELS_SOURCE, /countToTeamMedals: row\.countToTeamMedals/)
// 「个人奖牌并入单位金牌榜」只在 scope=athlete 的行显示;值本身原样回传。
assert.match(CHANNELS_SOURCE, /v-if="row\.scope === 'athlete'"/)
assert.match(CHANNELS_SOURCE, /切换 scope 时不会被清掉/)
})
test('新通道默认发奖牌并并入单位榜(与库表默认值一致)', () => {
assert.match(CHANNEL_ROWS_SOURCE, /countToMedals: true,\s*\n\s*countToTeamMedals: true,/)
})
test('小项编辑有「该项启用奖牌结算」开关,默认开并说清后果', () => {
assert.match(EVENTS_SOURCE, /awardMedals: true,/)
assert.match(EVENTS_SOURCE, /该项启用奖牌结算/)
assert.match(EVENTS_SOURCE, /关闭后该小项只出积分不出奖牌|关闭「该项启用奖牌结算」后,该小项只出积分不出奖牌/)
// 编辑已有小项时按当前值回填,不能每次都变成开。
assert.match(EVENTS_SOURCE, /awardMedals: normalizeAwardMedals\(event\.awardMedals\)/)
// 保存时带上该字段(PUT 整体替换)。
assert.match(EVENTS_SOURCE, /awardMedals: eventForm\.value\.awardMedals/)
})
test('小项页的通道摘要给「已关闭奖牌」的通道加可见徽标', () => {
assert.match(EVENTS_SOURCE, /!channel\.countToMedals/)
assert.match(EVENTS_SOURCE, /不发奖牌/)
assert.match(EVENTS_SOURCE, /!channel\.countToTeamMedals/)
assert.match(EVENTS_SOURCE, /个人奖牌不并入单位榜/)
})