import { TestBed } from '@angular/core/testing'; import { vi } from 'vitest'; import { provideZonelessChangeDetection } from '@angular/core'; import { CalendarService, DayType, SelectedDate, SelectedDates, SelectableDayType, VacDay, } from './calendar-service'; import { DateRange } from '@angular/material/datepicker'; import { IfThenRule, TimeWindowRule, RuleType } from './rule-service'; describe('CalendarService', () => { let service: CalendarService; beforeEach(() => { TestBed.configureTestingModule({ providers: [provideZonelessChangeDetection()], }); service = TestBed.inject(CalendarService); }); it('should be created', () => { expect(service).toBeTruthy(); }); describe('getHolidays', () => { it('should return an array of holidays', () => { const holidays = service.getHolidays(); expect(Array.isArray(holidays)).toBe(true); expect(holidays.length).toBeGreaterThan(0); }); it('should cache holidays for the same year', () => { const holidays1 = service.getHolidays(); const holidays2 = service.getHolidays(); expect(holidays1).toEqual(holidays2); }); }); describe('static date calculation methods', () => { it('should calculate days in a week', () => { const week = CalendarService.daysInWeek(0, 0, 2024); expect(week.length).toBe(7); }); it('should calculate weeks in a month', () => { const weeks = CalendarService.weeksInMonth(0, 2024); expect(weeks.length).toBeGreaterThan(0); expect(weeks.length).toBeLessThanOrEqual(6); }); it('should calculate months in a year', () => { const months = CalendarService.monthsInYear(2024); expect(months.length).toBe(12); }); it('should calculate months from now', () => { const months = service.monthsInAYearFromNow(); expect(months.length).toBe(12); }); it('should return week number for a date', () => { const date = new Date(2024, 0, 1); const weekNumber = CalendarService.getWeekNumber(date); expect(weekNumber).toBeGreaterThan(0); expect(weekNumber).toBeLessThanOrEqual(53); }); }); describe('getSelectedDatesForYear', () => { it('should return selected dates for the year', () => { const selectedDates = service.getSelectedDatesForYear(); expect(selectedDates).toBeDefined(); expect(selectedDates._datesSelected.length).toBeGreaterThan(0); }); it('should include weekends', () => { const selectedDates = service.getSelectedDatesForYear(); const hasWeekend = selectedDates._datesSelected.some( (date) => date.type === DayType.WEEKEND, ); expect(hasWeekend).toBe(true); }); it('should include holidays', () => { const selectedDates = service.getSelectedDatesForYear(); const hasHoliday = selectedDates._datesSelected.some( (date) => date.type === DayType.CLOSED_DAY, ); expect(hasHoliday).toBe(true); }); it('should include today', () => { const selectedDates = service.getSelectedDatesForYear(); const hasToday = selectedDates._datesSelected.some( (date) => date.type === DayType.TODAY, ); expect(hasToday).toBe(true); }); }); describe('setWeekendSelection', () => { it('should add weekend for Saturday', () => { const selectedDates = new SelectedDates(); const saturday = new Date(2024, 0, 6); // A Saturday service.setWeekendSelection(saturday, selectedDates); expect(selectedDates._datesSelected.length).toBeGreaterThan(0); }); }); }); describe('SelectedDate', () => { it('should check if date is selected', () => { const date = new Date(2024, 0, 1); const selectedDate = new SelectedDate(DayType.CP, new DateRange(date, date)); expect(selectedDate.isSelected(date)).toBe(true); }); it('should check if date is start', () => { const date = new Date(2024, 0, 1); const selectedDate = new SelectedDate(DayType.CP, new DateRange(date, date)); expect(selectedDate.isStart(date)).toBe(true); }); it('should check if date is end', () => { const date = new Date(2024, 0, 1); const selectedDate = new SelectedDate(DayType.CP, new DateRange(date, date)); expect(selectedDate.isEnd(date)).toBe(true); }); it('should return correct classes for date', () => { const date = new Date(2024, 0, 1); const selectedDate = new SelectedDate(DayType.CP, new DateRange(date, date)); const classes = selectedDate.getClasses(date); expect(classes).toContain(DayType.CP); }); it('should return tooltip for selected date', () => { const date = new Date(2024, 0, 1); const selectedDate = new SelectedDate(DayType.CP, new DateRange(date, date)); const tooltip = selectedDate.getTooltip(date); expect(tooltip).toBeTruthy(); }); it('should calculate length in days', () => { const startDate = new Date(2024, 0, 1); const endDate = new Date(2024, 0, 3); const selectedDate = new SelectedDate(DayType.CP, new DateRange(startDate, endDate)); expect(selectedDate.getLengthInDays()).toBe(3); }); it('should not mark TODAY as selected', () => { const date = new Date(2024, 0, 1); const selectedDate = new SelectedDate(DayType.TODAY, new DateRange(date, date)); expect(selectedDate.isSelected(date)).toBe(false); }); }); describe('SelectedDates', () => { it('should push selected dates', () => { const selectedDates = new SelectedDates(); const date = new Date(2024, 0, 1); selectedDates.push(new SelectedDate(DayType.CP, new DateRange(date, date))); expect(selectedDates._datesSelected.length).toBe(1); }); it('should check if date is selected', () => { const selectedDates = new SelectedDates(); const date = new Date(2024, 0, 1); selectedDates.push(new SelectedDate(DayType.CP, new DateRange(date, date))); expect(selectedDates.isSelected(date)).toBe(true); }); it('should group adjacent dates', () => { const selectedDates = new SelectedDates(); const date1 = new Date(2024, 0, 1); const date2 = new Date(2024, 0, 2); selectedDates._datesSelected.push( new SelectedDate(DayType.CP, new DateRange(date1, date1)), ); selectedDates._datesSelected.push( new SelectedDate(DayType.RTT, new DateRange(date2, date2)), ); selectedDates.update(); expect(selectedDates.datesSelected.length).toBeGreaterThan(2); }); it('should compute heuristics', () => { const selectedDates = new SelectedDates(); const date = new Date(2024, 0, 1); selectedDates.push(new SelectedDate(DayType.CP, new DateRange(date, date))); const heuristic = selectedDates.computeHeuristics(); expect(typeof heuristic).toBe('number'); }); it('should return correct classes', () => { const selectedDates = new SelectedDates(); const date = new Date(2024, 0, 1); selectedDates.push(new SelectedDate(DayType.CP, new DateRange(date, date))); const classes = selectedDates.getClasses(date); expect(classes).toContain(DayType.CP); }); it('should return tooltip', () => { const selectedDates = new SelectedDates(); const date = new Date(2024, 0, 1); selectedDates.push(new SelectedDate(DayType.CP, new DateRange(date, date))); const tooltip = selectedDates.getTooltip(date); expect(tooltip).toBeTruthy(); }); describe('optimizeVacations and rules engine', () => { let calendarService: CalendarService; let selectedDates: SelectedDates; beforeEach(() => { calendarService = TestBed.inject(CalendarService); selectedDates = new SelectedDates(); }); it('should execute rules sequentially in order', async () => { const vacationDays: VacDay[] = []; const rules: (TimeWindowRule | IfThenRule)[] = [ { id: '1', type: RuleType.IF_THEN, enabled: true } as IfThenRule, { id: '2', type: RuleType.TIME_WINDOW, enabled: true } as TimeWindowRule, { id: '3', type: RuleType.IF_THEN, enabled: true } as IfThenRule, ]; const applyTimeWindowRulesSpy = vi.spyOn(selectedDates, 'applyTimeWindowRules'); const applyIfThenRulesSpy = vi.spyOn(selectedDates, 'applyIfThenRules'); await selectedDates.optimizeVacations(vacationDays, rules, calendarService); expect(applyIfThenRulesSpy.mock.calls.length).toBe(2); expect(applyTimeWindowRulesSpy.mock.calls.length).toBe(1); // First rule is IF_THEN expect(applyIfThenRulesSpy.mock.calls[0][1][0].id).toBe('1'); // Second rule is TIME_WINDOW expect(applyTimeWindowRulesSpy.mock.calls[0][1][0].id).toBe('2'); // Third rule is IF_THEN expect(applyIfThenRulesSpy.mock.calls[1][1][0].id).toBe('3'); }); it('should apply time window rules', () => { const futureYear = new Date().getFullYear() + 2; const vacationDays: VacDay[] = [ { id: '1', type: SelectableDayType.CP, numberOfDays: 5, expiryDate: new Date(futureYear + 1, 11, 31), }, ]; const rules: TimeWindowRule[] = [ { id: 't1', name: 'Time Window', type: RuleType.TIME_WINDOW, enabled: true, startDate: new Date(futureYear, 11, 20), endDate: new Date(futureYear, 11, 25), placeDays: 3, placeType: SelectableDayType.CP, }, ]; // Mock monthsInAYearFromNow to return a controlled range of days vi.spyOn(calendarService, 'monthsInAYearFromNow').mockReturnValue([ [ [ new Date(futureYear, 11, 20), new Date(futureYear, 11, 21), new Date(futureYear, 11, 22), new Date(futureYear, 11, 23), new Date(futureYear, 11, 24), ], ], ]); selectedDates.applyTimeWindowRules(vacationDays, rules, calendarService); // Should have placed 3 days const placedCount = selectedDates._datesSelected.filter( (d) => d.type === DayType.CP, ).length; expect(placedCount).toBe(3); // Should have consumed 3 days from the pool expect(vacationDays[0].numberOfDays).toBe(2); }); it('should apply if-then rules', () => { const futureYear = new Date().getFullYear() + 2; const vacationDays: VacDay[] = [ { id: '1', type: SelectableDayType.CP, numberOfDays: 5, expiryDate: new Date(futureYear + 1, 11, 31), }, ]; const rules: IfThenRule[] = [ { id: 'i1', name: 'Samedi Malin', type: RuleType.IF_THEN, enabled: true, dayOfWeek: 6, // Saturday placeDays: 2, placeType: SelectableDayType.CP, placement: 'auto', rewardDays: 1, rewardType: SelectableDayType.OTHER, }, ]; // Setup a mock holiday on a Saturday // Use a date far in the future so it doesn't fail the "past date" check const saturdayHoliday = new Date(new Date().getFullYear() + 2, 11, 5); // Dec 5 is usually Saturday or close, let's just make sure we get a Saturday // To guarantee it's a Saturday for the test: while (saturdayHoliday.getDay() !== 6) { saturdayHoliday.setDate(saturdayHoliday.getDate() + 1); } selectedDates._datesSelected.push( new SelectedDate( DayType.CLOSED_DAY, new DateRange(saturdayHoliday, saturdayHoliday), ), ); selectedDates.applyIfThenRules(vacationDays, rules); // Should have placed 2 CP days (e.g. Thu/Fri, Mon/Tue, or Fri/Mon) const placedCP = selectedDates._datesSelected.filter( (d) => d.type === DayType.CP, ).length; expect(placedCP).toBe(2); // Should have consumed 2 CP days expect(vacationDays[0].numberOfDays).toBe(3); // Should have added 1 OTHER reward day const rewardDay = vacationDays.find((vd) => vd.type === SelectableDayType.OTHER); expect(rewardDay).toBeDefined(); expect(rewardDay?.numberOfDays).toBe(1); }); it('should apply time window rules with ANY type', () => { const futureYear = new Date().getFullYear() + 2; const vacationDays: VacDay[] = [ { id: '1', type: SelectableDayType.CP, numberOfDays: 2, expiryDate: new Date(futureYear, 11, 31), }, { id: '2', type: SelectableDayType.RTT, numberOfDays: 2, expiryDate: new Date(futureYear - 1, 11, 31), // Expires earlier }, ]; const rules: TimeWindowRule[] = [ { id: 'tw1', name: 'Vacances', type: RuleType.TIME_WINDOW, enabled: true, startDate: new Date(futureYear, 6, 1), endDate: new Date(futureYear, 6, 15), placeDays: 3, placeType: SelectableDayType.ANY, }, ]; vi.spyOn(calendarService, 'monthsInAYearFromNow').mockReturnValue([ [ [ new Date(futureYear, 6, 1), new Date(futureYear, 6, 2), new Date(futureYear, 6, 3), new Date(futureYear, 6, 4), new Date(futureYear, 6, 5), ], ], ]); selectedDates.applyTimeWindowRules(vacationDays, rules, calendarService); // Should have placed 3 days total: 2 RTT (earlier expiry) and 1 CP const placedRTT = selectedDates._datesSelected.filter( (d) => d.type === DayType.RTT, ).length; const placedCP = selectedDates._datesSelected.filter( (d) => d.type === DayType.CP, ).length; expect(placedRTT).toBe(2); expect(placedCP).toBe(1); // Should have consumed 2 RTT and 1 CP expect(vacationDays[0].numberOfDays).toBe(1); // CP remaining expect(vacationDays[1].numberOfDays).toBe(0); // RTT remaining }); it('should apply if-then rules with ANY type', () => { const futureYear = new Date().getFullYear() + 2; const vacationDays: VacDay[] = [ { id: '1', type: SelectableDayType.CP, numberOfDays: 2, expiryDate: new Date(futureYear + 2, 11, 31), }, { id: '2', type: SelectableDayType.RTT, numberOfDays: 2, expiryDate: new Date(futureYear + 1, 11, 31), // Expires earlier }, ]; const rules: IfThenRule[] = [ { id: 'i1', name: 'Rule', type: RuleType.IF_THEN, enabled: true, dayOfWeek: 6, // Saturday placeDays: 3, placeType: SelectableDayType.ANY, placement: 'auto', rewardDays: 0, rewardType: SelectableDayType.CP, }, ]; const saturdayHoliday = new Date(new Date().getFullYear() + 2, 11, 5); while (saturdayHoliday.getDay() !== 6) { saturdayHoliday.setDate(saturdayHoliday.getDate() + 1); } selectedDates._datesSelected.push( new SelectedDate( DayType.CLOSED_DAY, new DateRange(saturdayHoliday, saturdayHoliday), ), ); selectedDates.applyIfThenRules(vacationDays, rules); // Should have placed 3 days total: 2 RTT (earlier expiry) and 1 CP const placedRTT = selectedDates._datesSelected.filter( (d) => d.type === DayType.RTT, ).length; const placedCP = selectedDates._datesSelected.filter( (d) => d.type === DayType.CP, ).length; expect(placedRTT).toBe(2); expect(placedCP).toBe(1); }); }); });