@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.
- package/13.2_generateur_combustion.js +116 -31
- package/13.2_generateur_combustion.spec.js +286 -2
- package/13.2_generateur_combustion_pn.js +248 -0
- package/13.2_generateur_combustion_pn.spec.js +476 -0
- package/6.2_surface_sud_equivalente.js +77 -6
- package/6.2_surface_sud_equivalente.spec.js +138 -2
- package/README.md +22 -22
- package/conso.js +178 -4
- package/conso.spec.js +187 -1
- package/engine.js +7 -4
- package/features/engine/domain/apport_et_besoin/surface-sud-equivalente.service.js +136 -70
- package/features/engine/domain/apport_et_besoin/surface-sud-equivalente.service.spec.js +280 -0
- package/features/engine/domain/engine.service.js +4 -1
- package/features/engine/domain/engine.service.spec.js +14 -0
- package/features/engine/domain/enveloppe/deperdition-enveloppe.service.js +8 -7
- package/features/engine/domain/enveloppe/deperdition-enveloppe.service.spec.js +230 -0
- package/package.json +1 -1
- package/utils.js +5 -0
- package/utils.spec.js +53 -0
|
@@ -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
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 = {};
|