EdgeFunctionOverview.utils.test.ts219 lines · main
1import dayjs from 'dayjs'
2import { describe, expect, it } from 'vitest'
3
4import {
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
25describe('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})