mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
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
This commit is contained in:
parent
e43adba618
commit
e673d74b55
3 changed files with 874 additions and 25 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,551 @@
|
|||
// import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
//
|
||||
// import { ScheduleComponent } from './schedule.component';
|
||||
//
|
||||
// describe('ScheduleComponent', () => {
|
||||
// let component: ScheduleComponent;
|
||||
// let fixture: ComponentFixture<ScheduleComponent>;
|
||||
//
|
||||
// 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<ScheduleComponent>;
|
||||
let mockTaskService: jasmine.SpyObj<TaskService>;
|
||||
let mockLayoutService: jasmine.SpyObj<LayoutService>;
|
||||
let mockScheduleService: jasmine.SpyObj<ScheduleService>;
|
||||
let mockMatDialog: jasmine.SpyObj<MatDialog>;
|
||||
let mockGlobalTrackingIntervalService: jasmine.SpyObj<GlobalTrackingIntervalService>;
|
||||
let mockDateAdapter: jasmine.SpyObj<DateAdapter<any>>;
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue