opsdash-app/opsdash/test/chartWidgets.test.ts
2026-04-02 20:56:10 +07:00

214 lines
8.6 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { widgetsRegistry } from '../src/services/widgetsRegistry'
import { formatDateKey, getWeekdayOrder } from '../src/services/dateTime'
describe('chart widgets', () => {
it('filters pie chart by calendar selection', () => {
const entry = widgetsRegistry.chart_pie
const def: any = { options: { filterMode: 'calendar', filterIds: ['cal-1'] } }
const ctx: any = {
calendarChartData: {
pie: { ids: ['cal-1', 'cal-2'], labels: ['One', 'Two'], data: [10, 5], colors: ['#111111', '#222222'] },
},
colorsById: { 'cal-1': '#111111', 'cal-2': '#222222' },
colorsByName: {},
calendarGroups: [],
categoryColorMap: {},
}
const props = entry.buildProps(def, ctx) as any
expect(props.chartData.ids).toEqual(['cal-1'])
expect(props.chartData.data).toEqual([10])
})
it('filters calendar table rows', () => {
const entry = widgetsRegistry.calendar_table
const def: any = { options: { calendarFilter: ['cal-2'] } }
const ctx: any = {
byCal: [{ id: 'cal-1', total_hours: 2 }, { id: 'cal-2', total_hours: 4 }],
currentTargets: {},
calendarGroups: [],
calendarTodayHours: {},
}
const props = entry.buildProps(def, ctx) as any
expect(props.rows).toHaveLength(1)
expect(props.rows[0].id).toBe('cal-2')
})
it('keeps calendar table ungrouped for single goal', () => {
const entry = widgetsRegistry.calendar_table
const props = entry.buildProps({ options: {} } as any, {
byCal: [{ id: 'cal-1', total_hours: 2 }],
currentTargets: {},
onboardingStrategy: 'total_only',
targetsConfig: { totalHours: 40, categories: [] },
calendarGroups: [{ id: '__uncategorized__', label: 'Unassigned', rows: [{ id: 'cal-1', total_hours: 2 }] }],
calendarTodayHours: {},
} as any) as any
expect(props.mode).toBe('single_goal')
expect(props.totalTarget).toBe(40)
expect(props.groups).toEqual([])
})
it('keeps calendar table ungrouped for calendar goals', () => {
const entry = widgetsRegistry.calendar_table
const props = entry.buildProps({ options: {} } as any, {
byCal: [{ id: 'cal-1', total_hours: 2 }],
currentTargets: { 'cal-1': 8 },
onboardingStrategy: 'total_plus_categories',
targetsConfig: { totalHours: 8, categories: [] },
calendarGroups: [{ id: '__uncategorized__', label: 'Unassigned', rows: [{ id: 'cal-1', total_hours: 2 }] }],
calendarTodayHours: {},
} as any) as any
expect(props.mode).toBe('calendar_goals')
expect(props.groups).toEqual([])
expect(props.targets).toEqual({ 'cal-1': 8 })
})
it('keeps category grouping for category and calendar goals', () => {
const entry = widgetsRegistry.calendar_table
const groups = [{ id: 'deep-work', label: 'Deep Work', rows: [{ id: 'cal-1', total_hours: 2 }] }]
const props = entry.buildProps({ options: {} } as any, {
byCal: [{ id: 'cal-1', total_hours: 2 }],
currentTargets: { 'cal-1': 8 },
onboardingStrategy: 'full_granular',
targetsConfig: { totalHours: 12, categories: [{ id: 'deep-work', label: 'Deep Work', targetHours: 12 }] },
calendarGroups: groups,
calendarTodayHours: {},
} as any) as any
expect(props.mode).toBe('category_and_calendar_goals')
expect(props.groups).toEqual(groups)
})
it('prefers onboarding strategy over stale categories in single goal mode', () => {
const entry = widgetsRegistry.calendar_table
const props = entry.buildProps({ options: {} } as any, {
byCal: [{ id: 'cal-1', total_hours: 2 }],
currentTargets: {},
onboardingStrategy: 'total_only',
targetsConfig: {
totalHours: 40,
categories: [{ id: 'old-cat', label: 'Old Category', targetHours: 12 }],
},
calendarGroups: [{ id: 'old-cat', label: 'Old Category', rows: [{ id: 'cal-1', total_hours: 2 }] }],
calendarTodayHours: {},
} as any) as any
expect(props.mode).toBe('single_goal')
expect(props.groups).toEqual([])
})
it('applies projection mode per chart widget', () => {
const entry = widgetsRegistry.chart_stacked
const today = new Date()
const tomorrow = new Date(today)
tomorrow.setDate(today.getDate() + 1)
const def: any = {
options: {
filterMode: 'calendar',
filterIds: ['cal-1'],
forecastMode: 'total',
},
}
const ctx: any = {
charts: {
perDaySeries: {
labels: [formatDateKey(today, 'UTC'), formatDateKey(tomorrow, 'UTC')],
series: [{ id: 'cal-1', name: 'Cal 1', data: [1, 0] }],
},
},
targetsConfig: { totalHours: 3, categories: [] },
currentTargets: { 'cal-1': 3 },
calendarCategoryMap: {},
categoryColorMap: {},
colorsById: { 'cal-1': '#111111' },
}
const props = entry.buildProps(def, ctx) as any
expect(props.stacked.series[0].forecast?.[1]).toBe(2)
})
it('uses oldest-first ordering for daily lookback by default', () => {
const entry = widgetsRegistry.chart_per_day
const ctx: any = {
lookbackWeeks: 2,
rangeMode: 'week',
charts: {
perDaySeriesByOffset: [
{ offset: 0, labels: ['2025-10-20'], series: [{ id: 'cal-1', name: 'Cal 1', data: [5] }] },
{ offset: 1, labels: ['2025-10-20'], series: [{ id: 'cal-1', name: 'Cal 1', data: [3] }] },
],
},
colorsById: { 'cal-1': '#111111' },
calendarCategoryMap: {},
categoryColorMap: {},
}
const defaultProps = entry.buildProps({ options: { filterMode: 'calendar', filterIds: ['cal-1'], forecastMode: 'off' } } as any, ctx) as any
expect(defaultProps.legendItems[0].label).toContain('Week -1')
expect(defaultProps.legendItems[1].label).toContain('Current week')
const reversedProps = entry.buildProps({ options: { filterMode: 'calendar', filterIds: ['cal-1'], forecastMode: 'off', reverseOrder: true } } as any, ctx) as any
expect(reversedProps.legendItems[0].label).toContain('Current week')
expect(reversedProps.legendItems[1].label).toContain('Week -1')
})
it('uses oldest-first ordering for day-of-week lookback by default', () => {
const entry = widgetsRegistry.chart_dow
const ctx: any = {
lookbackWeeks: 2,
charts: {
perDaySeries: {
labels: ['2025-10-21'],
series: [{ id: 'cal-1', name: 'Cal 1', data: [5] }],
},
perDaySeriesByOffset: [
{ offset: 0, labels: ['2025-10-20'], series: [{ id: 'cal-1', name: 'Cal 1', data: [5] }] },
{ offset: 1, labels: ['2025-10-20'], series: [{ id: 'cal-1', name: 'Cal 1', data: [3] }] },
],
},
colorsById: { 'cal-1': '#111111' },
calendarCategoryMap: {},
categoryColorMap: {},
}
const props = entry.buildProps({ options: { filterMode: 'calendar', filterIds: ['cal-1'], forecastMode: 'off' } } as any, ctx) as any
const order = getWeekdayOrder()
const monIdx = order.indexOf('Mon')
expect(monIdx).toBeGreaterThanOrEqual(0)
expect(props.groupedData.series[0].data[monIdx]).toBe(3)
expect(props.groupedData.series[1].data[monIdx]).toBe(5)
const reversedProps = entry.buildProps({ options: { filterMode: 'calendar', filterIds: ['cal-1'], forecastMode: 'off', reverseOrder: true } } as any, ctx) as any
expect(reversedProps.groupedData.series[0].data[monIdx]).toBe(5)
expect(reversedProps.groupedData.series[1].data[monIdx]).toBe(3)
})
it('uses oldest-first ordering for heatmap lookback by default', () => {
const entry = widgetsRegistry.chart_hod
const ctx: any = {
lookbackWeeks: 2,
rangeMode: 'week',
charts: {
hod: { dows: ['Mon'], hours: [0], matrix: [[1]] },
hodLookback: { dows: ['Mon'], hours: [0], matrix: [[4]] },
hodByOffset: [
{ offset: 0, dows: ['Mon'], hours: [0], matrix: [[4]] },
{ offset: 1, dows: ['Mon'], hours: [0], matrix: [[2]] },
],
},
}
const props = entry.buildProps({ options: {} } as any, ctx) as any
expect(props.hodData).toEqual(ctx.charts.hodLookback)
expect(props.lookbackEntries).toHaveLength(2)
expect(props.lookbackEntries[0].id).toBe('offset-1')
expect(props.lookbackEntries[1].id).toBe('offset-0')
expect(props.lookbackEntries[0].color).toBe('#9aa6b2')
expect(props.lookbackEntries[1].color).toBe('#8895a3')
const reversedProps = entry.buildProps({ options: { reverseOrder: true } } as any, ctx) as any
expect(reversedProps.lookbackEntries[0].id).toBe('offset-0')
expect(reversedProps.lookbackEntries[1].id).toBe('offset-1')
expect(reversedProps.lookbackEntries[0].color).toBe('#9aa6b2')
expect(reversedProps.lookbackEntries[1].color).toBe('#8895a3')
})
})