jest.mock('@angular/common/http', () => ({ HttpClient: class {}, HttpErrorResponse: class {}, })); import { PLAYER_COLOR_PALETTE, RankingRow, applyRankingSort, applySolveToGraph, dedupEventLogBySolveId, formatSolveDateTime, mutateMatrixFromSolve, parseSolveEventIntoRanking, stablePlayerColorIndex, } from '../../frontend/src/app/features/scoreboard/scoreboard.pure'; describe('stablePlayerColorIndex', () => { it('is deterministic and in range', () => { const a = stablePlayerColorIndex('player-abc'); expect(a).toBeGreaterThanOrEqual(0); expect(a).toBeLessThan(PLAYER_COLOR_PALETTE.length); expect(stablePlayerColorIndex('player-abc')).toBe(a); expect(stablePlayerColorIndex('player-xyz')).not.toBe(a); }); }); describe('applyRankingSort', () => { it('produces competition ranks with zero-pt rows at the bottom', () => { const rows: RankingRow[] = [ { rank: 0, playerId: 'p1', playerName: 'p1', solvedCount: 2, points: 0, colorIndex: 0 }, { rank: 0, playerId: 'p2', playerName: 'p2', solvedCount: 1, points: 100, colorIndex: 1 }, { rank: 0, playerId: 'p3', playerName: 'p3', solvedCount: 1, points: 50, colorIndex: 2 }, { rank: 0, playerId: 'p4', playerName: 'p4', solvedCount: 0, points: 0, colorIndex: 3 }, ]; const sorted = applyRankingSort(rows); expect(sorted.map((r) => r.playerId)).toEqual(['p2', 'p3', 'p1', 'p4']); expect(sorted.map((r) => r.rank)).toEqual([1, 2, 3, 3]); }); }); describe('parseSolveEventIntoRanking', () => { it('adds a new player and re-sorts', () => { const rows: RankingRow[] = [ { rank: 1, playerId: 'p1', playerName: 'p1', solvedCount: 2, points: 100, colorIndex: 0 }, ]; const next = parseSolveEventIntoRanking(rows, { challengeId: 'c1', challengeName: 'alpha', categoryAbbreviation: 'CRY', playerId: 'p2', playerName: 'p2', awardedPoints: 50, rankBonus: 0, awardedAtUtc: '2025-01-01T00:00:00.000Z', position: 4, }); expect(next.length).toBe(2); const p2 = next.find((r) => r.playerId === 'p2')!; expect(p2.points).toBe(50); expect(p2.solvedCount).toBe(1); expect(next[0].playerId).toBe('p1'); }); }); describe('dedupEventLogBySolveId', () => { it('prepends new rows and removes duplicates', () => { const existing = [ { solveId: 's1', playerId: 'p1', playerName: 'p1', challengeId: 'c1', challengeName: 'alpha', categoryAbbreviation: 'CRY', position: 1, awardedPoints: 100, awardedAtUtc: '2025-01-01T00:00:00.000Z' }, ]; const newRows = [ { solveId: 's1', playerId: 'p1', playerName: 'p1', challengeId: 'c1', challengeName: 'alpha', categoryAbbreviation: 'CRY', position: 1, awardedPoints: 100, awardedAtUtc: '2025-01-01T00:00:00.000Z' }, { solveId: 's2', playerId: 'p2', playerName: 'p2', challengeId: 'c2', challengeName: 'beta', categoryAbbreviation: 'WEB', position: 1, awardedPoints: 50, awardedAtUtc: '2025-01-01T00:01:00.000Z' }, ]; const out = dedupEventLogBySolveId(existing, newRows); expect(out.length).toBe(2); expect(out[0].solveId).toBe('s2'); expect(out[1].solveId).toBe('s1'); }); }); describe('applySolveToGraph', () => { it('appends a new point to the matching series and re-applies the top-10 cutoff', () => { const baseView = { startUtc: '2025-01-01T00:00:00.000Z', endUtc: '2025-01-02T00:00:00.000Z', serverNowUtc: '2025-01-01T12:00:00.000Z', state: 'running' as const, series: [ { playerId: 'p0', playerName: 'p0', colorIndex: 0, points: [{ tUtc: '2025-01-01T00:00:00.000Z', value: 0 }, { tUtc: '2025-01-02T00:00:00.000Z', value: 50 }] }, ], }; const next = applySolveToGraph( baseView, { challengeId: 'c1', challengeName: 'alpha', categoryAbbreviation: 'CRY', playerId: 'p0', playerName: 'p0', awardedPoints: 30, rankBonus: 0, awardedAtUtc: '2025-01-01T06:00:00.000Z', position: 2 }, '2025-01-01T00:00:00.000Z', '2025-01-02T00:00:00.000Z', '2025-01-01T12:00:00.000Z', ); expect(next.series[0].points.length).toBe(3); expect(next.series[0].points[0]).toEqual({ tUtc: '2025-01-01T00:00:00.000Z', value: 0 }); expect(next.series[0].points[1].tUtc).toBe('2025-01-01T06:00:00.000Z'); expect(next.series[0].points[1].value).toBe(80); expect(next.series[0].points[2]).toEqual({ tUtc: '2025-01-02T00:00:00.000Z', value: 80 }); }); it('returns empty when event is not configured', () => { const next = applySolveToGraph( null, { challengeId: 'c1', challengeName: 'alpha', categoryAbbreviation: 'CRY', playerId: 'p0', playerName: 'p0', awardedPoints: 30, rankBonus: 0, awardedAtUtc: '2025-01-01T06:00:00.000Z', position: 1 }, null, null, '2025-01-01T12:00:00.000Z', ); expect(next.series).toEqual([]); }); }); describe('mutateMatrixFromSolve', () => { it('stores the rank when within top-3', () => { const matrix = { players: [{ rank: 1, playerId: 'p1', playerName: 'p1', solvedCount: 1, points: 100, colorIndex: 0 }], challenges: [{ id: 'c1', name: 'alpha', solveCount: 1, categoryAbbreviation: 'CRY' }], cells: { p1: { c1: 1 as 1 } }, }; const next = mutateMatrixFromSolve(matrix, { challengeId: 'c1', challengeName: 'alpha', categoryAbbreviation: 'CRY', playerId: 'p2', playerName: 'p2', awardedPoints: 50, rankBonus: 0, awardedAtUtc: '2025-01-01T00:00:00.000Z', position: 5, }); expect(next!.cells.p2.c1).toBe('solved'); }); }); describe('formatSolveDateTime', () => { it('returns empty string for invalid input', () => { expect(formatSolveDateTime(null)).toBe(''); expect(formatSolveDateTime('not a date')).toBe(''); }); });