/// Les trois boutons de mode — **le seul lieu d'écriture de `SetChargingInfo`**. /// /// Ils ont vécu sur la tuile des favoris, sur des pastilles de 7 à 10 px qui avalaient les /// refus. Ils sont depuis le 2026-08-28 sur l'écran « Configurer » de la borne, en un seul /// exemplaire : deux endroits qui écrivent la même chose sont deux comportements qui /// divergent. library; import 'package:etm_powersync_app/models/energy_data.dart' show ChargingMode; import 'package:etm_powersync_app/models/ev_charger_live.dart'; import 'package:etm_powersync_app/widgets/ev_mode_buttons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; EvChargerLive _v2c({String? apiMode = 'ChargingModeNormal'}) => EvChargerLive( thingId: '4d1adafd-cafc-4813-93b1-ca8933ce71ab', name: 'V2C Trydan', pluggedIn: false, charging: false, currentPowerW: 0, apiMode: apiMode, ); Widget _harness({ required EvChargerLive live, Future Function(ChargingMode)? onSetMode, }) => MaterialApp( home: Scaffold( body: EvModeButtons( live: live, onSetMode: onSetMode ?? (_) async => null, ), ), ); void main() { testWidgets('un refus de la box est affiché, avec son code', (tester) async { await tester.pumpWidget(_harness( live: _v2c(), onSetMode: (_) async => 'EnergyErrorInvalidParameter', )); await tester.tap(find.text('PV')); await tester.pump(); // écriture en cours await tester.pump(); // réponse await tester.pump(); // SnackBar expect(find.textContaining('EnergyErrorInvalidParameter'), findsOneWidget); expect(find.textContaining('a refusé'), findsOneWidget); }); testWidgets('un succès n\'affiche rien — c\'est la box qui dira le mode', (tester) async { var appels = 0; await tester.pumpWidget(_harness( live: _v2c(), onSetMode: (m) async { appels++; return null; }, )); await tester.tap(find.text('PV')); await tester.pumpAndSettle(); expect(appels, 1); expect(find.textContaining('a refusé'), findsNothing); }); testWidgets('la sélection suit la BOX, pas la dernière commande', (tester) async { // `_v2c()` est en ChargingModeNormal : c'est Manuel qui doit être allumé, et lui seul, // quoi qu'on ait tapé juste avant. await tester.pumpWidget(_harness(live: _v2c())); await tester.tap(find.text('PV')); await tester.pumpAndSettle(); final manuel = tester.widget( find.widgetWithText(OutlinedButton, 'Manuel')); final pv = tester.widget(find.widgetWithText(OutlinedButton, 'PV')); expect(manuel.style?.backgroundColor?.resolve({}), isNot(Colors.transparent), reason: 'Manuel est le mode que la box porte'); expect(pv.style?.backgroundColor?.resolve({}), Colors.transparent, reason: 'PV vient d\'être demandé, mais la box ne l\'a pas encore dit'); }); testWidgets('un mode non lu n\'allume RIEN', (tester) async { // Trois boutons éteints valent mieux qu'un allumé au hasard. await tester.pumpWidget(_harness(live: _v2c(apiMode: null))); for (final label in ['Manuel', 'PV', 'Min+PV']) { final b = tester .widget(find.widgetWithText(OutlinedButton, label)); expect(b.style?.backgroundColor?.resolve({}), Colors.transparent); } }); testWidgets('les cibles tactiles font au moins 44 px', (tester) async { await tester.pumpWidget(_harness(live: _v2c())); for (final label in ['Manuel', 'PV', 'Min+PV']) { final taille = tester.getSize(find.ancestor( of: find.text(label), matching: find.byType(OutlinedButton), )); expect(taille.height, greaterThanOrEqualTo(44), reason: '$label : cible tactile trop petite'); } }); group('« Manuel » se confirme — il peut ARRÊTER la charge', () { // Le bouton s'appelait « Boost ». `ChargingModeNormal` sort la borne de l'arbitrage et // lui restitue ses valeurs manuelles stockées : sur `.75` elles valaient // `manualChargingEnabled: false`, et l'appui a stoppé la charge le 2026-09-08 à // 11:09:51, avec un verrou de 300 s derrière. // // Renommer ne suffisait pas : « Manuel » dit QUI commande, pas ce que la borne va // faire. Tant que l'app n'offre pas de régler ces valeurs, le mode veut dire « ce qui // traînait la dernière fois ». testWidgets('l\'appui n\'écrit RIEN tant qu\'on n\'a pas confirmé', (tester) async { var appels = 0; await tester.pumpWidget(_harness( live: _v2c(apiMode: 'ChargingModeEco'), onSetMode: (_) async { appels++; return null; }, )); await tester.tap(find.text('Manuel')); await tester.pumpAndSettle(); expect(find.textContaining('Rendre la main à la borne'), findsOneWidget); expect(appels, 0, reason: 'rien ne part avant que l\'utilisateur ait dit oui'); expect(find.textContaining('la charge peut s\'arrêter'), findsOneWidget, reason: 'la conséquence se dit AVANT, pas après'); await tester.tap(find.text('Annuler')); await tester.pumpAndSettle(); expect(appels, 0); }); testWidgets('confirmé, il écrit une fois', (tester) async { var appels = 0; await tester.pumpWidget(_harness( live: _v2c(apiMode: 'ChargingModeEco'), onSetMode: (_) async { appels++; return null; }, )); await tester.tap(find.text('Manuel')); await tester.pumpAndSettle(); await tester.tap(find.text('Passer en manuel')); await tester.pumpAndSettle(); expect(appels, 1); }); testWidgets('déjà en manuel, aucune question — rien ne change', (tester) async { // Le contrôle qui MORD : la confirmation ne doit pas se déclencher sur un ré-appui // du mode déjà en place, sinon elle devient un péage qu'on apprend à cliquer sans // lire — et une confirmation qu'on ne lit plus ne protège plus de rien. var appels = 0; await tester.pumpWidget(_harness( live: _v2c(), // déjà ChargingModeNormal onSetMode: (_) async { appels++; return null; }, )); await tester.tap(find.text('Manuel')); await tester.pumpAndSettle(); expect(find.textContaining('Rendre la main'), findsNothing); expect(appels, 1); }); testWidgets('les autres modes ne demandent rien', (tester) async { var appels = 0; await tester.pumpWidget(_harness( live: _v2c(), onSetMode: (_) async { appels++; return null; }, )); await tester.tap(find.text('Min+PV')); await tester.pumpAndSettle(); expect(find.textContaining('Rendre la main'), findsNothing); expect(appels, 1); }); }); }