import { TestBed } from '@angular/core/testing'; import { provideNativeDateAdapter } from '@angular/material/core'; import { vi } from 'vitest'; import { emptyApartmentLegalProfile } from '../../services/apartment.service'; import { emptyLeaseLegalTerms } from '../../services/lease.service'; import { LeaseForm } from './lease-form'; describe('LeaseForm', () => { it('derives and serializes the standard lease end date', () => { TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] }); const fixture = TestBed.createComponent(LeaseForm); fixture.componentRef.setInput('apartments', []); fixture.componentRef.setInput('tenants', []); const component = fixture.componentInstance; component.leaseModel.set({ apartmentId: 'apartment-1', tenantIds: ['tenant-1'], leaseType: 'STANDARD', startDate: '2026-09-01', monthlyRent: 850, monthlyCharges: 50, securityDeposit: 850, dueDay: 5, chargeMethod: 'FLAT_RATE', indexFlatRate: true, legalTerms: emptyLeaseLegalTerms(), }); fixture.detectChanges(); let submitted: unknown; component.submitted.subscribe((lease) => (submitted = lease)); component.save(); expect(submitted).toEqual({ apartmentId: 'apartment-1', tenantIds: ['tenant-1'], guarantors: [], specialClauses: [], legalTerms: emptyLeaseLegalTerms(), leaseType: 'STANDARD', startDate: '2026-09-01', endDate: '2027-08-31', monthlyRent: 850, monthlyCharges: 50, securityDeposit: 850, dueDay: 5, chargeMethod: 'FLAT_RATE', indexFlatRate: true, }); expect(fixture.nativeElement.querySelector('#endDate')).toBeNull(); expect(fixture.nativeElement.textContent).toContain('reconductible tacitement'); }); it('allows overlapping draft dates but leaves active dates unavailable', () => { TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] }); const fixture = TestBed.createComponent(LeaseForm); fixture.componentRef.setInput('apartments', []); fixture.componentRef.setInput('tenants', []); fixture.componentRef.setInput('leases', [ { id: 'draft-1', apartmentId: 'apartment-1', tenantIds: [], guarantors: [], specialClauses: [], leaseType: 'STANDARD', startDate: '2026-01-01', endDate: '2026-01-31', monthlyRent: 800, monthlyCharges: 50, securityDeposit: 800, dueDay: 5, chargeMethod: 'FLAT_RATE', indexFlatRate: false, status: 'DRAFT', }, { id: 'active-1', apartmentId: 'apartment-1', tenantIds: [], guarantors: [], specialClauses: [], leaseType: 'STANDARD', startDate: '2026-02-01', endDate: '2026-02-28', monthlyRent: 800, monthlyCharges: 50, securityDeposit: 800, dueDay: 5, chargeMethod: 'FLAT_RATE', indexFlatRate: false, status: 'ACTIVE', }, ]); fixture.componentInstance.leaseModel.update((lease) => ({ ...lease, apartmentId: 'apartment-1', })); expect(fixture.componentInstance.dateFilter('2026-01-15')).toBe(true); expect(fixture.componentInstance.dateFilter('2026-02-15')).toBe(false); }); it('loads a draft for editing', () => { TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] }); const fixture = TestBed.createComponent(LeaseForm); fixture.componentRef.setInput('apartments', []); fixture.componentRef.setInput('tenants', []); fixture.componentRef.setInput('lease', { id: 'draft-1', apartmentId: 'apartment-1', tenantIds: ['tenant-1'], guarantors: [], specialClauses: [ { catalogId: null, parameters: {}, customText: 'Animaux admis avec accord.' }, ], leaseType: 'STUDENT', startDate: '2026-09-01', endDate: '2099-12-31', monthlyRent: 850, monthlyCharges: 50, securityDeposit: 850, dueDay: 5, chargeMethod: 'PROVISION', indexFlatRate: false, status: 'DRAFT', }); fixture.detectChanges(); expect(fixture.componentInstance.leaseModel().apartmentId).toBe('apartment-1'); expect(fixture.componentInstance.leaseModel().leaseType).toBe('STUDENT'); expect(fixture.componentInstance.leaseDuration().endDate).toBe('2027-05-31'); expect(fixture.componentInstance.specialClauses()).toEqual([ { catalogId: null, parameters: {}, customText: 'Animaux admis avec accord.' }, ]); expect(fixture.nativeElement.textContent).toContain('Modifier le brouillon'); }); it('falls back to a standard duration when editing a legacy draft', () => { TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] }); const fixture = TestBed.createComponent(LeaseForm); fixture.componentRef.setInput('apartments', []); fixture.componentRef.setInput('tenants', []); fixture.componentRef.setInput('lease', { apartmentId: 'apartment-1', tenantIds: ['tenant-1'], guarantors: [], startDate: '2026-09-01', endDate: '2027-08-31', monthlyRent: 850, monthlyCharges: 50, securityDeposit: 850, dueDay: 5, chargeMethod: 'FLAT_RATE', indexFlatRate: false, status: 'DRAFT', }); fixture.detectChanges(); expect(fixture.componentInstance.leaseModel().leaseType).toBe('STANDARD'); expect(fixture.componentInstance.leaseDuration().endDate).toBe('2027-08-31'); expect(fixture.componentInstance.specialClauses()).toEqual([]); }); it('clamps a student duration at month end and announces that it is not renewable', () => { TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] }); const fixture = TestBed.createComponent(LeaseForm); fixture.componentRef.setInput('apartments', []); fixture.componentRef.setInput('tenants', []); fixture.componentInstance.leaseModel.update((lease) => ({ ...lease, leaseType: 'STUDENT', startDate: '2026-05-31', })); fixture.detectChanges(); expect(fixture.componentInstance.leaseDuration().endDate).toBe('2027-02-27'); expect(fixture.nativeElement.textContent).toContain('non reconductible'); }); it('keeps guarantors in the lease form module', () => { TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] }); const fixture = TestBed.createComponent(LeaseForm); fixture.componentRef.setInput('apartments', []); fixture.componentRef.setInput('tenants', []); fixture.componentInstance.addGuarantor({ firstName: 'Alex', lastName: 'Durand', email: 'alex@example.com', phone: '', address: '', }); expect(fixture.componentInstance.guarantors()).toHaveLength(1); }); it('uses the lease type context unless several tenants make it a colocation', () => { TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] }); const fixture = TestBed.createComponent(LeaseForm); fixture.componentRef.setInput('apartments', []); fixture.componentRef.setInput('tenants', []); fixture.componentInstance.leaseModel.update((lease) => ({ ...lease, leaseType: 'STUDENT', tenantIds: ['tenant-1'], })); expect(fixture.componentInstance.clauseContext()).toBe('STUDENT'); fixture.componentInstance.leaseModel.update((lease) => ({ ...lease, tenantIds: ['tenant-1', 'tenant-2'], })); expect(fixture.componentInstance.clauseContext()).toBe('COLOCATION'); }); it('blocks submission when a custom clause is empty', () => { TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] }); const fixture = TestBed.createComponent(LeaseForm); fixture.componentRef.setInput('apartments', []); fixture.componentRef.setInput('tenants', []); const component = fixture.componentInstance; component.leaseModel.set({ apartmentId: 'apartment-1', tenantIds: ['tenant-1'], leaseType: 'STANDARD', startDate: '2026-09-01', monthlyRent: 850, monthlyCharges: 50, securityDeposit: 850, dueDay: 5, chargeMethod: 'FLAT_RATE', indexFlatRate: false, legalTerms: emptyLeaseLegalTerms(), }); component.specialClauses.set([{ catalogId: null, parameters: {}, customText: ' ' }]); const submitted = vi.fn(); component.submitted.subscribe(submitted); fixture.detectChanges(); component.save(); fixture.detectChanges(); expect(submitted).not.toHaveBeenCalled(); expect(fixture.nativeElement.textContent).toContain('Renseignez le texte'); }); it('shows the finalization warning only when an IRL clause concerns an F or G apartment', () => { TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] }); const fixture = TestBed.createComponent(LeaseForm); const apartment = { id: 'apartment-1', name: 'T2', address: '1 rue de Paris', purchasePrice: 250000, description: '', legalProfile: { ...emptyApartmentLegalProfile(), energyRating: 'F' as const }, }; fixture.componentRef.setInput('apartments', [apartment]); fixture.componentRef.setInput('tenants', []); fixture.componentInstance.leaseModel.update((lease) => ({ ...lease, apartmentId: apartment.id, legalTerms: { ...lease.legalTerms, rentRevision: { enabled: true, annualRevisionDate: '2026-09-01', referenceQuarter: 2, referenceYear: 2026, seriesId: '001515333', }, }, })); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('à confirmer lors de la finalisation'); fixture.componentRef.setInput('apartments', [ { ...apartment, legalProfile: { ...apartment.legalProfile, energyRating: 'D' as const } }, ]); fixture.detectChanges(); expect(fixture.nativeElement.textContent).not.toContain('à confirmer lors de la finalisation'); }); it('clears an unfinished rent supplement when the reference-rent scheme is disabled', async () => { TestBed.configureTestingModule({ providers: [provideNativeDateAdapter()] }); const fixture = TestBed.createComponent(LeaseForm); fixture.componentRef.setInput('apartments', []); fixture.componentRef.setInput('tenants', []); fixture.componentRef.setInput('lease', { apartmentId: 'apartment-1', tenantIds: ['tenant-1'], guarantors: [], specialClauses: [], legalTerms: { ...emptyLeaseLegalTerms(), rentControl: { evolutionCapApplicable: false, referenceRentApplicable: true, baseReferenceRent: 20, increasedReferenceRent: 24, supplement: null, supplementJustification: '', }, }, leaseType: 'STANDARD', startDate: '2026-09-01', endDate: '2027-08-31', monthlyRent: 850, monthlyCharges: 50, securityDeposit: 850, dueDay: 5, chargeMethod: 'FLAT_RATE', indexFlatRate: false, status: 'DRAFT', }); fixture.detectChanges(); await fixture.whenStable(); const supplementLabel = [...fixture.nativeElement.querySelectorAll('mat-label')].find( (label: Element) => label.textContent?.trim() === 'Complément de loyer', ); const supplementInput = supplementLabel?.closest('mat-form-field')?.querySelector('input'); if (!supplementInput) throw new Error('Champ complément de loyer introuvable.'); supplementInput.value = '-100'; supplementInput.dispatchEvent(new Event('input')); await fixture.whenStable(); const referenceRentCheckbox = [...fixture.nativeElement.querySelectorAll('mat-checkbox')] .find((checkbox: Element) => checkbox.textContent?.includes('loyer de référence')) ?.querySelector('input'); if (!referenceRentCheckbox) throw new Error('Option loyer de référence introuvable.'); referenceRentCheckbox.click(); await fixture.whenStable(); fixture.detectChanges(); const submitted = vi.fn(); fixture.componentInstance.submitted.subscribe(submitted); fixture.nativeElement.querySelector('button[form=lease-details]').click(); expect(submitted).toHaveBeenCalledWith( expect.objectContaining({ legalTerms: expect.objectContaining({ rentControl: expect.objectContaining({ referenceRentApplicable: false, supplement: null, supplementJustification: '', }), }), }), ); }); });