Files
pdf_oralia_vibe/frontend/tests/colonnesReferentiel.test.js
Bertrand Benjamin dc5e8b995f feat: ouvre la saisie des logements dans un tableau
La fiche compte une dizaine de champs pour une vingtaine de lots : un
formulaire par lot imposerait autant d'allers-retours. Le tableau garde
l'ergonomie du tableur d'où viennent ces données — une ligne par lot, les
colonnes dans le même ordre, chaque cellule enregistrée en la quittant.

Ce que le serveur calcule est affiché comme tel et non saisissable :
échéance du DPE (rouge si périmé, ambre à moins d'un an) et écart de
surface. Le type vu par le PDF reste visible à côté du type saisi quand les
deux se contredisent, plutôt que d'être remplacé sans le dire.

Tri et filtre vivent dans l'en-tête de chaque colonne, jamais au-dessus du
tableau : c'est là qu'on les cherche en lisant la colonne. Une liste de
choix peut proposer, dans un groupe à part, ce qui n'est pas une valeur de
la colonne — rattachement d'un lot aux comptes rendus, désaccord de type.
L'en-tête se fige au défilement, ce qui demande de borner la hauteur du
tableau : sans conteneur à hauteur limitée, `sticky` n'a rien à quoi se
tenir.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 17:40:11 +02:00

168 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Les filtres de colonnes décident de ce que l'utilisateur voit : un filtre trop
// zélé masque un lot sans rien dire, ce qui se remarque d'autant moins que le
// tableau reste plausible. Ils sont donc testés cas par cas.
import { describe, expect, it } from 'vitest'
import {
COLONNES,
ETAT_INUTILISES,
ETAT_UTILISES,
TYPE_EN_DESACCORD,
VIDE,
comparer,
groupesFiltre,
passeFiltre,
valeursDistinctes,
} from '../src/components/referentiel/colonnes.js'
const colonne = (cle) => COLONNES.find((c) => c.cle === cle)
function lot(numero, caracteristiques = null, extra = {}) {
return {
id: Number(numero),
numero,
immeuble_id: 1,
immeuble_code: '33689020',
immeuble_denomination: 'Servient',
type_effectif: caracteristiques?.type ?? null,
type_ecart: false,
nb_revenus: 0,
nb_depenses: 0,
caracteristiques,
...extra,
}
}
// Un immeuble réduit : un local décrit, un appartement décrit, un lot vierge.
const LOTS = [
lot('01', {
bat: 'Rue',
etage: 'RC',
type: 'Loc. Commercial',
surface: 148,
surface_impots: 146,
delta_surface: -2,
chauffage: 'Électrique',
dpe_classe: 'C',
dpe_echeance: '2031-02-08',
numero_fiscal: '690123456789',
}, { nb_revenus: 5 }),
lot('02', {
bat: 'Cour',
etage: '1',
type: 'Appartement T3',
surface: 62,
surface_impots: 62,
delta_surface: 0,
chauffage: 'Gaz',
dpe_classe: 'F',
dpe_echeance: '2020-01-01',
}, { nb_revenus: 3, type_ecart: true }),
lot('0003'),
]
const filtrer = (cle, saisie) => LOTS.filter((l) => passeFiltre(l, colonne(cle), saisie)).map((l) => l.numero)
describe('filtres de colonne', () => {
it('ne masque rien tant quaucune valeur nest saisie', () => {
expect(filtrer('chauffage', '')).toEqual(['01', '02', '0003'])
})
it('filtre sur une valeur exacte de liste', () => {
expect(filtrer('dpe_classe', 'F')).toEqual(['02'])
})
it('isole les lots dont la colonne est vide', () => {
expect(filtrer('chauffage', VIDE)).toEqual(['0003'])
})
it('isole les types en désaccord avec le PDF', () => {
expect(filtrer('type', TYPE_EN_DESACCORD)).toEqual(['02'])
})
it('compare les nombres au lieu de comparer leur écriture', () => {
// '100' contient '10', mais 62 n'est pas > 100 : une recherche textuelle
// renverrait ici les deux lots décrits.
expect(filtrer('surface', '> 100')).toEqual(['01'])
expect(filtrer('surface', '<=62')).toEqual(['02'])
})
it('sait isoler un écart de surface non nul', () => {
expect(filtrer('delta_surface', '!=0')).toEqual(['01'])
})
it('ne masque rien sur une saisie numérique incomplète', () => {
expect(filtrer('surface', '>')).toEqual(['01', '02', '0003'])
})
it('ignore casse et accents dans les filtres texte', () => {
expect(filtrer('numero_fiscal', '6901')).toEqual(['01'])
})
it('filtre par immeuble sur son nom dusage', () => {
const autre = { ...lot('01'), immeuble_id: 2, immeuble_code: 'M', immeuble_denomination: null }
const parc = [...LOTS, autre]
const servient = parc.filter((l) => passeFiltre(l, colonne('immeuble'), 'Servient'))
// Sans nom d'usage, l'immeuble reste filtrable par son code.
const marietton = parc.filter((l) => passeFiltre(l, colonne('immeuble'), 'M'))
expect(servient).toHaveLength(3)
expect(marietton).toEqual([autre])
})
it('sépare les lots rattachés à un compte rendu des autres', () => {
expect(filtrer('numero', ETAT_INUTILISES)).toEqual(['0003'])
expect(filtrer('numero', ETAT_UTILISES)).toEqual(['01', '02'])
})
it('filtre aussi sur un numéro de porte précis', () => {
expect(filtrer('numero', '02')).toEqual(['02'])
})
it('classe les DPE selon leur péremption', () => {
expect(filtrer('dpe_echeance', 'perime')).toEqual(['02'])
expect(filtrer('dpe_echeance', 'valide')).toEqual(['01'])
expect(filtrer('dpe_echeance', 'absent')).toEqual(['0003'])
})
})
describe('listes de choix', () => {
it('ne propose que les valeurs présentes, le vide en dernier', () => {
// Ordre alphabétique français : « É » se classe avec « E », donc avant « G ».
expect(valeursDistinctes(LOTS, colonne('chauffage'))).toEqual(['Électrique', 'Gaz', VIDE])
})
it('propose le désaccord à part des types, quand il y en a un', () => {
const groupes = groupesFiltre(LOTS, colonne('type'))
expect(groupes[0].options).toEqual([TYPE_EN_DESACCORD])
expect(groupes[1].options).toContain('Appartement T3')
})
it('ne propose le contrôle des types que sil a lieu dêtre', () => {
const sansDesaccord = LOTS.map((l) => ({ ...l, type_ecart: false }))
expect(groupesFiltre(sansDesaccord, colonne('type'))).toHaveLength(1)
})
it('propose les numéros de porte en plus du rattachement', () => {
const [rattachement, portes] = groupesFiltre(LOTS, colonne('numero'))
expect(rattachement.options).toEqual([ETAT_UTILISES, ETAT_INUTILISES])
expect(portes.options).toEqual(['01', '02', '0003'])
})
})
describe('tri', () => {
it('trie les nombres comme des nombres', () => {
const tries = [...LOTS].sort(comparer(colonne('surface'), 'desc')).map((l) => l.numero)
expect(tries).toEqual(['01', '02', '0003'])
})
it('renvoie les cases vides en fin, dans les deux sens', () => {
const asc = [...LOTS].sort(comparer(colonne('surface'), 'asc')).map((l) => l.numero)
expect(asc).toEqual(['02', '01', '0003'])
})
})