@open3cl/engine 1.8.7 → 1.8.9

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.
@@ -0,0 +1,230 @@
1
+ import { beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { DeperditionEnveloppeService } from './deperdition-enveloppe.service.js';
3
+
4
+ /**
5
+ * Tests unitaires isolés de `DeperditionEnveloppeService`.
6
+ *
7
+ * Les services de déperdition par type de paroi sont remplacés par des doubles de test
8
+ * (spies `vi.fn`) : seule la logique d'agrégation propre au service est vérifiée
9
+ * (sommes b × S × U, surfaces déperditives / isolées / menuiseries, espaces tampons).
10
+ */
11
+
12
+ let murService;
13
+ let porteService;
14
+ let plancherBasService;
15
+ let plancherHautService;
16
+ let baieVitreeService;
17
+ let espaceTamponService;
18
+ let pontThermiqueService;
19
+ let ventilationService;
20
+
21
+ /** @type {DeperditionEnveloppeService} */
22
+ let service;
23
+
24
+ const ctx = { zoneClimatique: { value: 'h1b' } };
25
+
26
+ /** Logement sans aucun élément d'enveloppe ni ventilation. */
27
+ function logementVide() {
28
+ return {
29
+ enveloppe: {
30
+ mur_collection: {},
31
+ porte_collection: {},
32
+ plancher_bas_collection: {},
33
+ plancher_haut_collection: {},
34
+ baie_vitree_collection: {},
35
+ ets_collection: {},
36
+ pont_thermique_collection: {}
37
+ },
38
+ ventilation_collection: {}
39
+ };
40
+ }
41
+
42
+ describe('DeperditionEnveloppeService (§3 déperditions de l’enveloppe)', () => {
43
+ beforeEach(() => {
44
+ murService = { execute: vi.fn() };
45
+ porteService = { execute: vi.fn() };
46
+ plancherBasService = { execute: vi.fn() };
47
+ plancherHautService = { execute: vi.fn() };
48
+ baieVitreeService = { execute: vi.fn() };
49
+ espaceTamponService = { execute: vi.fn() };
50
+ pontThermiqueService = { execute: vi.fn() };
51
+ ventilationService = { execute: vi.fn() };
52
+ service = new DeperditionEnveloppeService(
53
+ murService,
54
+ porteService,
55
+ plancherBasService,
56
+ plancherHautService,
57
+ baieVitreeService,
58
+ espaceTamponService,
59
+ pontThermiqueService,
60
+ ventilationService
61
+ );
62
+ });
63
+
64
+ test('logement sans aucun élément : toutes les déperditions sont nulles', () => {
65
+ const logement = logementVide();
66
+ expect(service.deperditions(ctx, logement)).toEqual({
67
+ deperdition_mur: 0,
68
+ deperdition_plancher_bas: 0,
69
+ deperdition_plancher_haut: 0,
70
+ deperdition_baie_vitree: 0,
71
+ deperdition_pont_thermique: 0,
72
+ deperdition_porte: 0,
73
+ hperm: 0,
74
+ hvent: 0,
75
+ deperdition_enveloppe: 0
76
+ });
77
+ expect(espaceTamponService.execute).not.toHaveBeenCalled();
78
+ });
79
+
80
+ test('agrège b × S × U par paroi et transmet les surfaces à la ventilation', () => {
81
+ const logement = logementVide();
82
+ const env = logement.enveloppe;
83
+ env.mur_collection.mur = [
84
+ // déperditif, non isolé (type 2)
85
+ { donnee_entree: { surface_paroi_opaque: 10, enum_type_isolation_id: '2' } },
86
+ // déperditif, isolé, mais sur local non déperditif (22) : hors surface déperditive
87
+ {
88
+ donnee_entree: {
89
+ surface_paroi_opaque: 5,
90
+ enum_type_isolation_id: '3',
91
+ enum_type_adjacence_id: '22'
92
+ }
93
+ },
94
+ // b = 0 : ni surface déperditive, ni isolée
95
+ { donnee_entree: { surface_paroi_opaque: 100, enum_type_isolation_id: '3' } }
96
+ ];
97
+ murService.execute
98
+ .mockReturnValueOnce({ b: 1, umur: 2 })
99
+ .mockReturnValueOnce({ b: 0.5, umur: 1 })
100
+ .mockReturnValueOnce({ b: 0, umur: 1 });
101
+
102
+ env.porte_collection.porte = [
103
+ { donnee_entree: { surface_porte: 2, presence_joint: true } },
104
+ { donnee_entree: { surface_porte: 3, presence_joint: false } }
105
+ ];
106
+ porteService.execute
107
+ .mockReturnValueOnce({ b: 1, uporte: 3 })
108
+ .mockReturnValueOnce({ b: 0, uporte: 3 });
109
+
110
+ env.plancher_bas_collection.plancher_bas = [{ donnee_entree: { surface_paroi_opaque: 20 } }];
111
+ plancherBasService.execute.mockReturnValue({ b: 1, upb_final: 0.5 });
112
+
113
+ env.plancher_haut_collection.plancher_haut = [
114
+ { donnee_entree: { surface_paroi_opaque: 30, enum_type_isolation_id: '1' } },
115
+ {
116
+ donnee_entree: {
117
+ surface_paroi_opaque: 7,
118
+ enum_type_isolation_id: '4',
119
+ enum_type_adjacence_id: '22'
120
+ }
121
+ },
122
+ { donnee_entree: { surface_paroi_opaque: 50, enum_type_isolation_id: '4' } }
123
+ ];
124
+ plancherHautService.execute
125
+ .mockReturnValueOnce({ b: 1, uph: 1 })
126
+ .mockReturnValueOnce({ b: 1, uph: 1 })
127
+ .mockReturnValueOnce({ b: 0, uph: 1 });
128
+
129
+ env.baie_vitree_collection.baie_vitree = [
130
+ { donnee_entree: { surface_totale_baie: 4, presence_joint: true } },
131
+ { donnee_entree: { surface_totale_baie: 6 } }
132
+ ];
133
+ baieVitreeService.execute
134
+ .mockReturnValueOnce({ b: 1, u_menuiserie: 2 })
135
+ .mockReturnValueOnce({ b: 0, u_menuiserie: 2 });
136
+
137
+ env.pont_thermique_collection.pont_thermique = [
138
+ { donnee_entree: { l: 10, pourcentage_valeur_pont_thermique: 0.5 } },
139
+ { donnee_entree: { l: 4 } }
140
+ ];
141
+ pontThermiqueService.execute.mockReturnValue({ k: 0.5 });
142
+
143
+ logement.ventilation_collection.ventilation = [{ donnee_entree: { id: 'v' } }];
144
+ ventilationService.execute.mockReturnValue({ hvent: 10, hperm: 5 });
145
+
146
+ const resultat = service.deperditions(ctx, logement);
147
+
148
+ expect(resultat).toEqual({
149
+ deperdition_mur: 10 * 2 + 0.5 * 5 * 1,
150
+ deperdition_porte: 2 * 3,
151
+ deperdition_plancher_bas: 20 * 0.5,
152
+ deperdition_plancher_haut: 30 + 7,
153
+ deperdition_baie_vitree: 4 * 2,
154
+ deperdition_pont_thermique: 10 * 0.5 * 0.5 + 4 * 0.5,
155
+ hvent: 10,
156
+ hperm: 5,
157
+ deperdition_enveloppe: 22.5 + 6 + 10 + 37 + 8 + 4.5 + 10 + 5
158
+ });
159
+
160
+ // Délégation aux services de paroi
161
+ expect(plancherBasService.execute).toHaveBeenCalledWith(
162
+ ctx,
163
+ env.plancher_bas_collection.plancher_bas[0].donnee_entree,
164
+ env.plancher_bas_collection.plancher_bas
165
+ );
166
+ expect(pontThermiqueService.execute).toHaveBeenCalledWith(
167
+ ctx,
168
+ env,
169
+ env.pont_thermique_collection.pont_thermique[0].donnee_entree
170
+ );
171
+
172
+ // Surfaces transmises à la ventilation :
173
+ // déperditive = mur 10 + porte 2 + plancher haut 30 + baie 4 = 46
174
+ // isolée = mur 5 + plancher haut 7 = 12 ; non isolée = mur 10 + plancher haut 30 = 40
175
+ // menuiseries avec joint = porte 2 + baie 4 = 6 ; sans joint = porte 3 + baie 6 = 9
176
+ expect(ventilationService.execute).toHaveBeenCalledWith(ctx, { id: 'v' }, 46, 12, 40, 6, 9);
177
+ });
178
+
179
+ test('les surfaces sont réinitialisées à chaque calcul', () => {
180
+ const logement = logementVide();
181
+ logement.enveloppe.porte_collection.porte = [{ donnee_entree: { surface_porte: 2 } }];
182
+ porteService.execute.mockReturnValue({ b: 1, uporte: 1 });
183
+ logement.ventilation_collection.ventilation = [{ donnee_entree: {} }];
184
+ ventilationService.execute.mockReturnValue({ hvent: 0, hperm: 0 });
185
+
186
+ service.deperditions(ctx, logement);
187
+ service.deperditions(ctx, logement);
188
+
189
+ expect(ventilationService.execute).toHaveBeenLastCalledWith(ctx, {}, 2, 0, 0, 0, 2);
190
+ });
191
+
192
+ describe('espaces tampons solarisés', () => {
193
+ test('un ETS unique (objet) reçoit ses données intermédiaires', () => {
194
+ const logement = logementVide();
195
+ const ets = { donnee_entree: { tv_coef_transparence_ets_id: '2' } };
196
+ logement.enveloppe.ets_collection.ets = ets;
197
+ espaceTamponService.execute.mockReturnValue({ bver: 0.55, coef_transparence_ets: 0.62 });
198
+
199
+ service.deperditions(ctx, logement);
200
+
201
+ expect(espaceTamponService.execute).toHaveBeenCalledWith(ctx, ets);
202
+ expect(ets.donnee_intermediaire).toEqual({ bver: 0.55, coef_transparence_ets: 0.62 });
203
+ });
204
+
205
+ test('plusieurs ETS : bver et T calculés pour chacun d’eux (issue #101)', () => {
206
+ const logement = logementVide();
207
+ const V1 = { donnee_entree: { reference: 'V1' } };
208
+ const V2 = { donnee_entree: { reference: 'V2' } };
209
+ logement.enveloppe.ets_collection.ets = [V1, V2];
210
+ espaceTamponService.execute
211
+ .mockReturnValueOnce({ bver: 0.55, coef_transparence_ets: 0.62 })
212
+ .mockReturnValueOnce({ bver: 0.85, coef_transparence_ets: 0.45 });
213
+
214
+ service.deperditions(ctx, logement);
215
+
216
+ expect(espaceTamponService.execute).toHaveBeenCalledTimes(2);
217
+ expect(espaceTamponService.execute).toHaveBeenNthCalledWith(1, ctx, V1);
218
+ expect(espaceTamponService.execute).toHaveBeenNthCalledWith(2, ctx, V2);
219
+ expect(V1.donnee_intermediaire).toEqual({ bver: 0.55, coef_transparence_ets: 0.62 });
220
+ expect(V2.donnee_intermediaire).toEqual({ bver: 0.85, coef_transparence_ets: 0.45 });
221
+ });
222
+
223
+ test('sans ets_collection : aucun calcul d’espace tampon', () => {
224
+ const logement = logementVide();
225
+ delete logement.enveloppe.ets_collection;
226
+ service.deperditions(ctx, logement);
227
+ expect(espaceTamponService.execute).not.toHaveBeenCalled();
228
+ });
229
+ });
230
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open3cl/engine",
3
- "version": "1.8.7",
3
+ "version": "1.8.9",
4
4
  "description": "Open Source 3CL-DPE engine",
5
5
  "main": "index.js",
6
6
  "directories": {
package/utils.js CHANGED
@@ -277,10 +277,15 @@ export function requestInput(de, du, field, type) {
277
277
  return enums[field][de[enum_name]];
278
278
  } else {
279
279
  // not enums
280
+ // `!type` couvre aussi NaN, 0 et '' (valeurs falsy)
280
281
  if (!type) {
281
282
  console.error(`requestInput: type is not defined for ${field}`);
282
283
  return null;
283
284
  }
285
+ if (!['string', 'number', 'boolean'].includes(typeof type)) {
286
+ console.error(`requestInput: type has an unexpected type (${typeof type}) for ${field}`);
287
+ return null;
288
+ }
284
289
  du[field] = type;
285
290
  return de[field];
286
291
  }
package/utils.spec.js CHANGED
@@ -287,6 +287,59 @@ describe('Utils unit tests', () => {
287
287
  spy.mockRestore();
288
288
  });
289
289
 
290
+ test.each([
291
+ ['NaN', Number.NaN],
292
+ ['0', 0],
293
+ ['une chaîne vide', '']
294
+ ])(
295
+ 'requestInput : champ non enum avec %s comme type est traité comme un type absent',
296
+ (_libelle, type) => {
297
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
298
+ const de = { epaisseur: 5 };
299
+ const du = {};
300
+ expect(requestInput(de, du, 'epaisseur', type)).toBeNull();
301
+ expect(spy).toHaveBeenCalledWith('requestInput: type is not defined for epaisseur');
302
+ // Le type invalide n'est pas publié dans donnee_utilisateur
303
+ expect(du).toStrictEqual({});
304
+ spy.mockRestore();
305
+ }
306
+ );
307
+
308
+ test.each([
309
+ ['un objet', { a: 1 }, 'object'],
310
+ ['un tableau', ['number'], 'object'],
311
+ ['une fonction', () => 'number', 'function'],
312
+ ['un symbole', Symbol('number'), 'symbol'],
313
+ ['un bigint', 10n, 'bigint']
314
+ ])(
315
+ 'requestInput : champ non enum avec %s comme type journalise une erreur et retourne null',
316
+ (_libelle, type, typeAttendu) => {
317
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
318
+ const de = { epaisseur: 5 };
319
+ const du = {};
320
+ expect(requestInput(de, du, 'epaisseur', type)).toBeNull();
321
+ expect(spy).toHaveBeenCalledWith(
322
+ `requestInput: type has an unexpected type (${typeAttendu}) for epaisseur`
323
+ );
324
+ expect(du).toStrictEqual({});
325
+ spy.mockRestore();
326
+ }
327
+ );
328
+
329
+ test.each([
330
+ ['une chaîne', 'string'],
331
+ ['un nombre', 1],
332
+ ['un booléen', true]
333
+ ])('requestInput : champ non enum avec %s comme type est accepté', (_libelle, type) => {
334
+ const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
335
+ const de = { epaisseur: 5 };
336
+ const du = {};
337
+ expect(requestInput(de, du, 'epaisseur', type)).toBe(5);
338
+ expect(du.epaisseur).toBe(type);
339
+ expect(spy).not.toHaveBeenCalled();
340
+ spy.mockRestore();
341
+ });
342
+
290
343
  test('requestInputID : sans type publie les clés et retourne l identifiant', () => {
291
344
  const de = { enum_materiaux_id: '1' };
292
345
  const du = {};