From e673d74b55af4b9b3239024aff61e5f360fc2fd4 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Tue, 20 Jan 2026 21:36:50 +0100 Subject: [PATCH] test(schedule): add comprehensive unit tests for navigation - Implement full test suite for ScheduleComponent covering: - Navigation state management (_selectedDate signal) - Week/month navigation (goToNextPeriod, goToPreviousPeriod, goToToday) - Today detection (isViewingToday computed) - Context-aware time calculations (_contextNow) - Schedule days computation with contextNow/realNow - Current time row display logic - Days to show calculations for week and month views - Add ScheduleService tests for: - getDaysToShow with reference dates - getMonthDaysToShow with padding days - buildScheduleDays parameter handling - getDayClass with reference month support - Fix test setup: - Use TranslateModule.forRoot() for proper i18n support - Add complete mock methods for ScheduleService - Handle timing-sensitive tests with appropriate tolerances - Use correct FH constant value (12) for time row calculations - Remove unused CollapsibleComponent import from config-page All 32 ScheduleComponent tests passing --- .../schedule/schedule.service.spec.ts | 323 ++++++++++ .../schedule/schedule.component.spec.ts | 574 +++++++++++++++++- .../config-page/config-page.component.ts | 2 - 3 files changed, 874 insertions(+), 25 deletions(-) diff --git a/src/app/features/schedule/schedule.service.spec.ts b/src/app/features/schedule/schedule.service.spec.ts index 2bf8df2b4..ddd201cb1 100644 --- a/src/app/features/schedule/schedule.service.spec.ts +++ b/src/app/features/schedule/schedule.service.spec.ts @@ -12,6 +12,7 @@ import { TaskService } from '../tasks/task.service'; describe('ScheduleService', () => { let service: ScheduleService; + let dateService: DateService; beforeEach(() => { TestBed.configureTestingModule({ @@ -43,12 +44,87 @@ describe('ScheduleService', () => { ], }); service = TestBed.inject(ScheduleService); + dateService = TestBed.inject(DateService); }); it('should be created', () => { expect(service).toBeTruthy(); }); + describe('getDaysToShow', () => { + it('should return correct number of days when referenceDate is null', () => { + // Arrange + const nrOfDaysToShow = 5; + + // Act + const result = service.getDaysToShow(nrOfDaysToShow, null); + + // Assert + expect(result.length).toBe(5); + }); + + it('should return days starting from today when referenceDate is null', () => { + // Arrange + const nrOfDaysToShow = 3; + const expectedTodayStr = dateService.todayStr(); + + // Act + const result = service.getDaysToShow(nrOfDaysToShow, null); + + // Assert + expect(result[0]).toBe(expectedTodayStr); + }); + + it('should return days starting from referenceDate when provided', () => { + // Arrange + const nrOfDaysToShow = 3; + const referenceDate = new Date(2026, 0, 25); // Jan 25, 2026 + const expectedFirstDay = dateService.todayStr(referenceDate.getTime()); + + // Act + const result = service.getDaysToShow(nrOfDaysToShow, referenceDate); + + // Assert + expect(result[0]).toBe(expectedFirstDay); + expect(result.length).toBe(3); + }); + + it('should return consecutive days from referenceDate', () => { + // Arrange + const nrOfDaysToShow = 7; + const referenceDate = new Date(2026, 0, 20); // Jan 20, 2026 + + // Act + const result = service.getDaysToShow(nrOfDaysToShow, referenceDate); + + // Assert + expect(result.length).toBe(7); + // Check that each day is consecutive + for (let i = 0; i < result.length - 1; i++) { + const currentDay = new Date(result[i]); + const nextDay = new Date(result[i + 1]); + const dayDiff = + (nextDay.getTime() - currentDay.getTime()) / (1000 * 60 * 60 * 24); + expect(dayDiff).toBe(1); + } + }); + + it('should handle transition across months', () => { + // Arrange + const nrOfDaysToShow = 5; + const referenceDate = new Date(2026, 0, 30); // Jan 30, 2026 + + // Act + const result = service.getDaysToShow(nrOfDaysToShow, referenceDate); + + // Assert + expect(result.length).toBe(5); + // Last days should be in February + const lastDay = new Date(result[4]); + expect(lastDay.getMonth()).toBe(1); // February + }); + }); + describe('getMonthDaysToShow', () => { it('should return correct number of days', () => { const numberOfWeeks = 5; @@ -138,5 +214,252 @@ describe('ScheduleService', () => { jasmine.clock().uninstall(); }); + + it('should use referenceDate to determine the month to display', () => { + // Arrange + const numberOfWeeks = 4; + const firstDayOfWeek = 1; // Monday + const referenceDate = new Date(2026, 5, 15); // June 15, 2026 + + // Act + const result = service.getMonthDaysToShow( + numberOfWeeks, + firstDayOfWeek, + referenceDate, + ); + + // Assert + expect(result.length).toBe(28); // 4 weeks + // The month view should include days from June 2026 + const juneFirst = new Date(2026, 5, 1); // June 1, 2026 + const juneFirstStr = dateService.todayStr(juneFirst.getTime()); + expect(result).toContain(juneFirstStr); + }); + + it('should include padding days from previous and next month', () => { + // Arrange + const numberOfWeeks = 5; + const firstDayOfWeek = 0; // Sunday + const referenceDate = new Date(2026, 0, 15); // Jan 15, 2026 + + // Act + const result = service.getMonthDaysToShow( + numberOfWeeks, + firstDayOfWeek, + referenceDate, + ); + + // Assert + // 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) + + // Should also have some days from February if weeks extend past January + const lastDay = new Date(result[result.length - 1]); + // With 5 weeks starting from late December, we should reach into February + expect(lastDay.getMonth()).toBeGreaterThanOrEqual(0); + }); + }); + + describe('buildScheduleDays', () => { + it('should return empty array when timelineTasks is null', () => { + // Arrange + const params = { + daysToShow: ['2026-01-20', '2026-01-21'], + timelineTasks: null, + taskRepeatCfgs: { withStartTime: [], withoutStartTime: [] }, + icalEvents: [], + plannerDayMap: {}, + timelineCfg: null, + currentTaskId: null, + }; + + // Act + const result = service.buildScheduleDays(params); + + // Assert + expect(result).toEqual([]); + }); + + it('should return empty array when taskRepeatCfgs is null', () => { + // Arrange + const params = { + daysToShow: ['2026-01-20', '2026-01-21'], + timelineTasks: { unPlanned: [], planned: [] }, + taskRepeatCfgs: null, + icalEvents: [], + plannerDayMap: {}, + timelineCfg: null, + currentTaskId: null, + }; + + // Act + const result = service.buildScheduleDays(params); + + // Assert + expect(result).toEqual([]); + }); + + it('should return empty array when plannerDayMap is null', () => { + // Arrange + const params = { + daysToShow: ['2026-01-20', '2026-01-21'], + timelineTasks: { unPlanned: [], planned: [] }, + taskRepeatCfgs: { withStartTime: [], withoutStartTime: [] }, + icalEvents: [], + plannerDayMap: null, + timelineCfg: null, + currentTaskId: null, + }; + + // Act + const result = service.buildScheduleDays(params); + + // Assert + expect(result).toEqual([]); + }); + + it('should pass realNow parameter through to mapToScheduleDays', () => { + // Arrange + const realNow = Date.now(); + const params = { + now: Date.now(), + realNow, + daysToShow: ['2026-01-20'], + timelineTasks: { unPlanned: [], planned: [] }, + taskRepeatCfgs: { withStartTime: [], withoutStartTime: [] }, + icalEvents: [], + plannerDayMap: {}, + timelineCfg: null, + currentTaskId: null, + }; + + // Act + const result = service.buildScheduleDays(params); + + // Assert + // The function should not throw and should process with realNow + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should default now to Date.now() when not provided', () => { + // Arrange + const params = { + daysToShow: ['2026-01-20'], + timelineTasks: { unPlanned: [], planned: [] }, + taskRepeatCfgs: { withStartTime: [], withoutStartTime: [] }, + icalEvents: [], + plannerDayMap: {}, + timelineCfg: null, + }; + + // Act + const result = service.buildScheduleDays(params); + + // Assert + // Should work without throwing + expect(result).toBeDefined(); + }); + }); + + describe('getDayClass', () => { + it('should return empty string for a day in current month when no referenceMonth provided', () => { + // Arrange + const today = new Date(); + const dayInCurrentMonth = new Date(today.getFullYear(), today.getMonth(), 15); + const dayStr = dateService.todayStr(dayInCurrentMonth.getTime()); + + // Act + const result = service.getDayClass(dayStr); + + // Assert + // Should not have 'other-month' class + expect(result).not.toContain('other-month'); + }); + + it('should return "today" class for today without referenceMonth', () => { + // Arrange + const today = new Date(); + const todayStr = dateService.todayStr(today.getTime()); + + // Act + const result = service.getDayClass(todayStr); + + // Assert + expect(result).toContain('today'); + }); + + it('should return "other-month" class for a day in a different month when referenceMonth provided', () => { + // Arrange + const referenceMonth = new Date(2026, 5, 15); // June 2026 + const dayInMay = new Date(2026, 4, 31); // May 31, 2026 + const dayStr = dateService.todayStr(dayInMay.getTime()); + + // Act + const result = service.getDayClass(dayStr, referenceMonth); + + // Assert + expect(result).toContain('other-month'); + }); + + it('should not return "other-month" class for a day in the reference month', () => { + // Arrange + const referenceMonth = new Date(2026, 5, 15); // June 2026 + const dayInJune = new Date(2026, 5, 20); // June 20, 2026 + const dayStr = dateService.todayStr(dayInJune.getTime()); + + // Act + const result = service.getDayClass(dayStr, referenceMonth); + + // Assert + expect(result).not.toContain('other-month'); + }); + + it('should return "today" class even when using referenceMonth', () => { + // Arrange + const today = new Date(); + const todayStr = dateService.todayStr(today.getTime()); + const referenceMonth = new Date(today.getFullYear(), today.getMonth(), 15); + + // Act + const result = service.getDayClass(todayStr, referenceMonth); + + // Assert + expect(result).toContain('today'); + }); + + it('should handle year boundaries correctly', () => { + // Arrange + const referenceMonth = new Date(2026, 0, 15); // January 2026 + const dayInDecember2025 = new Date(2025, 11, 31); // Dec 31, 2025 + const dayStr = dateService.todayStr(dayInDecember2025.getTime()); + + // Act + const result = service.getDayClass(dayStr, referenceMonth); + + // Assert + expect(result).toContain('other-month'); + }); + + it('should combine "today" and "other-month" classes when applicable', () => { + // Arrange - This is an edge case where today is in a different month than reference + const today = new Date(2026, 0, 20); // Jan 20, 2026 + jasmine.clock().install(); + jasmine.clock().mockDate(today); + + const todayStr = dateService.todayStr(today.getTime()); + const referenceMonth = new Date(2025, 11, 15); // December 2025 + + // Act + const result = service.getDayClass(todayStr, referenceMonth); + + // Assert + expect(result).toContain('today'); + expect(result).toContain('other-month'); + + jasmine.clock().uninstall(); + }); }); }); diff --git a/src/app/features/schedule/schedule/schedule.component.spec.ts b/src/app/features/schedule/schedule/schedule.component.spec.ts index 6422e4fbd..a79e1f7fb 100644 --- a/src/app/features/schedule/schedule/schedule.component.spec.ts +++ b/src/app/features/schedule/schedule/schedule.component.spec.ts @@ -1,23 +1,551 @@ -// import { ComponentFixture, TestBed } from '@angular/core/testing'; -// -// import { ScheduleComponent } from './schedule.component'; -// -// describe('ScheduleComponent', () => { -// let component: ScheduleComponent; -// let fixture: ComponentFixture; -// -// beforeEach(async () => { -// await TestBed.configureTestingModule({ -// imports: [ScheduleComponent] -// }) -// .compileComponents(); -// -// fixture = TestBed.createComponent(ScheduleComponent); -// component = fixture.componentInstance; -// fixture.detectChanges(); -// }); -// -// it('should create', () => { -// expect(component).toBeTruthy(); -// }); -// }); +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ScheduleComponent } from './schedule.component'; +import { TaskService } from '../../tasks/task.service'; +import { LayoutService } from '../../../core-ui/layout/layout.service'; +import { ScheduleService } from '../schedule.service'; +import { MatDialog } from '@angular/material/dialog'; +import { GlobalTrackingIntervalService } from '../../../core/global-tracking-interval/global-tracking-interval.service'; +import { DateAdapter } from '@angular/material/core'; +import { signal } from '@angular/core'; +import { of } from 'rxjs'; +import { provideMockStore } from '@ngrx/store/testing'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('ScheduleComponent', () => { + let component: ScheduleComponent; + let fixture: ComponentFixture; + let mockTaskService: jasmine.SpyObj; + let mockLayoutService: jasmine.SpyObj; + let mockScheduleService: jasmine.SpyObj; + let mockMatDialog: jasmine.SpyObj; + let mockGlobalTrackingIntervalService: jasmine.SpyObj; + let mockDateAdapter: jasmine.SpyObj>; + + beforeEach(async () => { + // Create mock services + mockTaskService = jasmine.createSpyObj('TaskService', ['currentTaskId']); + (mockTaskService as any).currentTaskId = signal(null); + + mockLayoutService = jasmine.createSpyObj('LayoutService', [], { + selectedTimeView: signal('week'), + }); + + mockScheduleService = jasmine.createSpyObj('ScheduleService', [ + 'getDaysToShow', + 'getMonthDaysToShow', + 'buildScheduleDays', + 'scheduleRefreshTick', + 'getTodayStr', + 'createScheduleDaysWithContext', + 'getDayClass', + 'hasEventsForDay', + 'getEventsForDay', + ]); + mockScheduleService.getDaysToShow.and.returnValue([ + '2026-01-20', + '2026-01-21', + '2026-01-22', + ]); + mockScheduleService.getMonthDaysToShow.and.returnValue([ + '2026-01-01', + '2026-01-02', + '2026-01-03', + ]); + mockScheduleService.buildScheduleDays.and.returnValue([]); + mockScheduleService.getTodayStr.and.callFake((timestamp?: number | Date) => { + const date = timestamp ? new Date(timestamp) : new Date(); + return date.toISOString().split('T')[0]; + }); + mockScheduleService.createScheduleDaysWithContext.and.returnValue([]); + mockScheduleService.getDayClass.and.returnValue(''); + mockScheduleService.hasEventsForDay.and.returnValue(false); + mockScheduleService.getEventsForDay.and.returnValue([]); + (mockScheduleService as any).scheduleRefreshTick = signal(0); + + mockMatDialog = jasmine.createSpyObj('MatDialog', ['open']); + + mockGlobalTrackingIntervalService = jasmine.createSpyObj( + 'GlobalTrackingIntervalService', + [], + { + todayDateStr$: of('2026-01-20'), + }, + ); + + mockDateAdapter = jasmine.createSpyObj('DateAdapter', ['getFirstDayOfWeek']); + mockDateAdapter.getFirstDayOfWeek.and.returnValue(0); + + await TestBed.configureTestingModule({ + imports: [ScheduleComponent, TranslateModule.forRoot()], + providers: [ + provideMockStore({ initialState: {} }), + { provide: TaskService, useValue: mockTaskService }, + { provide: LayoutService, useValue: mockLayoutService }, + { provide: ScheduleService, useValue: mockScheduleService }, + { provide: MatDialog, useValue: mockMatDialog }, + { + provide: GlobalTrackingIntervalService, + useValue: mockGlobalTrackingIntervalService, + }, + { provide: DateAdapter, useValue: mockDateAdapter }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ScheduleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('_selectedDate signal', () => { + it('should initialize as null (viewing today)', () => { + expect(component['_selectedDate']()).toBeNull(); + }); + + it('should update when goToNextPeriod is called in week view', () => { + // Arrange - default is week view + const initialDate = component['_selectedDate'](); + expect(initialDate).toBeNull(); + + // Act + component.goToNextPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate).not.toBeNull(); + expect(newDate?.getTime()).toBeGreaterThan(Date.now() - 1000); // Should be around now + 7 days + }); + + it('should update when goToPreviousPeriod is called in week view', () => { + // Arrange - start viewing today (null) + expect(component['_selectedDate']()).toBeNull(); + + // Act - navigate to previous week + component.goToPreviousPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate).not.toBeNull(); + expect(newDate?.getTime()).toBeLessThan(Date.now()); + }); + + it('should update when goToNextPeriod is called in month view', () => { + // Arrange - switch to month view + mockLayoutService.selectedTimeView.set('month'); + fixture.detectChanges(); + + // Act + component.goToNextPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate).not.toBeNull(); + // Should be first of next month + expect(newDate?.getDate()).toBe(1); + }); + + it('should update when goToPreviousPeriod is called in month view', () => { + // Arrange - switch to month view + mockLayoutService.selectedTimeView.set('month'); + fixture.detectChanges(); + + // Act + component.goToPreviousPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate).not.toBeNull(); + // Should be first of previous month + expect(newDate?.getDate()).toBe(1); + }); + }); + + describe('isViewingToday computed', () => { + it('should return true when _selectedDate is null', () => { + // Arrange + component['_selectedDate'].set(null); + + // Act & Assert + expect(component.isViewingToday()).toBe(true); + }); + + it('should return true when _selectedDate matches today', () => { + // Arrange - set to today + const today = new Date(); + component['_selectedDate'].set(today); + + // Act & Assert + expect(component.isViewingToday()).toBe(true); + }); + + it('should return false when _selectedDate is in the future', () => { + // Arrange + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 7); + component['_selectedDate'].set(futureDate); + + // Act & Assert + expect(component.isViewingToday()).toBe(false); + }); + + it('should return false when _selectedDate is in the past', () => { + // Arrange + const pastDate = new Date(); + pastDate.setDate(pastDate.getDate() - 7); + component['_selectedDate'].set(pastDate); + + // Act & Assert + expect(component.isViewingToday()).toBe(false); + }); + }); + + describe('goToPreviousPeriod', () => { + it('should subtract 7 days in week view when viewing a future date', () => { + // Arrange + const startDate = new Date(2026, 0, 20); // Jan 20, 2026 + component['_selectedDate'].set(startDate); + + // Act + component.goToPreviousPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate?.getDate()).toBe(13); // Jan 13, 2026 + }); + + it('should subtract 7 days from today when _selectedDate is null in week view', () => { + // Arrange + component['_selectedDate'].set(null); + const expectedDate = new Date(); + expectedDate.setDate(expectedDate.getDate() - 7); + + // Act + component.goToPreviousPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate?.getDate()).toBe(expectedDate.getDate()); + }); + + it('should go to previous month in month view', () => { + // Arrange + mockLayoutService.selectedTimeView.set('month'); + const startDate = new Date(2026, 1, 15); // Feb 15, 2026 + component['_selectedDate'].set(startDate); + + // Act + component.goToPreviousPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate?.getMonth()).toBe(0); // January + expect(newDate?.getDate()).toBe(1); // First of month + }); + + it('should go to previous year when navigating from January in month view', () => { + // Arrange + mockLayoutService.selectedTimeView.set('month'); + const startDate = new Date(2026, 0, 15); // Jan 15, 2026 + component['_selectedDate'].set(startDate); + + // Act + component.goToPreviousPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate?.getFullYear()).toBe(2025); + expect(newDate?.getMonth()).toBe(11); // December + }); + }); + + describe('goToNextPeriod', () => { + it('should add 7 days in week view', () => { + // Arrange + const startDate = new Date(2026, 0, 20); // Jan 20, 2026 + component['_selectedDate'].set(startDate); + + // Act + component.goToNextPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate?.getDate()).toBe(27); // Jan 27, 2026 + }); + + it('should add 7 days from today when _selectedDate is null in week view', () => { + // Arrange + component['_selectedDate'].set(null); + const expectedDate = new Date(); + expectedDate.setDate(expectedDate.getDate() + 7); + + // Act + component.goToNextPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate?.getDate()).toBe(expectedDate.getDate()); + }); + + it('should go to next month in month view', () => { + // Arrange + mockLayoutService.selectedTimeView.set('month'); + const startDate = new Date(2026, 0, 15); // Jan 15, 2026 + component['_selectedDate'].set(startDate); + + // Act + component.goToNextPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate?.getMonth()).toBe(1); // February + expect(newDate?.getDate()).toBe(1); // First of month + }); + + it('should go to next year when navigating from December in month view', () => { + // Arrange + mockLayoutService.selectedTimeView.set('month'); + const startDate = new Date(2025, 11, 15); // Dec 15, 2025 + component['_selectedDate'].set(startDate); + + // Act + component.goToNextPeriod(); + + // Assert + const newDate = component['_selectedDate'](); + expect(newDate?.getFullYear()).toBe(2026); + expect(newDate?.getMonth()).toBe(0); // January + }); + }); + + describe('goToToday', () => { + it('should reset _selectedDate to null', () => { + // Arrange + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 7); + component['_selectedDate'].set(futureDate); + + // Act + component.goToToday(); + + // Assert + expect(component['_selectedDate']()).toBeNull(); + }); + + it('should make isViewingToday return true', () => { + // Arrange + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 7); + component['_selectedDate'].set(futureDate); + expect(component.isViewingToday()).toBe(false); + + // Act + component.goToToday(); + + // Assert + expect(component.isViewingToday()).toBe(true); + }); + }); + + describe('_contextNow computed', () => { + it('should return current time when viewing today (selectedDate is null)', () => { + // Arrange + component['_selectedDate'].set(null); + + // Act + const contextNow = component['_contextNow'](); + + // Assert - just check it's a reasonable timestamp (within last hour and next minute) + const now = Date.now(); + // eslint-disable-next-line no-mixed-operators + const oneHourAgo = now - 60 * 60 * 1000; + // eslint-disable-next-line no-mixed-operators + const oneMinuteFromNow = now + 60 * 1000; + expect(contextNow).toBeGreaterThan(oneHourAgo); + expect(contextNow).toBeLessThan(oneMinuteFromNow); + }); + + it('should return midnight of selected date when viewing a different date', () => { + // Arrange + const selectedDate = new Date(2026, 0, 25, 14, 30, 45); // Jan 25, 2026, 2:30:45 PM + component['_selectedDate'].set(selectedDate); + + // Act + const contextNow = component['_contextNow'](); + const contextDate = new Date(contextNow); + + // Assert + expect(contextDate.getHours()).toBe(0); + expect(contextDate.getMinutes()).toBe(0); + expect(contextDate.getSeconds()).toBe(0); + expect(contextDate.getMilliseconds()).toBe(0); + expect(contextDate.getDate()).toBe(25); + expect(contextDate.getMonth()).toBe(0); + expect(contextDate.getFullYear()).toBe(2026); + }); + }); + + describe('scheduleDays computed', () => { + it('should call createScheduleDaysWithContext with contextNow', () => { + // Arrange + const selectedDate = new Date(2026, 0, 25); + component['_selectedDate'].set(selectedDate); + mockScheduleService.createScheduleDaysWithContext.calls.reset(); + + // Act + component.scheduleDays(); + + // Assert + expect(mockScheduleService.createScheduleDaysWithContext).toHaveBeenCalled(); + const callArgs = + mockScheduleService.createScheduleDaysWithContext.calls.mostRecent().args[0]; + expect(callArgs.contextNow).toBeDefined(); + // Context now should be midnight of selected date + const contextDate = new Date(callArgs.contextNow); + expect(contextDate.getHours()).toBe(0); + expect(contextDate.getMinutes()).toBe(0); + }); + + it('should always pass realNow as actual current time', () => { + // Arrange + const selectedDate = new Date(2026, 0, 25); + component['_selectedDate'].set(selectedDate); + mockScheduleService.createScheduleDaysWithContext.calls.reset(); + const before = Date.now(); + + // Act + component.scheduleDays(); + const after = Date.now(); + + // Assert + const callArgs = + mockScheduleService.createScheduleDaysWithContext.calls.mostRecent().args[0]; + expect(callArgs.realNow).toBeGreaterThanOrEqual(before); + expect(callArgs.realNow).toBeLessThanOrEqual(after); + }); + + it('should pass both contextNow and realNow when viewing a future date', () => { + // Arrange + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 7); + component['_selectedDate'].set(futureDate); + mockScheduleService.createScheduleDaysWithContext.calls.reset(); + + // Act + component.scheduleDays(); + + // Assert + const callArgs = + mockScheduleService.createScheduleDaysWithContext.calls.mostRecent().args[0]; + expect(callArgs.contextNow).toBeDefined(); + expect(callArgs.realNow).toBeDefined(); + // contextNow should be different from realNow when viewing future + expect(callArgs.contextNow).not.toBe(callArgs.realNow); + }); + }); + + describe('currentTimeRow computed', () => { + it('should return null when not viewing today', () => { + // Arrange + const futureDate = new Date(); + futureDate.setDate(futureDate.getDate() + 7); + component['_selectedDate'].set(futureDate); + + // Act + const timeRow = component.currentTimeRow(); + + // Assert + expect(timeRow).toBeNull(); + }); + + it('should return a number when viewing today', () => { + // Arrange + component['_selectedDate'].set(null); + + // Act + const timeRow = component.currentTimeRow(); + + // Assert + expect(timeRow).not.toBeNull(); + expect(typeof timeRow).toBe('number'); + }); + + it('should calculate time row based on current time', () => { + // Arrange + component['_selectedDate'].set(null); + + // Act + const timeRow = component.currentTimeRow(); + + // Assert - check it's a reasonable value (0-288 for 24 hours * FH=12) + expect(timeRow).not.toBeNull(); + expect(timeRow).toBeGreaterThanOrEqual(0); + expect(timeRow).toBeLessThanOrEqual(288); // 24 hours * 12 rows per hour + }); + }); + + describe('daysToShow computed', () => { + it('should call getDaysToShow in week view', () => { + // Arrange + mockLayoutService.selectedTimeView.set('week'); + // Trigger a change to force recomputation + component['_selectedDate'].set(new Date(2026, 0, 20)); + mockScheduleService.getDaysToShow.calls.reset(); + + // Act + component.daysToShow(); + + // Assert + expect(mockScheduleService.getDaysToShow).toHaveBeenCalled(); + }); + + it('should call getMonthDaysToShow in month view', () => { + // Arrange + mockLayoutService.selectedTimeView.set('month'); + mockScheduleService.getMonthDaysToShow.calls.reset(); + component['_selectedDate'].set(null); + + // Act + component.daysToShow(); + + // Assert + expect(mockScheduleService.getMonthDaysToShow).toHaveBeenCalled(); + }); + + it('should pass selectedDate to getDaysToShow', () => { + // Arrange + const testDate = new Date(2026, 0, 25); + component['_selectedDate'].set(testDate); + mockScheduleService.getDaysToShow.calls.reset(); + + // Act + component.daysToShow(); + + // Assert + expect(mockScheduleService.getDaysToShow).toHaveBeenCalledWith( + jasmine.any(Number), + testDate, + ); + }); + + it('should pass selectedDate to getMonthDaysToShow', () => { + // Arrange + mockLayoutService.selectedTimeView.set('month'); + const testDate = new Date(2026, 0, 25); + component['_selectedDate'].set(testDate); + mockScheduleService.getMonthDaysToShow.calls.reset(); + + // Act + component.daysToShow(); + + // Assert + expect(mockScheduleService.getMonthDaysToShow).toHaveBeenCalledWith( + jasmine.any(Number), + jasmine.any(Number), + testDate, + ); + }); + }); +}); diff --git a/src/app/pages/config-page/config-page.component.ts b/src/app/pages/config-page/config-page.component.ts index 31ee18072..b3a200800 100644 --- a/src/app/pages/config-page/config-page.component.ts +++ b/src/app/pages/config-page/config-page.component.ts @@ -41,7 +41,6 @@ import { SyncConfigService } from '../../imex/sync/sync-config.service'; import { WebdavApi } from '../../op-log/sync-providers/file-based/webdav/webdav-api'; import { AsyncPipe } from '@angular/common'; import { PluginManagementComponent } from '../../plugins/ui/plugin-management/plugin-management.component'; -import { CollapsibleComponent } from '../../ui/collapsible/collapsible.component'; import { PluginBridgeService } from '../../plugins/plugin-bridge.service'; import { createPluginShortcutFormItems } from '../../features/config/form-cfgs/plugin-keyboard-shortcuts'; import { PluginShortcutCfg } from '../../plugins/plugin-api.model'; @@ -74,7 +73,6 @@ import { MatTooltip } from '@angular/material/tooltip'; TranslatePipe, AsyncPipe, PluginManagementComponent, - CollapsibleComponent, MatTabGroup, MatTab, MatTabLabel,