Mesuré sur `.75` le 2026-08-30 : quand le lien Modbus de la V2C tombe, la box ne fige pas les états, elle publie des zéros — `pluggedIn` false, `charging` false, `currentPower` 0 — alors que la Skoda était branchée et en charge à 4 356 W. Avant ça, la dernière valeur connue reste publiée comme si elle était actuelle (vérifié : aucun état ne bouge en 45 s pendant une charge supposée). L'app refusait déjà d'inventer un zéro ; elle en relayait un. Sous `connected == false`, `EvChargerLive` met `pluggedIn`, `charging` et `currentPowerW` à `null` À L'ENTRÉE — un seul endroit, pas un test par écran — et porte `joignable` + `messageEtat`. La tuile dit « Hors ligne » et reprend la phrase du plugin, qui porte l'heure de la coupure et la piste WiFi. Deux garde-fous dans les tests : `joignable == null` n'est PAS hors ligne (une borne qui ne publie pas `connected` ne dit rien de son lien — supposer la panne serait le symétrique du zéro forgé), et le MODE reste affiché hors ligne, parce qu'il est stocké dans la box et non lu sur la borne. Brief moteur : les zéros à la perte du lien, `chargingState` figé à `Idle` pendant une charge réelle, et `ChargingModeNormal` qui n'est pas un boost — il restaure les valeurs manuelles mémorisées (ici arrêt, 6 A) et sort la borne de l'arbitrage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
201 lines
8.8 KiB
Dart
201 lines
8.8 KiB
Dart
/// 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<Text>(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);
|
|
});
|
|
});
|
|
}
|