174 lines
5.2 KiB
TypeScript
174 lines
5.2 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|