EdgeFunctionOverview.utils.test.ts219 lines · main
| 1 | import dayjs from 'dayjs' |
| 2 | import { describe, expect, it } from 'vitest' |
| 3 | |
| 4 | import { |
| 5 | EDGE_FUNCTION_CHART_INTERVALS, |
| 6 | formatChartTimestamp, |
| 7 | formatMetric, |
| 8 | formatRate, |
| 9 | formatReferenceDelta, |
| 10 | getBucketedTimeRange, |
| 11 | getChartEmptyStateCopy, |
| 12 | getChartTimeRangeLabels, |
| 13 | getExecutionMetrics, |
| 14 | getInvocationChartData, |
| 15 | getInvocationTotals, |
| 16 | getInvocationUpdateAnnotation, |
| 17 | getMemoryTooltipDetail, |
| 18 | getRollingTimeRange, |
| 19 | getSegmentedButtonClassName, |
| 20 | getUsageMetrics, |
| 21 | toEdgeFunctionChartData, |
| 22 | type EdgeFunctionChartRawDatum, |
| 23 | } from './EdgeFunctionOverview.utils' |
| 24 | |
| 25 | describe('EdgeFunctionOverview.utils', () => { |
| 26 | it('uses a full day interval for the 1 day option', () => { |
| 27 | expect( |
| 28 | EDGE_FUNCTION_CHART_INTERVALS.find((interval) => interval.key === '1day')?.startUnit |
| 29 | ).toBe('day') |
| 30 | }) |
| 31 | |
| 32 | it('builds invocation chart data and totals from combined stats', () => { |
| 33 | const chartData = toEdgeFunctionChartData([ |
| 34 | { |
| 35 | timestamp: '2026-03-20T10:00:00.000Z', |
| 36 | success_count: 10, |
| 37 | redirect_count: 2, |
| 38 | client_err_count: 1, |
| 39 | server_err_count: 3, |
| 40 | }, |
| 41 | { |
| 42 | timestamp: '2026-03-20T10:15:00.000Z', |
| 43 | success_count: '4', |
| 44 | redirect_count: '1', |
| 45 | client_err_count: '0', |
| 46 | server_err_count: '2', |
| 47 | }, |
| 48 | ] satisfies EdgeFunctionChartRawDatum[]) |
| 49 | const data = getInvocationChartData(chartData) |
| 50 | |
| 51 | expect(data).toEqual([ |
| 52 | { |
| 53 | timestamp: '2026-03-20T10:00:00.000Z', |
| 54 | ok_count: 10, |
| 55 | warning_count: 3, |
| 56 | error_count: 3, |
| 57 | }, |
| 58 | { |
| 59 | timestamp: '2026-03-20T10:15:00.000Z', |
| 60 | ok_count: 4, |
| 61 | warning_count: 1, |
| 62 | error_count: 2, |
| 63 | }, |
| 64 | ]) |
| 65 | |
| 66 | expect(chartData[1]).toEqual({ |
| 67 | timestamp: '2026-03-20T10:15:00.000Z', |
| 68 | success_count: 4, |
| 69 | redirect_count: 1, |
| 70 | client_err_count: 0, |
| 71 | server_err_count: 2, |
| 72 | avg_execution_time: 0, |
| 73 | max_execution_time: 0, |
| 74 | avg_cpu_time_used: 0, |
| 75 | max_cpu_time_used: 0, |
| 76 | avg_memory_used: 0, |
| 77 | avg_heap_memory_used: 0, |
| 78 | avg_external_memory_used: 0, |
| 79 | }) |
| 80 | |
| 81 | expect(getInvocationTotals(data)).toEqual({ |
| 82 | totalInvocationCount: 23, |
| 83 | totalWarningCount: 4, |
| 84 | totalErrorCount: 5, |
| 85 | }) |
| 86 | }) |
| 87 | |
| 88 | it('builds segmented button classes and chart range labels', () => { |
| 89 | expect(getSegmentedButtonClassName(0, 4)).toBe('rounded-tr-none rounded-br-none') |
| 90 | expect(getSegmentedButtonClassName(1, 4)).toBe('rounded-none') |
| 91 | expect(getSegmentedButtonClassName(3, 4)).toBe('rounded-tl-none rounded-bl-none') |
| 92 | |
| 93 | expect( |
| 94 | getChartTimeRangeLabels( |
| 95 | [{ timestamp: '2026-03-20T10:00:00.000Z' }, { timestamp: '2026-03-20T11:00:00.000Z' }], |
| 96 | 'MMM D, h:mma' |
| 97 | ) |
| 98 | ).toEqual({ |
| 99 | start: dayjs('2026-03-20T10:00:00.000Z').format('MMM D, h:mma'), |
| 100 | end: dayjs('2026-03-20T11:00:00.000Z').format('MMM D, h:mma'), |
| 101 | }) |
| 102 | expect(getChartTimeRangeLabels([], 'MMM D')).toBeUndefined() |
| 103 | expect(formatChartTimestamp('2026-03-20T10:00:00.000Z', 'MMM D, h:mma')).toBe( |
| 104 | dayjs('2026-03-20T10:00:00.000Z').format('MMM D, h:mma') |
| 105 | ) |
| 106 | }) |
| 107 | |
| 108 | it('computes execution and usage metrics', () => { |
| 109 | const stats = [ |
| 110 | { |
| 111 | timestamp: '2026-03-20T10:00:00.000Z', |
| 112 | success_count: 0, |
| 113 | redirect_count: 0, |
| 114 | client_err_count: 0, |
| 115 | server_err_count: 0, |
| 116 | avg_execution_time: 10, |
| 117 | max_execution_time: 18, |
| 118 | avg_cpu_time_used: 5, |
| 119 | max_cpu_time_used: 8, |
| 120 | avg_memory_used: 100, |
| 121 | avg_heap_memory_used: 75, |
| 122 | avg_external_memory_used: 25, |
| 123 | }, |
| 124 | { |
| 125 | timestamp: '2026-03-20T10:15:00.000Z', |
| 126 | success_count: 0, |
| 127 | redirect_count: 0, |
| 128 | client_err_count: 0, |
| 129 | server_err_count: 0, |
| 130 | avg_execution_time: 30, |
| 131 | max_execution_time: 45, |
| 132 | avg_cpu_time_used: 15, |
| 133 | max_cpu_time_used: 20, |
| 134 | avg_memory_used: 200, |
| 135 | avg_heap_memory_used: 150, |
| 136 | avg_external_memory_used: 50, |
| 137 | }, |
| 138 | ] |
| 139 | |
| 140 | expect(getExecutionMetrics(stats)).toEqual({ |
| 141 | averageExecutionTime: 20, |
| 142 | maxExecutionTime: 45, |
| 143 | }) |
| 144 | |
| 145 | expect(getUsageMetrics(stats)).toEqual({ |
| 146 | averageCpuTime: 10, |
| 147 | maxCpuTime: 20, |
| 148 | averageMemoryUsage: 150, |
| 149 | totalHeapMemory: 225, |
| 150 | totalExternalMemory: 75, |
| 151 | totalMemoryByType: 300, |
| 152 | }) |
| 153 | }) |
| 154 | |
| 155 | it('returns a snapped deploy annotation when updated_at falls within the selected window', () => { |
| 156 | const annotation = getInvocationUpdateAnnotation({ |
| 157 | updatedAt: '2026-03-20T10:16:30.000Z', |
| 158 | invocationChartData: [ |
| 159 | { timestamp: '2026-03-20T10:00:00.000Z', ok_count: 3, warning_count: 0, error_count: 0 }, |
| 160 | { timestamp: '2026-03-20T10:15:00.000Z', ok_count: 5, warning_count: 1, error_count: 1 }, |
| 161 | { timestamp: '2026-03-20T10:30:00.000Z', ok_count: 2, warning_count: 0, error_count: 0 }, |
| 162 | ], |
| 163 | windowStart: new Date('2026-03-20T09:45:00.000Z'), |
| 164 | windowEnd: new Date('2026-03-20T10:45:00.000Z'), |
| 165 | }) |
| 166 | |
| 167 | expect(annotation?.timestamp).toBe('2026-03-20T10:15:00.000Z') |
| 168 | expect(annotation?.position).toBeCloseTo(50) |
| 169 | expect(annotation?.updatedAt.toISOString()).toBe('2026-03-20T10:16:30.000Z') |
| 170 | }) |
| 171 | |
| 172 | it('hides the deploy annotation when updated_at is outside the selected window', () => { |
| 173 | const annotation = getInvocationUpdateAnnotation({ |
| 174 | updatedAt: '2026-03-20T11:05:00.000Z', |
| 175 | invocationChartData: [ |
| 176 | { timestamp: '2026-03-20T10:00:00.000Z', ok_count: 3, warning_count: 0, error_count: 0 }, |
| 177 | ], |
| 178 | windowStart: new Date('2026-03-20T09:45:00.000Z'), |
| 179 | windowEnd: new Date('2026-03-20T10:45:00.000Z'), |
| 180 | }) |
| 181 | |
| 182 | expect(annotation).toBeUndefined() |
| 183 | }) |
| 184 | |
| 185 | it('builds bucketed and rolling time windows from the selected interval', () => { |
| 186 | const interval = EDGE_FUNCTION_CHART_INTERVALS.find((item) => item.key === '1hr') |
| 187 | expect(interval).toBeDefined() |
| 188 | |
| 189 | const now = new Date('2026-03-20T10:37:00.000Z') |
| 190 | const [bucketedStart, bucketedEnd] = getBucketedTimeRange(interval!, now) |
| 191 | const [rollingStart, rollingEnd] = getRollingTimeRange(interval!, now) |
| 192 | |
| 193 | expect(bucketedStart.toISOString()).toBe('2026-03-20T09:00:00.000Z') |
| 194 | expect(bucketedEnd.toISOString()).toBe('2026-03-20T10:00:00.000Z') |
| 195 | expect(rollingStart.toISOString()).toBe('2026-03-20T09:37:00.000Z') |
| 196 | expect(rollingEnd.toISOString()).toBe('2026-03-20T10:37:00.000Z') |
| 197 | }) |
| 198 | |
| 199 | it('formats metric, rate, and reference deltas consistently', () => { |
| 200 | expect(formatMetric(12.34, 'MB')).toBe('12.3MB') |
| 201 | expect(formatMetric(1234, 'ms')).toBe('1,234ms') |
| 202 | expect(formatRate(1, 4)).toBe('25%') |
| 203 | expect(formatReferenceDelta(110, 100)).toBe('10% above average') |
| 204 | expect(formatReferenceDelta(90, 100)).toBe('10% below average') |
| 205 | expect(formatReferenceDelta(100, 100)).toBe('At average') |
| 206 | }) |
| 207 | |
| 208 | it('builds empty-state copy and tooltip detail strings', () => { |
| 209 | expect(getChartEmptyStateCopy('invocations', false, 'boom')).toEqual({ |
| 210 | title: 'No data to show', |
| 211 | description: undefined, |
| 212 | }) |
| 213 | expect(getChartEmptyStateCopy('CPU time', true, 'Request failed')).toEqual({ |
| 214 | title: 'Unable to load CPU time', |
| 215 | description: 'Request failed', |
| 216 | }) |
| 217 | expect(getMemoryTooltipDetail(12.34, 5.67)).toBe('Heap 12.3MB • External 5.7MB') |
| 218 | }) |
| 219 | }) |