AI Implementation feature(912): Scoreboard: Ranking, Matrix, Event Log and Score Graph 1.02 (#55)
This commit was merged in pull request #55.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
jest.mock('@angular/common/http', () => ({
|
||||
HttpClient: class {},
|
||||
HttpErrorResponse: class {},
|
||||
}));
|
||||
|
||||
import {
|
||||
GraphProjectionDims,
|
||||
GraphSeries,
|
||||
isValidEventWindow,
|
||||
projectGraphPoints,
|
||||
} from '../../frontend/src/app/features/scoreboard/scoreboard.pure';
|
||||
|
||||
const DIMS: GraphProjectionDims = {
|
||||
width: 800,
|
||||
height: 320,
|
||||
padL: 40,
|
||||
padR: 16,
|
||||
padT: 16,
|
||||
padB: 28,
|
||||
};
|
||||
|
||||
const innerW = DIMS.width - DIMS.padL - DIMS.padR;
|
||||
const innerH = DIMS.height - DIMS.padT - DIMS.padB;
|
||||
const xMin = DIMS.padL;
|
||||
const xMax = DIMS.padL + innerW;
|
||||
const yMin = DIMS.padT;
|
||||
const yMax = DIMS.padT + innerH;
|
||||
|
||||
function parsePoints(str: string): Array<{ x: number; y: number }> {
|
||||
if (!str) return [];
|
||||
return str.split(' ').map((pair) => {
|
||||
const [xs, ys] = pair.split(',');
|
||||
return { x: Number(xs), y: Number(ys) };
|
||||
});
|
||||
}
|
||||
|
||||
describe('isValidEventWindow', () => {
|
||||
it('returns false when end is before start', () => {
|
||||
expect(
|
||||
isValidEventWindow('2026-07-23T00:00:00.000Z', '2026-07-22T00:00:00.000Z'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when start equals end', () => {
|
||||
expect(
|
||||
isValidEventWindow('2026-07-23T00:00:00.000Z', '2026-07-23T00:00:00.000Z'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for non-finite Date strings', () => {
|
||||
expect(isValidEventWindow('not a date', '2026-07-22T00:00:00.000Z')).toBe(false);
|
||||
expect(isValidEventWindow('2026-07-23T00:00:00.000Z', 'garbage')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for null/undefined', () => {
|
||||
expect(isValidEventWindow(null, '2026-07-22T00:00:00.000Z')).toBe(false);
|
||||
expect(isValidEventWindow('2026-07-23T00:00:00.000Z', undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true only when end is strictly after start', () => {
|
||||
expect(
|
||||
isValidEventWindow('2026-07-22T00:00:00.000Z', '2026-07-23T00:00:00.000Z'),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectGraphPoints — reversed/invalid window (Job 912 repro)', () => {
|
||||
const view = {
|
||||
startUtc: '2026-07-23T00:00:00.000Z',
|
||||
endUtc: '2026-07-22T00:00:00.000Z',
|
||||
};
|
||||
const series: GraphSeries = {
|
||||
playerId: 'p1',
|
||||
playerName: 'p1',
|
||||
colorIndex: 0,
|
||||
points: [
|
||||
{ tUtc: '2026-07-22T12:00:00.000Z', value: 0 },
|
||||
{ tUtc: '2026-07-22T18:00:00.000Z', value: 100 },
|
||||
],
|
||||
};
|
||||
|
||||
it('returns empty string when endUtc is before startUtc (no off-canvas polyline)', () => {
|
||||
expect(projectGraphPoints(view, series, DIMS)).toBe('');
|
||||
});
|
||||
|
||||
it('renders zero series for the component when the event window is reversed', () => {
|
||||
const out = projectGraphPoints(view, series, DIMS);
|
||||
expect(parsePoints(out)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectGraphPoints — happy path', () => {
|
||||
const view = {
|
||||
startUtc: '2025-01-01T00:00:00.000Z',
|
||||
endUtc: '2025-01-02T00:00:00.000Z',
|
||||
};
|
||||
const series: GraphSeries = {
|
||||
playerId: 'p1',
|
||||
playerName: 'p1',
|
||||
colorIndex: 0,
|
||||
points: [
|
||||
{ tUtc: '2025-01-01T00:00:00.000Z', value: 0 },
|
||||
{ tUtc: '2025-01-01T12:00:00.000Z', value: 50 },
|
||||
{ tUtc: '2025-01-02T00:00:00.000Z', value: 100 },
|
||||
],
|
||||
};
|
||||
|
||||
it('anchors the first point at padL and keeps every X inside the plot area', () => {
|
||||
const out = projectGraphPoints(view, series, DIMS);
|
||||
const pts = parsePoints(out);
|
||||
expect(pts.length).toBe(3);
|
||||
pts.forEach((p) => {
|
||||
expect(Number.isFinite(p.x)).toBe(true);
|
||||
expect(p.x).toBeGreaterThanOrEqual(xMin);
|
||||
expect(p.x).toBeLessThanOrEqual(xMax);
|
||||
expect(p.y).toBeGreaterThanOrEqual(yMin);
|
||||
expect(p.y).toBeLessThanOrEqual(yMax);
|
||||
});
|
||||
expect(pts[0].x).toBeCloseTo(xMin, 2);
|
||||
expect(pts[pts.length - 1].x).toBeLessThanOrEqual(xMax + 1e-6);
|
||||
});
|
||||
|
||||
it('does not regress to the previous off-canvas X values', () => {
|
||||
const out = projectGraphPoints(view, series, DIMS);
|
||||
expect(out).not.toMatch(/16965896296/);
|
||||
expect(out).not.toMatch(/-64281599960/);
|
||||
expect(out).not.toMatch(/NaN/);
|
||||
expect(out).not.toMatch(/Infinity/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectGraphPoints — zero-span and invalid timestamps', () => {
|
||||
it('returns empty string when startUtc equals endUtc', () => {
|
||||
const view = {
|
||||
startUtc: '2025-01-01T00:00:00.000Z',
|
||||
endUtc: '2025-01-01T00:00:00.000Z',
|
||||
};
|
||||
const series: GraphSeries = {
|
||||
playerId: 'p1',
|
||||
playerName: 'p1',
|
||||
colorIndex: 0,
|
||||
points: [{ tUtc: '2025-01-01T00:00:00.000Z', value: 10 }],
|
||||
};
|
||||
expect(projectGraphPoints(view, series, DIMS)).toBe('');
|
||||
});
|
||||
|
||||
it('produces only finite, in-bounds coordinates when a point timestamp is invalid', () => {
|
||||
const view = {
|
||||
startUtc: '2025-01-01T00:00:00.000Z',
|
||||
endUtc: '2025-01-02T00:00:00.000Z',
|
||||
};
|
||||
const series: GraphSeries = {
|
||||
playerId: 'p1',
|
||||
playerName: 'p1',
|
||||
colorIndex: 0,
|
||||
points: [
|
||||
{ tUtc: 'not-a-date', value: 50 },
|
||||
{ tUtc: '2025-01-01T12:00:00.000Z', value: 100 },
|
||||
],
|
||||
};
|
||||
const out = projectGraphPoints(view, series, DIMS);
|
||||
const pts = parsePoints(out);
|
||||
expect(pts.length).toBe(2);
|
||||
pts.forEach((p) => {
|
||||
expect(Number.isFinite(p.x)).toBe(true);
|
||||
expect(Number.isFinite(p.y)).toBe(true);
|
||||
expect(p.x).toBeGreaterThanOrEqual(xMin);
|
||||
expect(p.x).toBeLessThanOrEqual(xMax);
|
||||
expect(p.y).toBeGreaterThanOrEqual(yMin);
|
||||
expect(p.y).toBeLessThanOrEqual(yMax);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user