Validate private areas across apartment form (#44)

This commit is contained in:
nessar committed 2026-08-17 14:18:16 +02:00
1 parent b9f5540394
commit 72d3f0ccfa
9 files changed
+323 -44

No files matched your search

@@ -102,7 +102,9 @@
<app-apartment-legal-profile-form [legalProfileForm]="apartmentForm.legalProfile" />
<app-private-area-editor
[areas]="apartmentModel().privateAreas"
[serverErrors]="saveFieldErrors()"
(areasChanged)="setPrivateAreas($event)"
(fieldChanged)="fieldChanged.emit($event)"
/>
<div class="form-actions">
@@ -94,6 +94,68 @@ describe('ApartmentForm', () => {
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;
@@ -1,4 +1,4 @@
import { Component, effect, input, output, signal } from '@angular/core';
import { Component, effect, input, output, signal, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormRoot, FormField, form, max, min, required } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
@@ -49,6 +49,7 @@ export class ApartmentForm {
readonly closed = output<void>();
readonly saved = output<Apartment>();
readonly fieldChanged = output<string>();
readonly privateAreaEditor = viewChild(PrivateAreaEditor);
readonly apartmentModel = signal<Apartment>({
name: '',
@@ -91,13 +92,21 @@ export class ApartmentForm {
save(): void {
if (this.saving()) return;
this.apartmentForm().markAsTouched();
const privateAreasValid = this.privateAreaEditor()?.validate() ?? true;
if (
this.apartmentForm().invalid() ||
!this.apartmentModel().name.trim() ||
!this.apartmentModel().address.trim()
!this.apartmentModel().address.trim() ||
!privateAreasValid
) {
const firstError = this.apartmentForm().errorSummary()[0];
this.error.set(firstError?.message ?? this.coreFieldError() ?? nameRequiredError);
this.error.set(
firstError?.message ??
this.coreFieldError() ??
this.privateAreaEditor()?.validationMessage() ??
nameRequiredError,
);
if (!privateAreasValid) this.privateAreaEditor()?.focusFirstInvalid();
return;
}