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