opsdash-app/opsdash/test/TimeSummaryCard.test.ts
2026-05-18 06:11:41 +07:00

303 lines
8.7 KiB
TypeScript

import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import TimeSummaryCard from '../src/components/widgets/cards/TimeSummaryCard.vue'
const baseSummary = {
rangeLabel: 'Week 10',
rangeStart: '2025-03-03',
rangeEnd: '2025-03-09',
offset: 0,
totalHours: 12.5,
futureHours: 2.5,
avgDay: 2.5,
avgEvent: 1.25,
medianDay: 2,
todayActualHours: 0,
todayPlannedHours: 0,
busiest: { date: '2025-03-04', hours: 5 },
workdayAvg: 3,
workdayMedian: 2.5,
weekendAvg: 1,
weekendMedian: 1.5,
weekendShare: 40,
activeCalendars: 3,
calendarSummary: 'Cal A 60%, Cal B 40%',
balanceIndex: 0.78,
delta: null,
topCategory: {
label: 'Work',
actualHours: 7.5,
targetHours: 10,
percent: 75,
statusLabel: 'At risk',
status: 'at_risk',
},
}
const baseHistoryEntry = {
offset: 1,
label: 'Week -1',
rangeStart: '2025-02-24',
rangeEnd: '2025-03-02',
totalHours: 9.5,
avgDay: 1.9,
avgEvent: 1.1,
medianDay: 1.7,
busiest: { date: '2025-02-26', hours: 3.5 },
workdayAvg: 2.1,
workdayMedian: 2.0,
weekendAvg: 1.2,
weekendMedian: 1.1,
weekendShare: 32,
activeCalendars: 2,
calendarSummary: 'Cal A 70%, Cal B 30%',
topCategory: null,
balanceIndex: 0.67,
activity: {
events: 8,
activeDays: 4,
typicalStart: '08:30',
typicalEnd: '16:30',
weekendShare: 32,
eveningShare: 21,
earliestStart: '07:45',
latestEnd: '19:20',
overlapEvents: 1,
longestSession: 2.8,
lastDayOff: '2025-02-23',
lastHalfDayOff: '2025-02-22',
},
}
describe('TimeSummaryCard', () => {
it('renders today badge when provided', () => {
const wrapper = mount(TimeSummaryCard, {
props: {
summary: {
...baseSummary,
todayActualHours: 6.5,
todayPlannedHours: 1.5,
},
mode: 'active',
},
})
const text = wrapper.text().replace(/\s+/g, ' ')
expect(text).toContain('Total today')
expect(text).toContain('6.50 h')
expect(text).toContain('Later today 1.50 h planned')
})
it('renders key metrics, top category badge, and weekend share', () => {
const wrapper = mount(TimeSummaryCard, {
props: {
summary: baseSummary,
mode: 'active',
showHeader: true,
displayMode: 'category_and_calendar_goals',
},
})
const text = wrapper.text()
expect(text).toContain('Time Summary · Week 10')
expect(text).toContain('12.50 h total')
expect(text).toContain('2.50 h planned later')
expect(text).toContain('2.50 h/day (active days)')
expect(text).toContain('Busiest 2025-03-04 — 5.00 h')
expect(text).toContain('Weekend 1.00 h avg · 1.50 h median (40.0%)')
const calendarRow = wrapper.find('.time-summary-row.calendars').text().replace(/\s+/g, ' ')
expect(calendarRow).toContain('3 calendars')
expect(calendarRow).toContain('Cal A 60%, Cal B 40%')
const badge = wrapper.find('.summary-badge')
expect(badge.exists()).toBe(true)
expect(badge.text()).toBe('At risk')
expect(badge.classes()).toContain('status-risk')
})
it('hides calendar and category-specific summary rows in single goal mode', () => {
const wrapper = mount(TimeSummaryCard, {
props: {
summary: baseSummary,
mode: 'active',
displayMode: 'single_goal',
config: {
showCalendarSummary: false,
showTopCategory: false,
},
},
})
const text = wrapper.text()
expect(text).not.toContain('3 calendars')
expect(text).not.toContain('Top category')
expect(wrapper.find('.time-summary-row.calendars').exists()).toBe(false)
expect(wrapper.find('.time-summary-row.top-category').exists()).toBe(false)
})
it('uses calendar-specific labels in calendar goals mode', () => {
const wrapper = mount(TimeSummaryCard, {
props: {
summary: {
...baseSummary,
topCategory: null,
},
mode: 'active',
displayMode: 'calendar_goals',
config: {
showCalendarSummary: true,
showTopCategory: false,
},
history: [baseHistoryEntry],
},
})
const calendarRow = wrapper.find('.time-summary-row.calendars').text().replace(/\s+/g, ' ')
expect(calendarRow).toContain('3 calendars')
expect(calendarRow).toContain('Cal A 60%, Cal B 40%')
expect(wrapper.text()).toContain('Calendars')
})
it('keeps category labels in category and calendar goals mode', () => {
const wrapper = mount(TimeSummaryCard, {
props: {
summary: baseSummary,
mode: 'active',
displayMode: 'category_and_calendar_goals',
history: [baseHistoryEntry],
},
})
expect(wrapper.find('.time-summary-row.top-category .label').text()).toBe('Top category')
expect(wrapper.text()).toContain('Categories & calendars')
})
it('honours config toggles to hide optional rows', () => {
const wrapper = mount(TimeSummaryCard, {
props: {
summary: {
...baseSummary,
topCategory: null,
},
mode: 'all',
displayMode: 'calendar_goals',
config: {
showWeekendShare: false,
showTopCategory: false,
showBusiest: false,
},
},
})
const text = wrapper.text()
expect(text).toContain('h/day (all days)')
const weekendRow = wrapper.findAll('.time-summary-row').find((row) => row.text().includes('Weekend'))
expect(weekendRow?.text().replace(/\s+/g, ' ')).toBe('Weekend 1.00 h avg · 1.50 h median')
expect(wrapper.find('.summary-badge').exists()).toBe(false)
expect(text).not.toContain('Busiest')
})
it('hides the header when showHeader is false', () => {
const wrapper = mount(TimeSummaryCard, {
props: {
summary: baseSummary,
mode: 'active',
showHeader: false,
displayMode: 'category_and_calendar_goals',
},
})
expect(wrapper.find('.time-summary-firstline').exists()).toBe(false)
})
it('supports new display toggles for today/activity/history core metrics', () => {
const wrapper = mount(TimeSummaryCard, {
props: {
summary: {
...baseSummary,
todayHours: 4,
delta: {
totalHours: 1.2,
avgPerDay: 0.2,
avgPerEvent: 0.1,
events: 1,
},
},
activitySummary: {
events: 10,
activeDays: 5,
typicalStart: '08:00',
typicalEnd: '17:00',
weekendShare: 40,
eveningShare: 25,
delta: null,
earliestStart: '07:30',
latestEnd: '19:00',
overlapEvents: 2,
longestSession: 3.2,
lastDayOff: '2025-03-01',
lastHalfDayOff: '2025-02-28',
},
mode: 'active',
displayMode: 'category_and_calendar_goals',
showToday: false,
showActivity: false,
showHistoryCoreMetrics: false,
history: [baseHistoryEntry],
},
})
const text = wrapper.text().replace(/\s+/g, ' ')
expect(text).not.toContain('Total today')
expect(text).not.toContain('Activity & Schedule')
expect(text).not.toContain('Δ vs. offset')
expect(text).not.toContain('Events')
expect(text).not.toContain('Active days')
expect(text).not.toContain('Typical')
expect(text).toContain('Lookback')
})
it('uses accordion by default and maps legacy list to timeline layout', () => {
const baseProps = {
summary: baseSummary,
mode: 'active' as const,
displayMode: 'category_and_calendar_goals' as const,
history: [baseHistoryEntry],
}
const accordion = mount(TimeSummaryCard, { props: baseProps })
expect(accordion.find('.time-summary-history__accordion').exists()).toBe(true)
expect(accordion.find('.time-summary-history__timeline').exists()).toBe(false)
const timeline = mount(TimeSummaryCard, {
props: {
...baseProps,
historyView: 'list',
},
})
expect(timeline.find('.time-summary-history__timeline').exists()).toBe(true)
expect(timeline.find('.time-summary-history__accordion').exists()).toBe(false)
})
it('shows a clear hint when lookback is enabled but configured to only one period', () => {
const wrapper = mount(TimeSummaryCard, {
props: {
summary: baseSummary,
mode: 'active',
displayMode: 'category_and_calendar_goals',
showOverview: false,
showLookback: true,
lookbackWeeks: 1,
rangeMode: 'week',
history: [],
},
})
const text = wrapper.text().replace(/\s+/g, ' ')
expect(text).toContain('Lookback data required')
expect(text).toContain('Increase trend lookback above 1 to compare previous weeks here.')
})
})