Skip to content

Commit e887be4

Browse files
chore: wip
1 parent b154d98 commit e887be4

File tree

1 file changed

+303
-0
lines changed

1 file changed

+303
-0
lines changed

test/analytics-dashboard.test.ts

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import { describe, expect, it } from 'bun:test'
2+
import {
3+
calculateAxisTicks,
4+
calculateChange,
5+
dateRangePresets,
6+
formatCompact,
7+
formatDate,
8+
formatDateRange,
9+
formatDuration,
10+
formatNumber,
11+
formatPercentage,
12+
getDateRangeFromPreset,
13+
} from '../src/analytics/dashboard/utils'
14+
15+
describe('Dashboard Utilities', () => {
16+
describe('formatNumber', () => {
17+
it('should format integers', () => {
18+
expect(formatNumber(1000)).toBe('1,000')
19+
expect(formatNumber(1234567)).toBe('1,234,567')
20+
})
21+
22+
it('should format with decimals', () => {
23+
expect(formatNumber(1234.5678, 2)).toBe('1,234.57')
24+
})
25+
})
26+
27+
describe('formatCompact', () => {
28+
it('should format small numbers as-is', () => {
29+
expect(formatCompact(999)).toBe('999')
30+
})
31+
32+
it('should format thousands with K', () => {
33+
expect(formatCompact(1000)).toBe('1.0K')
34+
expect(formatCompact(1500)).toBe('1.5K')
35+
expect(formatCompact(999999)).toBe('1000.0K')
36+
})
37+
38+
it('should format millions with M', () => {
39+
expect(formatCompact(1000000)).toBe('1.0M')
40+
expect(formatCompact(2500000)).toBe('2.5M')
41+
})
42+
43+
it('should format billions with B', () => {
44+
expect(formatCompact(1000000000)).toBe('1.0B')
45+
})
46+
})
47+
48+
describe('formatPercentage', () => {
49+
it('should format decimal to percentage', () => {
50+
expect(formatPercentage(0.5)).toBe('50.0%')
51+
expect(formatPercentage(0.123, 1)).toBe('12.3%')
52+
expect(formatPercentage(1)).toBe('100.0%')
53+
})
54+
})
55+
56+
describe('formatDuration', () => {
57+
it('should format milliseconds', () => {
58+
expect(formatDuration(500)).toBe('500ms')
59+
})
60+
61+
it('should format seconds', () => {
62+
expect(formatDuration(5000)).toBe('5s')
63+
expect(formatDuration(45000)).toBe('45s')
64+
})
65+
66+
it('should format minutes and seconds', () => {
67+
expect(formatDuration(90000)).toBe('1m 30s')
68+
expect(formatDuration(120000)).toBe('2m')
69+
})
70+
71+
it('should format hours and minutes', () => {
72+
expect(formatDuration(3600000)).toBe('1h')
73+
expect(formatDuration(5400000)).toBe('1h 30m')
74+
})
75+
})
76+
77+
describe('formatDate', () => {
78+
const testDate = new Date('2024-03-15T14:30:00')
79+
80+
it('should format short date', () => {
81+
const result = formatDate(testDate, 'short')
82+
expect(result).toContain('Mar')
83+
expect(result).toContain('15')
84+
})
85+
86+
it('should format long date', () => {
87+
const result = formatDate(testDate, 'long')
88+
expect(result).toContain('March')
89+
expect(result).toContain('15')
90+
expect(result).toContain('2024')
91+
})
92+
93+
it('should accept string dates', () => {
94+
const result = formatDate('2024-03-15', 'short')
95+
expect(result).toContain('Mar')
96+
})
97+
})
98+
99+
describe('formatDateRange', () => {
100+
it('should format date range', () => {
101+
const start = new Date('2024-03-01')
102+
const end = new Date('2024-03-15')
103+
const result = formatDateRange(start, end)
104+
expect(result).toContain('Mar')
105+
})
106+
107+
it('should handle same day', () => {
108+
const date = new Date('2024-03-15')
109+
const result = formatDateRange(date, date)
110+
expect(result).toContain('Mar')
111+
expect(result).not.toContain(' - ')
112+
})
113+
})
114+
115+
describe('calculateChange', () => {
116+
it('should calculate positive change', () => {
117+
expect(calculateChange(150, 100)).toBe(0.5)
118+
})
119+
120+
it('should calculate negative change', () => {
121+
expect(calculateChange(50, 100)).toBe(-0.5)
122+
})
123+
124+
it('should handle zero previous value', () => {
125+
expect(calculateChange(100, 0)).toBe(1)
126+
expect(calculateChange(0, 0)).toBe(0)
127+
})
128+
})
129+
130+
describe('calculateAxisTicks', () => {
131+
it('should generate nice tick values', () => {
132+
const ticks = calculateAxisTicks(0, 100, 5)
133+
expect(ticks.length).toBeGreaterThan(0)
134+
expect(ticks[0]).toBe(0)
135+
expect(ticks[ticks.length - 1]).toBeGreaterThanOrEqual(100)
136+
})
137+
138+
it('should handle large ranges', () => {
139+
const ticks = calculateAxisTicks(0, 10000, 5)
140+
expect(ticks.every(t => t >= 0)).toBe(true)
141+
})
142+
})
143+
144+
describe('dateRangePresets', () => {
145+
it('should have expected presets', () => {
146+
const presetValues = dateRangePresets.map(p => p.value)
147+
expect(presetValues).toContain('today')
148+
expect(presetValues).toContain('yesterday')
149+
expect(presetValues).toContain('7d')
150+
expect(presetValues).toContain('30d')
151+
})
152+
153+
it('should return valid date ranges', () => {
154+
for (const preset of dateRangePresets) {
155+
const range = preset.getRange()
156+
expect(range.start).toBeInstanceOf(Date)
157+
expect(range.end).toBeInstanceOf(Date)
158+
expect(range.start.getTime()).toBeLessThanOrEqual(range.end.getTime())
159+
}
160+
})
161+
})
162+
163+
describe('getDateRangeFromPreset', () => {
164+
it('should return date range for valid preset', () => {
165+
const range = getDateRangeFromPreset('7d')
166+
expect(range).not.toBeNull()
167+
expect(range!.start).toBeInstanceOf(Date)
168+
expect(range!.end).toBeInstanceOf(Date)
169+
})
170+
171+
it('should return null for invalid preset', () => {
172+
const range = getDateRangeFromPreset('invalid')
173+
expect(range).toBeNull()
174+
})
175+
})
176+
})
177+
178+
describe('Dashboard Types', () => {
179+
it('should export all required types', async () => {
180+
const dashboardModule = await import('../src/analytics/dashboard')
181+
182+
// Check components exist (they'll be DefineComponent types)
183+
expect(dashboardModule.AnalyticsDashboard).toBeDefined()
184+
expect(dashboardModule.StatCard).toBeDefined()
185+
expect(dashboardModule.RealtimeCounter).toBeDefined()
186+
expect(dashboardModule.TopList).toBeDefined()
187+
expect(dashboardModule.TimeSeriesChart).toBeDefined()
188+
expect(dashboardModule.DateRangePicker).toBeDefined()
189+
expect(dashboardModule.DeviceBreakdown).toBeDefined()
190+
191+
// Check composables
192+
expect(dashboardModule.AnalyticsClient).toBeDefined()
193+
expect(dashboardModule.createAnalyticsComposable).toBeDefined()
194+
expect(dashboardModule.createRealtimePoller).toBeDefined()
195+
expect(dashboardModule.useAnalytics).toBeDefined()
196+
197+
// Check themes
198+
expect(dashboardModule.defaultTheme).toBeDefined()
199+
expect(dashboardModule.darkTheme).toBeDefined()
200+
201+
// Check utilities
202+
expect(dashboardModule.formatNumber).toBeDefined()
203+
expect(dashboardModule.formatCompact).toBeDefined()
204+
expect(dashboardModule.formatPercentage).toBeDefined()
205+
expect(dashboardModule.formatDuration).toBeDefined()
206+
expect(dashboardModule.formatDate).toBeDefined()
207+
expect(dashboardModule.dateRangePresets).toBeDefined()
208+
expect(dashboardModule.calculateAxisTicks).toBeDefined()
209+
})
210+
})
211+
212+
describe('AnalyticsClient', () => {
213+
it('should be constructible with config', async () => {
214+
const { AnalyticsClient } = await import('../src/analytics/dashboard')
215+
216+
const client = new AnalyticsClient({
217+
baseUrl: 'https://api.example.com',
218+
siteId: 'test-site',
219+
})
220+
221+
expect(client).toBeInstanceOf(AnalyticsClient)
222+
})
223+
224+
it('should accept custom headers', async () => {
225+
const { AnalyticsClient } = await import('../src/analytics/dashboard')
226+
227+
const client = new AnalyticsClient({
228+
baseUrl: 'https://api.example.com',
229+
siteId: 'test-site',
230+
headers: {
231+
Authorization: 'Bearer token123',
232+
},
233+
})
234+
235+
expect(client).toBeInstanceOf(AnalyticsClient)
236+
})
237+
})
238+
239+
describe('createAnalyticsComposable', () => {
240+
it('should create composable with methods', async () => {
241+
const { createAnalyticsComposable } = await import('../src/analytics/dashboard')
242+
243+
const composable = createAnalyticsComposable({
244+
baseUrl: 'https://api.example.com',
245+
siteId: 'test-site',
246+
})
247+
248+
expect(composable.client).toBeDefined()
249+
expect(composable.getInitialDateRange).toBeDefined()
250+
expect(composable.fetchStats).toBeDefined()
251+
expect(composable.fetchRealtime).toBeDefined()
252+
expect(composable.fetchTopPages).toBeDefined()
253+
expect(composable.fetchTopReferrers).toBeDefined()
254+
expect(composable.fetchDevices).toBeDefined()
255+
expect(composable.fetchTimeSeries).toBeDefined()
256+
})
257+
258+
it('should compute initial date range based on preset', async () => {
259+
const { createAnalyticsComposable } = await import('../src/analytics/dashboard')
260+
261+
const composable7d = createAnalyticsComposable({
262+
baseUrl: 'https://api.example.com',
263+
siteId: 'test-site',
264+
initialDateRange: '7d',
265+
})
266+
267+
const range7d = composable7d.getInitialDateRange()
268+
const daysDiff = Math.round((range7d.end.getTime() - range7d.start.getTime()) / (1000 * 60 * 60 * 24))
269+
expect(daysDiff).toBeGreaterThanOrEqual(6)
270+
expect(daysDiff).toBeLessThanOrEqual(8)
271+
272+
const composable30d = createAnalyticsComposable({
273+
baseUrl: 'https://api.example.com',
274+
siteId: 'test-site',
275+
initialDateRange: '30d',
276+
})
277+
278+
const range30d = composable30d.getInitialDateRange()
279+
const daysDiff30 = Math.round((range30d.end.getTime() - range30d.start.getTime()) / (1000 * 60 * 60 * 24))
280+
expect(daysDiff30).toBeGreaterThanOrEqual(29)
281+
expect(daysDiff30).toBeLessThanOrEqual(31)
282+
})
283+
})
284+
285+
describe('createRealtimePoller', () => {
286+
it('should create poller with start/stop methods', async () => {
287+
const { createRealtimePoller } = await import('../src/analytics/dashboard')
288+
289+
const poller = createRealtimePoller(
290+
{
291+
baseUrl: 'https://api.example.com',
292+
siteId: 'test-site',
293+
},
294+
() => {},
295+
5000,
296+
)
297+
298+
expect(poller.start).toBeDefined()
299+
expect(poller.stop).toBeDefined()
300+
expect(typeof poller.start).toBe('function')
301+
expect(typeof poller.stop).toBe('function')
302+
})
303+
})

0 commit comments

Comments
 (0)