Files
lmnp-organizer/frontend/src/app/components/lease-form/lease-form.spec.ts
T

356 lines
12 KiB
TypeScript

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: '',
}),
}),
}),
);
});
});