/// **Le bouton de boost — ce qu'il refuse de faire compte autant que ce qu'il fait.** library; import 'package:etm_powersync_app/models/load_telemetry.dart'; import 'package:etm_powersync_app/widgets/ev_boost_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; LoadTelemetryEntry _entree({Map? decision}) => LoadTelemetryEntry.fromJson({ 'loadId': '{4d1adafd}', 'allocatedW': 4140.0, 'available': true, 'decision': decision ?? {'code': 'ECO_FLOOR_GRID', 'params': {'gridW': 4140}}, }); Widget _harness({ LoadTelemetryEntry? entry, Future Function({required int durationS, double? targetWh})? onSet, Future Function()? onClear, }) => MaterialApp( home: Scaffold( body: EvBoostButton( entry: entry, onSet: onSet ?? ({required durationS, targetWh}) async => null, onClear: onClear ?? () async => null, ), ), ); void main() { testWidgets('sans entrée de télémétrie, le bouton se TAIT', (tester) async { // « Pas d'entrée » n'est pas « pas de boost » : c'est « on ne sait pas ». Afficher un // bouton de boost sur une borne qu'on ne voit pas arbitrée promettrait une action dont // on ignore si elle a un sens. await tester.pumpWidget(_harness(entry: null)); expect(find.byType(OutlinedButton), findsNothing); expect(find.textContaining('Boost'), findsNothing); }); testWidgets('au repos, il propose des durées — et AUCUNE illimitée', (tester) async { await tester.pumpWidget(_harness(entry: _entree())); await tester.tap(find.text('Boost…')); await tester.pumpAndSettle(); for (final d in ['30 min', '1 h', '2 h', '4 h', '6 h']) { expect(find.text(d), findsOneWidget); } // Le contrôle qui MORD : une dérogation qui ne sait pas dire quand elle finit ne doit // pas exister (LM-1500), et le moteur refuserait au-delà de 6 h de toute façon. for (final interdit in ['Illimité', 'Jusqu\'à annulation', '12 h', '24 h']) { expect(find.text(interdit), findsNothing); } }); testWidgets('choisir une durée pose la dérogation avec CETTE durée', (tester) async { int? recu; await tester.pumpWidget(_harness( entry: _entree(), onSet: ({required durationS, targetWh}) async { recu = durationS; return null; }, )); await tester.tap(find.text('Boost…')); await tester.pumpAndSettle(); await tester.tap(find.text('2 h')); await tester.pumpAndSettle(); expect(recu, 7200); }); group('en cours', () { LoadTelemetryEntry active({String? endsAt, String? boundBy, double? maxW}) => _entree(decision: { 'code': 'DEROGATION_ACTIVE', 'params': { if (endsAt != null) 'endsAt': endsAt, if (boundBy != null) 'boundBy': boundBy, if (maxW != null) 'maxW': maxW, }, }); testWidgets('l\'état vient de la CHARGE, et offre d\'arrêter', (tester) async { await tester.pumpWidget(_harness( entry: active( endsAt: DateTime.now().toUtc().add(const Duration(minutes: 42)).toIso8601String()))); expect(find.textContaining('Boost —'), findsOneWidget); expect(find.text('Arrêter'), findsOneWidget); expect(find.text('Boost…'), findsNothing); }); testWidgets('une échéance DÉPASSÉE se nomme, elle ne se rabat pas à zéro', (tester) async { // La box annonce encore la dérogation active alors que son échéance est passée : // c'est une contradiction entre deux champs de la même trame. « 00:00 » l'effacerait. await tester.pumpWidget(_harness( entry: active( endsAt: DateTime.now() .toUtc() .subtract(const Duration(minutes: 3)) .toIso8601String()))); expect(find.textContaining('échéance dépassée'), findsOneWidget); expect(find.textContaining('00:00'), findsNothing); }); testWidgets('sans échéance publiée, pas de compte à rebours inventé', (tester) async { await tester.pumpWidget(_harness(entry: active())); expect(find.text('Boost en cours'), findsOneWidget); }); testWidgets('ce qui BRIDE se dit, le reste se tait', (tester) async { await tester.pumpWidget( _harness(entry: active(boundBy: 'drawCap', maxW: 7400))); expect(find.textContaining('limité par l\'abonnement'), findsOneWidget); expect(find.textContaining('7.4 kW'), findsOneWidget); // Rien ne bride : « rien ne te bride » n'est pas une information à répéter. await tester.pumpWidget(_harness(entry: active())); await tester.pumpAndSettle(); expect(find.textContaining('limité'), findsNothing); }); testWidgets('un refus « aucune dérogation » ne crie pas au refus', (tester) async { // Elle a pu finir toute seule pendant l'appui : une course que l'utilisateur ne peut // pas éviter ne doit pas lui être présentée comme une erreur. await tester.pumpWidget(_harness( entry: active(), onClear: () async => 'EnergyErrorInvalidParameter', )); await tester.tap(find.text('Arrêter')); await tester.pumpAndSettle(); expect(find.textContaining('a refusé'), findsNothing); }); testWidgets('un VRAI refus, lui, se voit', (tester) async { await tester.pumpWidget(_harness( entry: active(), onClear: () async => 'EnergyErrorHardwareFailure', )); await tester.tap(find.text('Arrêter')); await tester.pumpAndSettle(); expect(find.textContaining('EnergyErrorHardwareFailure'), findsOneWidget); }); }); }