import { provideZonelessChangeDetection } from '@angular/core'; import { TestBed, type ComponentFixture } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { MatExpansionPanel } from '@angular/material/expansion'; import { vi } from '../../../testing/vitest-compat'; import { RuleService, RuleType } from '../../services/rule-service'; import { TranslationService } from '../../services/translation-service'; import { calculatePlanningInternal } from '../../services/leave-planner.core'; import { OPTIMIZATION_PRESETS } from '../../services/planning-objectives'; import { RulesPanel } from './rules-panel'; describe('work-sequence rule editor and reload', () => { let fixture: ComponentFixture; let component: RulesPanel; async function choose(label: string, text: string) { const fields: HTMLElement[] = Array.from( fixture.nativeElement.querySelectorAll('mat-form-field'), ); const select = fields .find((field) => field.querySelector('mat-label')?.textContent?.trim() === label) ?.querySelector('mat-select'); if (!select) throw new Error(`Missing select ${label}`); select.click(); await fixture.whenStable(); const option = Array.from(document.querySelectorAll('mat-option')).find( (candidate) => candidate.textContent?.trim() === text, ); if (!option) throw new Error(`Missing option ${text}`); option.click(); await fixture.whenStable(); document.querySelector('.cdk-overlay-backdrop')?.click(); await fixture.whenStable(); } async function setInput(selector: string, value: string) { const input: HTMLInputElement = fixture.nativeElement.querySelector(selector); input.value = value; input.dispatchEvent(new Event('input')); await fixture.whenStable(); } beforeEach(async () => { localStorage.clear(); localStorage.setItem('vacationRules', '{"version":1,"rules":[]}'); vi.setSystemTime(new Date('2024-01-01T12:00:00Z')); await TestBed.configureTestingModule({ imports: [RulesPanel], providers: [provideZonelessChangeDetection()], }).compileComponents(); TestBed.inject(TranslationService).setLanguage('fr'); fixture = TestBed.createComponent(RulesPanel); component = fixture.componentInstance; fixture.detectChanges(); fixture.debugElement.query(By.directive(MatExpansionPanel)).componentInstance.open(); }); afterEach(() => { localStorage.clear(); vi.useRealTimers(); }); it('creates threshold five, edits it to three, and restores its effect, order and activation after reload', async () => { component.startAddNew(RuleType.OPTIMIZATION); await fixture.whenStable(); await setInput('input:not([type="number"])', 'Mes reprises'); await choose('Éléments du planning', 'Séquences de travail'); await choose('Mesure', 'Déficit des séquences de travail'); await choose('Conditions (toutes doivent être vérifiées)', 'Internes'); await choose('Conditions (toutes doivent être vérifiées)', 'Non vides'); const threshold: HTMLInputElement = fixture.nativeElement.querySelector('input[type="number"]'); expect(threshold.value).toBe('5'); expect(threshold.min).toBe('1'); expect(threshold.step).toBe('1'); expect(fixture.nativeElement.textContent).toContain('Seuil de préférence en jours ouvrés'); component.saveEdit(); await fixture.whenStable(); const rule = component.rules[0]; expect(rule).toMatchObject({ configuration: OPTIMIZATION_PRESETS.shortSequenceDeficit }); expect(component.getRuleSummary(rule)).toContain( 'Déficit des séquences de travail (jours ouvrés)', ); const request = { horizon: { start: '2024-01-08', end: '2024-01-13' }, balances: [{ id: 'cp', type: 'cp', quantity: 3, expiryDate: '2024-02-01' }], declaredDays: ['2024-01-08', '2024-01-12'].map((date) => ({ date, type: 'other' })), }; expect(calculatePlanningInternal({ ...request, rules: component.rules })).toMatchObject({ kind: 'Plan', placements: [{ date: '2024-01-09' }, { date: '2024-01-10' }, { date: '2024-01-11' }], }); component.startEdit(rule, new Event('click')); await fixture.whenStable(); await setInput('input[type="number"]', '3'); component.saveEdit(); await fixture.whenStable(); expect(component.getRuleSummary(component.rules[0])).toContain( 'seuil de préférence : 3 jours ouvrés', ); expect(calculatePlanningInternal({ ...request, rules: component.rules })).toMatchObject({ kind: 'Plan', placements: [], }); component.startAddNew(RuleType.OPTIMIZATION); await fixture.whenStable(); await setInput('input:not([type="number"])', 'Ma séquence maximale'); await choose('Éléments du planning', 'Séquences de travail'); await choose('Agrégation', 'Maximum'); component.saveEdit(); await fixture.whenStable(); const maximum = component.rules[1]; expect(maximum).toMatchObject({ configuration: OPTIMIZATION_PRESETS.longestWorkSequence }); component.toggleRule(maximum.id); await fixture.whenStable(); const moveUp: HTMLButtonElement = fixture.nativeElement .querySelectorAll('.rule-actions')[1] .querySelector('button'); moveUp.focus(); moveUp.click(); await fixture.whenStable(); const saved = component.rules; await new Promise((resolve) => setTimeout(resolve, 350)); TestBed.resetTestingModule(); const restored = TestBed.inject(RuleService).rules; expect(restored).toEqual(saved); expect(restored.map(({ id }) => id)).toEqual([maximum.id, rule.id]); expect(restored[0].enabled).toBe(false); expect(restored[1]).toMatchObject({ name: 'Mes reprises', enabled: true, configuration: { thresholdDays: 3 }, }); expect(calculatePlanningInternal({ ...request, rules: restored })).toMatchObject({ kind: 'Plan', placements: [], }); }); it('keeps the saved threshold and shows an accessible error for fractional input', async () => { component.startAddNew(RuleType.OPTIMIZATION); component.changeOptimizationMeasure('work-sequence-deficit'); await fixture.whenStable(); await setInput('input[type="number"]', '1.5'); component.saveEdit(); await fixture.whenStable(); expect(component.rules).toEqual([]); expect(fixture.nativeElement.querySelector('[role="alert"]').textContent).toContain( 'seuil entier positif', ); expect(fixture.nativeElement.querySelector('input[type="number"]').value).toBe('1.5'); }); it('distinguishes aggregation units from the working-day threshold', () => { const translations = TestBed.inject(TranslationService); expect( translations.optimizationSummary(OPTIMIZATION_PRESETS.longestWorkSequence), ).toContain('Longueur des séquences de travail (jours ouvrés)'); expect( translations.optimizationSummary({ ...OPTIMIZATION_PRESETS.shortSequenceDeficit, aggregation: 'count', }), ).toContain('Déficit des séquences de travail (Nombre)'); expect( translations.optimizationSummary({ ...OPTIMIZATION_PRESETS.shortSequenceDeficit, aggregation: 'count', }), ).toContain('seuil de préférence : 5 jours ouvrés'); }); });