From 97d59ffcb498ea9f6fbfacab3cd4f0ad12792cbe Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Wed, 21 Jan 2026 10:08:26 +0100 Subject: [PATCH] tests: fix --- .../create-schedule-days.spec.ts | 569 ++++++++++++++++++ .../schedule-month.component.spec.ts | 416 +++++++++++++ .../schedule/schedule.service.spec.ts | 4 +- .../schedule/schedule.component.spec.ts | 4 +- .../archive-operation-handler.service.spec.ts | 21 +- 5 files changed, 1009 insertions(+), 5 deletions(-) create mode 100644 src/app/features/schedule/map-schedule-data/create-schedule-days.spec.ts create mode 100644 src/app/features/schedule/schedule-month/schedule-month.component.spec.ts diff --git a/src/app/features/schedule/map-schedule-data/create-schedule-days.spec.ts b/src/app/features/schedule/map-schedule-data/create-schedule-days.spec.ts new file mode 100644 index 000000000..9e72eb089 --- /dev/null +++ b/src/app/features/schedule/map-schedule-data/create-schedule-days.spec.ts @@ -0,0 +1,569 @@ +import { createScheduleDays } from './create-schedule-days'; +import { DEFAULT_TASK, TaskWithoutReminder } from '../../tasks/task.model'; +import { PlannerDayMap } from '../../planner/planner.model'; +import { BlockedBlockByDayMap } from '../schedule.model'; + +// Helper function to create test tasks with required properties +const createTestTask = ( + id: string, + title: string, + options: { + timeEstimate?: number; + timeSpent?: number; + dueDay?: string; + dueWithTime?: number; + } = {}, +): TaskWithoutReminder => { + return { + ...DEFAULT_TASK, + id, + title, + projectId: 'test-project', + timeEstimate: options.timeEstimate ?? 3600000, + timeSpent: options.timeSpent ?? 0, + remindAt: undefined, + ...(options.dueDay && { dueDay: options.dueDay }), + ...(options.dueWithTime && { dueWithTime: options.dueWithTime }), + } as TaskWithoutReminder; +}; + +describe('createScheduleDays - Task Filtering', () => { + let now: number; + let realNow: number; + let todayStr: string; + let tomorrowStr: string; + let nextWeekStr: string; + let futureWeekStr: string; + + beforeEach(() => { + // Set up test dates + const today = new Date(2026, 0, 20, 10, 0, 0); // Jan 20, 2026, 10:00 AM + now = today.getTime(); + realNow = now; + + todayStr = today.toISOString().split('T')[0]; + + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrowStr = tomorrow.toISOString().split('T')[0]; + + const nextWeek = new Date(today); + nextWeek.setDate(nextWeek.getDate() + 7); + nextWeekStr = nextWeek.toISOString().split('T')[0]; + + const futureWeek = new Date(today); + futureWeek.setDate(futureWeek.getDate() + 14); + futureWeekStr = futureWeek.toISOString().split('T')[0]; + }); + + describe('Unscheduled tasks (no dueDay, no dueWithTime, no plannedForDay)', () => { + it('should appear in current week when viewing today', () => { + // Arrange + const unscheduledTask = createTestTask('task1', 'Unscheduled Task'); + + const dayDates = [todayStr, tomorrowStr]; + const plannerDayMap: PlannerDayMap = {}; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [unscheduledTask], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + // Unscheduled task should appear in the schedule + const hasTask = result.some((day) => + day.entries.some((entry) => entry.id === 'task1'), + ); + expect(hasTask).toBe(true); + }); + + it('should NOT appear when viewing next week (outside current week)', () => { + // Arrange + const unscheduledTask: TaskWithoutReminder = { + id: 'task1', + title: 'Unscheduled Task', + timeEstimate: 3600000, + timeSpent: 0, + } as TaskWithoutReminder; + + // Viewing a week starting 7 days from now + const dayDates = [nextWeekStr]; + const plannerDayMap: PlannerDayMap = {}; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [unscheduledTask], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + // Unscheduled task should NOT appear when viewing future week + const hasTask = result.some((day) => + day.entries.some((entry) => entry.id === 'task1'), + ); + expect(hasTask).toBe(false); + }); + }); + + describe('Tasks with dueDay', () => { + it('should be filtered out when viewing next week if dueDay is today', () => { + // Arrange + const taskWithDueToday = createTestTask('task2', 'Task Due Today', { + dueDay: todayStr, + }); + + // Viewing next week + const dayDates = [nextWeekStr]; + const plannerDayMap: PlannerDayMap = {}; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [taskWithDueToday], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasTask = result.some((day) => + day.entries.some((entry) => entry.id === 'task2'), + ); + expect(hasTask).toBe(false); + }); + + it('should appear when viewing a week that includes the dueDay', () => { + // Arrange + const taskDueNextWeek = createTestTask('task3', 'Task Due Next Week', { + dueDay: nextWeekStr, + }); + + // Viewing next week + const dayDates = [nextWeekStr]; + const plannerDayMap: PlannerDayMap = {}; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [taskDueNextWeek], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasTask = result.some((day) => + day.entries.some((entry) => entry.id === 'task3'), + ); + expect(hasTask).toBe(true); + }); + + it('should appear when dueDay is in the future relative to viewed week', () => { + // Arrange + const taskDueFutureWeek = createTestTask('task4', 'Task Due Future Week', { + dueDay: futureWeekStr, + }); + + // Viewing next week (before the due date) + const dayDates = [nextWeekStr]; + const plannerDayMap: PlannerDayMap = {}; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [taskDueFutureWeek], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasTask = result.some((day) => + day.entries.some((entry) => entry.id === 'task4'), + ); + expect(hasTask).toBe(true); + }); + + it('should NOT appear when dueDay is before the viewed week', () => { + // Arrange + const taskDueToday = createTestTask('task5', 'Task Due Today', { + dueDay: todayStr, + }); + + // Viewing future week (2 weeks ahead) + const dayDates = [futureWeekStr]; + const plannerDayMap: PlannerDayMap = {}; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [taskDueToday], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasTask = result.some((day) => + day.entries.some((entry) => entry.id === 'task5'), + ); + expect(hasTask).toBe(false); + }); + }); + + describe('Tasks with plannedForDay', () => { + it('should always appear on their planned day regardless of viewing week', () => { + // Arrange + const taskPlannedForNextWeek = createTestTask('task6', 'Task Planned Next Week'); + + // Planned for next week + const plannerDayMap: PlannerDayMap = { + [nextWeekStr]: [taskPlannedForNextWeek], + }; + const dayDates = [nextWeekStr]; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasTask = result.some( + (day) => + day.dayDate === nextWeekStr && + day.entries.some((entry) => entry.id === 'task6'), + ); + expect(hasTask).toBe(true); + }); + + it('should appear on planned day even when viewing outside current week', () => { + // Arrange + const taskPlannedForFutureWeek = createTestTask( + 'task7', + 'Task Planned Future Week', + ); + + // Planned for future week + const plannerDayMap: PlannerDayMap = { + [futureWeekStr]: [taskPlannedForFutureWeek], + }; + const dayDates = [futureWeekStr]; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasTask = result.some( + (day) => + day.dayDate === futureWeekStr && + day.entries.some((entry) => entry.id === 'task7'), + ); + expect(hasTask).toBe(true); + }); + }); + + describe('Initial filter when first day is outside current week', () => { + it('should filter out unscheduled tasks before processing when first day is outside current week', () => { + // Arrange + const unscheduledTask = createTestTask('task8', 'Unscheduled Task'); + + const taskWithDueInFuture = createTestTask('task9', 'Task Due Future', { + dueDay: futureWeekStr, + }); + + // Viewing future week (outside current week) + const dayDates = [futureWeekStr]; + const plannerDayMap: PlannerDayMap = {}; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [unscheduledTask, taskWithDueInFuture], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasUnscheduledTask = result.some((day) => + day.entries.some((entry) => entry.id === 'task8'), + ); + const hasTaskWithDue = result.some((day) => + day.entries.some((entry) => entry.id === 'task9'), + ); + expect(hasUnscheduledTask).toBe(false); + expect(hasTaskWithDue).toBe(true); + }); + + it('should keep tasks with plannedForDay even when first day is outside current week', () => { + // Arrange + const taskPlannedForFuture = createTestTask('task10', 'Task Planned for Future'); + + const plannerDayMap: PlannerDayMap = { + [futureWeekStr]: [taskPlannedForFuture], + }; + const dayDates = [futureWeekStr]; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasTask = result.some((day) => + day.entries.some((entry) => entry.id === 'task10'), + ); + expect(hasTask).toBe(true); + }); + }); + + describe('Per-day filter for tasks flowing from previous day', () => { + it('should filter tasks between days when viewing outside current week', () => { + // Arrange + const taskDueOnFirstDay = createTestTask('task11', 'Task Due on First Day', { + dueDay: nextWeekStr, + }); + + // Viewing two days in next week + const secondDayNextWeek = new Date(nextWeekStr); + secondDayNextWeek.setDate(secondDayNextWeek.getDate() + 1); + const secondDayStr = secondDayNextWeek.toISOString().split('T')[0]; + + const dayDates = [nextWeekStr, secondDayStr]; + const plannerDayMap: PlannerDayMap = {}; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [taskDueOnFirstDay], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + // Task should appear on first day + const firstDayHasTask = result[0].entries.some((entry) => entry.id === 'task11'); + expect(firstDayHasTask).toBe(true); + + // If task doesn't complete and flows to second day, check if filtering applies + // (This depends on implementation details of budget and beyond budget logic) + }); + }); + + describe('End-of-day filter for tasks flowing to next day', () => { + it('should allow tasks with plannedForDay to flow to next day', () => { + // Arrange + const taskPlannedForToday = createTestTask('task12', 'Task Planned for Today', { + timeEstimate: 86400000, // 24 hours - won't fit in one day + }); + + const plannerDayMap: PlannerDayMap = { + [todayStr]: [taskPlannedForToday], + }; + const dayDates = [todayStr, tomorrowStr]; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + // Task should appear on today + const todayHasTask = result[0].entries.some((entry) => entry.id === 'task12'); + expect(todayHasTask).toBe(true); + }); + }); + + describe('Tasks with dueWithTime', () => { + it('should appear when viewing a week that includes the dueWithTime', () => { + // Arrange + const nextWeekDate = new Date(nextWeekStr); + nextWeekDate.setHours(14, 0, 0, 0); // 2 PM next week + const taskWithDueTime = createTestTask('task13', 'Task With Due Time', { + dueWithTime: nextWeekDate.getTime(), + }); + + const dayDates = [nextWeekStr]; + const plannerDayMap: PlannerDayMap = {}; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [taskWithDueTime], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasTask = result.some((day) => + day.entries.some((entry) => entry.id === 'task13'), + ); + expect(hasTask).toBe(true); + }); + + it('should NOT appear when dueWithTime is before the viewed week', () => { + // Arrange + const todayDate = new Date(todayStr); + todayDate.setHours(14, 0, 0, 0); + const taskWithDueTime = createTestTask('task14', 'Task With Due Time Today', { + dueWithTime: todayDate.getTime(), + }); + + // Viewing future week + const dayDates = [futureWeekStr]; + const plannerDayMap: PlannerDayMap = {}; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [taskWithDueTime], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasTask = result.some((day) => + day.entries.some((entry) => entry.id === 'task14'), + ); + expect(hasTask).toBe(false); + }); + }); + + describe('Mixed scenarios', () => { + it('should correctly filter multiple tasks with different scheduling types when viewing future week', () => { + // Arrange + const unscheduledTask = createTestTask('unscheduled', 'Unscheduled'); + + const taskDueToday = createTestTask('dueToday', 'Due Today', { + dueDay: todayStr, + }); + + const taskDueNextWeek = createTestTask('dueNextWeek', 'Due Next Week', { + dueDay: nextWeekStr, + }); + + const taskPlannedNextWeek = createTestTask('plannedNextWeek', 'Planned Next Week'); + + const plannerDayMap: PlannerDayMap = { + [nextWeekStr]: [taskPlannedNextWeek], + }; + const dayDates = [nextWeekStr]; + const blockerBlocksDayMap: BlockedBlockByDayMap = {}; + + // Act + const result = createScheduleDays( + [unscheduledTask, taskDueToday, taskDueNextWeek], + [], + dayDates, + plannerDayMap, + blockerBlocksDayMap, + undefined, + now, + realNow, + ); + + // Assert + const hasUnscheduled = result.some((day) => + day.entries.some((entry) => entry.id === 'unscheduled'), + ); + const hasDueToday = result.some((day) => + day.entries.some((entry) => entry.id === 'dueToday'), + ); + const hasDueNextWeek = result.some((day) => + day.entries.some((entry) => entry.id === 'dueNextWeek'), + ); + const hasPlannedNextWeek = result.some((day) => + day.entries.some((entry) => entry.id === 'plannedNextWeek'), + ); + + expect(hasUnscheduled).toBe(false); + expect(hasDueToday).toBe(false); + expect(hasDueNextWeek).toBe(true); + expect(hasPlannedNextWeek).toBe(true); + }); + }); +}); diff --git a/src/app/features/schedule/schedule-month/schedule-month.component.spec.ts b/src/app/features/schedule/schedule-month/schedule-month.component.spec.ts new file mode 100644 index 000000000..ca14c4793 --- /dev/null +++ b/src/app/features/schedule/schedule-month/schedule-month.component.spec.ts @@ -0,0 +1,416 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ScheduleMonthComponent } from './schedule-month.component'; +import { ScheduleService } from '../schedule.service'; +import { DateTimeFormatService } from '../../../core/date-time-format/date-time-format.service'; + +describe('ScheduleMonthComponent', () => { + let component: ScheduleMonthComponent; + let fixture: ComponentFixture; + let mockScheduleService: jasmine.SpyObj; + let mockDateTimeFormatService: jasmine.SpyObj; + + beforeEach(async () => { + mockScheduleService = jasmine.createSpyObj('ScheduleService', [ + 'getDayClass', + 'hasEventsForDay', + 'getEventsForDay', + 'getEventDayStr', + ]); + mockScheduleService.getDayClass.and.returnValue(''); + mockScheduleService.hasEventsForDay.and.returnValue(false); + mockScheduleService.getEventsForDay.and.returnValue([]); + mockScheduleService.getEventDayStr.and.returnValue(null); + + mockDateTimeFormatService = jasmine.createSpyObj('DateTimeFormatService', ['-'], { + currentLocale: 'en-US', + }); + + await TestBed.configureTestingModule({ + imports: [ScheduleMonthComponent], + providers: [ + { provide: ScheduleService, useValue: mockScheduleService }, + { provide: DateTimeFormatService, useValue: mockDateTimeFormatService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ScheduleMonthComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('referenceMonth computed', () => { + it('should return current date when daysToShow is empty', () => { + // Arrange + fixture.componentRef.setInput('daysToShow', []); + fixture.detectChanges(); + + // Act + const result = component.referenceMonth(); + + // Assert + expect(result).toBeInstanceOf(Date); + // Should be close to current date + const now = new Date(); + expect(Math.abs(result.getTime() - now.getTime())).toBeLessThan(1000); + }); + + it('should use middle day from daysToShow as reference', () => { + // Arrange - Create a month view for January 2026 + const days = [ + '2025-12-29', // Week 1 - padding from prev month + '2025-12-30', + '2025-12-31', + '2026-01-01', + '2026-01-02', + '2026-01-03', + '2026-01-04', + '2026-01-05', // Week 2 + '2026-01-06', + '2026-01-07', + '2026-01-08', + '2026-01-09', + '2026-01-10', + '2026-01-11', + '2026-01-12', // Week 3 - Middle of month + '2026-01-13', + '2026-01-14', // Day 14 - near middle + '2026-01-15', // Middle index (14/2 = 7, but floor(28/2) = 14) + '2026-01-16', + '2026-01-17', + '2026-01-18', + '2026-01-19', // Week 4 + '2026-01-20', + '2026-01-21', + '2026-01-22', + '2026-01-23', + '2026-01-24', + '2026-01-25', + ]; + fixture.componentRef.setInput('daysToShow', days); + fixture.detectChanges(); + + // Act + const result = component.referenceMonth(); + + // Assert + // Middle index = floor(28/2) = 14, which is '2026-01-12' + expect(result.getFullYear()).toBe(2026); + expect(result.getMonth()).toBe(0); // January + expect(result.getDate()).toBe(12); + }); + + it('should handle a 5-week month view', () => { + // Arrange - 35 days (5 weeks) + const days: string[] = []; + const startDate = new Date(2026, 0, 1); // Jan 1, 2026 + for (let i = 0; i < 35; i++) { + const date = new Date(startDate); + date.setDate(date.getDate() + i); + days.push(date.toISOString().split('T')[0]); + } + fixture.componentRef.setInput('daysToShow', days); + fixture.detectChanges(); + + // Act + const result = component.referenceMonth(); + + // Assert + // Middle index = floor(35/2) = 17 + const middleDay = new Date(days[17]); + expect(result.getFullYear()).toBe(middleDay.getFullYear()); + expect(result.getMonth()).toBe(middleDay.getMonth()); + expect(result.getDate()).toBe(middleDay.getDate()); + }); + + it('should handle a 6-week month view', () => { + // Arrange - 42 days (6 weeks) + const days: string[] = []; + const startDate = new Date(2026, 0, 1); + for (let i = 0; i < 42; i++) { + const date = new Date(startDate); + date.setDate(date.getDate() + i); + days.push(date.toISOString().split('T')[0]); + } + fixture.componentRef.setInput('daysToShow', days); + fixture.detectChanges(); + + // Act + const result = component.referenceMonth(); + + // Assert + // Middle index = floor(42/2) = 21 + const middleDay = new Date(days[21]); + expect(result.getFullYear()).toBe(middleDay.getFullYear()); + expect(result.getMonth()).toBe(middleDay.getMonth()); + }); + }); + + describe('getDayClass', () => { + it('should pass referenceMonth to service.getDayClass', () => { + // Arrange + const days = ['2026-01-15', '2026-01-16', '2026-01-17']; + fixture.componentRef.setInput('daysToShow', days); + fixture.detectChanges(); + mockScheduleService.getDayClass.calls.reset(); + + const testDay = '2026-01-15'; + + // Act + component.getDayClass(testDay); + + // Assert + expect(mockScheduleService.getDayClass).toHaveBeenCalled(); + const args = mockScheduleService.getDayClass.calls.mostRecent().args; + expect(args[0]).toBe(testDay); + expect(args[1]).toBeInstanceOf(Date); + // Reference month should be the middle day + const referenceMonth = args[1] as Date; + expect(referenceMonth.getFullYear()).toBe(2026); + expect(referenceMonth.getMonth()).toBe(0); // January + }); + + it('should return the class string from service', () => { + // Arrange + mockScheduleService.getDayClass.and.returnValue('test-class'); + const days = ['2026-01-15']; + fixture.componentRef.setInput('daysToShow', days); + fixture.detectChanges(); + + // Act + const result = component.getDayClass('2026-01-15'); + + // Assert + expect(result).toBe('test-class'); + }); + + it('should handle "other-month" class for padding days', () => { + // Arrange + mockScheduleService.getDayClass.and.callFake((day: string, ref?: Date) => { + const dayDate = new Date(day); + if (ref && dayDate.getMonth() !== ref.getMonth()) { + return 'other-month'; + } + return ''; + }); + + const days = [ + '2025-12-31', // Previous month + '2026-01-01', // Current month + '2026-02-01', // Next month + ]; + fixture.componentRef.setInput('daysToShow', days); + fixture.detectChanges(); + + // Act + const prevMonthClass = component.getDayClass('2025-12-31'); + const currentMonthClass = component.getDayClass('2026-01-01'); + const nextMonthClass = component.getDayClass('2026-02-01'); + + // Assert + expect(prevMonthClass).toBe('other-month'); + expect(currentMonthClass).toBe(''); + expect(nextMonthClass).toBe('other-month'); + }); + }); + + describe('getWeekIndex', () => { + it('should return 0 for first week (days 0-6)', () => { + expect(component.getWeekIndex(0)).toBe(0); + expect(component.getWeekIndex(3)).toBe(0); + expect(component.getWeekIndex(6)).toBe(0); + }); + + it('should return 1 for second week (days 7-13)', () => { + expect(component.getWeekIndex(7)).toBe(1); + expect(component.getWeekIndex(10)).toBe(1); + expect(component.getWeekIndex(13)).toBe(1); + }); + + it('should return correct week index for later weeks', () => { + expect(component.getWeekIndex(14)).toBe(2); + expect(component.getWeekIndex(21)).toBe(3); + expect(component.getWeekIndex(28)).toBe(4); + expect(component.getWeekIndex(35)).toBe(5); + }); + }); + + describe('getDayIndex', () => { + it('should return 0-6 for days within a week', () => { + expect(component.getDayIndex(0)).toBe(0); + expect(component.getDayIndex(1)).toBe(1); + expect(component.getDayIndex(6)).toBe(6); + expect(component.getDayIndex(7)).toBe(0); + expect(component.getDayIndex(13)).toBe(6); + expect(component.getDayIndex(14)).toBe(0); + }); + }); + + describe('weekdayHeaders computed', () => { + it('should generate 7 weekday headers', () => { + // Arrange + fixture.componentRef.setInput('firstDayOfWeek', 0); // Sunday + fixture.detectChanges(); + + // Act + const headers = component.weekdayHeaders(); + + // Assert + expect(headers.length).toBe(7); + }); + + it('should start with Sunday when firstDayOfWeek is 0', () => { + // Arrange + fixture.componentRef.setInput('firstDayOfWeek', 0); + fixture.detectChanges(); + + // Act + const headers = component.weekdayHeaders(); + + // Assert + // Sunday should be first + expect(headers[0]).toContain('Sun'); + }); + + it('should start with Monday when firstDayOfWeek is 1', () => { + // Arrange + fixture.componentRef.setInput('firstDayOfWeek', 1); + fixture.detectChanges(); + + // Act + const headers = component.weekdayHeaders(); + + // Assert + // Monday should be first + expect(headers[0]).toContain('Mon'); + }); + + it('should cycle correctly for all days of week', () => { + // Arrange + fixture.componentRef.setInput('firstDayOfWeek', 0); // Sunday + fixture.detectChanges(); + + // Act + const headers = component.weekdayHeaders(); + + // Assert + expect(headers.length).toBe(7); + // Should have all unique days + const uniqueHeaders = new Set(headers); + expect(uniqueHeaders.size).toBe(7); + }); + }); + + describe('Service method delegation', () => { + it('should delegate hasEventsForDay to service', () => { + // Arrange + mockScheduleService.hasEventsForDay.and.returnValue(true); + const testDay = '2026-01-15'; + const testEvents = [] as any; + fixture.componentRef.setInput('events', testEvents); + fixture.detectChanges(); + + // Act + const result = component.hasEventsForDay(testDay); + + // Assert + expect(mockScheduleService.hasEventsForDay).toHaveBeenCalledWith( + testDay, + testEvents, + ); + expect(result).toBe(true); + }); + + it('should delegate getEventsForDay to service', () => { + // Arrange + const testEvents = [{ id: 'event1' }] as any; + mockScheduleService.getEventsForDay.and.returnValue(testEvents); + const testDay = '2026-01-15'; + fixture.componentRef.setInput('events', []); + fixture.detectChanges(); + + // Act + const result = component.getEventsForDay(testDay); + + // Assert + expect(mockScheduleService.getEventsForDay).toHaveBeenCalledWith(testDay, []); + expect(result).toEqual(testEvents); + }); + + it('should delegate getEventDayStr to service', () => { + // Arrange + const testEvent = { id: 'event1' } as any; + mockScheduleService.getEventDayStr.and.returnValue('2026-01-15'); + + // Act + const result = component.getEventDayStr(testEvent); + + // Assert + expect(mockScheduleService.getEventDayStr).toHaveBeenCalledWith(testEvent); + expect(result).toBe('2026-01-15'); + }); + }); + + describe('Input handling', () => { + it('should accept events input', () => { + // Arrange + const testEvents = [{ id: 'event1' }] as any; + + // Act + fixture.componentRef.setInput('events', testEvents); + fixture.detectChanges(); + + // Assert + expect(component.events()).toEqual(testEvents); + }); + + it('should accept daysToShow input', () => { + // Arrange + const testDays = ['2026-01-01', '2026-01-02', '2026-01-03']; + + // Act + fixture.componentRef.setInput('daysToShow', testDays); + fixture.detectChanges(); + + // Assert + expect(component.daysToShow()).toEqual(testDays); + }); + + it('should accept weeksToShow input', () => { + // Arrange + const testWeeks = 5; + + // Act + fixture.componentRef.setInput('weeksToShow', testWeeks); + fixture.detectChanges(); + + // Assert + expect(component.weeksToShow()).toBe(testWeeks); + }); + + it('should accept firstDayOfWeek input', () => { + // Arrange + const testFirstDay = 1; // Monday + + // Act + fixture.componentRef.setInput('firstDayOfWeek', testFirstDay); + fixture.detectChanges(); + + // Assert + expect(component.firstDayOfWeek()).toBe(testFirstDay); + }); + + it('should default weeksToShow to 6', () => { + // Act & Assert + expect(component.weeksToShow()).toBe(6); + }); + + it('should default firstDayOfWeek to 1', () => { + // Act & Assert + expect(component.firstDayOfWeek()).toBe(1); + }); + }); +}); diff --git a/src/app/features/schedule/schedule.service.spec.ts b/src/app/features/schedule/schedule.service.spec.ts index ddd201cb1..a09bfb101 100644 --- a/src/app/features/schedule/schedule.service.spec.ts +++ b/src/app/features/schedule/schedule.service.spec.ts @@ -253,7 +253,9 @@ describe('ScheduleService', () => { // January 2026 starts on a Thursday, so with Sunday start, // we should have padding days from December 2025 const firstDay = new Date(result[0]); - expect(firstDay.getMonth()).toBeLessThan(0); // December (previous year) + // December 2025 would be month 11 (previous year) + expect(firstDay.getMonth()).toBe(11); + expect(firstDay.getFullYear()).toBe(2025); // Should also have some days from February if weeks extend past January const lastDay = new Date(result[result.length - 1]); diff --git a/src/app/features/schedule/schedule/schedule.component.spec.ts b/src/app/features/schedule/schedule/schedule.component.spec.ts index a79e1f7fb..a74f35fbb 100644 --- a/src/app/features/schedule/schedule/schedule.component.spec.ts +++ b/src/app/features/schedule/schedule/schedule.component.spec.ts @@ -173,8 +173,8 @@ describe('ScheduleComponent', () => { }); it('should return true when _selectedDate matches today', () => { - // Arrange - set to today - const today = new Date(); + // Arrange - set to today (must match the mock todayDateStr$ value) + const today = new Date('2026-01-20'); component['_selectedDate'].set(today); // Act & Assert diff --git a/src/app/op-log/apply/archive-operation-handler.service.spec.ts b/src/app/op-log/apply/archive-operation-handler.service.spec.ts index 3b1ce1ecc..caa164365 100644 --- a/src/app/op-log/apply/archive-operation-handler.service.spec.ts +++ b/src/app/op-log/apply/archive-operation-handler.service.spec.ts @@ -128,11 +128,13 @@ describe('ArchiveOperationHandler', () => { 'updateTask', 'updateTasks', 'hasTask', + 'hasTasksBatch', 'removeAllArchiveTasksForProject', 'removeTagsFromAllTasks', 'removeRepeatCfgFromArchiveTasks', 'unlinkIssueProviderFromArchiveTasks', ]); + mockTaskArchiveService.hasTasksBatch.and.returnValue(Promise.resolve(new Map())); mockTimeTrackingService = jasmine.createSpyObj('TimeTrackingService', [ 'cleanupDataEverywhereForProject', 'cleanupArchiveDataForTag', @@ -381,6 +383,16 @@ describe('ArchiveOperationHandler', () => { describe('updateTasks action (batch)', () => { it('should update multiple archived tasks for remote operations', async () => { + // Both tasks exist in archive + mockTaskArchiveService.hasTasksBatch.and.returnValue( + Promise.resolve( + new Map([ + ['task-1', true], + ['task-2', true], + ]), + ), + ); + const action = { type: TaskSharedActions.updateTasks.type, tasks: [ @@ -415,8 +427,13 @@ describe('ArchiveOperationHandler', () => { it('should only update tasks that exist in archive', async () => { // Only task-1 is in archive, task-2 is not - mockTaskArchiveService.hasTask.and.callFake((id: string) => - Promise.resolve(id === 'task-1'), + mockTaskArchiveService.hasTasksBatch.and.returnValue( + Promise.resolve( + new Map([ + ['task-1', true], + ['task-2', false], + ]), + ), ); const action = {