feat(planning): implement constraint-first leave planner
This commit is contained in:
1 parent
ba8dbe92d9
commit
7ceaf1a57a
32 files changed
+4842
-880
No files matched your search
@@ -0,0 +1,146 @@
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { CalendarYear } from './calendar-year';
|
||||
import { CalendarService, SelectedDates, SelectableDayType } from '../../services/calendar-service';
|
||||
import {
|
||||
PlanOutcome,
|
||||
PlanningConflictOutcome,
|
||||
PlanningIncompleteOutcome,
|
||||
InvalidInputOutcome,
|
||||
PlanningOutcome,
|
||||
} from '../../services/leave-planner.service';
|
||||
import { UserInputService } from '../../services/user-input-service';
|
||||
|
||||
const invalid: InvalidInputOutcome = {
|
||||
kind: 'InvalidInput',
|
||||
reasons: [],
|
||||
};
|
||||
|
||||
const conflict: PlanningConflictOutcome = {
|
||||
kind: 'PlanningConflict',
|
||||
reasons: [],
|
||||
conflictingRuleIds: ['rule'],
|
||||
};
|
||||
|
||||
const incomplete: PlanningIncompleteOutcome = {
|
||||
kind: 'PlanningIncomplete',
|
||||
completedStages: [],
|
||||
reasons: [],
|
||||
};
|
||||
|
||||
const plan: PlanOutcome = {
|
||||
kind: 'Plan',
|
||||
horizon: { start: '2024-01-01', end: '2025-01-01' },
|
||||
placements: [],
|
||||
timeOffPeriods: [],
|
||||
preferences: [],
|
||||
remainingBalances: [],
|
||||
metrics: {
|
||||
unplacedAtRisk: 0,
|
||||
nonWorkingDayGain: 0,
|
||||
longestWorkSequence: 0,
|
||||
shortSequenceDeficit: 0,
|
||||
supportingPlacements: 0,
|
||||
preferenceSatisfaction: [],
|
||||
},
|
||||
completedStages: ['dates'],
|
||||
lastCompletedStage: 'dates',
|
||||
};
|
||||
|
||||
describe('CalendarYear planning outcome boundary', () => {
|
||||
it.each([
|
||||
['InvalidInput', invalid],
|
||||
['PlanningConflict', conflict],
|
||||
['PlanningIncomplete without incumbent', incomplete],
|
||||
] as const)('clears stale calendar results for %s', async (_, outcome: PlanningOutcome) => {
|
||||
const calendar = {
|
||||
getSelectedDatesForYear: vi.fn(() => new SelectedDates()),
|
||||
getDefaultPlanningHorizon: vi.fn(() => ({ start: '2024-01-01', end: '2025-01-01' })),
|
||||
planLeave: vi.fn(async () => outcome),
|
||||
selectedDatesForPlan: vi.fn(() => new SelectedDates()),
|
||||
};
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear],
|
||||
providers: [
|
||||
provideZonelessChangeDetection(),
|
||||
{ provide: CalendarService, useValue: calendar },
|
||||
],
|
||||
}).compileComponents();
|
||||
const userInput = TestBed.inject(UserInputService);
|
||||
userInput.remainingVacationDaysSignal.set([
|
||||
{
|
||||
id: 'stale',
|
||||
type: SelectableDayType.CP,
|
||||
numberOfDays: 1,
|
||||
expiryDate: new Date('2025-01-01'),
|
||||
},
|
||||
]);
|
||||
const fixture: ComponentFixture<CalendarYear> = TestBed.createComponent(CalendarYear);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(userInput.remainingVacationDaysSignal()).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps a returned incumbent/result visible only when an outcome supplies one', async () => {
|
||||
const calendar = {
|
||||
getSelectedDatesForYear: vi.fn(() => new SelectedDates()),
|
||||
getDefaultPlanningHorizon: vi.fn(() => ({ start: '2024-01-01', end: '2025-01-01' })),
|
||||
planLeave: vi.fn(async () => plan),
|
||||
selectedDatesForPlan: vi.fn(() => new SelectedDates()),
|
||||
};
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear],
|
||||
providers: [
|
||||
provideZonelessChangeDetection(),
|
||||
{ provide: CalendarService, useValue: calendar },
|
||||
],
|
||||
}).compileComponents();
|
||||
const userInput = TestBed.inject(UserInputService);
|
||||
userInput.remainingVacationDaysSignal.set([]);
|
||||
const fixture = TestBed.createComponent(CalendarYear);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(userInput.remainingVacationDaysSignal()).toEqual([]);
|
||||
});
|
||||
|
||||
it('turns planner rejection into an incomplete error and clears loading', async () => {
|
||||
const calendar = {
|
||||
getSelectedDatesForYear: vi.fn(() => new SelectedDates()),
|
||||
getDefaultPlanningHorizon: vi.fn(() => ({ start: '2024-01-01', end: '2025-01-01' })),
|
||||
planLeave: vi.fn(async () => {
|
||||
throw new Error('planner exploded');
|
||||
}),
|
||||
selectedDatesForPlan: vi.fn(() => new SelectedDates()),
|
||||
};
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear],
|
||||
providers: [
|
||||
provideZonelessChangeDetection(),
|
||||
{ provide: CalendarService, useValue: calendar },
|
||||
],
|
||||
}).compileComponents();
|
||||
const userInput = TestBed.inject(UserInputService);
|
||||
userInput.remainingVacationDaysSignal.set([
|
||||
{
|
||||
id: 'stale',
|
||||
type: SelectableDayType.CP,
|
||||
numberOfDays: 1,
|
||||
expiryDate: new Date('2025-01-01'),
|
||||
},
|
||||
]);
|
||||
const fixture = TestBed.createComponent(CalendarYear);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
fixture.detectChanges();
|
||||
|
||||
const outcome = fixture.componentInstance['planningOutcome']();
|
||||
expect(outcome?.kind).toBe('PlanningIncomplete');
|
||||
if (outcome?.kind === 'PlanningIncomplete')
|
||||
expect(outcome.reasons[0].code).toBe('planner-error');
|
||||
expect(fixture.componentInstance['isLoading']()).toBe(false);
|
||||
expect(userInput.remainingVacationDaysSignal()).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user