CI / policy (push) Successful in 7s
CI / validate (push) Successful in 1m15s
CI / test (push) Successful in 3m18s
CI / benchmark (push) Failing after 5m53s
CI / build (push) Successful in 1m23s
CI / security (push) Successful in 7m6s
CI / policy (pull_request) Successful in 7s
CI / validate (pull_request) Successful in 1m9s
CI / build (pull_request) Successful in 43s
CI / test (pull_request) Successful in 3m19s
CI / images (push) Successful in 3m7s
CI / required (push) Successful in 2s
CI / benchmark (pull_request) Failing after 5m28s
CI / security (pull_request) Successful in 6m37s
CI / images (pull_request) Successful in 3m18s
CI / required (pull_request) Successful in 1s
276 lines
9.8 KiB
TypeScript
276 lines
9.8 KiB
TypeScript
import { TestBed } from '@angular/core/testing';
|
|
import { RuleService, RuleType } from './rule-service';
|
|
import { vi } from 'vitest';
|
|
import { CalendarSettingsService } from './calendar-settings-service';
|
|
import { PlanningDurationService } from './planning-duration.service';
|
|
import { UserInputService } from './user-input-service';
|
|
import { SelectableDayType } from './calendar-service';
|
|
import { createPlanningCalculation } from './leave-planner.service';
|
|
|
|
describe('saved rule migration', () => {
|
|
beforeEach(() => localStorage.clear());
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.unstubAllGlobals();
|
|
TestBed.resetTestingModule();
|
|
localStorage.clear();
|
|
});
|
|
|
|
it('restores every legacy record unchanged between loss prevention and the other five presets', () => {
|
|
const legacy = [
|
|
{
|
|
id: 'personal-window',
|
|
name: 'My fixed window',
|
|
type: RuleType.TIME_WINDOW,
|
|
enabled: true,
|
|
force: 'obligation',
|
|
startDate: '2024-01-10',
|
|
endDate: '2024-01-12',
|
|
placeDays: 2,
|
|
placeType: 'cp',
|
|
},
|
|
{
|
|
id: 'personal-pattern',
|
|
name: 'My disabled pattern',
|
|
type: RuleType.IF_THEN,
|
|
enabled: false,
|
|
dayOfWeek: 6,
|
|
placeDays: 2,
|
|
placeType: 'cp',
|
|
placement: 'around',
|
|
rewardDays: 1,
|
|
rewardType: 'other',
|
|
},
|
|
];
|
|
localStorage.setItem('vacationRules', JSON.stringify(legacy));
|
|
const service = TestBed.inject(RuleService);
|
|
expect(service.rules).toHaveLength(8);
|
|
expect(service.rules.slice(1, 3).map(({ id }) => id)).toEqual([
|
|
'personal-window',
|
|
'personal-pattern',
|
|
]);
|
|
const saved = JSON.parse(localStorage.getItem('vacationRules')!);
|
|
expect(saved.version).toBe(1);
|
|
expect(saved.rules.slice(1, 3)).toEqual(legacy);
|
|
expect(service.rules[2].force).toBe('preference');
|
|
expect(saved.rules[2]).not.toHaveProperty('force');
|
|
});
|
|
|
|
it('gives fresh users the six active presets around the two existing initial rules', () => {
|
|
const service = TestBed.inject(RuleService);
|
|
expect(service.rules.map(({ id }) => id)).toEqual([
|
|
'default-unplaced-at-risk',
|
|
'default-samedi-malin',
|
|
'default-christmas',
|
|
'default-holiday-week-value',
|
|
'default-non-working-day-gain',
|
|
'default-longest-work-sequence',
|
|
'default-short-sequence-deficit',
|
|
'default-supporting-placements',
|
|
]);
|
|
const presets = service.rules.filter(({ type }) => type === RuleType.OPTIMIZATION);
|
|
expect(presets).toHaveLength(6);
|
|
expect(presets.every(({ enabled, force }) => enabled && force === 'preference')).toBe(true);
|
|
});
|
|
|
|
it('migrates an old empty list once and retains editing, order, disabled and deleted choices immediately', () => {
|
|
localStorage.setItem('vacationRules', '[]');
|
|
let service = TestBed.inject(RuleService);
|
|
expect(service.rules).toHaveLength(6);
|
|
const deficit = service.rules[4];
|
|
if (
|
|
deficit.type !== RuleType.OPTIMIZATION ||
|
|
deficit.configuration.measure !== 'work-sequence-deficit'
|
|
)
|
|
throw new Error('Expected the deficit preset');
|
|
service.updateRule({
|
|
...deficit,
|
|
name: 'My three-day deficit',
|
|
configuration: { ...deficit.configuration, thresholdDays: 3 },
|
|
});
|
|
service.reorderRules(4, 0);
|
|
service.toggleRule(deficit.id);
|
|
service.deleteRule('default-holiday-week-value');
|
|
const expected = service.rules;
|
|
TestBed.resetTestingModule();
|
|
service = TestBed.inject(RuleService);
|
|
expect(service.rules).toEqual(expected);
|
|
expect(service.rules[0]).toMatchObject({
|
|
name: 'My three-day deficit',
|
|
enabled: false,
|
|
configuration: { thresholdDays: 3 },
|
|
});
|
|
for (const rule of service.rules) service.deleteRule(rule.id);
|
|
TestBed.resetTestingModule();
|
|
expect(TestBed.inject(RuleService).rules).toEqual([]);
|
|
expect(JSON.parse(localStorage.getItem('vacationRules')!)).toEqual({
|
|
version: 1,
|
|
rules: [],
|
|
});
|
|
});
|
|
|
|
it('avoids colliding with existing custom identities without replacing their records', () => {
|
|
const custom = ['default-unplaced-at-risk', 'default-unplaced-at-risk-2'].map((id) => ({
|
|
id,
|
|
name: id,
|
|
type: RuleType.IF_THEN,
|
|
enabled: false,
|
|
dayOfWeek: 6,
|
|
placeDays: 2,
|
|
placeType: 'cp',
|
|
placement: 'auto',
|
|
rewardDays: 1,
|
|
rewardType: 'other',
|
|
}));
|
|
localStorage.setItem('vacationRules', JSON.stringify(custom));
|
|
const rules = TestBed.inject(RuleService).rules;
|
|
expect(rules[0].id).toBe('default-unplaced-at-risk-3');
|
|
expect(new Set(rules.map(({ id }) => id)).size).toBe(8);
|
|
expect(JSON.parse(localStorage.getItem('vacationRules')!).rules.slice(1, 3)).toEqual(
|
|
custom,
|
|
);
|
|
});
|
|
|
|
it('saves the migrated list and version in one atomic write and retries after a failed save', () => {
|
|
localStorage.setItem('vacationRules', '[]');
|
|
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
const save = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
throw new Error('quota');
|
|
});
|
|
let service = TestBed.inject(RuleService);
|
|
expect(service.rules).toHaveLength(6);
|
|
expect(save).toHaveBeenCalledOnce();
|
|
expect(localStorage.getItem('vacationRules')).toBe('[]');
|
|
service.toggleRule(service.rules[0].id);
|
|
expect(localStorage.getItem('vacationRules')).toBe('[]');
|
|
save.mockRestore();
|
|
TestBed.resetTestingModule();
|
|
service = TestBed.inject(RuleService);
|
|
expect(service.rules).toHaveLength(6);
|
|
expect(service.rules[0].enabled).toBe(true);
|
|
expect(JSON.parse(localStorage.getItem('vacationRules')!).version).toBe(1);
|
|
});
|
|
|
|
it('keeps edits usable and saves the entire current list once storage recovers', () => {
|
|
localStorage.setItem('vacationRules', '[]');
|
|
const service = TestBed.inject(RuleService);
|
|
const prior = localStorage.getItem('vacationRules');
|
|
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
const save = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
|
throw new Error('quota');
|
|
});
|
|
service.toggleRule(service.rules[0].id);
|
|
expect(service.rules[0].enabled).toBe(false);
|
|
expect(localStorage.getItem('vacationRules')).toBe(prior);
|
|
save.mockRestore();
|
|
service.deleteRule(service.rules[1].id);
|
|
const expected = service.rules;
|
|
TestBed.resetTestingModule();
|
|
expect(TestBed.inject(RuleService).rules).toEqual(expected);
|
|
});
|
|
|
|
it.each(['{"version":2,"rules":[]}', '{broken', 'null', '[null]'])(
|
|
'preserves unsupported saved data through subsequent edits: %s',
|
|
(saved) => {
|
|
localStorage.setItem('vacationRules', saved);
|
|
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
const save = vi.spyOn(Storage.prototype, 'setItem');
|
|
const service = TestBed.inject(RuleService);
|
|
service.toggleRule(service.rules[0].id);
|
|
expect(save).not.toHaveBeenCalled();
|
|
expect(localStorage.getItem('vacationRules')).toBe(saved);
|
|
},
|
|
);
|
|
|
|
it('prevents edits from overwriting unknown valid saved data after a read failure', () => {
|
|
const saved = '{"version":1,"rules":[]}';
|
|
localStorage.setItem('vacationRules', saved);
|
|
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
const read = vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
throw new Error('denied');
|
|
});
|
|
const service = TestBed.inject(RuleService);
|
|
read.mockRestore();
|
|
const write = vi.spyOn(Storage.prototype, 'setItem');
|
|
service.toggleRule(service.rules[0].id);
|
|
expect(write).not.toHaveBeenCalled();
|
|
expect(localStorage.getItem('vacationRules')).toBe(saved);
|
|
});
|
|
|
|
it('works in memory when browser storage is unavailable', () => {
|
|
vi.stubGlobal('localStorage', undefined);
|
|
const service = TestBed.inject(RuleService);
|
|
expect(service.rules).toHaveLength(8);
|
|
service.toggleRule(service.rules[0].id);
|
|
expect(service.rules[0].enabled).toBe(false);
|
|
});
|
|
|
|
it.each(['disabled', 'deleted'])(
|
|
'keeps a migrated %s list inactive after reload while preserving declarations and at-risk quantities',
|
|
async (choice) => {
|
|
localStorage.setItem('vacationRules', '[]');
|
|
const service = TestBed.inject(RuleService);
|
|
for (const rule of service.rules) {
|
|
if (choice === 'disabled') service.toggleRule(rule.id);
|
|
else service.deleteRule(rule.id);
|
|
}
|
|
const saved = localStorage.getItem('vacationRules');
|
|
TestBed.resetTestingModule();
|
|
const restored = TestBed.inject(RuleService).rules;
|
|
expect(restored).toHaveLength(choice === 'disabled' ? 6 : 0);
|
|
expect(restored.every(({ enabled }) => !enabled)).toBe(true);
|
|
expect(localStorage.getItem('vacationRules')).toBe(saved);
|
|
vi.stubGlobal('window', undefined);
|
|
const calculation = createPlanningCalculation({
|
|
horizon: { start: '2024-01-02', end: '2024-01-05' },
|
|
balances: [{ id: 'cp', type: 'cp', quantity: 2, expiryDate: '2024-01-04' }],
|
|
declaredDays: [{ date: '2024-01-03', type: 'other' }],
|
|
rules: restored,
|
|
});
|
|
const outcome = await calculation.outcome;
|
|
expect(outcome).toMatchObject({
|
|
kind: 'Plan',
|
|
placements: [],
|
|
declaredDays: [{ date: '2024-01-03', type: 'other' }],
|
|
remainingBalances: [
|
|
{
|
|
id: 'cp',
|
|
quantity: 2,
|
|
classification: 'at-risk',
|
|
reason: { code: 'active-priorities' },
|
|
},
|
|
],
|
|
});
|
|
expect(await calculation.explanation).toMatchObject({
|
|
kind: 'PlanExplanation',
|
|
placements: [],
|
|
});
|
|
},
|
|
);
|
|
|
|
it('migrates before the first duration request and actual calculation', async () => {
|
|
localStorage.setItem('vacationRules', '[]');
|
|
TestBed.inject(CalendarSettingsService).setPlanningDates('2024-01-02', '2024-01-05');
|
|
TestBed.inject(UserInputService).updateBalances(() => [
|
|
{
|
|
id: 'cp',
|
|
type: SelectableDayType.CP,
|
|
numberOfDays: 1,
|
|
availableDate: new Date('2024-01-02'),
|
|
expiryDate: new Date('2024-01-05'),
|
|
},
|
|
]);
|
|
const request = TestBed.inject(PlanningDurationService).request()!;
|
|
expect(request.rules).toHaveLength(6);
|
|
expect(JSON.parse(localStorage.getItem('vacationRules')!).version).toBe(1);
|
|
vi.stubGlobal('window', undefined);
|
|
const calculation = createPlanningCalculation(request);
|
|
const outcome = await calculation.outcome;
|
|
expect(outcome).toMatchObject({
|
|
kind: 'Plan',
|
|
metrics: { unplacedAtRisk: 0 },
|
|
placements: [{ balanceId: 'cp' }],
|
|
});
|
|
});
|
|
});
|