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

560 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { vi } from 'vitest';
import { ApartmentLegalProfileForm } from '../apartment-legal-profile-form/apartment-legal-profile-form';
import { ApartmentForm } from './apartment-form';
import { emptyApartmentLegalProfile } from '../../services/apartment.service';
describe('ApartmentForm', () => {
const completeLegalProfile = {
taxIdentifier: '751150001234',
buildingType: 'APARTMENT_BUILDING' as const,
ownershipRegime: 'Copropriété',
constructionPeriod: '1990_2005' as const,
habitableSurface: 42,
equipment: 'Cuisine équipée',
heating: 'Individuel électrique',
hotWater: 'Ballon électrique',
energyRating: 'D' as const,
estimatedAnnualEnergyCost: 850,
estimatedAnnualEnergyCostReferenceYear: 2021,
destination: 'Habitation',
privateAccessories: 'Cave',
commonAreas: 'Local vélos',
internetAccess: 'Fibre',
departmentCode: '75',
};
it('initializes its fields from the apartment being edited', () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('apartment', {
id: 'apt-1',
name: 'T2 Paris 15',
address: '45 rue des Entrepreneurs, 75015 Paris',
purchasePrice: 250000,
description: 'Cinquième étage',
privateAreas: [],
legalProfile: completeLegalProfile,
});
fixture.detectChanges();
const form = fixture.componentInstance;
expect(form.apartmentModel().name).toBe('T2 Paris 15');
expect(form.apartmentModel().purchasePrice).toBe(250000);
expect(form.apartmentModel().legalProfile).toEqual(completeLegalProfile);
});
it('shows a validation error instead of emitting an incomplete apartment', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.save();
expect(form.error()).toContain('Le nom de l’appartement est obligatoire.');
expect(saved).not.toHaveBeenCalled();
});
it('shows every invalid section and focuses the first control in form order', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.detectChanges();
await fixture.whenStable();
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: ' ',
address: ' ',
purchasePrice: 0,
description: '',
privateAreas: [
{ id: 'area-1', family: 'PIECE_PRINCIPALE', subtype: 'OTHER', label: ' ' },
],
legalProfile: {
...emptyApartmentLegalProfile(),
habitableSurface: -1,
departmentCode: '20',
},
});
form.save();
fixture.detectChanges();
await fixture.whenStable();
const text = fixture.nativeElement.textContent;
expect(saved).not.toHaveBeenCalled();
expect(text).toContain('Le nom de l’appartement est obligatoire.');
expect(text).toContain('Le prix d’acquisition doit être strictement positif.');
expect(text).toContain('L’adresse complète est obligatoire.');
expect(text).toContain('Saisissez un département français');
expect(text).toContain('La surface habitable doit être positive ou nulle.');
expect(text).toContain('Le libellé est obligatoire.');
expect(document.activeElement).toBe(fixture.nativeElement.querySelector('#name'));
});
it('clears its client summary when a field is corrected', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.detectChanges();
await fixture.whenStable();
const form = fixture.componentInstance;
form.save();
fixture.nativeElement.querySelector('#name').dispatchEvent(new Event('input'));
expect(form.error()).toBe('');
});
it('focuses the first mapped server error after a failed save', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.detectChanges();
await fixture.whenStable();
fixture.componentRef.setInput('saveFieldErrors', [
{ field: 'address', code: 'not_blank' },
{ field: 'legalProfile.departmentCodeValid', code: 'assert_true' },
{ field: 'privateAreas[0].label', code: 'not_blank' },
]);
fixture.detectChanges();
await fixture.whenStable();
expect(document.activeElement).toBe(fixture.nativeElement.querySelector('#address'));
});
it('rejects whitespace-only fields before the backend does', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: ' ',
address: ' ',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: emptyApartmentLegalProfile(),
});
form.save();
expect(form.nameError()).toBe('Le nom de l’appartement est obligatoire.');
expect(form.addressError()).toBe('L’adresse complète est obligatoire.');
expect(saved).not.toHaveBeenCalled();
});
it('accepts every strictly positive purchase price, including a sub-euro value', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 0.5,
description: '',
privateAreas: [],
legalProfile: emptyApartmentLegalProfile(),
});
form.save();
expect(saved).toHaveBeenCalledWith(expect.objectContaining({ purchasePrice: 0.5 }));
});
it('keeps invalid private-area cards visible, shows their inline error, and focuses the label', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.detectChanges();
await fixture.whenStable();
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: 'Studio',
address: '1 rue du Test',
purchasePrice: 1,
description: '',
privateAreas: [
{ id: 'area-1', family: 'PIECE_PRINCIPALE', subtype: 'OTHER', label: ' ' },
],
legalProfile: emptyApartmentLegalProfile(),
});
fixture.detectChanges();
form.save();
fixture.detectChanges();
await fixture.whenStable();
expect(saved).not.toHaveBeenCalled();
expect(form.error()).toContain('Renseignez le libellé de chaque espace privatif.');
expect(fixture.nativeElement.querySelectorAll('mat-card')).toHaveLength(1);
expect(fixture.nativeElement.textContent).toContain('Le libellé est obligatoire.');
expect(document.activeElement).toBe(
fixture.nativeElement.querySelector('.private-area-label'),
);
});
it('forwards only the edited private-area server error path', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('saveFieldErrors', [
{ field: 'privateAreas[0].label', code: 'not_blank' },
{ field: 'privateAreas[0].subtypeCompatibleWithFamily', code: 'assert_true' },
]);
fixture.detectChanges();
await fixture.whenStable();
const changed = vi.fn();
fixture.componentInstance.fieldChanged.subscribe(changed);
fixture.componentInstance.apartmentModel.set({
name: 'Studio',
address: '1 rue du Test',
purchasePrice: 1,
description: '',
privateAreas: [
{ id: 'area-1', family: 'PIECE_PRINCIPALE', subtype: 'OTHER', label: 'Séjour' },
],
legalProfile: emptyApartmentLegalProfile(),
});
fixture.detectChanges();
fixture.nativeElement
.querySelector('.private-area-label')
.dispatchEvent(new Event('input'));
expect(changed).toHaveBeenCalledTimes(1);
expect(changed).toHaveBeenCalledWith('privateAreas[0].label');
});
it('shows backend core errors beside their controls and clears one when it is edited', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const changed = vi.fn();
form.fieldChanged.subscribe(changed);
fixture.componentRef.setInput('saveFieldErrors', [
{ field: 'name', code: 'not_blank' },
{ field: 'address', code: 'not_blank' },
]);
fixture.detectChanges();
await fixture.whenStable();
expect(form.nameError()).toBe('Le nom de l’appartement est obligatoire.');
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('.field-error')[0].textContent).toContain(
'Le nom de l’appartement est obligatoire.',
);
expect(fixture.nativeElement.querySelectorAll('.field-error')[1].textContent).toContain(
'L’adresse complète est obligatoire.',
);
fixture.nativeElement.querySelector('input#name').dispatchEvent(new Event('input'));
expect(changed).toHaveBeenCalledWith('name');
});
it('keeps unknown server validation codes in the generic save alert', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('saveError', 'Veuillez corriger les champs signalés.');
fixture.componentRef.setInput('saveFieldErrors', [{ field: 'name', code: 'unknown' }]);
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('.field-error')).toBeNull();
expect(fixture.nativeElement.querySelector('.alert-error').textContent).toContain(
'Veuillez corriger les champs signalés.',
);
});
it('keeps malformed private-area server paths in the generic alert', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('saveError', 'Veuillez corriger les champs signalés.');
fixture.componentRef.setInput('saveFieldErrors', [
{ field: 'privateAreas[].label', code: 'not_blank' },
{ field: 'privateAreas[-1].label', code: 'not_blank' },
]);
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.alert-error').textContent).toContain(
'Veuillez corriger les champs signalés.',
);
});
it('keeps only unknown server validation failures in the generic alert', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('saveError', 'Veuillez corriger les champs signalés.');
fixture.componentRef.setInput('saveFieldErrors', [{ field: 'name', code: 'not_blank' }]);
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.alert-error')).toBeNull();
expect(fixture.nativeElement.textContent).toContain(
'Le nom de l’appartement est obligatoire.',
);
});
it('emits a trimmed apartment draft when valid', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: ' T2 Paris 15 ',
address: ' 45 rue des Entrepreneurs ',
purchasePrice: 250000,
description: ' Cinquième étage ',
privateAreas: [],
legalProfile: {
...completeLegalProfile,
taxIdentifier: ' 751150001234 ',
equipment: ' Cuisine équipée ',
departmentCode: ' 2a ',
},
});
form.save();
expect(saved).toHaveBeenCalledWith({
id: undefined,
name: 'T2 Paris 15',
address: '45 rue des Entrepreneurs',
purchasePrice: 250000,
description: 'Cinquième étage',
privateAreas: [],
legalProfile: {
...completeLegalProfile,
taxIdentifier: '751150001234',
equipment: 'Cuisine équipée',
departmentCode: '2A',
},
});
});
it('disables the whole form and preserves a save error while pending', () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('saving', true);
fixture.componentRef.setInput('saveError', 'Impossible d’enregistrer cet appartement.');
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('fieldset').disabled).toBe(true);
expect(fixture.nativeElement.querySelector('[role=status]').closest('fieldset')).toBeNull();
expect(fixture.nativeElement.textContent).toContain('Enregistrement…');
expect(fixture.nativeElement.textContent).toContain(
'Impossible d’enregistrer cet appartement.',
);
});
it('preserves entered values after a save fails', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.detectChanges();
await fixture.whenStable();
const form = fixture.componentInstance;
form.apartmentModel.set({
name: 'T2 rénové',
address: '45 rue des Entrepreneurs',
purchasePrice: 265000,
description: '',
privateAreas: [],
legalProfile: emptyApartmentLegalProfile(),
});
fixture.componentRef.setInput('saveError', 'Impossible d’enregistrer cet appartement.');
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('input#name').value).toBe('T2 rénové');
expect(fixture.nativeElement.querySelector('input#address').value).toBe(
'45 rue des Entrepreneurs',
);
expect(fixture.nativeElement.textContent).toContain(
'Impossible d’enregistrer cet appartement.',
);
});
it('rejects negative legal-profile numbers', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: { ...emptyApartmentLegalProfile(), habitableSurface: -1 },
});
form.save();
expect(form.error()).toContain('surface habitable');
expect(saved).not.toHaveBeenCalled();
});
it('accepts zero legal-profile numbers with the first valid reference year', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: {
...emptyApartmentLegalProfile(),
habitableSurface: 0,
estimatedAnnualEnergyCost: 0,
estimatedAnnualEnergyCostReferenceYear: 1900,
},
});
form.save();
expect(saved).toHaveBeenCalledWith(
expect.objectContaining({
legalProfile: expect.objectContaining({
habitableSurface: 0,
estimatedAnnualEnergyCost: 0,
estimatedAnnualEnergyCostReferenceYear: 1900,
}),
}),
);
});
it('rejects an out-of-range energy reference year when an estimated cost is provided', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: {
...emptyApartmentLegalProfile(),
estimatedAnnualEnergyCost: 850,
estimatedAnnualEnergyCostReferenceYear: 1800,
},
});
form.save();
expect(form.error()).toContain('1900 et 2100');
expect(saved).not.toHaveBeenCalled();
});
it('rejects an invalid department code inline', () => {
const fixture = TestBed.createComponent(ApartmentForm);
const form = fixture.componentInstance;
const saved = vi.fn();
form.saved.subscribe(saved);
form.apartmentModel.set({
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: { ...emptyApartmentLegalProfile(), departmentCode: '20' },
});
form.save();
expect(form.error()).toContain('département français');
expect(saved).not.toHaveBeenCalled();
});
it('shows known legal-profile server errors next to their controls', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('saveFieldErrors', [
{ field: 'legalProfile.departmentCodeValid', code: 'assert_true' },
]);
const changed = vi.fn();
fixture.componentInstance.fieldChanged.subscribe(changed);
fixture.detectChanges();
await fixture.whenStable();
expect(
fixture.debugElement
.query(By.directive(ApartmentLegalProfileForm))
.componentInstance.departmentCodeError(),
).toContain('Saisissez un département français');
fixture.nativeElement.querySelector('#departmentCode').dispatchEvent(new Event('input'));
expect(changed).toHaveBeenCalledWith('legalProfile.departmentCodeValid');
});
it('turns an emptied nullable number field back into null', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.detectChanges();
await fixture.whenStable();
const surfaceInput: HTMLInputElement =
fixture.nativeElement.querySelector('input#habitableSurface');
surfaceInput.value = '42';
surfaceInput.dispatchEvent(new Event('input'));
surfaceInput.value = '';
surfaceInput.dispatchEvent(new Event('input'));
await fixture.whenStable();
expect(fixture.componentInstance.apartmentModel().legalProfile.habitableSurface).toBeNull();
});
it('asks for the energy reference year when an estimated cost is provided', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('apartment', {
id: 'apartment-1',
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: {
...emptyApartmentLegalProfile(),
estimatedAnnualEnergyCost: 850,
},
});
fixture.detectChanges();
await fixture.whenStable();
const saved = vi.fn();
fixture.componentInstance.saved.subscribe(saved);
fixture.nativeElement.querySelector('button[type=submit]').click();
fixture.detectChanges();
expect(saved).not.toHaveBeenCalled();
expect(fixture.nativeElement.textContent).toContain(
"Renseignez l'année de référence du coût énergétique.",
);
});
it('clears a stale energy reference year that has no estimated cost', async () => {
const fixture = TestBed.createComponent(ApartmentForm);
fixture.componentRef.setInput('apartment', {
id: 'apartment-1',
name: 'T2',
address: '1 rue de Paris',
purchasePrice: 250000,
description: '',
privateAreas: [],
legalProfile: {
...emptyApartmentLegalProfile(),
estimatedAnnualEnergyCostReferenceYear: 1800,
},
});
fixture.detectChanges();
await fixture.whenStable();
const saved = vi.fn();
fixture.componentInstance.saved.subscribe(saved);
fixture.nativeElement.querySelector('button[type=submit]').click();
expect(saved).toHaveBeenCalledWith(
expect.objectContaining({
legalProfile: expect.objectContaining({
estimatedAnnualEnergyCostReferenceYear: null,
}),
}),
);
});
});