/// **Deux grandeurs, deux phrases, aucune ne ment.** /// /// Les tuiles disent la JOURNÉE — différence de compteurs sur `periodStartedAt → maintenant`. /// La ligne des ratios dit ce que la BOX publie, qui part de la **baseline** et non du début /// de journée. /// /// Le cas est arrivé de lui-même le 2026-09-05 : `nymead` redémarré à 09:23, sonde à 09:26, /// et la box publiait **100 % d'autoconsommation**. Vrai sur trois minutes ; absurde intitulé /// « aujourd'hui ». C'est ce que le drapeau rend lisible — et c'est pour ça que la ligne ne /// s'affiche QUE lorsque l'origine diffère : quand les deux coïncident, une phrase de plus /// serait du bruit, et un texte permanent cesse d'être lu. library; import 'package:etm_powersync_app/features/dashboard/widgets/kpi_row.dart'; import 'package:etm_powersync_app/models/daily_balance.dart'; import 'package:etm_powersync_app/models/energy_data.dart'; import 'package:etm_powersync_app/models/energy_ratios_snapshot.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; /// Relevé `.75` du 2026-09-05 09:26, après le redémarrage de 09:23. EnergyRatiosSnapshot _releve({int measuredSince = 1788592995}) => EnergyRatiosSnapshot.fromJson({ 'autonomyRate': 28.570199887530475, 'selfConsumptionRate': 100, 'periodStartedAt': 1788559200, 'measuredSince': measuredSince, 'timeZone': 'Europe/Paris', }); DailyBalance _bilan() => DailyBalance( from: DateTime(2026, 9, 5, 0, 15), to: DateTime(2026, 9, 5, 9, 26), periodStart: DateTime(2026, 9, 5), sampleCount: 37, productionKwh: 8.0, consumptionKwh: 12.0, importKwh: 5.0, exportKwh: 1.0, ); /// La NUIT du 2026-09-07 sur `.75` : consommation réelle, production nulle depuis minuit. DailyBalance _bilanDeNuit() => DailyBalance( from: DateTime(2026, 9, 7), to: DateTime(2026, 9, 7, 2, 30), periodStart: DateTime(2026, 9, 7), sampleCount: 10, productionKwh: 0.0, consumptionKwh: 1.4, importKwh: 1.4, exportKwh: 0.0, ); Widget _harnessBilan(DailyBalance b) => MaterialApp( home: Scaffold( body: SingleChildScrollView( child: KpiRow(data: const EnergyData(), balance: b), ), ), ); Widget _harness({EnergyRatiosSnapshot? ratios}) => MaterialApp( home: Scaffold( body: SingleChildScrollView( child: KpiRow( data: const EnergyData(), balance: _bilan(), ratios: ratios), ), ), ); void main() { testWidgets('baseline reposée en journée : l\'origine est DITE', (tester) async { await tester.binding.setSurfaceSize(const Size(1000, 2000)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(_harness(ratios: _releve())); expect(find.textContaining('Instantané depuis 09:23'), findsOneWidget); // Le 100 % est affiché — il est vrai — mais jamais sous le mot « aujourd'hui ». expect(find.textContaining('autoconsommation 100 %'), findsOneWidget); }); testWidgets('les tuiles restent la JOURNÉE, calculée sur la fenêtre de la box', (tester) async { await tester.binding.setSurfaceSize(const Size(1000, 2000)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(_harness(ratios: _releve())); // Les anneaux animent leur valeur : sans laisser l'animation finir, ils affichent 0 — // une absence qui se lirait comme un succès si on cherchait justement une absence. await tester.pumpAndSettle(); expect(find.text('Aujourd\'hui'), findsOneWidget); // 8 kWh produits, 1 exporté -> 87,5 % ; et surtout PAS le 100 % de la box. expect(find.textContaining('88'), findsOneWidget); }); testWidgets('origine confondue avec le début de journée : rien de plus', (tester) async { await tester.binding.setSurfaceSize(const Size(1000, 2000)); addTearDown(() => tester.binding.setSurfaceSize(null)); // Le cas normal : la baseline date du début de journée. await tester.pumpWidget(_harness(ratios: _releve(measuredSince: 1788559200))); expect(find.textContaining('Instantané depuis'), findsNothing); }); testWidgets('les deux coïncident : la ligne se TAIT — vu sur appareil', (tester) async { await tester.binding.setSurfaceSize(const Size(1000, 2000)); addTearDown(() => tester.binding.setSurfaceSize(null)); // Relevé du 2026-09-05 16:52 : bilan 51,8 / 54,1 sur 00:00→16:45, box 51,9 / 53,7 // depuis 09:23. L'origine diffère bien, mais les nombres non — et la ligne écrivait // « 52 %, 54 % » juste sous des tuiles qui affichaient déjà 52 % et 54 %. final coincide = EnergyRatiosSnapshot.fromJson({ 'autonomyRate': 53.67, 'selfConsumptionRate': 51.92, 'periodStartedAt': 1788559200, 'measuredSince': 1788592995, 'timeZone': 'Europe/Paris', }); await tester.pumpWidget(MaterialApp( home: Scaffold( body: SingleChildScrollView( child: KpiRow( data: const EnergyData(), // 8 kWh produits / 1 exporté -> 87,5 % ; 12 consommés / 5 importés -> 58,3 %. // On force ici un bilan proche des valeurs de la box pour éprouver le seuil. balance: DailyBalance( from: DateTime(2026, 9, 5, 0, 15), to: DateTime(2026, 9, 5, 16, 45), periodStart: DateTime(2026, 9, 5), sampleCount: 72, productionKwh: 59.89, consumptionKwh: 57.31, importKwh: 26.31, exportKwh: 28.89, ), ratios: coincide, ), ), ), )); await tester.pumpAndSettle(); expect(find.textContaining('Instantané depuis'), findsNothing, reason: 'répéter les mêmes nombres oblige le lecteur à raisonner pour ' 'découvrir que les fenêtres diffèrent'); }); testWidgets('box antérieure : aucune ligne, aucune supposition', (tester) async { await tester.binding.setSurfaceSize(const Size(1000, 2000)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(_harness()); expect(find.textContaining('Instantané'), findsNothing); }); group('un taux sans dénominateur — le « — » est juste, le MOTIF ne l\'était pas', () { // Signalé sur appareil le 2026-09-07 : « Autoconsommation — · pas mesurable » de nuit, // sur une installation dont les quatre compteurs marchaient. Deux causes distinctes // portaient la même phrase, et celle qui s'affichait accusait la mesure. testWidgets('sans production, la tuile dit « sans production »', (tester) async { await tester.binding.setSurfaceSize(const Size(1000, 2000)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(_harnessBilan(_bilanDeNuit())); await tester.pumpAndSettle(); expect(find.textContaining('Autoconsommation · sans production'), findsOneWidget); // Le contrôle qui MORD : l'ancien motif ne doit plus être là pour cette tuile. expect(find.textContaining('Autoconsommation · pas mesurable'), findsNothing); // Et surtout PAS un zéro sur CETTE tuile : « 0 % » affirmerait qu'aucune production // n'a été autoconsommée, une phrase sur une production qui n'existe pas. Le tiret y // est, et il y est une seule fois — l'autre tuile, elle, a bien un nombre. // // ⚠️ Ne pas écrire `find.text('0 %'), findsNothing` : le 0 % de l'Autonomie est une // MESURE et le contrôle mordrait sur elle. Première rédaction de ce test, corrigée // en la voyant échouer. expect(find.text('—'), findsOneWidget); }); testWidgets('l\'autonomie, elle, a son dénominateur et vaut 0 %', (tester) async { await tester.binding.setSurfaceSize(const Size(1000, 2000)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(_harnessBilan(_bilanDeNuit())); await tester.pumpAndSettle(); // Tout vient du réseau : 0 % est une MESURE, pas un trou. Les deux tuiles sont // voisines et leurs deux « rien » sont de natures opposées — c'est le cas qui // interdit de traiter les taux ensemble. expect(find.text('0 %'), findsOneWidget); expect(find.textContaining('Autonomie · '), findsNothing); }); testWidgets('sans bilan du tout, le motif reste « pas mesurable »', (tester) async { await tester.binding.setSurfaceSize(const Size(1000, 2000)); addTearDown(() => tester.binding.setSurfaceSize(null)); await tester.pumpWidget(MaterialApp( home: Scaffold( body: SingleChildScrollView( child: KpiRow(data: const EnergyData(), balance: null), ), ), )); await tester.pumpAndSettle(); expect(find.textContaining('Autoconsommation · pas mesurable'), findsOneWidget); expect(find.textContaining('Autonomie · pas mesurable'), findsOneWidget); }); }); }