/** * Utility Functions Tests */ import { formatPercentage, formatProgress, calculateTrend, isValidPostcode, debounce, buildOfstedListBadge, metricKind, shortName, computeYBounds, } from '@/lib/utils'; describe('formatPercentage', () => { it('formats percentages correctly', () => { expect(formatPercentage(75.5)).toBe('75.5%'); expect(formatPercentage(100)).toBe('100.0%'); expect(formatPercentage(0)).toBe('0.0%'); }); it('handles null values', () => { expect(formatPercentage(null)).toBe('N/A'); }); }); describe('formatProgress', () => { it('formats progress scores correctly', () => { expect(formatProgress(2.5)).toBe('+2.5'); expect(formatProgress(-1.3)).toBe('-1.3'); expect(formatProgress(0)).toBe('0.0'); }); it('handles null values', () => { expect(formatProgress(null)).toBe('N/A'); }); }); describe('calculateTrend', () => { it('calculates upward trend', () => { expect(calculateTrend(75, 70)).toBe('up'); }); it('calculates downward trend', () => { expect(calculateTrend(70, 75)).toBe('down'); }); it('calculates stable trend', () => { expect(calculateTrend(75, 75)).toBe('stable'); }); it('handles null previous value', () => { expect(calculateTrend(75, null)).toBe('stable'); }); it('handles null current value', () => { expect(calculateTrend(null, 75)).toBe('stable'); }); }); describe('isValidPostcode', () => { it('validates correct UK postcodes', () => { expect(isValidPostcode('SW1A 1AA')).toBe(true); expect(isValidPostcode('M1 1AE')).toBe(true); expect(isValidPostcode('B33 8TH')).toBe(true); }); it('rejects invalid postcodes', () => { expect(isValidPostcode('INVALID')).toBe(false); expect(isValidPostcode('12345')).toBe(false); expect(isValidPostcode('')).toBe(false); }); }); describe('debounce', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); it('delays function execution', () => { const mockFn = jest.fn(); const debouncedFn = debounce(mockFn, 300); debouncedFn('test'); expect(mockFn).not.toHaveBeenCalled(); jest.advanceTimersByTime(300); expect(mockFn).toHaveBeenCalledWith('test'); expect(mockFn).toHaveBeenCalledTimes(1); }); it('cancels previous calls', () => { const mockFn = jest.fn(); const debouncedFn = debounce(mockFn, 300); debouncedFn('first'); jest.advanceTimersByTime(150); debouncedFn('second'); jest.advanceTimersByTime(150); debouncedFn('third'); jest.advanceTimersByTime(300); expect(mockFn).toHaveBeenCalledWith('third'); expect(mockFn).toHaveBeenCalledTimes(1); }); }); describe('buildOfstedListBadge', () => { it('returns grade word + year for OEIF Outstanding', () => { const badge = buildOfstedListBadge({ ofsted_grade: 1, ofsted_date: '2023-11-15', ofsted_framework: 'OEIF' }); expect(badge.label).toBe('Outstanding · 2023'); expect(badge.cssClass).toBe('ofsted1'); }); it('returns grade word for each OEIF grade', () => { expect(buildOfstedListBadge({ ofsted_grade: 2, ofsted_date: '2022-05-01' }).label).toBe('Good · 2022'); expect(buildOfstedListBadge({ ofsted_grade: 3, ofsted_date: '2021-01-01' }).label).toBe('Req. Improvement · 2021'); expect(buildOfstedListBadge({ ofsted_grade: 4, ofsted_date: '2020-03-01' }).label).toBe('Inadequate · 2020'); }); it('returns grade word without year when date is missing', () => { const badge = buildOfstedListBadge({ ofsted_grade: 2, ofsted_date: null }); expect(badge.label).toBe('Good'); expect(badge.cssClass).toBe('ofsted2'); }); it('returns Report Card badge when framework is ReportCard', () => { const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: '2025-11-01', ofsted_framework: 'ReportCard' }); expect(badge.label).toBe('Report Card · 2025'); expect(badge.cssClass).toBe('ofstedRc'); }); it('returns an "Inspected" badge for an OEIF inspection with no overall grade (post-Sept-2024)', () => { // Inspected after Sept 2024: inspection on record (date + framework) but // Ofsted no longer issues an overall grade. Must NOT read as "Not yet inspected". const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: '2024-11-01', ofsted_framework: 'OEIF' }); expect(badge.label).toBe('Inspected · 2024'); expect(badge.cssClass).toBe('ofstedInspected'); }); it('returns an "Inspected" badge without a year when the grade is missing and date is absent but a record exists', () => { const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: null, ofsted_framework: 'OEIF' }); expect(badge.label).toBe('Inspected'); expect(badge.cssClass).toBe('ofstedInspected'); }); it('returns pending badge when no grade and no inspection on record', () => { const badge = buildOfstedListBadge({ ofsted_grade: null, ofsted_date: null, ofsted_framework: null }); expect(badge.label).toBe('Not yet inspected'); expect(badge.cssClass).toBe('ofstedPending'); }); it('returns pending badge when all fields are undefined', () => { const badge = buildOfstedListBadge({}); expect(badge.label).toBe('Not yet inspected'); expect(badge.cssClass).toBe('ofstedPending'); }); }); describe('metricKind', () => { it('classifies metrics by key', () => { expect(metricKind('rwm_expected_pct')).toBe('percentage'); expect(metricKind('absence_rate')).toBe('percentage'); expect(metricKind('reading_progress')).toBe('progress'); expect(metricKind('progress_8_score')).toBe('progress'); expect(metricKind('attainment_8_score')).toBe('score'); expect(metricKind('reading_avg_score')).toBe('score'); }); }); describe('computeYBounds', () => { it('tightens clustered percentages instead of framing 0-100', () => { const b = computeYBounds([86, 86, 86, 80, 96], 'percentage'); expect(b.min).toBeGreaterThanOrEqual(0); expect(b.max).toBeLessThanOrEqual(100); expect(b.min).toBeGreaterThan(50); expect(b.max! - b.min!).toBeGreaterThanOrEqual(10); }); it('never widens percentages beyond 0-100 for non-negative data', () => { const b = computeYBounds([2, 5, 98], 'percentage'); expect(b.min).toBe(0); expect(b.max).toBe(100); }); it('does not clamp to zero when pct-named trend data is negative', () => { const b = computeYBounds([-12, -3, 4], 'percentage'); expect(b.min).toBeLessThan(-12); }); it('keeps progress bounds symmetric around zero', () => { const b = computeYBounds([-1.2, 0.4, 2.1], 'progress'); expect(b.min).toBe(-b.max!); expect(b.min).toBeLessThanOrEqual(-1.2); expect(b.max).toBeGreaterThanOrEqual(2.1); }); it('fits score metrics without a fixed frame', () => { const b = computeYBounds([42.3, 48.9, 51.2], 'score'); expect(b.min).toBeGreaterThanOrEqual(0); expect(b.min).toBeLessThanOrEqual(42.3); expect(b.max).toBeGreaterThanOrEqual(51.2); }); it('returns empty bounds when there is no numeric data', () => { expect(computeYBounds([null, undefined, NaN], 'percentage')).toEqual({}); expect(computeYBounds([], 'progress')).toEqual({}); }); }); describe('isProposedToClose', () => { const { isProposedToClose } = require('@/lib/utils'); it('is true only for the exact GIAS proposed-to-close status', () => { expect(isProposedToClose({ status: 'Open, but proposed to close' })).toBe(true); expect(isProposedToClose({ status: 'Open' })).toBe(false); expect(isProposedToClose({ status: null })).toBe(false); expect(isProposedToClose({})).toBe(false); }); }); describe('shortName', () => { it('drops the trailing establishment-type words', () => { expect(shortName('Barclay Primary School')).toBe('Barclay'); expect(shortName('Elmhurst Primary School')).toBe('Elmhurst'); expect(shortName("St Mary's Catholic Primary School")).toBe("St Mary's"); expect(shortName('Riverside Community Junior School')).toBe('Riverside'); }); it('keeps a name that carries no type suffix, capping very long ones', () => { expect(shortName('Beaver Road')).toBe('Beaver Road'); expect(shortName('A'.repeat(30), 10)).toBe('AAAAAAAAA…'); }); });