import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import type { Team } from '@/shared' import { entryShareIndex, entrySquadIndex, readEntrySquad, readEntryTeamShares, rowLogoCount, rowTeamLogos, rowTeamText, tickerRowText, } from '../jointTeams' /** * 大屏的联队与队伍渲染(devdocs/19、迁移 0025)。要钉住的是: * 1. **单单位(普通报名)时逐字/逐图不变**:文案还是 `shortName ?? teamName`,logo 还是刚那一张, * 联队成员表与队伍表里根本没有这条报名的位置; * 2. 联队时写「A+B联队」(两个单位的名字都写全),单位列两个 logo 都画; * 3. 填了队名时写「第一分公司二队」(不加括号),没填时一个字不变——一个单位两个队 * 各自占一个名次,两行必须分得开; * 4. 成员单位的 logo 只能从快照的单位表里取(entries 接口不下发 logoPath), * 取不到时那个位置是 null,TeamLogo 不渲染,不会留空洞。 */ function team(id: number, over: Partial = {}): Team { return { id, code: `T${id}`, name: `第${id}分公司`, shortName: null, logoPath: null, color: null, sortOrder: id, active: true, deletedAt: null, ...over, } } const TEAMS: Team[] = [ team(1, { code: 'A', name: '第一分公司', shortName: '一司', logoPath: '/uploads/a.png' }), team(2, { code: 'B', name: '第二分公司', shortName: '二司', logoPath: '/uploads/b.png' }), team(3, { code: 'C', name: '第三分公司', shortName: '三司', logoPath: null }), ] test('teamShares 解析:缺字段 / 类型不对一律当「没有配置」,不抛异常', () => { assert.deepEqual(readEntryTeamShares({}), []) assert.deepEqual(readEntryTeamShares({ teamShares: null }), []) assert.deepEqual(readEntryTeamShares({ teamShares: 'A+B' }), []) assert.deepEqual(readEntryTeamShares(null), []) assert.equal(readEntryTeamShares({ teamShares: [{ teamId: 1, weight: 1 }] }).length, 1) }) test('联队进表:成员单位带上各自的 logo(名字与 logo 都来自两处不同的接口)', () => { const index = entryShareIndex( [ { id: 11, teamId: 1, teamCode: 'A', teamName: '第一分公司', teamShares: [ { teamId: 1, weight: 1, teamCode: 'A', teamName: '第一分公司' }, { teamId: 2, weight: 1, teamCode: 'B', teamName: '第二分公司' }, ], }, ], TEAMS, ) const members = index.get(11) assert.equal(members?.length, 2) assert.equal(members?.[1]?.logoPath, '/uploads/b.png') assert.equal(members?.[1]?.percent, 50) }) test('普通报名(单单位 / 老服务端没有 teamShares)不进表 = 屏上按原来那样渲染', () => { const index = entryShareIndex( [ { id: 11, teamId: 1, teamCode: 'A', teamName: '第一分公司' }, { id: 12, teamId: 1, teamCode: 'A', teamName: '第一分公司', teamShares: [{ teamId: 1, weight: 1, teamCode: 'A', teamName: '第一分公司' }], }, ], TEAMS, ) assert.equal(index.size, 0) assert.equal(index.get(11), undefined) }) test('单位列文案:单单位与改造前逐字一致,联队写「A+B联队」,填了队名写「第一分公司二队」', () => { const row = { shortName: '一司', teamName: '第一分公司' } assert.equal(rowTeamText(row, null), '一司') assert.equal(rowTeamText({ shortName: null, teamName: '第一分公司' }, null), '第一分公司') // 老服务端不下发 squad(null)与空串都走同一条路:一个字符都不多。 assert.equal(rowTeamText(row, null, ''), '一司') assert.equal(rowTeamText(row, null, null), '一司') // 队名直接拼在单位名后面,不加括号(大屏单位列宽紧张)。 assert.equal(rowTeamText(row, null, '二队'), '一司二队') assert.equal(rowTeamText({ shortName: null, teamName: '第一分公司' }, null, '青年队'), '第一分公司青年队') const members = entryShareIndex( [ { id: 11, teamId: 1, teamName: '第一分公司', teamShares: [ { teamId: 1, weight: 1, teamName: '第一分公司' }, { teamId: 2, weight: 1, teamName: '第二分公司' }, ], }, ], TEAMS, ).get(11) assert.ok(members !== null && members !== undefined) // 联队写全称:两个单位名一个不少(简称在别的单位的人眼里认不出来)。 assert.equal(rowTeamText(row, members), '第一分公司+第二分公司联队') assert.equal(rowTeamText(row, members, '二队'), '第一分公司+第二分公司联队二队') }) test('队名索引:只收填了队名的报名,空串 / 缺字段 / 脏数据一律不进表', () => { const index = entrySquadIndex([ { id: 11, squad: '一队' }, { id: 12, squad: '' }, { id: 13 }, { id: 14, squad: ' 二队 ' }, { id: 15, squad: 2 }, { id: 0, squad: '一队' }, null, ]) assert.equal(index.get(11), '一队') assert.equal(index.get(14), '二队', '两侧空白去掉后再上屏') assert.equal(index.size, 2, '不填队名的报名不在表里 = 按原来那样渲染') assert.equal(index.has(0), false, '没有合法 id 的报名项丢掉') assert.deepEqual(entrySquadIndex([]), new Map()) assert.equal(readEntrySquad({ squad: '一队' }), '一队') assert.equal(readEntrySquad({ squad: null }), '') assert.equal(readEntrySquad({}), '') assert.equal(readEntrySquad(null), '') }) test('底栏一行:有运动员写运动员;团体行的联队写「A+B联队」,带队伍时队名也在', () => { const members = entryShareIndex( [ { id: 11, teamId: 1, teamName: '第一分公司', teamShares: [ { teamId: 1, weight: 1, teamName: '第一分公司' }, { teamId: 2, weight: 1, teamName: '第二分公司' }, ], }, ], TEAMS, ).get(11) assert.ok(members !== null && members !== undefined) assert.equal( tickerRowText({ athleteName: '张三', shortName: '一司', teamName: '第一分公司' }, members), '张三', ) assert.equal( tickerRowText({ athleteName: null, shortName: '一司', teamName: '第一分公司' }, members), '第一分公司+第二分公司联队', ) assert.equal( tickerRowText({ athleteName: null, shortName: '一司', teamName: '第一分公司' }, null), '一司', ) // 底栏一行只差队名时,队名就是唯一的分辨依据(同一个单位的两个队都进了名次)。 assert.equal( tickerRowText({ athleteName: null, shortName: '一司', teamName: '第一分公司' }, null, '一队'), '一司一队', ) assert.equal( tickerRowText({ athleteName: '', shortName: '一司', teamName: '第一分公司' }, null, '二队'), '一司二队', 'athleteName 为空串(团体行)时同样带上队名', ) }) test('单位列的 logo:普通报名只有行的那一张,联队把两个单位都放上', () => { const row = { teamId: 1, logoPath: '/uploads/a.png' } assert.deepEqual(rowTeamLogos(row, null), ['/uploads/a.png']) assert.equal(rowLogoCount(row, null), 1) const members = entryShareIndex( [ { id: 11, teamId: 1, teamName: '第一分公司', teamShares: [ { teamId: 1, weight: 1, teamName: '第一分公司' }, { teamId: 2, weight: 1, teamName: '第二分公司' }, ], }, ], TEAMS, ).get(11) assert.ok(members !== null && members !== undefined) // 主单位的 logo 来自行本身(服务端读侧口径),成员单位的 logo 来自快照单位表,不重复画。 assert.deepEqual(rowTeamLogos(row, members), ['/uploads/a.png', '/uploads/b.png']) assert.equal(rowLogoCount(row, members), 2) // 成员没有 logo 时是 null(TeamLogo 不渲染),不影响另一个 logo 与名字。 const noLogo = entryShareIndex( [ { id: 12, teamId: 1, teamName: '第一分公司', teamShares: [ { teamId: 1, weight: 1, teamName: '第一分公司' }, { teamId: 3, weight: 1, teamName: '第三分公司' }, ], }, ], TEAMS, ).get(12) assert.ok(noLogo !== null && noLogo !== undefined) assert.deepEqual(rowTeamLogos(row, noLogo), ['/uploads/a.png', null]) assert.equal(rowLogoCount(row, noLogo), 1, '没有 logo 的那一份不占 logo 宽度') }) test('大屏为屏上的小项读报名表:读失败静默(屏上仍是主单位,不清屏)', () => { const source = readFileSync(new URL('../useScoreboard.ts', import.meta.url), 'utf8') assert.match(source, /api\.events\.entries\(eventId\)/, '成员单位只能来自报名表') assert.match(source, /entryShareIndex\(entries, teams\.value\)/, 'logo 要从快照单位表里补') assert.match(source, /entrySquadIndex\(entries\)/, '队名与成员单位同一次请求,不为它多发一个请求') assert.match(source, /catch \{/, '拿不到报名表时按单单位渲染,不能弹错或清屏') assert.match(source, /jointTeamOf/, '页面通过 jointTeamOf 取成员单位') assert.match(source, /squadOf/, '页面通过 squadOf 取队名') }) // 三处渲染点都要把队名接上:少接一处,那一页上同一个单位的两个队就分不开了。 test('分项成绩 / 实时赛况 / 底栏三处都把队名传进单位列文案', () => { const call = /rowTeamText\(row, jointTeamOf\(row\.entryId\), squadOf\(row\.entryId\)\)/ const pages: [string, RegExp][] = [ ['../components/pages/EventResultsPage.vue', call], ['../components/pages/LivePage.vue', call], ['../components/TickerBar.vue', /tickerRowText\(row, jointTeamOf\(row\.entryId\), squadOf\(row\.entryId\)\)/], ] for (const [file, pattern] of pages) { const source = readFileSync(new URL(file, import.meta.url), 'utf8') assert.match(source, pattern, `${file} 没把队名接进单位列`) assert.match(source, /squadOf/, `${file} 要从 useScoreboard 取 squadOf`) } })