/// La tuile « Borne EV » **rendue muette** — éprouvée sur ce qu'elle affiche. /// /// Le défaut qu'elle corrigeait ne se voyait pas en test de fonction : les trois grandeurs /// affichées (« 3600 W », « 82 % solaire », « Mode PV ») étaient des valeurs par défaut de /// `EnergyData`, justes au sens du type et fausses à l'écran. Ces tests montent donc le /// widget et lisent le texte, comme ceux des notes de configuration. library; import 'package:etm_powersync_app/models/ev_charger_live.dart'; import 'package:etm_powersync_app/widgets/ev_favorite_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; Widget _harness({required EvChargerLive? live}) => MaterialApp(home: Scaffold(body: EvFavoriteTile(live: live))); /// La V2C du banc, telle que `.75` la publiait le 2026-08-28 : états `pluggedIn`, /// `charging`, `currentPower` sur le Thing, mode par `GetChargingInfos`. EvChargerLive _v2c({ bool? pluggedIn = false, bool? charging = false, double? powerW = 0, String? apiMode = 'ChargingModeNormal', }) => EvChargerLive( thingId: '4d1adafd-cafc-4813-93b1-ca8933ce71ab', name: 'V2C Trydan', pluggedIn: pluggedIn, charging: charging, currentPowerW: powerW, apiMode: apiMode, ); void main() { group('borne injoignable — ni le zéro forgé, ni la valeur périmée', () { // Mesuré sur `.75` le 2026-08-30 : quand le lien Modbus de la V2C tombe, la box // publie `connected: false`, `pluggedIn: false`, `charging: false`, // `currentPower: 0` et un `statusMessage`. La voiture, elle, était branchée et en // charge à 4 356 W. `NymeaService.evChargerFor` jette ces zéros ; ces tests // vérifient ce que la tuile en fait. EvChargerLive horsLigne({String? message}) => EvChargerLive( thingId: '4d1adafd-cafc-4813-93b1-ca8933ce71ab', name: 'V2C Trydan', apiMode: 'ChargingModeNormal', joignable: false, messageEtat: message, ); testWidgets('elle dit la coupure, avec la phrase du plugin', (tester) async { await tester.pumpWidget(_harness( live: horsLigne( message: 'Hors ligne depuis 30/08 12:39 — timeout Modbus — ' 'vérifier signal WiFi de la borne'))); expect(find.text('Hors ligne'), findsOneWidget); expect(find.textContaining('timeout Modbus'), findsOneWidget); // …et surtout PAS ce que la box publiait au même instant. expect(find.textContaining('Aucun véhicule branché'), findsNothing); expect(find.textContaining('0 W'), findsNothing); }); testWidgets('sans phrase du plugin, elle nomme quand même l\'ignorance', (tester) async { await tester.pumpWidget(_harness(live: horsLigne())); expect(find.textContaining('ne sait plus si un véhicule est branché'), findsOneWidget); }); testWidgets('le MODE reste lu — il vient de la box, pas de la borne', (tester) async { // Un lien coupé n'invalide pas un réglage stocké dans la box : le cacher // priverait le client de la seule chose encore vraie à l'écran. await tester.pumpWidget(_harness(live: horsLigne())); expect(find.textContaining('Mode Boost'), findsOneWidget); }); testWidgets('`joignable == null` n\'est PAS hors ligne', (tester) async { // Une borne qui ne publie pas `connected` ne dit rien de son lien. Supposer la // panne serait le symétrique exact du zéro forgé. await tester.pumpWidget(_harness( live: _v2c(pluggedIn: true, charging: true, powerW: 4356))); expect(find.text('Hors ligne'), findsNothing); expect(find.text('4356 W'), findsOneWidget); }); }); group('la tuile ne montre que ce que la box publie', () { testWidgets('sans borne, elle le dit — elle ne disparaît pas', (tester) async { // Retirer la tuile laisserait un trou dans une liste que le client a composée. await tester.pumpWidget(_harness(live: null)); expect(find.textContaining('Aucune borne déclarée'), findsOneWidget); expect(find.textContaining('W'), findsNothing); }); testWidgets('une borne qui ne publie rien n\'affiche AUCUN zéro', (tester) async { await tester.pumpWidget(_harness( live: _v2c( pluggedIn: null, charging: null, powerW: null, apiMode: null))); // Les trois absences se disent, chacune à sa place. expect(find.text('Puissance non publiée'), findsOneWidget); expect(find.textContaining('Branchement non publié'), findsOneWidget); expect(find.text('Mode non lu'), findsOneWidget); // Et surtout : pas de « 0 W », qui se lirait « la borne ne consomme rien ». expect(find.textContaining('0 W'), findsNothing); }); testWidgets('elle affiche la puissance MESURÉE et le mode DE LA BOX', (tester) async { await tester.pumpWidget(_harness( live: _v2c( pluggedIn: true, charging: true, powerW: 3907, apiMode: 'ChargingModeEco'))); expect(find.text('3907 W'), findsOneWidget); expect(find.text('En charge'), findsOneWidget); expect(find.text('Mode PV'), findsOneWidget); expect(find.text('V2C Trydan'), findsOneWidget); // Aucune part solaire : la box ne dit pas d'où vient le courant d'une borne. expect(find.textContaining('solaire'), findsNothing); }); testWidgets('un mode à échéance se lit comme tel, un mode inconnu aussi', (tester) async { await tester.pumpWidget( _harness(live: _v2c(apiMode: 'ChargingModeEcoMinWithTargetTime'))); expect(find.text('Mode Min+PV + échéance'), findsOneWidget); // Une box peut porter un mode qu'aucune version d'app ne connaît. Deviner serait // pire que le nommer. await tester.pumpWidget(_harness(live: _v2c(apiMode: 'ChargingModeFutur'))); await tester.pump(); expect(find.textContaining('Mode non reconnu'), findsOneWidget); }); testWidgets('branché mais à l\'arrêt n\'est pas « en charge »', (tester) async { await tester.pumpWidget( _harness(live: _v2c(pluggedIn: true, charging: false, powerW: 0))); expect(find.textContaining('branché, à l\'arrêt'), findsOneWidget); // Ici le zéro est une MESURE, pas une absence : la borne publie `currentPower`. expect(find.text('0 W'), findsOneWidget); }); }); group('pleine puissance = hors arbitrage, et l\'écran le dit', () { testWidgets('ChargingModeNormal porte la note — c\'est le mode d\'office', (tester) async { // Une borne détectée arrive en ChargingModeNormal : pleine puissance, hors // arbitrage. Sans cette ligne, l'installateur pose une borne, elle charge à fond, // et il cherche pourquoi le HEMS l'ignore. await tester.pumpWidget(_harness(live: _v2c(apiMode: 'ChargingModeNormal'))); expect(find.textContaining('hors arbitrage'), findsOneWidget); expect(find.textContaining('mode d\'office'), findsOneWidget); // Elle n'accuse personne : la box publie le même code pour un boost délibéré. final note = tester .widget(find.textContaining('hors arbitrage')) .data!; expect(note, isNot(contains('personne'))); expect(note, isNot(contains('oubli'))); }); testWidgets('un mode éco la fait disparaître', (tester) async { await tester.pumpWidget(_harness(live: _v2c(apiMode: 'ChargingModeEco'))); expect(find.textContaining('hors arbitrage'), findsNothing); }); testWidgets('mode non lu : aucune note — on ne conclut pas d\'une absence', (tester) async { await tester.pumpWidget(_harness(live: _v2c(apiMode: null))); expect(find.textContaining('hors arbitrage'), findsNothing); expect(find.text('Mode non lu'), findsOneWidget); }); }); group('la tuile est en LECTURE SEULE depuis le 2026-08-28', () { testWidgets('aucun bouton de mode — l\'écriture a un seul lieu, « Configurer »', (tester) async { // Deux écrans qui écrivent SetChargingInfo sont deux comportements qui divergent. await tester.pumpWidget(_harness(live: _v2c())); for (final label in ['PV', 'Min+PV', 'Boost']) { expect(find.widgetWithText(OutlinedButton, label), findsNothing, reason: '$label ne doit plus être écrivable depuis les favoris'); } expect(find.byType(OutlinedButton), findsNothing); }); testWidgets('mais l\'avertissement « hors arbitrage » reste, lui', (tester) async { // C'est le client qui ouvre ses favoris : il doit voir que sa borne n'est pas // pilotée, même s'il doit aller ailleurs pour le corriger. await tester.pumpWidget(_harness(live: _v2c(apiMode: 'ChargingModeNormal'))); expect(find.textContaining('hors arbitrage'), findsOneWidget); }); }); }