- 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.
283 lines
9.8 KiB
TypeScript
283 lines
9.8 KiB
TypeScript
import { TestBed } from '@angular/core/testing';
|
||
import { vi } from 'vitest';
|
||
import { FinalizationRevision } from '../../services/lease-finalization.service';
|
||
import { LeaseFinalizationForm } from './lease-finalization-form';
|
||
|
||
function eventFor(target: EventTarget): Event {
|
||
const event = new Event('change');
|
||
Object.defineProperty(event, 'target', { value: target });
|
||
return event;
|
||
}
|
||
|
||
describe('LeaseFinalizationForm', () => {
|
||
const revision: FinalizationRevision = {
|
||
number: 4,
|
||
templateVersion: '2026.10',
|
||
sourceFingerprint: 'source',
|
||
sha256: 'sha256',
|
||
generatedAt: '2026-07-19T10:00:00Z',
|
||
authorId: 'owner-1',
|
||
furnitureInventory: {
|
||
templateVersion: 'FR-MEUBLE-2025-01',
|
||
lines: [
|
||
{
|
||
id: 'line-1',
|
||
category: null,
|
||
presence: null,
|
||
equipmentKind: 'NONE',
|
||
privateAreaId: 'area-1',
|
||
designation: 'Chaise',
|
||
quantity: 1,
|
||
description: '',
|
||
},
|
||
],
|
||
},
|
||
privateAreas: [
|
||
{ id: 'area-1', family: 'PIECE_PRINCIPALE', subtype: 'LIVING_ROOM', label: 'Séjour' },
|
||
],
|
||
annexes: [],
|
||
warningCodes: [],
|
||
warnings: [],
|
||
};
|
||
|
||
function createForm(warningCodes: string[] = []) {
|
||
const fixture = TestBed.createComponent(LeaseFinalizationForm);
|
||
fixture.componentRef.setInput('revision', revision);
|
||
fixture.componentRef.setInput('warningCodes', warningCodes);
|
||
fixture.componentRef.setInput(
|
||
'warnings',
|
||
warningCodes.map((code) => ({ code, ruleVersion: '2026-07-01' })),
|
||
);
|
||
fixture.detectChanges();
|
||
return fixture;
|
||
}
|
||
|
||
it('emits the exact civil date string and confirmations', () => {
|
||
const fixture = createForm([
|
||
'SECURITY_DEPOSIT_ABOVE_TWO_MONTHS',
|
||
'IRL_REVISION_WITH_F_OR_G_DPE',
|
||
'FUTURE_WARNING',
|
||
]);
|
||
const component = fixture.componentInstance;
|
||
const file = new File(['lease'], 'bail-signe.pdf', { type: 'application/pdf' });
|
||
const requested = vi.fn();
|
||
component.finalizationRequested.subscribe(requested);
|
||
component.signedLease.set(file);
|
||
component.lastSignatureDate.set('2026-07-19');
|
||
component.allPartiesSigned.set(true);
|
||
component.revisionMatches.set(true);
|
||
component.discrepancyAnswer.set(false);
|
||
component.setWarning('SECURITY_DEPOSIT_ABOVE_TWO_MONTHS', true);
|
||
component.setWarning('IRL_REVISION_WITH_F_OR_G_DPE', true);
|
||
component.setWarning('FUTURE_WARNING', true);
|
||
|
||
component.submit();
|
||
|
||
expect(requested).toHaveBeenCalledWith({
|
||
signedLease: file,
|
||
revisionNumber: 4,
|
||
lastSignatureDate: '2026-07-19',
|
||
allPartiesSigned: true,
|
||
revisionMatches: true,
|
||
confirmedWarningCodes: [
|
||
'SECURITY_DEPOSIT_ABOVE_TWO_MONTHS',
|
||
'IRL_REVISION_WITH_F_OR_G_DPE',
|
||
'FUTURE_WARNING',
|
||
],
|
||
entryInventoryDiscrepancies: false,
|
||
inventoryCorrections: [],
|
||
allInventoryCorrectionsInitialed: false,
|
||
});
|
||
});
|
||
|
||
it('uses actionable French warning labels without exposing raw codes', () => {
|
||
const fixture = createForm([
|
||
'SECURITY_DEPOSIT_ABOVE_TWO_MONTHS',
|
||
'IRL_REVISION_WITH_F_OR_G_DPE',
|
||
'FUTURE_WARNING',
|
||
]);
|
||
const text = fixture.nativeElement.textContent;
|
||
|
||
expect(text).toContain('dépasse deux mois de loyer hors charges');
|
||
expect(text).toContain('confirme forcer ce montant');
|
||
expect(text).toContain('classé F ou G');
|
||
expect(text).toContain('maintenir la clause de révision IRL');
|
||
expect(text).toContain('avertissement légal signalé');
|
||
expect(text).not.toContain('SECURITY_DEPOSIT_ABOVE_TWO_MONTHS');
|
||
expect(text).not.toContain('FUTURE_WARNING');
|
||
expect(text).not.toContain('IRL_REVISION_WITH_F_OR_G_DPE');
|
||
expect(text).toContain('Version de règle : 2026-07-01');
|
||
});
|
||
|
||
it('explains every document warning precisely in French', () => {
|
||
const warningCodes = [
|
||
'DPE_MISSING',
|
||
'DPE_EXPIRED',
|
||
'DIAGNOSTIC_EXPIRED',
|
||
'CO_OWNERSHIP_RULES_EXTRACT_MISSING',
|
||
'PRIOR_RENTAL_AUTHORIZATION_MISSING',
|
||
];
|
||
const fixture = createForm(warningCodes);
|
||
const text = fixture.nativeElement.textContent;
|
||
|
||
expect(text).toContain('DPE est absent du dossier');
|
||
expect(text).toContain('DPE présent dans le dossier est expiré');
|
||
expect(text).toContain('diagnostic autre que le DPE est expiré');
|
||
expect(text).toContain('extrait du règlement de copropriété manque');
|
||
expect(text).toContain('autorisation préalable de mise en location manque');
|
||
for (const code of warningCodes) expect(text).not.toContain(code);
|
||
});
|
||
|
||
it('blocks an incomplete submission with an actionable error', () => {
|
||
const fixture = createForm(['SECURITY_DEPOSIT_ABOVE_TWO_MONTHS']);
|
||
const requested = vi.fn();
|
||
fixture.componentInstance.finalizationRequested.subscribe(requested);
|
||
|
||
fixture.componentInstance.submit();
|
||
fixture.detectChanges();
|
||
|
||
expect(requested).not.toHaveBeenCalled();
|
||
expect(fixture.nativeElement.textContent).toContain('Sélectionnez le bail signé');
|
||
});
|
||
|
||
it('blocks finalization while a required guarantee deed is missing', () => {
|
||
const fixture = createForm();
|
||
fixture.componentRef.setInput('guaranteesComplete', false);
|
||
fixture.detectChanges();
|
||
|
||
expect(fixture.nativeElement.querySelector('button[type=submit]').disabled).toBe(true);
|
||
expect(fixture.nativeElement.textContent).toContain('Téléversez tous les actes');
|
||
});
|
||
|
||
it('sends an initialed structured inventory correction after a yes answer', () => {
|
||
const fixture = createForm();
|
||
const component = fixture.componentInstance;
|
||
const file = new File(['lease'], 'bail-signe.pdf', { type: 'application/pdf' });
|
||
const requested = vi.fn();
|
||
component.finalizationRequested.subscribe(requested);
|
||
component.signedLease.set(file);
|
||
component.lastSignatureDate.set('2026-07-19');
|
||
component.allPartiesSigned.set(true);
|
||
component.revisionMatches.set(true);
|
||
component.discrepancyAnswer.set(true);
|
||
component.inventoryCorrections.set([
|
||
{
|
||
id: 'correction-1',
|
||
inventoryLineId: null,
|
||
privateAreaId: 'area-1',
|
||
designation: 'Chaise',
|
||
quantity: 0,
|
||
description: 'Absente',
|
||
},
|
||
]);
|
||
component.allInventoryCorrectionsInitialed.set(true);
|
||
|
||
component.submit();
|
||
|
||
expect(requested).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
entryInventoryDiscrepancies: true,
|
||
inventoryCorrections: [
|
||
expect.objectContaining({ id: 'correction-1', quantity: 0 }),
|
||
],
|
||
allInventoryCorrectionsInitialed: true,
|
||
}),
|
||
);
|
||
});
|
||
|
||
it('prefills known entry discrepancies with readable inventory choices', () => {
|
||
const fixture = createForm();
|
||
fixture.componentRef.setInput('suggestedInventoryCorrections', [
|
||
{
|
||
id: 'entry-line-1',
|
||
inventoryLineId: 'line-1',
|
||
privateAreaId: 'area-1',
|
||
designation: 'Chaise',
|
||
quantity: 0,
|
||
description: 'Chaise manquante',
|
||
},
|
||
]);
|
||
|
||
fixture.componentInstance.answerYes();
|
||
fixture.detectChanges();
|
||
|
||
expect(fixture.componentInstance.inventoryCorrections()).toEqual([
|
||
expect.objectContaining({ inventoryLineId: 'line-1', quantity: 0 }),
|
||
]);
|
||
expect(fixture.nativeElement.textContent).toContain('Chaise');
|
||
expect(fixture.nativeElement.textContent).toContain('Séjour');
|
||
expect(fixture.nativeElement.textContent).not.toContain('ID d’espace privatif');
|
||
});
|
||
|
||
it('accepts only a PDF upload, captures the civil date, and toggles warning confirmations', () => {
|
||
const fixture = createForm(['DPE_MISSING']);
|
||
const component = fixture.componentInstance;
|
||
const input = document.createElement('input');
|
||
const invalid = new File(['text'], 'notes.txt', { type: 'text/plain' });
|
||
Object.defineProperty(input, 'files', {
|
||
value: { item: () => invalid },
|
||
configurable: true,
|
||
});
|
||
|
||
component.selectSignedLease({ target: input } as unknown as Event);
|
||
expect(component.signedLease()).toBeNull();
|
||
expect(component.validationError()).toContain('format PDF');
|
||
|
||
const pdf = new File(['pdf'], 'bail.pdf', { type: 'application/pdf' });
|
||
Object.defineProperty(input, 'files', { value: { item: () => pdf }, configurable: true });
|
||
component.selectSignedLease({ target: input } as unknown as Event);
|
||
const date = document.createElement('input');
|
||
date.value = '2026-07-20';
|
||
component.setLastSignatureDate(eventFor(date));
|
||
component.setWarning('DPE_MISSING', true);
|
||
component.setWarning('DPE_MISSING', false);
|
||
|
||
expect(component.signedLease()).toBe(pdf);
|
||
expect(component.lastSignatureDate()).toBe('2026-07-20');
|
||
expect(component.confirmedWarningCodes().size).toBe(0);
|
||
expect(component.warningLabel('DPE_MISSING')).toContain('DPE');
|
||
});
|
||
|
||
it('edits a handwritten inventory correction from the revision snapshot', () => {
|
||
const fixture = createForm();
|
||
const component = fixture.componentInstance;
|
||
const requested = vi.fn();
|
||
component.finalizationRequested.subscribe(requested);
|
||
component.signedLease.set(new File(['pdf'], 'bail.pdf', { type: 'application/pdf' }));
|
||
component.lastSignatureDate.set('2026-07-20');
|
||
component.allPartiesSigned.set(true);
|
||
component.revisionMatches.set(true);
|
||
const dialog = fixture.nativeElement.querySelector('dialog');
|
||
Object.defineProperty(dialog, 'close', { value: vi.fn() });
|
||
component.answerYes();
|
||
const select = document.createElement('select');
|
||
select.append(new Option('Chaise', 'line-1'));
|
||
select.value = 'line-1';
|
||
component.selectInventoryLine(0, eventFor(select));
|
||
const description = document.createElement('textarea');
|
||
description.value = 'Pied rayé';
|
||
component.setCorrection(0, 'description', eventFor(description));
|
||
component.setCorrection(0, 'inventoryLineId', eventFor(document.createElement('input')));
|
||
|
||
component.continueWithCorrections();
|
||
expect(component.validationError()).toContain('correction');
|
||
component.selectInventoryLine(0, eventFor(select));
|
||
component.allInventoryCorrectionsInitialed.set(true);
|
||
component.continueWithCorrections();
|
||
|
||
expect(requested).toHaveBeenCalledWith(
|
||
expect.objectContaining({
|
||
entryInventoryDiscrepancies: true,
|
||
inventoryCorrections: [
|
||
expect.objectContaining({
|
||
inventoryLineId: 'line-1',
|
||
description: 'Pied rayé',
|
||
}),
|
||
],
|
||
}),
|
||
);
|
||
component.removeCorrection(0);
|
||
expect(component.inventoryCorrections()).toEqual([]);
|
||
});
|
||
});
|