@open3cl/engine 1.6.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/10_besoin_fr.spec.js +122 -0
  2. package/10_clim.spec.js +116 -0
  3. package/11_besoin_ecs.spec.js +69 -0
  4. package/11_ecs.spec.js +282 -0
  5. package/11_nadeq.spec.js +37 -0
  6. package/12.4_pac.spec.js +131 -0
  7. package/13.2_generateur_combustion.js +2 -0
  8. package/13.2_generateur_combustion.spec.js +537 -0
  9. package/13.2_generateur_combustion_bouilleur.js +4 -1
  10. package/13.2_generateur_combustion_bouilleur.spec.js +74 -0
  11. package/13.2_generateur_combustion_ch.spec.js +323 -0
  12. package/13.2_generateur_combustion_chaudiere.spec.js +85 -0
  13. package/13.2_generateur_pac.spec.js +64 -0
  14. package/13_rendement_distribution_ecs.spec.js +38 -0
  15. package/14_generateur_ecs.js +4 -4
  16. package/14_generateur_ecs.spec.js +745 -0
  17. package/15_conso_aux.spec.js +225 -0
  18. package/16.2_production_enr.spec.js +188 -0
  19. package/16_conso_eclairage.spec.js +62 -0
  20. package/2021_04_13_confort_ete.spec.js +159 -0
  21. package/2021_04_13_qualite_isolation.js +1 -1
  22. package/2021_04_13_qualite_isolation.spec.js +286 -0
  23. package/3.1_b.js +1 -1
  24. package/3.1_b.spec.js +367 -0
  25. package/3.2.1_mur.spec.js +812 -39
  26. package/3.2.2_plancher_bas.spec.js +643 -78
  27. package/3.2.3_plancher_haut.spec.js +452 -0
  28. package/3.3.1.4_porte.spec.js +114 -0
  29. package/3.3_baie_vitree.spec.js +449 -295
  30. package/3.4_pont_thermique.spec.js +890 -0
  31. package/3_deperdition.spec.js +397 -0
  32. package/4_ventilation.js +1 -1
  33. package/4_ventilation.spec.js +406 -0
  34. package/5_conso_ventilation.spec.js +186 -0
  35. package/6.2_surface_sud_equivalente.spec.js +138 -0
  36. package/8_intermittence.spec.js +33 -0
  37. package/9_besoin_ch.js +2 -2
  38. package/9_besoin_ch.spec.js +381 -0
  39. package/9_chauffage.js +4 -0
  40. package/9_chauffage.spec.js +478 -92
  41. package/9_conso_ch.spec.js +502 -248
  42. package/9_emetteur_ch.spec.js +339 -0
  43. package/9_generateur_ch.spec.js +467 -81
  44. package/apport_et_besoin.spec.js +126 -0
  45. package/conso.js +1 -1
  46. package/conso.spec.js +471 -82
  47. package/core/assets/domain/add-additionnal-ue-values-tables.spec.js +53 -0
  48. package/core/assets/domain/synchronize-solicitations-tables.spec.js +25 -0
  49. package/core/assets/domain/synchronize-valeur-tables.spec.js +48 -0
  50. package/core/conf/infrastructure/application.config.spec.js +35 -0
  51. package/core/file/infrastructure/adapter/csv-parser.store.spec.js +31 -13
  52. package/core/file/infrastructure/adapter/file.store.spec.js +148 -12
  53. package/core/tv/infrastructure/assets/additional-ue-values.spec.js +36 -0
  54. package/core/util/infrastructure/object-util.spec.js +20 -0
  55. package/core/util/logger/log-service.js +16 -7
  56. package/core/util/logger/log-service.spec.js +101 -0
  57. package/dpe-sanitizer.service.spec.js +138 -0
  58. package/engine.js +4 -1
  59. package/engine.spec.js +854 -0
  60. package/enums.spec.js +46 -0
  61. package/features/dpe/domain/models/type-generateur.model.js +1 -2
  62. package/features/dpe/domain/models/type-generateur.model.spec.js +25 -0
  63. package/features/dpe/domain/models/type-habitation.model.spec.js +41 -0
  64. package/features/dpe/domain/models/type-stockage.model.spec.js +18 -0
  65. package/features/dpe/domain/models/type-ventilation.model.spec.js +19 -0
  66. package/features/dpe/infrastructure/ch/chTv.store.spec.js +4 -4
  67. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.js +14 -2
  68. package/features/dpe/infrastructure/enveloppe/baieVitreeTv.store.spec.js +16 -0
  69. package/features/dpe/infrastructure/tv.store.js +28 -9
  70. package/features/dpe/infrastructure/tv.store.spec.js +44 -0
  71. package/features/engine/domain/apport_et_besoin/apport-et-besoin.service.spec.js +2 -1
  72. package/features/engine/domain/apport_et_besoin/ch/besoin-ch.service.spec.js +18 -0
  73. package/features/engine/domain/apport_et_besoin/ch/perte-ch-recup.service.spec.js +33 -1
  74. package/features/engine/domain/apport_et_besoin/ecs/besoin-ecs.service.spec.js +21 -1
  75. package/features/engine/domain/apport_et_besoin/ecs/perte-ecs-recup.service.spec.js +41 -1
  76. package/features/engine/domain/apport_et_besoin/froid/besoin-froid.service.spec.js +44 -1
  77. package/features/engine/domain/apport_et_besoin/surface-sud-equivalente.service.spec.js +2 -1
  78. package/features/engine/domain/ch/generateur-ch.service.js +2 -0
  79. package/features/engine/domain/ch/generateur-ch.service.spec.js +117 -2
  80. package/features/engine/domain/ch/installation-ch.service.spec.js +14 -0
  81. package/features/engine/domain/conso/froid/conso-froid.service.spec.js +82 -1
  82. package/features/engine/domain/constants.spec.js +14 -0
  83. package/features/engine/domain/contexte.builder.spec.js +21 -0
  84. package/features/engine/domain/ecs/generateur-ecs.service.spec.js +11 -0
  85. package/features/engine/domain/ecs/installation-ecs.service.spec.js +55 -2
  86. package/features/engine/domain/engine.service.spec.js +227 -4
  87. package/features/engine/domain/enveloppe/baie_vitree/deperdition-baie-vitree.service.spec.js +124 -1
  88. package/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js +270 -2
  89. package/features/engine/domain/enveloppe/deperdition.service.spec.js +141 -0
  90. package/features/engine/domain/enveloppe/espace_tampon/espace-tampon.service.spec.js +2 -1
  91. package/features/engine/domain/enveloppe/mur/deperdition-mur.service.spec.js +117 -1
  92. package/features/engine/domain/enveloppe/plancher_bas/deperdition-plancher-bas.service.spec.js +134 -1
  93. package/features/engine/domain/enveloppe/plancher_haut/deperdition-plancher-haut.service.spec.js +126 -1
  94. package/features/engine/domain/enveloppe/pont_thermique/deperdition-pont-thermique.service.spec.js +18 -1
  95. package/features/engine/domain/enveloppe/porte/deperdition-porte.service.spec.js +2 -1
  96. package/features/engine/domain/enveloppe/ventilation/deperdition-ventilation.service.spec.js +67 -1
  97. package/features/normalizer/domain/dpe-normalizer.service.js +1 -1
  98. package/features/normalizer/domain/dpe-normalizer.service.spec.js +53 -2
  99. package/ficheTechnique.spec.js +26 -0
  100. package/index.spec.js +54 -0
  101. package/package.json +5 -2
  102. package/preprocessor/index.js +20 -0
  103. package/preprocessor/index.spec.js +15 -0
  104. package/tv.spec.js +38 -0
  105. package/utils.js +43 -1
  106. package/utils.spec.js +518 -2
  107. package/output.js +0 -1
package/utils.spec.js CHANGED
@@ -1,5 +1,60 @@
1
- import { convertExpression, getRange, getThicknessFromDescription } from './utils.js';
2
- import { describe, expect, test } from 'vitest';
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+
3
+ // --- Mocks des dépendances de données (contenu réel non testé ici) -----------------------------
4
+ // enums.js et tv.js sont des tables de valeurs : on les mocke pour rendre les tests autonomes
5
+ // vis-à-vis de l'évolution des données réelles. lodash-es, fast-xml-parser et mathjs restent
6
+ // réels car leur comportement (set, parsing XML, evaluate) fait partie de ce que l'on vérifie.
7
+ vi.mock('./enums.js', () => ({
8
+ default: {
9
+ materiaux: { 1: 'brique', 2: 'beton' }
10
+ }
11
+ }));
12
+
13
+ vi.mock('./tv.js', () => ({
14
+ default: {
15
+ // Table dédiée à tvColumnIDs : plusieurs colonnes enum_x_id, dont une indéfinie.
16
+ colonnes: [{ enum_x_id: '1|2' }, { enum_x_id: '2|3' }, { enum_x_id: undefined }],
17
+ // Table dédiée à tvMatch (version historique) et à tv().
18
+ table_defaut: [
19
+ { enum_x_id: '1', val: 'r0' },
20
+ { enum_x_id: '2|3', val: 'r1' },
21
+ { seuil: '≥50', val: 'r2' },
22
+ { code: 'iti+ite', val: 'r3' },
23
+ { code: 'abc', val: 'r4' }
24
+ ],
25
+ // Table dédiée à tvMatchOptimized (version optimisée).
26
+ table_opt: [{ a: 'brique', b: '1|2', c: '≥50', d: 'isolation par exterieur', e: 'zone 3' }]
27
+ }
28
+ }));
29
+
30
+ const {
31
+ xmlParser,
32
+ set_bug_for_bug_compat,
33
+ useEnumAsString,
34
+ set_use_enum_as_string,
35
+ set_tv_match_optimized_version,
36
+ unset_tv_match_optimized_version,
37
+ add_references,
38
+ requestInputID,
39
+ requestInput,
40
+ getKeyByValue,
41
+ tvColumnIDs,
42
+ tvColumnLines,
43
+ tv,
44
+ removeKeyFromJSON,
45
+ clean_dpe,
46
+ getThicknessFromDescription,
47
+ collectionCanBeEmpty,
48
+ getVolumeStockageFromDescription,
49
+ cleanReference,
50
+ compareReferences,
51
+ isEffetJoule,
52
+ containsAnySubstring,
53
+ excel_to_js_exec,
54
+ convertExpression,
55
+ getRange
56
+ } = await import('./utils.js');
57
+ const utils = await import('./utils.js');
3
58
 
4
59
  describe('Utils unit tests', () => {
5
60
  test.each([
@@ -34,4 +89,465 @@ describe('Utils unit tests', () => {
34
89
  ])('should for values %s and inputNumber %s return range %s', (ranges, inputNumber, expected) => {
35
90
  expect(getRange(inputNumber, ranges)).toStrictEqual(expected);
36
91
  });
92
+
93
+ describe('excel_to_js_exec', () => {
94
+ // Règle : une valeur numérique est retournée telle quelle (le séparateur décimal virgule
95
+ // est converti en point au préalable).
96
+ test.each([
97
+ ['12', 12],
98
+ ['0,6', 0.6],
99
+ [42, 42]
100
+ ])('retourne la valeur numérique %s telle quelle', (value, expected) => {
101
+ expect(excel_to_js_exec(value, 25000)).toBe(expected);
102
+ });
103
+
104
+ // Règle : `Pn` est exprimé en kW (l'argument `pn` fourni en W est divisé par 1000) et
105
+ // `logPn` vaut son logarithme décimal.
106
+ test('expose Pn en kW et logPn = log10(Pn)', () => {
107
+ // Pn = 25000 / 1000 = 25 → logPn = log10(25)
108
+ expect(excel_to_js_exec('Pn', 25000)).toBe(25);
109
+ expect(excel_to_js_exec('logPn', 25000)).toBeCloseTo(Math.log10(25), 12);
110
+ });
111
+
112
+ // Règle : le pourcentage est interprété comme une fraction (`0.6%` → 0.006).
113
+ test('interprète un pourcentage comme une fraction', () => {
114
+ expect(excel_to_js_exec('0.6%', 25000)).toBeCloseTo(0.006, 12);
115
+ });
116
+
117
+ // Valeurs de référence de régression (calculées via le vrai module).
118
+ test.each([
119
+ // formule, pn, E, F, résultat attendu
120
+ ['84 + 2 logPn', 25000, undefined, undefined, 86.79588001734407],
121
+ ['Pn * (E + F * logPn) / 100', 50000, 2.5, -0.8, 0.5704119982655924],
122
+ ['0,085*Pn*(Pn)^-0,4', 25000, undefined, undefined, 0.5863851061210162]
123
+ ])('évalue la formule %s (pn=%s)', (formula, pn, E, F, expected) => {
124
+ expect(excel_to_js_exec(formula, pn, E, F)).toBeCloseTo(expected, 9);
125
+ });
126
+ });
127
+
128
+ // -------------------------------------------------------------------------------------------
129
+ describe('xmlParser', () => {
130
+ // Règle : les balises listées dans collectionNames sont toujours forcées en tableau, même
131
+ // si la collection ne contient qu'un seul élément.
132
+ test('force les collections connues en tableau', () => {
133
+ const result = xmlParser.parse('<root><mur>1</mur></root>');
134
+ expect(Array.isArray(result.root.mur)).toBe(true);
135
+ expect(result.root.mur).toStrictEqual([1]);
136
+ });
137
+
138
+ // Règle : une balise hors collectionNames n'est pas forcée en tableau.
139
+ test('ne force pas les balises hors collection en tableau', () => {
140
+ const result = xmlParser.parse('<root><autre>1</autre></root>');
141
+ expect(Array.isArray(result.root.autre)).toBe(false);
142
+ });
143
+
144
+ // Règle du tagValueProcessor : les balises enum_* conservent leur valeur brute (chaîne), les
145
+ // valeurs numériques sont converties en nombre, les autres restent des chaînes.
146
+ test('préserve les enum_* en chaîne et convertit les nombres', () => {
147
+ const result = xmlParser.parse(
148
+ '<root><enum_type_id>05</enum_type_id><count>42</count><name>hello</name></root>'
149
+ );
150
+ expect(result.root.enum_type_id).toBe('05');
151
+ expect(result.root.count).toBe(42);
152
+ expect(result.root.name).toBe('hello');
153
+ });
154
+ });
155
+
156
+ // -------------------------------------------------------------------------------------------
157
+ describe('drapeaux globaux', () => {
158
+ test('set_bug_for_bug_compat active le drapeau', () => {
159
+ set_bug_for_bug_compat();
160
+ expect(utils.bug_for_bug_compat).toBe(true);
161
+ });
162
+
163
+ test('set_use_enum_as_string active le drapeau', () => {
164
+ set_use_enum_as_string();
165
+ expect(utils.use_enum_as_string).toBe(true);
166
+ });
167
+
168
+ test('set/unset_tv_match_optimized_version bascule le drapeau', () => {
169
+ set_tv_match_optimized_version();
170
+ expect(utils.tv_match_new_version).toBe(true);
171
+ unset_tv_match_optimized_version();
172
+ expect(utils.tv_match_new_version).toBe(false);
173
+ });
174
+ });
175
+
176
+ // -------------------------------------------------------------------------------------------
177
+ describe('useEnumAsString', () => {
178
+ // Règle : seules les clés enum_* non nulles sont converties en chaîne, récursivement.
179
+ test('convertit récursivement les enum_* non nuls en chaîne', () => {
180
+ const obj = {
181
+ enum_a: 5,
182
+ enum_b: null,
183
+ other: 3,
184
+ nested: { enum_c: 7 }
185
+ };
186
+ useEnumAsString(obj);
187
+ expect(obj.enum_a).toBe('5');
188
+ expect(obj.enum_b).toBeNull();
189
+ expect(obj.other).toBe(3);
190
+ expect(obj.nested.enum_c).toBe('7');
191
+ });
192
+ });
193
+
194
+ // -------------------------------------------------------------------------------------------
195
+ describe('add_references', () => {
196
+ const buildCollections = (items) => ({
197
+ mur_collection: { mur: items ? [...items] : undefined },
198
+ plancher_haut_collection: { plancher_haut: items ? [...items] : undefined },
199
+ plancher_bas_collection: { plancher_bas: items ? [...items] : undefined },
200
+ baie_vitree_collection: { baie_vitree: items ? [...items] : undefined },
201
+ porte_collection: { porte: items ? [...items] : undefined }
202
+ });
203
+
204
+ // Règle : chaque élément sans référence reçoit `<type>_<index>` ; ceux qui en ont une la
205
+ // conservent.
206
+ test('affecte une référence aux éléments qui n en ont pas', () => {
207
+ const enveloppe = {
208
+ mur_collection: {
209
+ mur: [{ donnee_entree: {} }, { donnee_entree: { reference: 'M' } }]
210
+ },
211
+ plancher_haut_collection: {
212
+ plancher_haut: [{ donnee_entree: {} }, { donnee_entree: { reference: 'PH' } }]
213
+ },
214
+ plancher_bas_collection: {
215
+ plancher_bas: [{ donnee_entree: {} }, { donnee_entree: { reference: 'PB' } }]
216
+ },
217
+ baie_vitree_collection: {
218
+ baie_vitree: [{ donnee_entree: {} }, { donnee_entree: { reference: 'BV' } }]
219
+ },
220
+ porte_collection: {
221
+ porte: [{ donnee_entree: {} }, { donnee_entree: { reference: 'P' } }]
222
+ }
223
+ };
224
+
225
+ add_references(enveloppe);
226
+
227
+ expect(enveloppe.mur_collection.mur[0].donnee_entree.reference).toBe('mur_0');
228
+ expect(enveloppe.mur_collection.mur[1].donnee_entree.reference).toBe('M');
229
+ expect(enveloppe.plancher_haut_collection.plancher_haut[0].donnee_entree.reference).toBe(
230
+ 'plancher_haut_0'
231
+ );
232
+ expect(enveloppe.plancher_bas_collection.plancher_bas[0].donnee_entree.reference).toBe(
233
+ 'plancher_bas_0'
234
+ );
235
+ expect(enveloppe.baie_vitree_collection.baie_vitree[0].donnee_entree.reference).toBe(
236
+ 'baie_vitree_0'
237
+ );
238
+ expect(enveloppe.porte_collection.porte[0].donnee_entree.reference).toBe('porte_0');
239
+ });
240
+
241
+ // Règle : une collection absente (tableau indéfini) est traitée comme vide, sans erreur.
242
+ test('supporte des collections absentes', () => {
243
+ const enveloppe = buildCollections(null);
244
+ expect(() => add_references(enveloppe)).not.toThrow();
245
+ });
246
+ });
247
+
248
+ // -------------------------------------------------------------------------------------------
249
+ describe('requestInput / requestInputID', () => {
250
+ test('requestInput : champ enum sans type retourne la valeur enum et publie les clés', () => {
251
+ const de = { enum_materiaux_id: '1' };
252
+ const du = {};
253
+ expect(requestInput(de, du, 'materiaux')).toBe('brique');
254
+ expect(du.enum_materiaux_id).toStrictEqual(['1', '2']);
255
+ });
256
+
257
+ test('requestInput : champ enum avec type publie le type fourni', () => {
258
+ const de = { enum_materiaux_id: '2' };
259
+ const du = {};
260
+ expect(requestInput(de, du, 'materiaux', ['2'])).toBe('beton');
261
+ expect(du.enum_materiaux_id).toStrictEqual(['2']);
262
+ });
263
+
264
+ test('requestInput : champ non enum avec type retourne la valeur brute', () => {
265
+ const de = { epaisseur: 5 };
266
+ const du = {};
267
+ expect(requestInput(de, du, 'epaisseur', 'number')).toBe(5);
268
+ expect(du.epaisseur).toBe('number');
269
+ });
270
+
271
+ test('requestInput : champ non enum sans type journalise une erreur et retourne null', () => {
272
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
273
+ const du = {};
274
+ expect(requestInput({}, du, 'epaisseur')).toBeNull();
275
+ expect(spy).toHaveBeenCalledOnce();
276
+ spy.mockRestore();
277
+ });
278
+
279
+ test('requestInputID : sans type publie les clés et retourne l identifiant', () => {
280
+ const de = { enum_materiaux_id: '1' };
281
+ const du = {};
282
+ expect(requestInputID(de, du, 'materiaux')).toBe('1');
283
+ expect(du.enum_materiaux_id).toStrictEqual(['1', '2']);
284
+ });
285
+
286
+ test('requestInputID : avec type publie le type fourni', () => {
287
+ const de = { enum_materiaux_id: '2' };
288
+ const du = {};
289
+ expect(requestInputID(de, du, 'materiaux', ['2'])).toBe('2');
290
+ expect(du.enum_materiaux_id).toStrictEqual(['2']);
291
+ });
292
+ });
293
+
294
+ // -------------------------------------------------------------------------------------------
295
+ describe('getKeyByValue', () => {
296
+ test('retourne la clé associée à la valeur', () => {
297
+ expect(getKeyByValue({ a: 1, b: 2 }, 2)).toBe('b');
298
+ });
299
+ test('retourne undefined si la valeur est absente', () => {
300
+ expect(getKeyByValue({ a: 1 }, 99)).toBeUndefined();
301
+ });
302
+ });
303
+
304
+ // -------------------------------------------------------------------------------------------
305
+ describe('tvColumnIDs', () => {
306
+ // Règle : agrège les identifiants uniques de la colonne enum_<field>_id, en éclatant les
307
+ // valeurs multiples séparées par « | » et en ignorant les cellules vides.
308
+ test('retourne les identifiants uniques éclatés', () => {
309
+ expect(tvColumnIDs('colonnes', 'x')).toStrictEqual(['1', '2', '3']);
310
+ });
311
+ });
312
+
313
+ // -------------------------------------------------------------------------------------------
314
+ describe('tv / tvMatch (version historique)', () => {
315
+ beforeEach(() => {
316
+ unset_tv_match_optimized_version();
317
+ });
318
+
319
+ // Règle enum : la valeur est ancrée (^...$) et une cellule « a|b » matche si l'une des
320
+ // alternatives correspond.
321
+ test('matche une valeur enum contenue dans une cellule « a|b »', () => {
322
+ expect(tv('table_defaut', { enum_x_id: '3' })).toStrictEqual({
323
+ enum_x_id: '2|3',
324
+ val: 'r1'
325
+ });
326
+ });
327
+
328
+ // Règle seuil : pour un entier et une cellule « ≥/≤ », la comparaison est évaluée.
329
+ test('évalue un seuil « ≥ » pour une valeur entière', () => {
330
+ expect(tv('table_defaut', { seuil: 60 })).toStrictEqual({ seuil: '≥50', val: 'r2' });
331
+ // 40 < 50 → pas de correspondance complète, retourne le meilleur match partiel (ici aucun
332
+ // autre champ commun → premier candidat comptabilisé nul).
333
+ });
334
+
335
+ // Règle d'échappement : « ^iti+ite$ » est transformé pour échapper le « + » littéral.
336
+ test('échappe le « + » dans un motif ancré', () => {
337
+ expect(tv('table_defaut', { code: '^iti+ite$' })).toStrictEqual({
338
+ code: 'iti+ite',
339
+ val: 'r3'
340
+ });
341
+ });
342
+
343
+ // Règle : sans correspondance complète, tv retourne la ligne au plus grand nombre de
344
+ // champs concordants.
345
+ test('retourne le meilleur match partiel faute de correspondance complète', () => {
346
+ expect(tv('table_defaut', { enum_x_id: '1', absent: 'z' })).toStrictEqual({
347
+ enum_x_id: '1',
348
+ val: 'r0'
349
+ });
350
+ });
351
+
352
+ // Règle : une clé absente de la ligne ne matche jamais.
353
+ test('une clé absente de la ligne empêche la correspondance', () => {
354
+ // « seuil » n'existe que sur r2 ; matcher enum_x_id + seuil ne matche complètement aucune
355
+ // ligne → meilleur partiel = r0 (enum_x_id concorde).
356
+ expect(tv('table_defaut', { enum_x_id: '1', seuil: 60 })).toStrictEqual({
357
+ enum_x_id: '1',
358
+ val: 'r0'
359
+ });
360
+ });
361
+ });
362
+
363
+ // -------------------------------------------------------------------------------------------
364
+ describe('tvColumnLines', () => {
365
+ beforeEach(() => {
366
+ unset_tv_match_optimized_version();
367
+ });
368
+
369
+ // Règle : retourne les valeurs de la colonne demandée pour toutes les lignes qui matchent le
370
+ // matcher.
371
+ test('retourne les colonnes des lignes concordantes', () => {
372
+ expect(tvColumnLines('table_defaut', 'val', { enum_x_id: '1' })).toStrictEqual(['r0']);
373
+ });
374
+ });
375
+
376
+ // -------------------------------------------------------------------------------------------
377
+ describe('tv / tvMatchOptimized (version optimisée)', () => {
378
+ beforeEach(() => {
379
+ set_tv_match_optimized_version();
380
+ });
381
+
382
+ // Correspondance exacte.
383
+ test('matche une valeur identique', () => {
384
+ expect(tv('table_opt', { a: 'brique' }).a).toBe('brique');
385
+ });
386
+
387
+ // Correspondance après suppression des ancres ^...$.
388
+ test('matche après suppression des ancres du motif', () => {
389
+ expect(tv('table_opt', { a: '^brique$' }).a).toBe('brique');
390
+ });
391
+
392
+ // Correspondance par sous-chaîne pour une valeur non numérique.
393
+ test('matche par sous-chaîne (valeur non numérique)', () => {
394
+ expect(tv('table_opt', { d: 'exterieur' }).d).toBe('isolation par exterieur');
395
+ });
396
+
397
+ // Correspondance sur une cellule « a|b ».
398
+ test('matche une alternative d une cellule « a|b »', () => {
399
+ expect(tv('table_opt', { b: '2' }).b).toBe('1|2');
400
+ });
401
+
402
+ // Comparaison de seuil « ≥ » pour un entier.
403
+ test('évalue un seuil « ≥ » pour une valeur entière', () => {
404
+ expect(tv('table_opt', { c: 60 }).c).toBe('≥50');
405
+ });
406
+
407
+ // Correspondance par sous-chaîne finale (valeur numérique présente en fin de chaîne).
408
+ test('matche par sous-chaîne finale une valeur numérique', () => {
409
+ expect(tv('table_opt', { e: 3 }).e).toBe('zone 3');
410
+ });
411
+
412
+ // Absence de correspondance : cellule vide.
413
+ test('ne matche pas une clé absente de la ligne', () => {
414
+ expect(tv('table_opt', { absent: 'x' })).toBeNull();
415
+ });
416
+
417
+ // Absence de correspondance : sous-chaîne introuvable.
418
+ test('ne matche pas une sous-chaîne introuvable', () => {
419
+ expect(tv('table_opt', { a: 'zzz' })).toBeNull();
420
+ });
421
+ });
422
+
423
+ // -------------------------------------------------------------------------------------------
424
+ describe('removeKeyFromJSON', () => {
425
+ // Règle : supprime récursivement la clé cible, sauf sous les branches listées dans skipKeys.
426
+ test('supprime récursivement la clé sauf sous les branches ignorées', () => {
427
+ const obj = {
428
+ a: 1,
429
+ remove_me: 2,
430
+ skip: { remove_me: 3 },
431
+ nested: { remove_me: 4, b: 5 }
432
+ };
433
+ removeKeyFromJSON(obj, 'remove_me', ['skip']);
434
+ expect(obj.remove_me).toBeUndefined();
435
+ expect(obj.skip.remove_me).toBe(3);
436
+ expect(obj.nested.remove_me).toBeUndefined();
437
+ expect(obj.nested.b).toBe(5);
438
+ });
439
+ });
440
+
441
+ // -------------------------------------------------------------------------------------------
442
+ describe('clean_dpe', () => {
443
+ // Règle : supprime les donnee_intermediaire sauf sous generateur_ecs/generateur_chauffage et
444
+ // remet logement.sortie à null.
445
+ test('nettoie les données intermédiaires et vide la sortie', () => {
446
+ const dpe = {
447
+ logement: {
448
+ sortie: { x: 1 },
449
+ donnee_intermediaire: { y: 2 },
450
+ generateur_ecs: [{ donnee_intermediaire: { keep: 1 } }]
451
+ }
452
+ };
453
+ clean_dpe(dpe);
454
+ expect(dpe.logement.donnee_intermediaire).toBeUndefined();
455
+ expect(dpe.logement.generateur_ecs[0].donnee_intermediaire).toStrictEqual({ keep: 1 });
456
+ expect(dpe.logement.sortie).toBeNull();
457
+ });
458
+ });
459
+
460
+ // -------------------------------------------------------------------------------------------
461
+ describe('collectionCanBeEmpty', () => {
462
+ const buildLogement = (ponts, deperditionMur) => ({
463
+ enveloppe: {
464
+ pont_thermique_collection: ponts ? { pont_thermique: ponts } : {}
465
+ },
466
+ sortie: { deperdition: { deperdition_mur: deperditionMur } }
467
+ });
468
+
469
+ // Règle : vide autorisé si aucun pont thermique de la liaison ET déperdition nulle.
470
+ test('vrai si aucun pont de liaison et déperdition nulle', () => {
471
+ const logement = buildLogement([{ donnee_entree: { enum_type_liaison_id: 5 } }], 0);
472
+ expect(collectionCanBeEmpty(logement, 'mur', 9)).toBe(true);
473
+ });
474
+
475
+ // Règle : faux si au moins un pont de la liaison existe.
476
+ test('faux si un pont de la liaison existe', () => {
477
+ const logement = buildLogement([{ donnee_entree: { enum_type_liaison_id: 5 } }], 0);
478
+ expect(collectionCanBeEmpty(logement, 'mur', 5)).toBe(false);
479
+ });
480
+
481
+ // Règle : faux si la déperdition est non nulle.
482
+ test('faux si la déperdition est non nulle', () => {
483
+ const logement = buildLogement([], 10);
484
+ expect(collectionCanBeEmpty(logement, 'mur', 5)).toBe(false);
485
+ });
486
+
487
+ // Règle : collection de ponts absente traitée comme vide.
488
+ test('supporte une collection de ponts absente', () => {
489
+ const logement = buildLogement(null, 0);
490
+ expect(collectionCanBeEmpty(logement, 'mur', 5)).toBe(true);
491
+ });
492
+ });
493
+
494
+ // -------------------------------------------------------------------------------------------
495
+ describe('getVolumeStockageFromDescription', () => {
496
+ test.each([
497
+ [null, 0],
498
+ ['pas de contenance', 0],
499
+ ['ballon type contenance ballon 200 litres', 200]
500
+ ])('extrait le volume de « %s » → %s', (description, expected) => {
501
+ expect(getVolumeStockageFromDescription(description)).toBe(expected);
502
+ });
503
+ });
504
+
505
+ // -------------------------------------------------------------------------------------------
506
+ describe('cleanReference / compareReferences', () => {
507
+ test.each([
508
+ ['Réf Élément 1', 'refelement1'],
509
+ [42, '42'],
510
+ [null, null],
511
+ [undefined, undefined]
512
+ ])('nettoie « %s » → %s', (input, expected) => {
513
+ expect(cleanReference(input)).toBe(expected);
514
+ });
515
+
516
+ test('compareReferences ignore espaces et accents', () => {
517
+ expect(compareReferences('Réf 1', 'ref1')).toBe(true);
518
+ expect(compareReferences('a', 'b')).toBe(false);
519
+ });
520
+ });
521
+
522
+ // -------------------------------------------------------------------------------------------
523
+ describe('isEffetJoule', () => {
524
+ const buildInstal = (genId, surface) => ({
525
+ generateur_chauffage_collection: {
526
+ generateur_chauffage: [{ donnee_entree: { enum_type_generateur_ch_id: genId } }]
527
+ },
528
+ donnee_entree: { surface_chauffee: surface }
529
+ });
530
+
531
+ // Règle : effet joule si la surface chauffée par résistance électrique est majoritaire (>= 50 %).
532
+ test('retourne « 1 » quand l effet joule est majoritaire', () => {
533
+ const instal = [buildInstal('100', 60), buildInstal('50', 40)];
534
+ expect(isEffetJoule(instal)).toBe('1');
535
+ });
536
+
537
+ test('retourne « 0 » quand l effet joule est minoritaire', () => {
538
+ const instal = [buildInstal('100', 30), buildInstal('50', 70)];
539
+ expect(isEffetJoule(instal)).toBe('0');
540
+ });
541
+ });
542
+
543
+ // -------------------------------------------------------------------------------------------
544
+ describe('containsAnySubstring', () => {
545
+ test.each([
546
+ ['Hello World', ['xyz', 'wor'], true],
547
+ ['abc', ['xyz'], false],
548
+ [123, ['2'], true]
549
+ ])('« %s » contient une des sous-chaînes %s → %s', (mainString, substrings, expected) => {
550
+ expect(containsAnySubstring(mainString, substrings)).toBe(expected);
551
+ });
552
+ });
37
553
  });
package/output.js DELETED
@@ -1 +0,0 @@
1
- filecontent