import { TIME_CONSTANTS, calculateSevenDayTicks, calculateDayTicks, calculateHourTicks, getTicks, getEdgeMargin, calculateLabelVisibility, calculateAlertPosition, getNavigationAction, calculateNavigationIndex, getNavigationStateUpdate, } from './GlobalHealthBar.utils'; describe('Health Bar Utils', () => { describe('Tick Calculations', () => { describe('calculateSevenDayTicks', () => { it('should calculate 7 ticks for a week span with proper spacing', () => { const endTimestamp = new Date('2023-12-07T12:00:00Z').getTime(); const ticks = calculateSevenDayTicks(endTimestamp); expect(ticks).toHaveLength(7); // Check that ticks are spaced by one day for (let i = 1; i < ticks.length; i++) { expect(ticks[i - 1] - ticks[i]).toBe(TIME_CONSTANTS.ONE_DAY); } }); it('should apply margin hours offset', () => { const endTimestamp = new Date('2023-12-07T12:00:00Z').getTime(); const ticks = calculateSevenDayTicks(endTimestamp); // First tick should be less than endTimestamp due to margin expect(ticks[0]).toBeLessThan(endTimestamp); // The difference should account for margin hours const expectedMargin = TIME_CONSTANTS.MARGIN_HOURS * TIME_CONSTANTS.ONE_HOUR; expect(endTimestamp - ticks[0]).toBeGreaterThan(expectedMargin); }); it('should round to nearest half day', () => { const endTimestamp = new Date('2023-12-07T15:30:45Z').getTime(); const ticks = calculateSevenDayTicks(endTimestamp); const firstTickDate = new Date(ticks[0]); // Should be rounded to either 00:00 or 12:00 expect([0, 12]).toContain(firstTickDate.getUTCHours()); expect(firstTickDate.getUTCMinutes()).toBe(0); expect(firstTickDate.getUTCSeconds()).toBe(0); }); }); describe('calculateDayTicks', () => { it('should calculate 5 ticks when end timestamp aligns with 6-hour boundary', () => { const endTimestamp = new Date('2023-12-07T12:00:00Z').getTime(); const ticks = calculateDayTicks(endTimestamp); expect(ticks).toHaveLength(5); }); it('should calculate 4 ticks when end timestamp does not align with 6-hour boundary', () => { const endTimestamp = new Date('2023-12-07T13:30:00Z').getTime(); const ticks = calculateDayTicks(endTimestamp); expect(ticks).toHaveLength(4); }); it('should space ticks by 6 hours', () => { const endTimestamp = new Date('2023-12-07T12:00:00Z').getTime(); const ticks = calculateDayTicks(endTimestamp); for (let i = 1; i < ticks.length; i++) { expect(ticks[i - 1] - ticks[i]).toBe(TIME_CONSTANTS.SIX_HOURS); } }); it('should start from closest 6-hour boundary', () => { const endTimestamp = new Date('2023-12-07T14:30:00Z').getTime(); const ticks = calculateDayTicks(endTimestamp); const firstTickDate = new Date(ticks[0]); expect(firstTickDate.getUTCHours() % 6).toBe(0); expect(firstTickDate.getUTCMinutes()).toBe(0); expect(firstTickDate.getUTCSeconds()).toBe(0); }); }); describe('calculateHourTicks', () => { it('should calculate 5 ticks when end timestamp aligns with 15-minute boundary', () => { const endTimestamp = new Date('2023-12-07T12:15:00Z').getTime(); const ticks = calculateHourTicks(endTimestamp); expect(ticks).toHaveLength(5); }); it('should calculate 4 ticks when end timestamp does not align with 15-minute boundary', () => { const endTimestamp = new Date('2023-12-07T12:17:00Z').getTime(); const ticks = calculateHourTicks(endTimestamp); expect(ticks).toHaveLength(4); }); it('should space ticks by 15 minutes', () => { const endTimestamp = new Date('2023-12-07T12:15:00Z').getTime(); const ticks = calculateHourTicks(endTimestamp); for (let i = 1; i < ticks.length; i++) { expect(ticks[i - 1] - ticks[i]).toBe(TIME_CONSTANTS.FIFTEEN_MINUTES); } }); it('should start from closest 15-minute boundary', () => { const endTimestamp = new Date('2023-12-07T12:17:30Z').getTime(); const ticks = calculateHourTicks(endTimestamp); const firstTickDate = new Date(ticks[0]); // Test time is 12:17 so the first tick should be 12:15 expect(firstTickDate.getUTCHours()).toBe(12); expect(firstTickDate.getUTCMinutes()).toBe(15); expect(firstTickDate.getUTCSeconds()).toBe(0); }); }); describe('getTicks', () => { it('should return seven day ticks for week span', () => { const startTimestamp = new Date('2023-12-01T00:00:00Z').getTime(); const endTimestamp = startTimestamp + 7 * TIME_CONSTANTS.ONE_DAY; const ticks = getTicks(startTimestamp, endTimestamp); expect(ticks).toHaveLength(7); }); it('should return day ticks for 24-hour span', () => { const startTimestamp = new Date('2023-12-07T00:00:00Z').getTime(); const endTimestamp = startTimestamp + 24 * TIME_CONSTANTS.ONE_HOUR; const ticks = getTicks(startTimestamp, endTimestamp); expect(ticks.length).toBe(5); }); it('should return hour ticks for 1-hour span', () => { const startTimestamp = new Date('2023-12-07T12:00:00Z').getTime(); const endTimestamp = startTimestamp + TIME_CONSTANTS.ONE_HOUR; const ticks = getTicks(startTimestamp, endTimestamp); expect(ticks.length).toBe(5); }); it('should return empty array for unsupported time spans', () => { const startTimestamp = new Date('2023-12-07T12:00:00Z').getTime(); const endTimestamp = startTimestamp + 2 * TIME_CONSTANTS.ONE_HOUR; const ticks = getTicks(startTimestamp, endTimestamp); expect(ticks).toEqual([]); }); }); }); describe('Edge Margin Calculations', () => { describe('getEdgeMargin', () => { it('should return -8 for first tick in day span with 5 total ticks', () => { const margin = getEdgeMargin(0, 5, true); expect(margin).toBe(-8); }); it('should return 8 for last tick in day span with 5 total ticks', () => { const margin = getEdgeMargin(4, 5, true); expect(margin).toBe(8); }); it('should return 0 for middle ticks in day span with 5 total ticks', () => { const margin = getEdgeMargin(2, 5, true); expect(margin).toBe(0); }); it('should return 0 for non-day spans', () => { const margin = getEdgeMargin(0, 5, false); expect(margin).toBe(0); }); it('should return 0 for day span with non-5 total ticks', () => { const margin = getEdgeMargin(0, 4, true); expect(margin).toBe(0); }); }); }); describe('Label Visibility', () => { describe('calculateLabelVisibility', () => { it('should return true when chart has enough space per tick', () => { const chartWidth = 500; const totalTicks = 5; const span = TIME_CONSTANTS.ONE_DAY; const index = 0; const endTimestamp = Date.now(); const visible = calculateLabelVisibility( chartWidth, totalTicks, span, index, endTimestamp, ); expect(visible).toBe(true); }); it('should apply week span rules when space is limited', () => { const chartWidth = 200; const totalTicks = 7; const span = TIME_CONSTANTS.ONE_WEEK; const endTimestamp = Date.now(); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 0, endTimestamp, ), ).toBe(true); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 1, endTimestamp, ), ).toBe(false); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 2, endTimestamp, ), ).toBe(true); }); it('should apply day span rules when space is limited', () => { const chartWidth = 200; const totalTicks = 4; const span = TIME_CONSTANTS.ONE_DAY; const endTimestamp = new Date('2023-12-07T12:10:00Z').getTime(); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 0, endTimestamp, ), ).toBe(true); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 1, endTimestamp, ), ).toBe(false); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 2, endTimestamp, ), ).toBe(false); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 3, endTimestamp, ), ).toBe(true); }); it('should handle special cases for day span with hour-aligned end timestamp', () => { const chartWidth = 200; const totalTicks = 5; const span = TIME_CONSTANTS.ONE_DAY; const endTimestamp = new Date('2023-12-07T12:00:00Z').getTime(); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 0, endTimestamp, ), ).toBe(true); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 2, endTimestamp, ), ).toBe(true); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 4, endTimestamp, ), ).toBe(true); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 3, endTimestamp, ), ).toBe(false); }); it('should apply day span rules when space is limited for non-round hour', () => { const chartWidth = 200; const totalTicks = 4; const span = TIME_CONSTANTS.ONE_DAY; const endTimestamp = new Date('2023-12-07T12:10:00Z').getTime(); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 0, endTimestamp, ), ).toBe(true); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 1, endTimestamp, ), ).toBe(false); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 2, endTimestamp, ), ).toBe(false); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 3, endTimestamp, ), ).toBe(true); }); it('should handle special cases for hour span with 15-minute aligned end timestamp', () => { const chartWidth = 200; const totalTicks = 5; const span = TIME_CONSTANTS.ONE_HOUR; const endTimestamp = new Date('2023-12-07T12:15:00Z').getTime(); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 0, endTimestamp, ), ).toBe(true); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 2, endTimestamp, ), ).toBe(true); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 4, endTimestamp, ), ).toBe(true); expect( calculateLabelVisibility( chartWidth, totalTicks, span, 3, endTimestamp, ), ).toBe(false); }); it('should return false for unsupported time spans when space is limited', () => { const chartWidth = 200; const totalTicks = 5; const span = 123456; const index = 0; const endTimestamp = Date.now(); const visible = calculateLabelVisibility( chartWidth, totalTicks, span, index, endTimestamp, ); expect(visible).toBe(false); }); }); }); describe('Alert Position Calculations', () => { describe('calculateAlertPosition', () => { it('should calculate position for alert within time range', () => { const alertStartTimestamp = 2000; const alertEndTimestamp = 3000; const chartStartTimestamp = 1000; const chartEndTimestamp = 5000; const availableWidth = 200; const baseX = 50; const result = calculateAlertPosition( alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth, baseX, ); const expectedRelativeSize = (3000 - 2000) / (5000 - 1000); const expectedWidth = expectedRelativeSize * 200; const expectedStartX = 50 + ((2000 - 1000) / (5000 - 1000)) * 200; expect(result.width).toBe(expectedWidth); expect(result.startX).toBe(expectedStartX); expect(result.relativeSize).toBe(expectedRelativeSize); }); it('should handle alert starting before time range', () => { const alertStartTimestamp = 1000; const alertEndTimestamp = 3000; const chartStartTimestamp = 2000; const chartEndTimestamp = 5000; const availableWidth = 200; const baseX = 50; const result = calculateAlertPosition( alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth, baseX, ); const expectedRelativeSize = (3000 - 2000) / (5000 - 2000); const expectedWidth = expectedRelativeSize * 200; const expectedStartX = 50; // baseX since alert starts before chart range expect(result.width).toBe(expectedWidth); expect(result.startX).toBe(expectedStartX); expect(result.relativeSize).toBe(expectedRelativeSize); }); it('should handle alert ending after time range', () => { const alertStartTimestamp = 2500; const alertEndTimestamp = 6000; const chartStartTimestamp = 1000; const chartEndTimestamp = 4000; const availableWidth = 200; const baseX = 50; const result = calculateAlertPosition( alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth, baseX, ); const expectedRelativeSize = (4000 - 2500) / (4000 - 1000); const expectedWidth = expectedRelativeSize * 200; const expectedStartX = 50 + ((2500 - 1000) / (4000 - 1000)) * 200; expect(result.width).toBe(expectedWidth); expect(result.startX).toBe(expectedStartX); expect(result.relativeSize).toBe(expectedRelativeSize); }); it('should handle alert spanning entire time range', () => { const alertStartTimestamp = 1000; const alertEndTimestamp = 5000; const chartStartTimestamp = 2000; const chartEndTimestamp = 4000; const availableWidth = 200; const baseX = 50; const result = calculateAlertPosition( alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth, baseX, ); const expectedStartX = 50; // baseX since alert starts before chart range expect(result.width).toBe(200); expect(result.startX).toBe(expectedStartX); expect(result.relativeSize).toBe(1); }); it('should work with default baseX of 0', () => { const alertStartTimestamp = 2000; const alertEndTimestamp = 3000; const chartStartTimestamp = 1000; const chartEndTimestamp = 5000; const availableWidth = 200; const result = calculateAlertPosition( alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth, ); const expectedRelativeSize = (3000 - 2000) / (5000 - 1000); const expectedWidth = expectedRelativeSize * 200; const expectedStartX = 0 + ((2000 - 1000) / (5000 - 1000)) * 200; expect(result.width).toBe(expectedWidth); expect(result.startX).toBe(expectedStartX); expect(result.relativeSize).toBe(expectedRelativeSize); }); }); }); describe('Keyboard Navigation Utils', () => { describe('getNavigationAction', () => { it('should map arrow keys to navigation actions', () => { expect(getNavigationAction('ArrowLeft')).toBe('previous'); expect(getNavigationAction('ArrowUp')).toBe('previous'); expect(getNavigationAction('ArrowRight')).toBe('next'); expect(getNavigationAction('ArrowDown')).toBe('next'); }); it('should map Home and End keys', () => { expect(getNavigationAction('Home')).toBe('first'); expect(getNavigationAction('End')).toBe('last'); }); it('should map Escape key', () => { expect(getNavigationAction('Escape')).toBe('escape'); }); it('should return null for unmapped keys', () => { expect(getNavigationAction('Enter')).toBe(null); expect(getNavigationAction('Space')).toBe(null); expect(getNavigationAction('Tab')).toBe(null); }); }); describe('calculateNavigationIndex', () => { const arrayLength = 5; it('should handle previous navigation', () => { expect(calculateNavigationIndex('previous', 2, arrayLength)).toBe(1); expect(calculateNavigationIndex('previous', 0, arrayLength)).toBe(4); // wraps to end }); it('should handle next navigation', () => { expect(calculateNavigationIndex('next', 2, arrayLength)).toBe(3); expect(calculateNavigationIndex('next', 4, arrayLength)).toBe(0); // wraps to start }); it('should handle first navigation', () => { expect(calculateNavigationIndex('first', 3, arrayLength)).toBe(0); }); it('should handle last navigation', () => { expect(calculateNavigationIndex('last', 1, arrayLength)).toBe(4); }); it('should handle escape navigation', () => { expect(calculateNavigationIndex('escape', 2, arrayLength)).toBe(-1); }); it('should return -1 for empty array', () => { expect(calculateNavigationIndex('next', 0, 0)).toBe(-1); expect(calculateNavigationIndex('previous', 0, 0)).toBe(-1); }); it('should handle single item array', () => { expect(calculateNavigationIndex('next', 0, 1)).toBe(0); expect(calculateNavigationIndex('previous', 0, 1)).toBe(0); expect(calculateNavigationIndex('first', 0, 1)).toBe(0); expect(calculateNavigationIndex('last', 0, 1)).toBe(0); }); }); describe('getNavigationStateUpdate', () => { const mockAlerts = [ { id: 1, name: 'Alert 1' }, { id: 2, name: 'Alert 2' }, { id: 3, name: 'Alert 3' }, ]; it('should return correct state for next navigation', () => { const result = getNavigationStateUpdate('next', 0, mockAlerts); expect(result.newIndex).toBe(1); expect(result.selectedAlert).toEqual(mockAlerts[1]); expect(result.shouldActivateKeyboard).toBe(true); }); it('should return correct state for previous navigation', () => { const result = getNavigationStateUpdate('previous', 2, mockAlerts); expect(result.newIndex).toBe(1); expect(result.selectedAlert).toEqual(mockAlerts[1]); expect(result.shouldActivateKeyboard).toBe(true); }); it('should return correct state for first navigation', () => { const result = getNavigationStateUpdate('first', 2, mockAlerts); expect(result.newIndex).toBe(0); expect(result.selectedAlert).toEqual(mockAlerts[0]); expect(result.shouldActivateKeyboard).toBe(true); }); it('should return correct state for last navigation', () => { const result = getNavigationStateUpdate('last', 0, mockAlerts); expect(result.newIndex).toBe(2); expect(result.selectedAlert).toEqual(mockAlerts[2]); expect(result.shouldActivateKeyboard).toBe(true); }); it('should return correct state for escape navigation', () => { const result = getNavigationStateUpdate('escape', 1, mockAlerts); expect(result.newIndex).toBe(-1); expect(result.selectedAlert).toBe(null); expect(result.shouldActivateKeyboard).toBe(false); }); it('should handle empty array', () => { const result = getNavigationStateUpdate('next', 0, []); expect(result.newIndex).toBe(-1); expect(result.selectedAlert).toBe(null); expect(result.shouldActivateKeyboard).toBe(true); }); it('should handle wrapping navigation', () => { // Next from last index should wrap to first const nextResult = getNavigationStateUpdate('next', 2, mockAlerts); expect(nextResult.newIndex).toBe(0); expect(nextResult.selectedAlert).toEqual(mockAlerts[0]); // Previous from first index should wrap to last const prevResult = getNavigationStateUpdate('previous', 0, mockAlerts); expect(prevResult.newIndex).toBe(2); expect(prevResult.selectedAlert).toEqual(mockAlerts[2]); }); }); }); });