147 lines
4.7 KiB
TypeScript
147 lines
4.7 KiB
TypeScript
jest.mock('@angular/common/http', () => ({
|
|
HttpClient: class {},
|
|
HttpErrorResponse: class {},
|
|
}));
|
|
|
|
import { ScoreboardStore } from '../../frontend/src/app/features/scoreboard/scoreboard.store';
|
|
import {
|
|
EventLogRow,
|
|
GraphView,
|
|
MatrixView,
|
|
RankingRow,
|
|
} from '../../frontend/src/app/features/scoreboard/scoreboard.pure';
|
|
import { Observable, of } from 'rxjs';
|
|
|
|
class FakeApi {
|
|
rankingCallCount = 0;
|
|
matrixCallCount = 0;
|
|
eventLogCallCount = 0;
|
|
graphCallCount = 0;
|
|
|
|
getRanking(): Observable<RankingRow[]> {
|
|
this.rankingCallCount += 1;
|
|
return of<RankingRow[]>([
|
|
{ rank: 1, playerId: 'p1', playerName: 'p1', solvedCount: 1, points: 100, colorIndex: 0 },
|
|
]);
|
|
}
|
|
getMatrix(): Observable<MatrixView> {
|
|
this.matrixCallCount += 1;
|
|
return of<MatrixView>({ players: [], challenges: [], cells: {} });
|
|
}
|
|
getEventLog(): Observable<EventLogRow[]> {
|
|
this.eventLogCallCount += 1;
|
|
return of<EventLogRow[]>([
|
|
{ solveId: 's1', playerId: 'p1', playerName: 'p1', challengeId: 'c1', challengeName: 'alpha', categoryAbbreviation: 'CRY', position: 1, awardedPoints: 100, awardedAtUtc: '2025-01-01T00:00:00.000Z' },
|
|
]);
|
|
}
|
|
getGraph(): Observable<GraphView> {
|
|
this.graphCallCount += 1;
|
|
return of<GraphView>({
|
|
startUtc: '2025-01-01T00:00:00.000Z',
|
|
endUtc: '2025-01-02T00:00:00.000Z',
|
|
serverNowUtc: '2025-01-01T00:00:00.000Z',
|
|
state: 'running',
|
|
series: [],
|
|
});
|
|
}
|
|
}
|
|
|
|
function makeFakeSse(handler: (type: string, payload: any) => void): any {
|
|
return {
|
|
addEventListener(type: string, fn: (ev: any) => void) {
|
|
handler(type, fn);
|
|
},
|
|
close() {},
|
|
};
|
|
}
|
|
|
|
describe('ScoreboardStore', () => {
|
|
it('loadAll fetches all four projections once', async () => {
|
|
const api = new FakeApi();
|
|
const destroyRef: any = { onDestroy: () => {} };
|
|
const store = new ScoreboardStore(api as any, destroyRef);
|
|
await store.loadAll();
|
|
expect(api.rankingCallCount).toBe(1);
|
|
expect(api.matrixCallCount).toBe(1);
|
|
expect(api.eventLogCallCount).toBe(1);
|
|
expect(api.graphCallCount).toBe(1);
|
|
await store.loadAll();
|
|
expect(api.rankingCallCount).toBe(1);
|
|
});
|
|
|
|
it('applySolveEvent mutates ranking, eventLog, graph and matrix', () => {
|
|
const api = new FakeApi();
|
|
const destroyRef: any = { onDestroy: () => {} };
|
|
const store = new ScoreboardStore(api as any, destroyRef);
|
|
void store.loadAll();
|
|
return new Promise<void>((resolve) => {
|
|
const source = makeFakeSse((type, fn) => {
|
|
if (type === 'solve') {
|
|
setTimeout(() => {
|
|
fn(new MessageEvent('solve', { data: JSON.stringify({
|
|
challenge_id: 'c2',
|
|
player_id: 'p1',
|
|
player_name: 'p1',
|
|
awarded_points: 25,
|
|
rank_bonus: 0,
|
|
awarded_at_utc: '2025-01-01T01:00:00.000Z',
|
|
position: 4,
|
|
}) }));
|
|
setTimeout(() => {
|
|
expect(store.ranking()?.[0]?.points).toBe(125);
|
|
expect(store.eventLog()?.[0]?.awardedPoints).toBe(25);
|
|
expect(store.graph()?.series[0]?.playerId).toBe('p1');
|
|
resolve();
|
|
}, 0);
|
|
}, 0);
|
|
}
|
|
});
|
|
store.wireSse(() => source);
|
|
});
|
|
});
|
|
|
|
it('does not duplicate event-log rows on replayed solve frame', () => {
|
|
const api = new FakeApi();
|
|
const destroyRef: any = { onDestroy: () => {} };
|
|
const store = new ScoreboardStore(api as any, destroyRef);
|
|
void store.loadAll();
|
|
return new Promise<void>((resolve) => {
|
|
let fire: ((ev: any) => void) | null = null;
|
|
const source = makeFakeSse((type, fn) => {
|
|
if (type === 'solve') fire = fn;
|
|
});
|
|
store.wireSse(() => source);
|
|
const payload = {
|
|
challenge_id: 'c2',
|
|
player_id: 'p1',
|
|
player_name: 'p1',
|
|
awarded_points: 25,
|
|
rank_bonus: 0,
|
|
awarded_at_utc: '2025-01-01T01:00:00.000Z',
|
|
position: 4,
|
|
};
|
|
setTimeout(() => {
|
|
fire!(new MessageEvent('solve', { data: JSON.stringify(payload) }));
|
|
fire!(new MessageEvent('solve', { data: JSON.stringify(payload) }));
|
|
setTimeout(() => {
|
|
const log = store.eventLog() ?? [];
|
|
const p1c2 = log.filter((r) => r.playerId === 'p1' && r.challengeId === 'c2');
|
|
expect(p1c2.length).toBe(1);
|
|
resolve();
|
|
}, 0);
|
|
}, 0);
|
|
});
|
|
});
|
|
|
|
it('reset clears all signals and stops SSE', () => {
|
|
const api = new FakeApi();
|
|
const destroyRef: any = { onDestroy: () => {} };
|
|
const store = new ScoreboardStore(api as any, destroyRef);
|
|
void store.loadAll();
|
|
store.reset();
|
|
expect(store.ranking()).toBeNull();
|
|
expect(store.matrix()).toBeNull();
|
|
expect(store.eventLog()).toBeNull();
|
|
expect(store.graph()).toBeNull();
|
|
});
|
|
}); |