- Created a pre-commit hook in .husky to run lint-staged. - Added .lintstagedrc.mjs to format frontend files with Prettier and apply Spotless to backend Java files. - Introduced .prettierignore to exclude specific directories from formatting.
996 lines
29 KiB
TypeScript
996 lines
29 KiB
TypeScript
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<PropertyConditionReport>();
|
||
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');
|
||
});
|
||
});
|