Files
vac-optimizer/src/app/services/rule-service.spec.ts
T
nessarandClaude Opus 5.5 94d57add2c test: run the suite and benchmark with Bun's native test runner
Replace Vitest with bun test across the whole suite. A preload compiles
components through Angular's JIT transform with inlined templates, sets
up jsdom, TestBed and zoneless change detection, and a small shim covers
the Vitest APIs bun:test lacks. Files run isolated, real-worker specs
spawn the running Bun, and line coverage from lcov is converted to the
existing HTML and Cobertura reports. The CI benchmark replaces its heap
cap with a container memory limit. Vitest, Jest, Nx and their Vite
configuration are removed.

Refs #291

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:40:42 +02:00

186 lines
5.2 KiB
TypeScript

import { TestBed } from '@angular/core/testing';
import {
RuleService,
RuleType,
IfThenRule,
TimeWindowRule,
OptimizationRule,
} from './rule-service';
import { SelectableDayType } from './calendar-service';
import { OPTIMIZATION_PRESETS } from './planning-objectives';
import { vi } from '../../testing/vitest-compat';
describe('RuleService', () => {
let service: RuleService;
beforeEach(() => {
// Clear localStorage before each test to ensure a clean state
localStorage.clear();
TestBed.configureTestingModule({});
service = TestBed.inject(RuleService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should initialize with default rules', () => {
const rules = service.rules;
expect(rules.length).toBe(8);
expect(rules[0].id).toBe('default-unplaced-at-risk');
expect(rules[1].id).toBe('default-samedi-malin');
expect(rules[2].id).toBe('default-christmas');
});
it('should parse dates correctly for TimeWindowRules', () => {
const rules = service.rules;
const christmasRule = rules.find((r) => r.type === RuleType.TIME_WINDOW) as TimeWindowRule;
expect(christmasRule).toBeDefined();
expect(christmasRule.startDate instanceof Date).toBe(true);
expect(christmasRule.endDate instanceof Date).toBe(true);
});
it('migrates legacy force values without changing rule order or dates', () => {
TestBed.resetTestingModule();
localStorage.setItem(
'vacationRules',
JSON.stringify([
{
id: 'legacy-first',
name: 'First',
type: RuleType.TIME_WINDOW,
enabled: true,
startDate: '2024-02-01',
endDate: '2024-02-05',
placeDays: 1,
placeType: SelectableDayType.CP,
},
{
id: 'legacy-second',
name: 'Second',
type: RuleType.TIME_WINDOW,
enabled: true,
force: 'obligation',
startDate: '2024-03-01',
endDate: '2024-03-02',
placeDays: 1,
placeType: SelectableDayType.RTT,
},
]),
);
TestBed.configureTestingModule({});
service = TestBed.inject(RuleService);
expect(service.rules.slice(1, 3).map((rule) => rule.id)).toEqual([
'legacy-first',
'legacy-second',
]);
expect(service.rules[1].force).toBe('preference');
const first = service.rules[1] as TimeWindowRule;
expect(first.startDate.toISOString().slice(0, 10)).toBe('2024-02-01');
});
it('should add a new rule', () => {
const newRule: IfThenRule = {
id: 'test-rule',
name: 'Test Rule',
type: RuleType.IF_THEN,
enabled: true,
dayOfWeek: 1, // Monday
placeDays: 1,
placeType: SelectableDayType.RTT,
placement: 'after',
rewardDays: 0,
rewardType: SelectableDayType.OTHER,
};
service.addRule(newRule);
const rules = service.rules;
expect(rules.length).toBe(9);
expect(rules.find((r) => r.id === 'test-rule')).toBeDefined();
});
it('should update an existing rule', () => {
const rules = service.rules;
const targetRule = { ...rules.find((rule) => rule.type === RuleType.IF_THEN)! };
targetRule.name = 'Updated Name';
service.updateRule(targetRule);
const updatedRules = service.rules;
const updated = updatedRules.find((r) => r.id === targetRule.id);
expect(updated?.name).toBe('Updated Name');
});
it('should delete a rule', () => {
const rules = service.rules;
const idToDelete = rules[0].id;
service.deleteRule(idToDelete);
const updatedRules = service.rules;
expect(updatedRules.length).toBe(7);
expect(updatedRules.find((r) => r.id === idToDelete)).toBeUndefined();
});
it('should toggle rule enabled state', () => {
const targetId = service.rules[0].id;
const initialState = service.rules[0].enabled;
service.toggleRule(targetId);
expect(service.rules.find((r) => r.id === targetId)?.enabled).toBe(!initialState);
});
it('should reorder rules correctly', () => {
const rulesBefore = service.rules;
const firstRuleId = rulesBefore[0].id;
const secondRuleId = rulesBefore[1].id;
// Move first rule to second position
service.reorderRules(0, 1);
const rulesAfter = service.rules;
expect(rulesAfter[0].id).toBe(secondRuleId);
expect(rulesAfter[1].id).toBe(firstRuleId);
});
it('restores the full optimization identity, parameters, enabled state and position', () => {
vi.useFakeTimers();
try {
TestBed.tick();
const optimization: OptimizationRule = {
id: 'personal-loss',
name: 'My loss priority',
type: RuleType.OPTIMIZATION,
enabled: true,
force: 'preference',
configuration: structuredClone(OPTIMIZATION_PRESETS.unplacedAtRisk),
};
service.addRule(optimization);
optimization.configuration.conditions = [];
service.reorderRules(
service.rules.findIndex(({ id }) => id === optimization.id),
0,
);
service.toggleRule('personal-loss');
const saved = service.rules;
TestBed.tick();
vi.advanceTimersByTime(350);
TestBed.resetTestingModule();
service = TestBed.inject(RuleService);
expect(service.rules).toEqual(saved);
expect(service.rules[0]).toMatchObject({
id: 'personal-loss',
name: 'My loss priority',
enabled: false,
configuration: OPTIMIZATION_PRESETS.unplacedAtRisk,
});
service.deleteRule('personal-loss');
expect(service.rules.some((rule) => rule.id === 'personal-loss')).toBe(false);
} finally {
vi.useRealTimers();
}
});
});