Files
HIPCTF2/tests/frontend/scoreboard.pure.spec.ts
T

146 lines
5.7 KiB
TypeScript

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('');
});
});