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