508 lines
16 KiB
TypeScript
508 lines
16 KiB
TypeScript
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('getHolidaysForPeriod', () => {
|
|
it('should return an array of holidays', () => {
|
|
const startDate = new Date();
|
|
const endDate = new Date();
|
|
endDate.setFullYear(endDate.getFullYear() + 1);
|
|
const holidays = service.getHolidaysForPeriod(startDate, endDate);
|
|
expect(Array.isArray(holidays)).toBe(true);
|
|
expect(holidays.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
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 in period', () => {
|
|
const startDate = new Date();
|
|
const endDate = new Date();
|
|
endDate.setFullYear(endDate.getFullYear() + 1);
|
|
const months = service.monthsInPeriod(startDate, endDate);
|
|
expect(months.length).toBeGreaterThanOrEqual(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('getSelectedDatesForPeriod', () => {
|
|
it('should return selected dates for the period', () => {
|
|
const startDate = new Date();
|
|
const endDate = new Date();
|
|
endDate.setFullYear(endDate.getFullYear() + 1);
|
|
const selectedDates = service.getSelectedDatesForPeriod(startDate, endDate);
|
|
expect(selectedDates).toBeDefined();
|
|
expect(selectedDates._datesSelected.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should include weekends', () => {
|
|
const startDate = new Date();
|
|
const endDate = new Date();
|
|
endDate.setFullYear(endDate.getFullYear() + 1);
|
|
const selectedDates = service.getSelectedDatesForPeriod(startDate, endDate);
|
|
const hasWeekend = selectedDates._datesSelected.some(
|
|
(date) => date.type === DayType.WEEKEND,
|
|
);
|
|
expect(hasWeekend).toBe(true);
|
|
});
|
|
|
|
it('should include holidays', () => {
|
|
const startDate = new Date();
|
|
const endDate = new Date();
|
|
endDate.setFullYear(endDate.getFullYear() + 1);
|
|
const selectedDates = service.getSelectedDatesForPeriod(startDate, endDate);
|
|
const hasHoliday = selectedDates._datesSelected.some(
|
|
(date) => date.type === DayType.CLOSED_DAY,
|
|
);
|
|
expect(hasHoliday).toBe(true);
|
|
});
|
|
|
|
it('should include today', () => {
|
|
const startDate = new Date();
|
|
const endDate = new Date();
|
|
endDate.setFullYear(endDate.getFullYear() + 1);
|
|
const selectedDates = service.getSelectedDatesForPeriod(startDate, endDate);
|
|
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, 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, 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, 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, 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, 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<Date>(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, 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, 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, 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<Date>(date1, date1)),
|
|
);
|
|
selectedDates._datesSelected.push(
|
|
new SelectedDate(DayType.RTT, new DateRange<Date>(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, 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, 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, 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');
|
|
|
|
const startDate = new Date();
|
|
const endDate = new Date();
|
|
endDate.setFullYear(endDate.getFullYear() + 1);
|
|
await selectedDates.optimizeVacations(
|
|
vacationDays,
|
|
rules,
|
|
calendarService,
|
|
startDate,
|
|
endDate,
|
|
);
|
|
|
|
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 monthsInPeriod to return a controlled range of days
|
|
vi.spyOn(calendarService, 'monthsInPeriod').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),
|
|
],
|
|
],
|
|
]);
|
|
|
|
const startDate = new Date(futureYear, 11, 20);
|
|
const endDate = new Date(futureYear, 11, 25);
|
|
selectedDates.applyTimeWindowRules(
|
|
vacationDays,
|
|
rules,
|
|
calendarService,
|
|
startDate,
|
|
endDate,
|
|
);
|
|
|
|
// 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<Date>(saturdayHoliday, saturdayHoliday),
|
|
),
|
|
);
|
|
|
|
const startDate = new Date();
|
|
const endDate = new Date();
|
|
endDate.setFullYear(endDate.getFullYear() + 3);
|
|
selectedDates.applyIfThenRules(vacationDays, rules, startDate, endDate);
|
|
|
|
// 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, 'monthsInPeriod').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),
|
|
],
|
|
],
|
|
]);
|
|
|
|
const startDate = new Date(futureYear, 6, 1);
|
|
const endDate = new Date(futureYear, 6, 15);
|
|
selectedDates.applyTimeWindowRules(
|
|
vacationDays,
|
|
rules,
|
|
calendarService,
|
|
startDate,
|
|
endDate,
|
|
);
|
|
|
|
// 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<Date>(saturdayHoliday, saturdayHoliday),
|
|
),
|
|
);
|
|
|
|
const startDate = new Date();
|
|
const endDate = new Date();
|
|
endDate.setFullYear(endDate.getFullYear() + 3);
|
|
selectedDates.applyIfThenRules(vacationDays, rules, startDate, endDate);
|
|
|
|
// 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);
|
|
});
|
|
});
|
|
});
|