feat: add PlanningDurationService for estimating planning duration
- Implemented PlanningDurationService to provide an indicative wait time before calculation based on user input and calendar settings. - Introduced duration estimation logic that considers horizon dates, balances, and rules without triggering a calculation. - Enhanced user input component to display estimated duration using the new service. - Updated translation files to include new duration estimate strings in multiple languages. - Added tests for PlanningDurationService to ensure accurate duration estimation and calibration. - Refactored leave planner service to remove hardcoded deadlines and improve worker responsiveness.
This commit is contained in:
1 parent
2edf2004f5
commit
bc8570d8ca
29 files changed
+1064
-123
No files matched your search
@@ -0,0 +1,125 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { CalendarSettingsService } from './calendar-settings-service';
|
||||
import { SelectableDayType } from './calendar-service';
|
||||
import { RuleService, RuleType } from './rule-service';
|
||||
import { UserInputService } from './user-input-service';
|
||||
import { createPlanningCalculation } from './leave-planner.service';
|
||||
import { PlanningDurationService } from './planning-duration.service';
|
||||
|
||||
describe('Planning duration estimate', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
localStorage.setItem('vacationRules', '[]');
|
||||
TestBed.inject(CalendarSettingsService).setPlanningDates('2024-01-01', '2024-12-31');
|
||||
TestBed.inject(UserInputService).updateBalances(() => [
|
||||
{
|
||||
id: 'cp',
|
||||
type: SelectableDayType.CP,
|
||||
numberOfDays: 30,
|
||||
availableDate: new Date('2024-01-01T00:00:00'),
|
||||
expiryDate: new Date('2024-12-31T00:00:00'),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('offers a coarse seconds range on the first launch without calculating', () => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
expect(duration.estimate()).toEqual({ minimum: 1, maximum: 3, unit: 'seconds' });
|
||||
expect(TestBed.inject(UserInputService).computeTriggerSignal()).toBe(0);
|
||||
expect(TestBed.inject(RuleService).rules).toEqual([]);
|
||||
});
|
||||
|
||||
it('updates for dates, balances and active rules without requesting a calculation', () => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const settings = TestBed.inject(CalendarSettingsService);
|
||||
const input = TestBed.inject(UserInputService);
|
||||
const initial = duration.estimate()!;
|
||||
settings.setPlanningDates('2024-01-01', '2026-12-31');
|
||||
const longer = duration.estimate()!;
|
||||
expect(longer.maximum).toBeGreaterThan(initial.maximum);
|
||||
input.updateBalances((balances) =>
|
||||
balances.map((balance) => ({ ...balance, numberOfDays: 120 })),
|
||||
);
|
||||
const moreLeave = duration.estimate()!;
|
||||
expect(moreLeave.maximum).toBeGreaterThan(longer.maximum);
|
||||
const rules = TestBed.inject(RuleService);
|
||||
rules.addRule({
|
||||
id: 'window',
|
||||
name: 'Spring',
|
||||
type: RuleType.TIME_WINDOW,
|
||||
enabled: true,
|
||||
force: 'obligation',
|
||||
startDate: new Date('2024-04-01T00:00:00'),
|
||||
endDate: new Date('2024-04-30T00:00:00'),
|
||||
placeDays: 15,
|
||||
placeType: SelectableDayType.CP,
|
||||
});
|
||||
expect(duration.estimate()!.maximum).toBeGreaterThan(moreLeave.maximum);
|
||||
rules.toggleRule('window');
|
||||
expect(duration.estimate()).toEqual(moreLeave);
|
||||
settings.setPlanningDates('2026-12-31', '2024-01-01');
|
||||
expect(duration.estimate()).toBeNull();
|
||||
expect(input.computeTriggerSignal()).toBe(0);
|
||||
});
|
||||
|
||||
it('refines the submitted configuration using elapsed calendar time and displays minutes for a long wait', () => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const submitted = duration.request()!;
|
||||
duration.recordCompletion(submitted, 120_000);
|
||||
expect(duration.estimate()).toEqual({ minimum: 1, maximum: 6, unit: 'minutes' });
|
||||
});
|
||||
|
||||
it('calibrates against the submitted snapshot even when the form has since changed', () => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const submitted = duration.request()!;
|
||||
TestBed.inject(CalendarSettingsService).setPlanningDates('2024-01-01', '2026-12-31');
|
||||
duration.recordCompletion(submitted, 20_000);
|
||||
expect(duration.estimate()!.maximum).toBeGreaterThan(60);
|
||||
TestBed.inject(CalendarSettingsService).setPlanningDates('2024-01-01', '2024-12-31');
|
||||
expect(duration.estimate()).toEqual({ minimum: 10, maximum: 60, unit: 'seconds' });
|
||||
});
|
||||
|
||||
it('calibrates the same configuration after Date rules become ISO strings in the immutable snapshot', () => {
|
||||
TestBed.inject(RuleService).addRule({
|
||||
id: 'future-rule',
|
||||
name: 'Outside the historical horizon',
|
||||
type: RuleType.TIME_WINDOW,
|
||||
force: 'preference',
|
||||
enabled: true,
|
||||
startDate: new Date(2026, 3, 1),
|
||||
endDate: new Date(2026, 3, 30),
|
||||
placeDays: 60,
|
||||
placeType: SelectableDayType.CP,
|
||||
});
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const calculation = createPlanningCalculation(duration.request()!);
|
||||
duration.recordCompletion(calculation.snapshot, 20_000);
|
||||
expect(duration.estimate()).toEqual({ minimum: 10, maximum: 60, unit: 'seconds' });
|
||||
});
|
||||
|
||||
it('uses recent typical waits rather than an isolated slow calculation, and resets on a fresh visit', () => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const submitted = duration.request()!;
|
||||
for (const elapsed of [20_000, 25_000, 900_000])
|
||||
duration.recordCompletion(submitted, elapsed);
|
||||
expect(duration.estimate()).toEqual({ minimum: 12, maximum: 75, unit: 'seconds' });
|
||||
TestBed.resetTestingModule();
|
||||
expect(TestBed.inject(PlanningDurationService).estimate()).toEqual({
|
||||
minimum: 1,
|
||||
maximum: 3,
|
||||
unit: 'seconds',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([0, -1, NaN, Infinity])('ignores unusable elapsed measurements: %s', (elapsed) => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const initial = duration.estimate();
|
||||
duration.recordCompletion(duration.request()!, elapsed);
|
||||
expect(duration.estimate()).toEqual(initial);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user