import { TestBed } from '@angular/core/testing'; import { ActivatedRoute, convertToParamMap } from '@angular/router'; import { of, Subject, throwError } from 'rxjs'; import { vi } from 'vitest'; import { ConditionPhoto, ConditionReportComparison, ConditionReportService, PropertyConditionReportSupplement, PropertyConditionReport, } from '../../services/condition-report.service'; import { FileDownloadService } from '../../services/file-download.service'; import { ConditionReportDetailsSection } from './condition-report-details-section'; import { ConditionReportEditor } from './condition-report-editor'; import { ConditionReportEvidenceSection } from './condition-report-evidence-section'; import { ConditionReportInventorySection } from './condition-report-inventory-section'; import { ConditionReportPage } from './condition-report-page'; const report: PropertyConditionReport = { id: 'report-1', type: 'ENTRY', status: 'DRAFT', occurredOn: '2026-07-22', timeZone: 'Europe/Paris', participants: [], forwardingAddresses: [], zones: [{ id: 'kitchen', privateAreaId: '', label: 'Cuisine', equipment: 'NONE' }], observations: [], meterReadings: [], accessMeans: [], inventoryConditions: [], photos: [], zonePhotoAbsences: [], entryPhotoDispositions: [], supplements: [], entrySnapshot: null, revisions: [], currentRevision: null, finalization: null, deliveryAttestations: [], leaseRevisionNumber: 3, leaseRevisionFingerprint: 'lease-revision-fingerprint', version: 1, }; const supplementPhoto: ConditionPhoto = { reference: 'supplement-photo-1', zoneId: 'kitchen', sha256: 'supplement-photo-sha', width: 1200, height: 800, uploadedBy: 'owner', uploadedAt: '2026-07-22T00:00:00Z', }; const supplement: PropertyConditionReportSupplement = { id: 'supplement-1', type: 'GENERAL', signedPdf: { reference: 'supplement-pdf', sha256: 'supplement-pdf-sha' }, photos: [supplementPhoto], uploadedBy: 'owner', uploadedAt: '2026-07-22T00:00:00Z', requestedOn: '2026-07-22', heatingPeriodStartedOn: null, acceptedAdditions: 'Rayure acceptée', audit: { status: 'ON_TIME', deadlineOn: '2026-07-30' }, }; function eventFor(target: EventTarget): Event { const event = new Event('change'); Object.defineProperty(event, 'target', { value: target }); return event; } function valueEvent(value: string): Event { const input = document.createElement('input'); input.value = value; return eventFor(input); } describe('ConditionReportEditor', () => { it('uses a rear-camera input and explains an invalid degraded observation before saving', () => { const service = { getReport: vi.fn().mockReturnValue(of(report)), saveReport: vi.fn(), uploadPhoto: vi.fn(), deletePhoto: vi.fn(), createRevision: vi.fn(), downloadRevision: vi.fn(), finalize: vi.fn(), downloadBundle: vi.fn(), getComparison: vi.fn(), appendSupplement: vi.fn(), attestDelivery: vi.fn(), }; TestBed.configureTestingModule({ providers: [{ provide: ConditionReportService, useValue: service }], }); const fixture = TestBed.createComponent(ConditionReportEditor); fixture.componentRef.setInput('leaseId', 'lease-1'); fixture.componentRef.setInput('type', 'ENTRY'); fixture.componentRef.setInput('report', report); fixture.componentRef.setInput('entryPhotos', []); fixture.detectChanges(); const editor = fixture.componentInstance; let error = ''; editor.errorOccurred.subscribe((message) => (error = message)); fixture.componentRef.setInput('report', { ...report, observations: [{ subjectId: 'kitchen', state: 'DEGRADE', observation: '' }], }); editor.save(); fixture.detectChanges(); expect( fixture.nativeElement .querySelector('input[type=file][capture]') .getAttribute('capture'), ).toBe('environment'); expect(error).toContain('observation'); expect(service.saveReport).not.toHaveBeenCalled(); }); it('covers each draft validation boundary before emitting the save request', () => { const service = { photoUrl: vi.fn().mockReturnValue('/photo.webp') }; TestBed.configureTestingModule({ providers: [{ provide: ConditionReportService, useValue: service }], }); const valid: PropertyConditionReport = { ...report, participants: [ { partyId: 'tenant-1', role: 'TENANT', name: 'Camille Martin', attendance: 'PRESENT', representativeName: '', }, ], observations: [{ subjectId: 'kitchen', state: 'BON_ETAT', observation: '' }], photos: [ { reference: 'photo-1', zoneId: 'kitchen', sha256: 'sha', width: 1, height: 1, uploadedBy: 'owner', uploadedAt: '2026-07-22T00:00:00Z', }, ], }; const fixture = TestBed.createComponent(ConditionReportEditor); fixture.componentRef.setInput('leaseId', 'lease-1'); fixture.componentRef.setInput('type', 'ENTRY'); fixture.componentRef.setInput('report', valid); fixture.componentRef.setInput('entryPhotos', []); fixture.detectChanges(); const editor = fixture.componentInstance; const errors: string[] = []; const requests = vi.fn(); const changes = vi.fn(); editor.errorOccurred.subscribe((error) => errors.push(error)); editor.saveRequested.subscribe(requests); editor.reportChanged.subscribe(changes); const invalidReports: [PropertyConditionReport, string][] = [ [{ ...valid, occurredOn: '' }, 'date'], [ { ...valid, participants: [{ ...valid.participants[0]!, attendance: 'REPRESENTED' }], }, 'mandataire', ], [{ ...valid, zones: [{ ...valid.zones[0]!, label: '' }] }, 'libellé'], [ { ...valid, observations: [{ subjectId: 'kitchen', state: 'DEGRADE', observation: '' }], }, 'observation', ], [ { ...valid, meterReadings: [ { id: 'meter-1', type: 'OTHER', state: 'RECORDED', index: -1, unit: '', label: '', photoId: '', observation: '', }, ], }, 'compteurs', ], [ { ...valid, meterReadings: [ { id: 'meter-1', type: 'EAU_FROIDE', state: 'RECORDED', index: null, unit: 'm3', label: '', photoId: '', observation: '', }, ], }, 'compteurs', ], [ { ...valid, meterReadings: [ { id: 'meter-1', type: 'EAU_FROIDE', state: 'INACCESSIBLE', index: 0, unit: 'm3', label: '', photoId: '', observation: 'Local fermé', }, ], }, 'compteurs', ], [ { ...valid, meterReadings: [ { id: 'meter-1', type: 'EAU_FROIDE', state: 'INACCESSIBLE', index: null, unit: 'm3', label: '', photoId: '', observation: '', }, ], }, 'compteurs', ], [ { ...valid, meterReadings: [ { id: 'meter-1', type: 'OTHER', state: 'RECORDED', index: 1, unit: 'u', label: '', photoId: '', observation: '', }, ], }, 'compteurs', ], [ { ...valid, accessMeans: [ { id: 'key-1', type: 'CLE', destination: '', quantity: -1, observation: '', }, ], }, 'moyens d’accès', ], [ { ...valid, inventoryConditions: [ { inventoryLineId: 'chair-1', privateAreaId: 'kitchen', privateAreaLabel: 'Cuisine', designation: 'Chaise', contractualQuantity: 1, splits: [ { inventoryLineId: 'chair-1', state: 'DEGRADE', quantity: 0, observation: '', }, ], }, ], }, 'Répartissez', ], [{ ...valid, photos: [] }, 'photo'], ]; for (const [invalid, fragment] of invalidReports) { fixture.componentRef.setInput('report', invalid); editor.save(); expect(errors.at(-1)).toContain(fragment); } fixture.componentRef.setInput('report', { ...valid, meterReadings: [ { id: 'meter-1', type: 'EAU_FROIDE', state: 'INACCESSIBLE', index: null, unit: 'm3', label: '', photoId: '', observation: 'Local fermé', }, ], }); editor.save(); fixture.componentRef.setInput('type', 'EXIT'); fixture.componentRef.setInput('entryPhotos', valid.photos); fixture.componentRef.setInput('report', valid); editor.save(); expect(errors.at(-1)).toContain('adresse de correspondance'); fixture.componentRef.setInput('report', { ...valid, forwardingAddresses: [{ tenantPartyId: 'tenant-1', address: '12 rue A' }], }); editor.save(); expect(errors.at(-1)).toContain('photo d’entrée'); fixture.componentRef.setInput('entryPhotos', [ ...valid.photos, { ...valid.photos[0]!, reference: 'photo-2' }, ]); fixture.componentRef.setInput('report', { ...valid, forwardingAddresses: [{ tenantPartyId: 'tenant-1', address: '12 rue A' }], entryPhotoDispositions: [ { entryPhotoId: 'photo-1', exitPhotoId: 'photo-1' }, { entryPhotoId: 'photo-2', exitPhotoId: 'photo-1' }, ], }); editor.save(); expect(errors.at(-1)).toContain('unique'); fixture.componentRef.setInput('type', 'ENTRY'); fixture.componentRef.setInput('entryPhotos', []); fixture.componentRef.setInput('report', valid); editor.save(); fixture.componentRef.setInput('saving', true); editor.changeReport(valid); fixture.componentRef.setInput('saving', false); fixture.componentRef.setInput('finalizing', true); editor.changeReport(valid); expect(requests).toHaveBeenCalledWith( expect.objectContaining({ version: 1, zones: valid.zones }), ); expect(changes).not.toHaveBeenCalled(); }); }); describe('Condition-report sections', () => { it('updates the report details, participants, zones and observations', () => { const value: PropertyConditionReport = { ...report, participants: [ { partyId: 'tenant-1', role: 'TENANT', name: 'Camille Martin', attendance: 'PRESENT', representativeName: '', }, ], observations: [{ subjectId: 'kitchen', state: 'BON_ETAT', observation: '' }], }; const fixture = TestBed.createComponent(ConditionReportDetailsSection); fixture.componentRef.setInput('report', value); fixture.detectChanges(); const section = fixture.componentInstance; let current = value; section.reportChanged.subscribe((next) => { current = next; fixture.componentRef.setInput('report', next); }); section.setOccurredOn(valueEvent('2026-07-23')); section.setTimeZone(valueEvent('Europe/Paris')); section.updateParticipant(0, 'attendance', valueEvent('REPRESENTED')); section.updateParticipant(0, 'representativeName', valueEvent('Alex Martin')); section.updateZone('kitchen', 'label', valueEvent('Cuisine équipée')); section.updateObservation('kitchen', 'state', valueEvent('DEGRADE')); section.updateObservation('kitchen', 'observation', valueEvent('Rayure')); section.addZone(); const added = current.zones.find((zone) => zone.id !== 'kitchen'); expect(current.occurredOn).toBe('2026-07-23'); expect(current.participants[0]).toEqual( expect.objectContaining({ representativeName: 'Alex Martin' }), ); expect(current.observations).toContainEqual( expect.objectContaining({ subjectId: 'kitchen', state: 'DEGRADE', observation: 'Rayure', }), ); expect(added?.privateAreaId).toBe(''); expect(section.canRemoveZone('inventory:chair')).toBe(false); expect(section.canRemoveZone('kitchen:walls')).toBe(false); expect(section.canRemoveZone('custom')).toBe(true); expect(section.needsObservation('ABSENT')).toBe(true); expect(section.needsObservation('BON_ETAT')).toBe(false); expect(section.privateAreas()).toEqual([]); expect(section.observationLabel('missing')).toBe('Zone'); if (added) section.removeZone(added.id); expect(current.zones).toHaveLength(1); }); it('updates meter, access, inventory and forwarding facts', () => { const value: PropertyConditionReport = { ...report, participants: [ { partyId: 'tenant-1', role: 'TENANT', name: 'Camille Martin', attendance: 'PRESENT', representativeName: '', }, ], meterReadings: [ { id: 'meter-1', type: 'ELECTRICITE', state: 'RECORDED', index: 1, unit: 'kWh', label: '', photoId: '', observation: '', }, ], accessMeans: [ { id: 'key-1', type: 'CLE', destination: 'Porte', quantity: 1, observation: '' }, ], inventoryConditions: [ { inventoryLineId: 'chair-1', privateAreaId: 'kitchen', privateAreaLabel: 'Cuisine', designation: 'Chaise', contractualQuantity: 2, splits: [ { inventoryLineId: 'chair-1', state: 'BON_ETAT', quantity: 2, observation: '', }, ], }, ], }; const fixture = TestBed.createComponent(ConditionReportInventorySection); fixture.componentRef.setInput('report', value); fixture.componentRef.setInput('type', 'EXIT'); fixture.detectChanges(); const section = fixture.componentInstance; let current = value; section.reportChanged.subscribe((next) => { current = next; fixture.componentRef.setInput('report', next); }); section.updateMeter('meter-1', 'state', valueEvent('INACCESSIBLE')); expect(current.meterReadings[0]?.index).toBeNull(); section.updateMeter('meter-1', 'state', valueEvent('RECORDED')); section.updateMeter('meter-1', 'index', valueEvent('12.5')); section.addMeter(); section.removeMeter('meter-1'); section.updateAccessMean('key-1', 'quantity', valueEvent('2')); section.addAccessMean(); section.removeAccessMean('key-1'); section.addInventorySplit(0); section.updateInventorySplit(0, 1, 'quantity', valueEvent('0')); section.updateInventorySplit(0, 1, 'state', valueEvent('ABSENT')); section.removeInventorySplit(0, 1); section.updateForwardingAddress('tenant-1', valueEvent('12 rue A')); expect(current.meterReadings).toHaveLength(1); expect(current.meterReadings[0]?.index).toBe(0); expect(current.accessMeans).toHaveLength(1); expect(current.accessMeans[0]?.type).toBe('CLE'); expect(current.inventoryConditions[0]?.splits).toHaveLength(1); expect(section.forwardingAddress('tenant-1')).toBe('12 rue A'); expect(section.forwardingAddress('missing')).toBe(''); expect(section.needsObservation('NON_APPLICABLE')).toBe(true); expect(section.needsObservation('NEUF')).toBe(false); }); it('keeps pending evidence local and reports upload and deletion outcomes', () => { const service = { uploadPhoto: vi.fn().mockReturnValue(of(void 0)), deletePhoto: vi.fn().mockReturnValue(of(void 0)), photoUrl: vi.fn().mockReturnValue('/photo.webp'), }; TestBed.configureTestingModule({ providers: [{ provide: ConditionReportService, useValue: service }], }); const fixture = TestBed.createComponent(ConditionReportEvidenceSection); fixture.componentRef.setInput('leaseId', 'lease-1'); fixture.componentRef.setInput('type', 'EXIT'); fixture.componentRef.setInput('report', report); fixture.componentRef.setInput('entryPhotos', []); fixture.detectChanges(); const section = fixture.componentInstance; let current = report; section.reportChanged.subscribe((next) => { current = next; fixture.componentRef.setInput('report', next); }); const success = vi.fn(); const reload = vi.fn(); section.successOccurred.subscribe(success); section.reloadRequested.subscribe(reload); const createUrl = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:photo'); const revokeUrl = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined); const input = document.createElement('input'); const file = new File(['photo'], 'cuisine.webp', { type: 'image/webp' }); Object.defineProperty(input, 'files', { value: { item: () => file } }); section.setZoneAbsence('kitchen', valueEvent('Accès impossible')); section.setZoneAbsence('kitchen', valueEvent('')); section.setEntryPhotoDisposition('entry-photo', 'exitPhotoId', valueEvent('exit-photo')); section.setEntryPhotoDisposition('entry-photo', 'absenceReason', valueEvent('')); section.selectPhoto('kitchen', { target: input } as unknown as Event); section.uploadPhoto('kitchen'); section.removePhoto({ reference: 'photo-1', zoneId: 'kitchen', sha256: 'sha', width: 1, height: 1, uploadedBy: 'owner', uploadedAt: '2026-07-22T00:00:00Z', }); expect(current.zonePhotoAbsences).toEqual([]); expect(section.disposition('entry-photo')).toEqual({ entryPhotoId: 'entry-photo', exitPhotoId: 'exit-photo', }); fixture.componentRef.setInput('report', { ...report, photos: [ { ...supplementPhoto, reference: 'exit-photo', zoneId: 'kitchen' }, { ...supplementPhoto, reference: 'used-photo', zoneId: 'kitchen' }, ], entryPhotoDispositions: [ { entryPhotoId: 'entry-photo', exitPhotoId: 'exit-photo' }, { entryPhotoId: 'other-entry-photo', exitPhotoId: 'used-photo' }, ], }); expect( section.exitPhotosFor({ ...supplementPhoto, reference: 'entry-photo', zoneId: 'kitchen', }), ).toEqual([expect.objectContaining({ reference: 'exit-photo' })]); expect(service.uploadPhoto).toHaveBeenCalledWith('lease-1', 'EXIT', file, 'kitchen'); expect(service.deletePhoto).toHaveBeenCalledWith('lease-1', 'EXIT', 'photo-1', 1); expect(section.photoUrl('ENTRY', 'entry-photo')).toBe('/photo.webp'); expect(success).toHaveBeenCalledWith('Photo ajoutée.'); expect(reload).toHaveBeenCalledTimes(2); expect(createUrl).toHaveBeenCalled(); expect(revokeUrl).toHaveBeenCalled(); createUrl.mockRestore(); revokeUrl.mockRestore(); }); }); describe('ConditionReportPage', () => { it('preserves the established report while it refreshes', () => { const refresh = new Subject(); const service = { getReport: vi.fn().mockReturnValueOnce(of(report)).mockReturnValue(refresh), }; TestBed.configureTestingModule({ providers: [ { provide: ActivatedRoute, useValue: { snapshot: { paramMap: convertToParamMap({ leaseId: 'lease-1', type: 'ENTRY' }), }, }, }, { provide: ConditionReportService, useValue: service }, { provide: FileDownloadService, useValue: { download: vi.fn(), isPending: vi.fn(() => false), error: vi.fn(() => ''), }, }, ], }); const fixture = TestBed.createComponent(ConditionReportPage); fixture.detectChanges(); fixture.componentInstance.load(); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('Révision de bail source 3'); expect(fixture.nativeElement.textContent).toContain('Mise à jour de l’état des lieux'); refresh.next({ ...report, version: 2 }); refresh.complete(); }); it('refreshes the revision version, requires attestation, and renders every comparison section', () => { const refreshed = { ...report, version: 2 }; const comparison: ConditionReportComparison = { observations: [ { reference: 'kitchen:walls', entry: { subjectId: 'kitchen:walls', state: 'DEGRADE', observation: 'Rayure' }, exit: { subjectId: 'kitchen:walls', state: 'BON_ETAT', observation: '' }, changed: true, }, ], zones: [ { reference: 'kitchen', entry: { id: 'kitchen', privateAreaId: 'area-1', label: 'Cuisine', equipment: 'OTHER', }, exit: { id: 'kitchen', privateAreaId: 'area-1', label: 'Cuisine', equipment: 'NONE', }, changed: true, }, ], inventory: [ { reference: 'chair', entry: { inventoryLineId: 'chair', privateAreaId: 'area-1', privateAreaLabel: 'Cuisine', designation: 'Chaise', contractualQuantity: 1, splits: [ { inventoryLineId: 'chair', state: 'ETAT_USAGE', quantity: 1, observation: '', }, ], }, exit: null, changed: true, absence: { reason: 'Non constaté', auditedBy: 'owner', auditedAt: '2026-07-23T00:00:00Z', }, }, ], meters: [ { reference: 'electricity', entry: { id: 'electricity', type: 'ELECTRICITE', state: 'INACCESSIBLE', index: null, unit: 'kWh', label: '', photoId: '', observation: 'Local fermé', }, exit: null, changed: true, }, ], accessMeans: [ { reference: 'key', entry: { id: 'key', type: 'CLE', destination: 'Porte', quantity: 2, observation: 'Une clé usée', }, exit: null, changed: true, }, ], supplements: [ { reference: 'supplement-1', entry: supplement, exit: null, changed: true }, ], photos: [ { reference: 'supplement-photo-1', entry: supplementPhoto, exit: null, changed: true, absence: { reason: 'Photo impossible', auditedBy: 'owner', auditedAt: '2026-07-23T00:00:00Z', }, }, ], }; const service = { getReport: vi.fn().mockReturnValueOnce(of(report)).mockReturnValue(of(refreshed)), createRevision: vi .fn() .mockReturnValue( of({ number: 1, sha256: 'sha', sourceFingerprint: 'source', obsolete: false }), ), downloadRevision: vi.fn().mockReturnValue(of(new Blob(['pdf']))), finalize: vi.fn().mockReturnValue(of({ ...refreshed, status: 'FINALIZED' as const })), getComparison: vi.fn().mockReturnValue(of(comparison)), photoUrl: vi.fn().mockReturnValue('/photo.webp'), }; const downloads = { download: vi.fn(), isPending: vi.fn(() => false), error: vi.fn(() => ''), }; TestBed.configureTestingModule({ providers: [ { provide: ActivatedRoute, useValue: { snapshot: { paramMap: convertToParamMap({ leaseId: 'lease-1', type: 'ENTRY' }), }, }, }, { provide: ConditionReportService, useValue: service }, { provide: FileDownloadService, useValue: downloads }, ], }); const fixture = TestBed.createComponent(ConditionReportPage); fixture.detectChanges(); const page = fixture.componentInstance; page.dirty.set(true); page.createRevision(); expect(service.createRevision).not.toHaveBeenCalled(); page.dirty.set(false); page.createRevision(); expect(service.getReport).toHaveBeenCalledTimes(2); expect(page.report()?.version).toBe(2); page.signedScan.set(new File(['pdf'], 'signed.pdf', { type: 'application/pdf' })); page.finalize(); expect(service.finalize).not.toHaveBeenCalled(); page.finalizationAttested.set(true); page.finalize(); expect(service.finalize).not.toHaveBeenCalled(); page.allPartiesSigned.set(true); page.finalize(); expect(service.finalize).toHaveBeenCalledWith('lease-1', 'ENTRY', { signedReport: expect.any(File), version: 2, allPartiesSigned: true, }); page.loadComparison(); expect(page.comparisonSections().map((section) => section.title)).toEqual([ 'Observations', 'Zones', 'Inventaire', 'Compteurs', 'Moyens d’accès', 'Compléments acceptés', ]); }); it('handles save, finalization, comparison, delivery and supplements through their local states', () => { const exitReport: PropertyConditionReport = { ...report, type: 'EXIT', entrySnapshot: { reportId: 'entry-1', participants: [], zones: [], observations: [], meterReadings: [], accessMeans: [], inventoryConditions: [], photos: [], supplements: [supplement], }, deliveryAttestations: [ { partyId: 'tenant-1', attestedBy: 'owner', attestedAt: '2026-07-22T00:00:00Z' }, ], }; const comparison: ConditionReportComparison = { observations: [], zones: [], inventory: [], meters: [], accessMeans: [], supplements: [], photos: [], }; const service = { getReport: vi.fn().mockReturnValue(of(exitReport)), saveReport: vi.fn().mockReturnValue(of({ ...exitReport, version: 2 })), createRevision: vi.fn(), downloadRevision: vi.fn(), finalize: vi.fn().mockReturnValue(of({ ...exitReport, status: 'FINALIZED' as const })), downloadBundle: vi.fn().mockReturnValue(of(new Blob(['zip']))), getComparison: vi.fn().mockReturnValue(of(comparison)), attestDelivery: vi.fn().mockReturnValue(of(exitReport)), appendSupplement: vi.fn().mockReturnValue(of(exitReport)), photoUrl: vi.fn().mockReturnValue('/photo.webp'), }; const downloads = { download: vi.fn(), isPending: vi.fn((key: string) => key.includes('bundle')), error: vi.fn((key: string) => key.includes('bundle') ? 'Téléchargement impossible.' : '', ), }; TestBed.configureTestingModule({ providers: [ { provide: ActivatedRoute, useValue: { snapshot: { paramMap: convertToParamMap({ leaseId: 'lease-1', type: 'EXIT' }), }, }, }, { provide: ConditionReportService, useValue: service }, { provide: FileDownloadService, useValue: downloads }, ], }); const fixture = TestBed.createComponent(ConditionReportPage); fixture.detectChanges(); const page = fixture.componentInstance; const scan = new File(['pdf'], 'signed.pdf', { type: 'application/pdf' }); expect(page.entryPhotos()).toEqual([supplementPhoto]); page.supplementType.set('HEATING'); page.heatingPeriodStartedOn.set('2026-01-31'); expect(page.supplementDeadline()).toBe('2026-02-28'); page.supplementType.set('GENERAL'); page.save({ version: 1 } as never); page.selectSignedScan(eventFor(document.createElement('input'))); page.signedScan.set(scan); page.finalizationAttested.set(true); page.allPartiesSigned.set(true); page.dirty.set(true); page.finalize(); page.dirty.set(false); page.finalize(); page.downloadBundle(); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('Téléchargement…'); expect(fixture.nativeElement.textContent).toContain('Téléchargement impossible.'); page.loadComparison(); page.attestDelivery('tenant-2'); page.changeReport({ ...exitReport, occurredOn: '2026-07-23' }); page.appendSupplement(); expect(page.error()).toContain('date, les ajouts acceptés'); page.supplementSignedPdf.set(scan); page.supplementRequestedOn.set('2026-07-22'); page.supplementAcceptedAdditions.set('Rayure déclarée'); page.supplementType.set('HEATING'); page.heatingPeriodStartedOn.set(''); page.appendSupplement(); expect(page.error()).toContain('période de chauffe'); page.heatingPeriodStartedOn.set('2026-06-01'); page.appendSupplement(); expect(page.error()).toContain('avertissement de délai'); page.lateWarningConfirmed.set(true); page.appendSupplement(); expect(page.error()).toContain('motivez'); page.supplementReason.set('Demande reçue tardivement'); const photoInput = document.createElement('input'); const photo = new File(['photo'], 'cuisine.webp', { type: 'image/webp' }); Object.defineProperty(photoInput, 'files', { value: { 0: photo, length: 1, item: () => photo }, }); page.selectSupplementPhotos(eventFor(photoInput)); expect(page.error()).toContain('zone'); page.supplementPhotoZoneId.set('kitchen'); page.selectSupplementPhotos(eventFor(photoInput)); page.removeSupplementPhoto(0); page.selectSupplementPhotos(eventFor(photoInput)); page.appendSupplement(); expect(service.saveReport).toHaveBeenCalled(); expect(service.finalize).toHaveBeenCalledWith( 'lease-1', 'EXIT', expect.objectContaining({ version: 2 }), ); expect(downloads.download).toHaveBeenCalled(); expect(page.comparison()).toEqual(comparison); expect(service.attestDelivery).toHaveBeenCalledWith('lease-1', 'EXIT', 1, 'tenant-2'); expect(page.hasAttested('tenant-1')).toBe(true); expect(service.appendSupplement).toHaveBeenCalledWith( 'lease-1', expect.objectContaining({ signedPdf: scan, photos: [{ file: photo, zoneId: 'kitchen' }], requestedOn: '2026-07-22', supplementType: 'HEATING', heatingPeriodStartedOn: '2026-06-01', lateWarningConfirmed: true, reason: 'Demande reçue tardivement', }), ); expect(page.photoUrl('ENTRY', 'photo-1')).toBe('/photo.webp'); page.report.set(null); expect(page.hasAttested('tenant-1')).toBe(false); }); it('keeps asynchronous failures truthful', () => { const service = { getReport: vi.fn().mockReturnValue(throwError(() => new Error('offline'))), getComparison: vi.fn().mockReturnValue(throwError(() => new Error('offline'))), }; TestBed.configureTestingModule({ providers: [ { provide: ActivatedRoute, useValue: { snapshot: { paramMap: convertToParamMap({ leaseId: 'lease-1', type: 'ENTRY' }), }, }, }, { provide: ConditionReportService, useValue: service }, { provide: FileDownloadService, useValue: { download: vi.fn(), isPending: vi.fn(() => false), error: vi.fn(() => ''), }, }, ], }); const fixture = TestBed.createComponent(ConditionReportPage); fixture.detectChanges(); const page = fixture.componentInstance; page.loadComparison(); expect(page.loading()).toBe(false); expect(page.comparisonPending()).toBe(false); expect(page.error()).toContain('Impossible de charger la comparaison'); page.report.set(report); page.success.set('Ancienne réussite'); fixture.detectChanges(); expect(fixture.nativeElement.textContent).not.toContain('Ancienne réussite'); }); });