398 lines
12 KiB
TypeScript
398 lines
12 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 the daily KPI hero when today data is 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('Today')
|
|
expect(text).toContain('6.5h')
|
|
expect(text).toContain('1.5 h planned later')
|
|
})
|
|
|
|
it('renders category lanes and the week mini chart by default in granular mode', () => {
|
|
const wrapper = mount(TimeSummaryCard, {
|
|
props: {
|
|
summary: baseSummary,
|
|
mode: 'active',
|
|
showHeader: true,
|
|
displayMode: 'category_and_calendar_goals',
|
|
categoryTodayItems: [
|
|
{ id: 'work', label: 'Work', todayHours: 4, color: '#2563EB' },
|
|
{ id: '__uncategorized__', label: 'Unassigned', todayHours: 1, color: '#64748b', isUnassigned: true },
|
|
],
|
|
weekDays: [
|
|
{ date: '2025-03-03', label: 'Mon', hours: 2 },
|
|
{ date: '2025-03-04', label: 'Tue', hours: 5, isToday: true },
|
|
],
|
|
},
|
|
})
|
|
|
|
const text = wrapper.text()
|
|
expect(text).toContain('Time Summary · Week 10')
|
|
expect(text).toContain('Daily')
|
|
expect(text).toContain('Calendars')
|
|
expect(text).toContain('Categories')
|
|
expect(text).toContain('Today by category')
|
|
expect(text).toContain('Work')
|
|
expect(text).toContain('4.0 h')
|
|
expect(text).not.toContain('avg/event')
|
|
expect(wrapper.find('.time-summary-week').exists()).toBe(true)
|
|
})
|
|
|
|
it('can use a concise overview title without repeating the selected range', () => {
|
|
const wrapper = mount(TimeSummaryCard, {
|
|
props: {
|
|
summary: baseSummary,
|
|
mode: 'active',
|
|
title: "Today's time summary",
|
|
showRangeInTitle: false,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain("Today's time summary")
|
|
expect(wrapper.text()).not.toContain("Today's time summary · Week 10")
|
|
})
|
|
|
|
it('fills the daily tab with average and median KPI cards in granular mode', async () => {
|
|
const wrapper = mount(TimeSummaryCard, {
|
|
props: {
|
|
summary: {
|
|
...baseSummary,
|
|
avgDay: 4.25,
|
|
medianDay: 3.5,
|
|
},
|
|
mode: 'active',
|
|
displayMode: 'category_and_calendar_goals',
|
|
categoryTodayItems: [
|
|
{ id: 'work', label: 'Work', todayHours: 4, color: '#2563EB' },
|
|
],
|
|
},
|
|
})
|
|
|
|
const dailyButton = wrapper.findAll('button').find((button) => button.text() === 'Daily')
|
|
expect(dailyButton).toBeTruthy()
|
|
await dailyButton!.trigger('click')
|
|
|
|
const text = wrapper.text().replace(/\s+/g, ' ')
|
|
expect(text).toContain('4.25 havg/day')
|
|
expect(text).toContain('3.50 hmedian/day')
|
|
expect(text).toContain('avg/event')
|
|
expect(text).toContain('latest end')
|
|
expect(text).not.toContain('Today by category')
|
|
})
|
|
|
|
it('shows average and median KPI cards in single goal mode without the activity note', () => {
|
|
const wrapper = mount(TimeSummaryCard, {
|
|
props: {
|
|
summary: {
|
|
...baseSummary,
|
|
avgDay: 4.25,
|
|
medianDay: 3.5,
|
|
delta: { totalHours: 1, avgPerDay: 0.75, avgPerEvent: 0, events: 0 },
|
|
},
|
|
activitySummary: {
|
|
events: 9,
|
|
activeDays: 3,
|
|
typicalStart: '08:00',
|
|
typicalEnd: '17:00',
|
|
weekendShare: 0,
|
|
eveningShare: 56.3,
|
|
delta: null,
|
|
earliestStart: '07:30',
|
|
latestEnd: '19:30',
|
|
overlapEvents: 0,
|
|
longestSession: 2,
|
|
lastDayOff: null,
|
|
lastHalfDayOff: null,
|
|
},
|
|
mode: 'active',
|
|
displayMode: 'single_goal',
|
|
},
|
|
})
|
|
|
|
const text = wrapper.text().replace(/\s+/g, ' ')
|
|
expect(text).toContain('4.25 havg/day+0.75h')
|
|
expect(text).toContain('3.50 hmedian/day')
|
|
expect(text).toContain('3active days')
|
|
expect(text).toContain('2.0 hlongest')
|
|
expect(text).not.toContain('busiest')
|
|
expect(text).not.toContain('evening')
|
|
})
|
|
|
|
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(text).not.toContain('Calendars')
|
|
expect(text).not.toContain('Categories')
|
|
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 lanes in calendar goals mode', () => {
|
|
const wrapper = mount(TimeSummaryCard, {
|
|
props: {
|
|
summary: {
|
|
...baseSummary,
|
|
topCategory: null,
|
|
},
|
|
mode: 'active',
|
|
displayMode: 'calendar_goals',
|
|
config: {
|
|
showCalendarSummary: true,
|
|
showTopCategory: false,
|
|
},
|
|
calendarTodayItems: [
|
|
{ id: 'cal-a', label: 'Cal A', todayHours: 2.5, color: '#ff0000' },
|
|
{ id: 'cal-b', label: 'Cal B', todayHours: 1, color: '#00ff00' },
|
|
],
|
|
},
|
|
})
|
|
|
|
const text = wrapper.text()
|
|
expect(text).toContain('Today by calendar')
|
|
expect(text).toContain('Cal A')
|
|
expect(text).toContain('2.5 h')
|
|
expect(wrapper.find('.time-summary-lane').attributes('style')).toContain('--lane-color')
|
|
expect(wrapper.text()).toContain('Calendars')
|
|
expect(wrapper.text()).not.toContain('Categories')
|
|
})
|
|
|
|
it('keeps category lanes in category and calendar goals mode', () => {
|
|
const wrapper = mount(TimeSummaryCard, {
|
|
props: {
|
|
summary: baseSummary,
|
|
mode: 'active',
|
|
displayMode: 'category_and_calendar_goals',
|
|
categoryTodayItems: [
|
|
{ id: 'work', label: 'Work', todayHours: 3, color: '#2563eb' },
|
|
],
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Today by category')
|
|
expect(wrapper.text()).toContain('Work')
|
|
expect(wrapper.text()).toContain('Categories')
|
|
})
|
|
|
|
it('honours new overview toggles', () => {
|
|
const wrapper = mount(TimeSummaryCard, {
|
|
props: {
|
|
summary: {
|
|
...baseSummary,
|
|
topCategory: null,
|
|
},
|
|
mode: 'all',
|
|
displayMode: 'calendar_goals',
|
|
config: {
|
|
showWeekendShare: false,
|
|
showTopCategory: false,
|
|
showBusiest: false,
|
|
},
|
|
showDailyKpis: false,
|
|
showWeekMiniChart: false,
|
|
showActivityNote: false,
|
|
},
|
|
})
|
|
|
|
const text = wrapper.text()
|
|
expect(text).toContain('Today')
|
|
expect(wrapper.find('.time-summary-kpis').exists()).toBe(false)
|
|
expect(wrapper.find('.time-summary-week').exists()).toBe(false)
|
|
expect(wrapper.find('.time-summary-activity-note').exists()).toBe(false)
|
|
})
|
|
|
|
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 legacy lookback toggles only when lookback is explicitly enabled', () => {
|
|
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,
|
|
showLookback: true,
|
|
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,
|
|
showLookback: true,
|
|
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.')
|
|
})
|
|
})
|