Consigne app §D, mise à jour du 2026-10-15, vérifiée sur le banc (etm16) : un plan qui publie forecastWeatherSpreadValid ne compte que si ce témoin vaut true et que le pourcentage du jour est un nombre (null sinon) — pas de bande, jamais une bande à 0. Un 0 % valide est une vraie mesure. La fourchette datée (forecastWeatherSpreadTime) n'est plus « non datée ». Les plans antérieurs gardent l'ancienne règle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
448 lines
18 KiB
Dart
448 lines
18 KiB
Dart
/// La prévision PV de `+etm68` — `NymeaEnergy.GetForecastSnapshot`.
|
||
///
|
||
/// La fixture est la réponse RÉELLE du banc `hems` (10.42.60.21), capturée le 2026-10-14.
|
||
/// Les cas de trou, de pas et de défaut sont dérivés d'elle ou écrits à la main, et chaque
|
||
/// test dit ce qu'il aurait attrapé s'il avait échoué.
|
||
library;
|
||
|
||
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
import 'package:etm_powersync_app/models/axe_echantillons.dart';
|
||
import 'package:etm_powersync_app/models/pv_forecast.dart';
|
||
import 'package:etm_powersync_app/services/nymea_service.dart';
|
||
import 'package:etm_powersync_app/widgets/pv_forecast_card.dart';
|
||
import 'package:fl_chart/fl_chart.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
|
||
Map<String, dynamic> _banc() => jsonDecode(
|
||
File('test/fixtures/forecast_snapshot_hems_etm68.json').readAsStringSync())
|
||
as Map<String, dynamic>;
|
||
|
||
Map<String, dynamic> _pt(String t, num wh) => {'targetAt': t, 'forecastWh': wh};
|
||
|
||
Widget _monte(PvForecastView v) => MaterialApp(
|
||
// Contrainte BORNÉE en largeur, hauteur infinie : c'est le montage réel (un
|
||
// ListView dans l'écran Énergie).
|
||
home: Scaffold(body: ListView(children: [v])),
|
||
);
|
||
|
||
PvForecastView _vue(PvForecastSnapshot? s,
|
||
{String? erreur,
|
||
PvForecastSite? site,
|
||
List<({double x, double w})> mesure = const []}) {
|
||
// Axe de test : heures depuis le premier créneau — l'écran Énergie fournit le sien.
|
||
final t0 = s != null && s.points.isNotEmpty
|
||
? s.points.first.debutUtc
|
||
: DateTime.utc(2026, 10, 14);
|
||
double x(DateTime t) => t.toUtc().difference(t0).inSeconds / 3600;
|
||
return PvForecastView(
|
||
chargement: false,
|
||
snapshot: s,
|
||
erreur: erreur,
|
||
site: site,
|
||
avecMesure: mesure.isNotEmpty,
|
||
nomDuThing: (id) => id == '{8e0b7b38-3180-46be-ba41-08b08d630173}'
|
||
? 'Kutzenhausen'
|
||
: id,
|
||
graphe: (_, snap) => PvForecastLineChart(
|
||
snapshot: snap,
|
||
mesure: mesure,
|
||
x: x,
|
||
minX: 0,
|
||
maxX: x(snap.points.last.debutUtc) + 1,
|
||
etiquette: (_) => null,
|
||
xInterval: 1,
|
||
),
|
||
);
|
||
}
|
||
|
||
void main() {
|
||
group('lecture de la réponse du banc', () {
|
||
final s = PvForecastSnapshot.fromJson(_banc());
|
||
|
||
test('48 créneaux, en UTC, triés', () {
|
||
expect(s.etat, PvForecastEtat.courbe);
|
||
expect(s.points, hasLength(48));
|
||
expect(s.points.first.debutUtc.isUtc, isTrue);
|
||
expect(s.points.first.debutUtc, DateTime.utc(2026, 10, 13, 22));
|
||
});
|
||
|
||
test('le pas est DÉDUIT : une heure sur le banc', () {
|
||
expect(s.pas, const Duration(hours: 1));
|
||
expect(s.creneauxManquants, 0);
|
||
expect(s.segments, hasLength(1));
|
||
});
|
||
|
||
test('métadonnées +etm68 lues', () {
|
||
expect(s.corrections, ['temperature']);
|
||
expect(s.conversion, 'physics');
|
||
expect(s.model, 'meteofrance_seamless');
|
||
expect(s.perField.single.corrections, ['temperature']);
|
||
expect(s.acLimitW, isNull); // pas de plafond configuré sur le banc
|
||
// Un INSTANT se rend chez le lecteur : l'attendu se dérive de toLocal(), il ne se
|
||
// recopie pas de la charge utile (sous TZ=UTC ce contrôle ne mord pas).
|
||
expect(s.contentChangedAt,
|
||
DateTime.utc(2026, 10, 14, 5, 15).toLocal());
|
||
expect(s.contentChangedAt!.isUtc, isFalse);
|
||
});
|
||
});
|
||
|
||
group('un créneau absent n\'est pas un zéro', () {
|
||
final s = PvForecastSnapshot.fromJson({
|
||
'capturedAt': '2026-10-14T05:00:00Z',
|
||
'forecastPoints': [
|
||
_pt('2026-10-14T08:00:00Z', 500),
|
||
_pt('2026-10-14T09:00:00Z', 900),
|
||
// 10:00 et 11:00 manquent
|
||
_pt('2026-10-14T12:00:00Z', 1100),
|
||
],
|
||
});
|
||
|
||
test('le pas reste une heure malgré le trou', () {
|
||
// Aurait attrapé un pas pris sur le premier écart venu, ou sur la moyenne.
|
||
expect(s.pas, const Duration(hours: 1));
|
||
expect(s.creneauxManquants, 2);
|
||
expect(s.segments.map((g) => g.length), [2, 1]);
|
||
});
|
||
|
||
testWidgets('la courbe est COUPÉE au trou, sans point à zéro', (t) async {
|
||
await t.pumpWidget(_monte(_vue(s)));
|
||
final chart = t.widget<LineChart>(find.byType(LineChart));
|
||
final spots = chart.data.lineBarsData.first.spots;
|
||
expect(spots.where((p) => p == FlSpot.nullSpot), hasLength(1));
|
||
// Aucun zéro n'a été inventé pour combler 10:00–12:00.
|
||
expect(spots.where((p) => p != FlSpot.nullSpot && p.y == 0), isEmpty);
|
||
// Plus de texte sous le graphe (Patrick, 2026-10-15) : le trou se voit sur la courbe.
|
||
expect(find.textContaining('sans prévision'), findsNothing);
|
||
});
|
||
});
|
||
|
||
test('un pas de 15 min se déduit aussi — rien n\'est codé en dur', () {
|
||
final s = PvForecastSnapshot.fromJson({
|
||
'forecastPoints': [
|
||
_pt('2026-10-14T08:00:00Z', 100),
|
||
_pt('2026-10-14T08:15:00Z', 120),
|
||
_pt('2026-10-14T08:30:00Z', 140),
|
||
],
|
||
});
|
||
expect(s.pas, const Duration(minutes: 15));
|
||
});
|
||
|
||
testWidgets('Wh par créneau → kW moyen : 250 Wh sur 15 min = 1 kW', (t) async {
|
||
final s = PvForecastSnapshot.fromJson({
|
||
'forecastPoints': [
|
||
_pt('2026-10-14T08:00:00Z', 250),
|
||
_pt('2026-10-14T08:15:00Z', 250),
|
||
],
|
||
});
|
||
await t.pumpWidget(_monte(_vue(s)));
|
||
final spots =
|
||
t.widget<LineChart>(find.byType(LineChart)).data.lineBarsData.first.spots;
|
||
// Aurait attrapé une heure codée en dur (0,25 kW) au lieu du pas déduit.
|
||
expect(spots.first.y, closeTo(1.0, 1e-9));
|
||
});
|
||
|
||
group('fault présent ⇒ pas de courbe', () {
|
||
testWidgets('MISSING_FORECAST_SITE propose les candidats, n\'adopte rien',
|
||
(t) async {
|
||
final s = PvForecastSnapshot.fromJson({
|
||
'capturedAt': '2026-10-14T05:00:00Z',
|
||
'fault': 'MISSING_FORECAST_SITE',
|
||
// Des points qui traîneraient ne doivent PAS être tracés.
|
||
'forecastPoints': [_pt('2026-10-14T08:00:00Z', 500)],
|
||
});
|
||
expect(s.etat, PvForecastEtat.siteManquant);
|
||
await t.pumpWidget(_monte(_vue(s,
|
||
site: const PvForecastSite(
|
||
candidats: ['{8e0b7b38-3180-46be-ba41-08b08d630173}']))));
|
||
expect(find.byType(LineChart), findsNothing);
|
||
expect(find.text('Kutzenhausen'), findsOneWidget);
|
||
expect(find.text('Désigner'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('FORECAST_PAYLOAD_REFUSED montre sa cause', (t) async {
|
||
final s = PvForecastSnapshot.fromJson({
|
||
'fault': 'FORECAST_PAYLOAD_REFUSED',
|
||
'faultParams': {'cause': 'unknown version 3'},
|
||
});
|
||
await t.pumpWidget(_monte(_vue(s)));
|
||
expect(find.byType(LineChart), findsNothing);
|
||
expect(find.textContaining('unknown version 3'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('un défaut inconnu est montré, jamais tu', (t) async {
|
||
final s = PvForecastSnapshot.fromJson({'fault': 'SOMETHING_NEW'});
|
||
expect(s.etat, PvForecastEtat.faultInconnu);
|
||
await t.pumpWidget(_monte(_vue(s)));
|
||
expect(find.text('SOMETHING_NEW'), findsOneWidget);
|
||
});
|
||
});
|
||
|
||
testWidgets('ni courbe ni fault : « aucune prévision publiée »', (t) async {
|
||
final s = PvForecastSnapshot.fromJson({'capturedAt': '2026-10-14T05:00:00Z'});
|
||
expect(s.etat, PvForecastEtat.rienPublie);
|
||
await t.pumpWidget(_monte(_vue(s)));
|
||
expect(find.text('Aucune prévision publiée'), findsOneWidget);
|
||
expect(find.byType(LineChart), findsNothing);
|
||
});
|
||
|
||
testWidgets('non lu ≠ rien de publié', (t) async {
|
||
await t.pumpWidget(_monte(_vue(null, erreur: 'TimeoutException')));
|
||
expect(find.text('Prévision non lue'), findsOneWidget);
|
||
expect(find.text('Aucune prévision publiée'), findsNothing);
|
||
});
|
||
|
||
testWidgets('libellé honnête, et plus de détails installateur sous le graphe',
|
||
(t) async {
|
||
// Retirés à la demande de Patrick le 2026-10-14.
|
||
final s = PvForecastSnapshot.fromJson(_banc());
|
||
await t.pumpWidget(_monte(_vue(s)));
|
||
expect(find.textContaining('plafond optimiste'), findsWidgets);
|
||
expect(find.text('Détails installateur'), findsNothing);
|
||
});
|
||
|
||
testWidgets('la production mesurée se superpose, signe déjà retourné',
|
||
(t) async {
|
||
final s = PvForecastSnapshot.fromJson(_banc());
|
||
await t.pumpWidget(_monte(_vue(s, mesure: [
|
||
(x: 10.0, w: 600),
|
||
(x: 10.25, w: 700),
|
||
])));
|
||
final bars = t.widget<LineChart>(find.byType(LineChart)).data.lineBarsData;
|
||
expect(bars, hasLength(2));
|
||
// Plein = réel, contour = prévu : la mesure est une surface, la prévision un trait
|
||
// pointillé sans remplissage, tracé par-dessus.
|
||
expect(bars[0].spots.map((p) => p.y), [0.6, 0.7]);
|
||
expect(bars[0].belowBarData.show, isTrue);
|
||
expect(bars[1].dashArray, isNotNull);
|
||
expect(bars[1].belowBarData.show, isFalse);
|
||
});
|
||
|
||
group('bornes VE : `{uuid}` minuscules (jusqu\'à etm69)', () {
|
||
test('sans accolades, en majuscules → canonique', () {
|
||
expect(idBorneCanonique('4D1ADAFD-CAFC-4813-93B1-CA8933CE71AB'),
|
||
'{4d1adafd-cafc-4813-93b1-ca8933ce71ab}');
|
||
});
|
||
test('déjà canonique → inchangé', () {
|
||
const id = '{4d1adafd-cafc-4813-93b1-ca8933ce71ab}';
|
||
expect(idBorneCanonique(id), id);
|
||
});
|
||
test('un nom de charge n\'est pas réécrit', () {
|
||
expect(idBorneCanonique('chauffe-eau'), 'chauffe-eau');
|
||
});
|
||
});
|
||
|
||
group('axe commun avec l\'écran Énergie', () {
|
||
final t0 = DateTime.utc(2026, 10, 14, 8);
|
||
final q = const Duration(minutes: 15);
|
||
// 08:00, 08:15, [08:30 manquant], 08:45
|
||
final axe = AxeEchantillons([t0, t0.add(q), t0.add(q * 3)], q);
|
||
|
||
test('un index entier rend l\'horodatage exact de l\'échantillon', () {
|
||
expect(axe.instant(2), t0.add(q * 3));
|
||
expect(axe.x(t0.add(q * 3)), 2);
|
||
});
|
||
|
||
test('entre deux échantillons : interpolé, même par-dessus un trou', () {
|
||
// 08:30 est au milieu de 08:15 et 08:45 — index 1,5 et non 2.
|
||
// Aurait attrapé une conversion « (t − t0) / pas » qui ignore le trou et décale
|
||
// toute la prévision d'un quart d'heure par rapport aux courbes du dessus.
|
||
expect(axe.x(t0.add(q * 2)), closeTo(1.5, 1e-9));
|
||
});
|
||
|
||
test('au-delà de la dernière mesure : extrapolé au pas — le futur', () {
|
||
expect(axe.x(t0.add(q * 3 + const Duration(hours: 1))), closeTo(6, 1e-9));
|
||
expect(axe.instant(6), t0.add(q * 3 + const Duration(hours: 1)));
|
||
});
|
||
});
|
||
|
||
group('fourchette météo (section D)', () {
|
||
// Valeurs RÉELLES du plan « Toiture sud 2 kWc » du banc, lues le 2026-10-14.
|
||
const banc = (
|
||
id: '{3cee9703-5406-426e-9a85-913746175253}',
|
||
dispersionAujourdhuiPct: 7.8,
|
||
dispersionDemainPct: 19.7,
|
||
modele: 'icon_seamless',
|
||
kwc: 2.0,
|
||
valide: null, // plugin antérieur à etm16 : ni daté, ni invalidé
|
||
instant: null,
|
||
);
|
||
final snap = PvForecastSnapshot.fromJson(_banc());
|
||
final f = FourchetteMeteo.depuisPlans([banc]);
|
||
final matin = DateTime.utc(2026, 10, 14, 8); // 10:00 à Paris
|
||
final bande = bandePrevision(snap, f, maintenant: matin)!;
|
||
PvForecastPoint pt(DateTime t) => snap.points.firstWhere((p) => p.debutUtc == t);
|
||
|
||
test('jour civil de PARIS, heure d\'été comprise', () {
|
||
// 22:00Z le 13 = minuit à Paris le 14 (CEST, +2).
|
||
expect(jourParis(DateTime.utc(2026, 10, 13, 22)), DateTime.utc(2026, 10, 14));
|
||
expect(jourParis(DateTime.utc(2026, 10, 13, 21, 59)), DateTime.utc(2026, 10, 13));
|
||
// Après le 25 octobre 01:00Z : CET, +1.
|
||
expect(jourParis(DateTime.utc(2026, 10, 26, 22, 30)), DateTime.utc(2026, 10, 26));
|
||
expect(jourParis(DateTime.utc(2026, 10, 26, 23)), DateTime.utc(2026, 10, 27));
|
||
});
|
||
|
||
test('±s autour de la courbe : 10:00 Paris aujourd\'hui, 7,8 %', () {
|
||
final t = DateTime.utc(2026, 10, 14, 8);
|
||
final b = bande.parCreneau[t]!;
|
||
expect(bande.source, SourceBande.fourchetteMeteo);
|
||
expect(b.basWh, closeTo(pt(t).wh * (1 - 0.078), 1e-6));
|
||
expect(b.hautWh, closeTo(pt(t).wh * (1 + 0.078), 1e-6));
|
||
});
|
||
|
||
test('demain a SA dispersion (19,7 %), pas celle du jour', () {
|
||
// Aurait attrapé une dispersion du jour appliquée aux 48 créneaux.
|
||
final t = DateTime.utc(2026, 10, 15, 8);
|
||
final b = bande.parCreneau[t]!;
|
||
expect(b.basWh, closeTo(pt(t).wh * (1 - 0.197), 1e-6));
|
||
});
|
||
|
||
test('le haut est arrêté à la crête des plans (2 kWc → 2 000 Wh/h)', () {
|
||
// 1 900 Wh × 1,197 = 2 274 Wh dépasserait ce que 2 kWc peuvent produire en une heure.
|
||
final s = PvForecastSnapshot.fromJson({
|
||
'forecastPoints': [
|
||
_pt('2026-10-15T10:00:00Z', 1900),
|
||
_pt('2026-10-15T11:00:00Z', 1900),
|
||
],
|
||
});
|
||
final b = bandePrevision(s, f, maintenant: matin)!;
|
||
expect(b.parCreneau.values.first.hautWh, 2000);
|
||
});
|
||
|
||
test('le plafond AC l\'emporte sur la crête quand il existe', () {
|
||
final s = PvForecastSnapshot.fromJson({
|
||
'acLimitW': 1500,
|
||
'forecastPoints': [
|
||
_pt('2026-10-15T10:00:00Z', 1400),
|
||
_pt('2026-10-15T11:00:00Z', 1400),
|
||
],
|
||
});
|
||
expect(bandePrevision(s, f, maintenant: matin)!.parCreneau.values.first.hautWh,
|
||
1500);
|
||
});
|
||
|
||
test('le bas est arrêté à 0', () {
|
||
final large = FourchetteMeteo.depuisPlans(
|
||
[(id: 'x', dispersionAujourdhuiPct: 140.0, dispersionDemainPct: null, modele: 'm', kwc: 2.0, valide: null, instant: null)]);
|
||
final b = bandePrevision(snap, large, maintenant: matin)!;
|
||
expect(b.parCreneau[DateTime.utc(2026, 10, 14, 8)]!.basWh, 0);
|
||
// Et sans dispersion pour demain, demain n'a pas de bande.
|
||
expect(b.parCreneau[DateTime.utc(2026, 10, 15, 8)], isNull);
|
||
});
|
||
|
||
test('plusieurs plans : la plus forte dispersion l\'emporte, les crêtes s\'additionnent',
|
||
() {
|
||
final g = FourchetteMeteo.depuisPlans([
|
||
banc,
|
||
(id: 'y', dispersionAujourdhuiPct: 12.0, dispersionDemainPct: 5.0, modele: 'icon_seamless', kwc: 3.0, valide: null, instant: null),
|
||
]);
|
||
expect(g.aujourdhui, closeTo(0.12, 1e-9));
|
||
expect(g.demain, closeTo(0.197, 1e-9));
|
||
expect(g.kwcTotal, 5.0);
|
||
});
|
||
|
||
test('dispersion absente → pas de bande (0 % sans modèle = état jamais écrit)', () {
|
||
final jamais = FourchetteMeteo.depuisPlans(
|
||
[(id: 'x', dispersionAujourdhuiPct: 0.0, dispersionDemainPct: 0.0, modele: '', kwc: 2.0, valide: null, instant: null)]);
|
||
expect(jamais.vide, isTrue);
|
||
expect(bandePrevision(snap, jamais, maintenant: matin), isNull);
|
||
expect(bandePrevision(snap, null, maintenant: matin), isNull);
|
||
});
|
||
|
||
test('non datée tant qu\'openmeteo ne date pas la valeur', () {
|
||
expect(bande.attenuee, isTrue);
|
||
});
|
||
|
||
testWidgets('un créneau absent est un trou — bande comprise', (t) async {
|
||
final troue = PvForecastSnapshot.fromJson({
|
||
'forecastPoints': [
|
||
_pt('2026-10-14T08:00:00Z', 500),
|
||
_pt('2026-10-14T09:00:00Z', 900),
|
||
_pt('2026-10-14T11:00:00Z', 1100), // 10:00Z manque
|
||
],
|
||
});
|
||
final b = bandePrevision(troue, f, maintenant: matin)!;
|
||
expect(b.parCreneau.containsKey(DateTime.utc(2026, 10, 14, 10)), isFalse);
|
||
final t0 = troue.points.first.debutUtc;
|
||
await t.pumpWidget(_monte(PvForecastView(
|
||
chargement: false,
|
||
snapshot: troue,
|
||
nomDuThing: (id) => id,
|
||
bande: b,
|
||
graphe: (_, s) => PvForecastLineChart(
|
||
snapshot: s,
|
||
bande: b,
|
||
x: (d) => d.difference(t0).inSeconds / 3600,
|
||
minX: 0,
|
||
maxX: 4,
|
||
etiquette: (_) => null,
|
||
xInterval: 1,
|
||
),
|
||
)));
|
||
final data = t.widget<LineChart>(find.byType(LineChart)).data;
|
||
// Deux segments de bande, chacun une paire (haut, bas) remplie entre elles.
|
||
// Aurait attrapé un remplissage qui enjambe le trou.
|
||
expect(data.betweenBarsData, hasLength(2));
|
||
expect(find.textContaining('Fourchette météo (dispersion des modèles) · non datée'),
|
||
findsOneWidget);
|
||
expect(find.textContaining('intervalle'), findsNothing);
|
||
expect(find.textContaining('confiance'), findsNothing);
|
||
});
|
||
|
||
test('p10W / p90W présents → ils REMPLACENT la fourchette météo', () {
|
||
final appris = PvForecastSnapshot.fromJson({
|
||
'forecastPoints': [
|
||
{'targetAt': '2026-10-14T08:00:00Z', 'forecastWh': 500, 'p10W': 200, 'p90W': 600},
|
||
{'targetAt': '2026-10-14T09:00:00Z', 'forecastWh': 900, 'p10W': 400, 'p90W': 1000},
|
||
],
|
||
});
|
||
final b = bandePrevision(appris, f, maintenant: matin)!;
|
||
expect(b.source, SourceBande.p10p90);
|
||
expect(b.parCreneau[DateTime.utc(2026, 10, 14, 8)],
|
||
(basWh: 200.0, hautWh: 600.0));
|
||
expect(b.attenuee, isFalse);
|
||
});
|
||
});
|
||
|
||
group('fourchette météo datée (openmeteo etm16)', () {
|
||
final snap = PvForecastSnapshot.fromJson(_banc());
|
||
final matin = DateTime.utc(2026, 10, 14, 8);
|
||
PlanMeteo plan({required bool valide, double? auj = 11.7, double? dem = 24.6}) => (
|
||
id: '{3cee9703-5406-426e-9a85-913746175253}',
|
||
dispersionAujourdhuiPct: auj,
|
||
dispersionDemainPct: dem,
|
||
modele: 'icon_seamless',
|
||
kwc: 2.0,
|
||
valide: valide,
|
||
instant: DateTime.utc(2026, 10, 15, 6, 11),
|
||
);
|
||
|
||
test('valide et daté : bande tracée, plus de « non datée »', () {
|
||
final f = FourchetteMeteo.depuisPlans([plan(valide: true)]);
|
||
expect(f.datee, isTrue);
|
||
final b = bandePrevision(snap, f, maintenant: matin)!;
|
||
expect(b.attenuee, isFalse);
|
||
});
|
||
|
||
test('invalide : pas de bande — jamais une bande à 0', () {
|
||
// Aurait attrapé une valeur gardée après échec de l'API d'ensemble.
|
||
final f = FourchetteMeteo.depuisPlans([plan(valide: false)]);
|
||
expect(f.vide, isTrue);
|
||
expect(bandePrevision(snap, f, maintenant: matin), isNull);
|
||
});
|
||
|
||
test('valide mais pourcentage null (JSON) : pas de bande ce jour-là', () {
|
||
final f = FourchetteMeteo.depuisPlans([plan(valide: true, auj: null)]);
|
||
expect(f.aujourdhui, isNull);
|
||
expect(f.demain, closeTo(0.246, 1e-9));
|
||
});
|
||
|
||
test('valide et 0 % : une vraie mesure, la bande se confond avec la courbe', () {
|
||
final f = FourchetteMeteo.depuisPlans([plan(valide: true, auj: 0)]);
|
||
expect(f.aujourdhui, 0);
|
||
});
|
||
});
|
||
}
|