etm-powersync-app/test/ev_favorite_tile_test.dart
Patrick Schurig 95769938fc fix(ev): la tuile des favoris devient muette — elle lit la borne, ou le dit
Elle reste : la retirer aurait laissé un trou dans une liste que le client a composée
lui-même. Elle affiche maintenant ce que la box publie, et là où il n'y a rien à lire,
elle le dit.

Ce qui est lu, et sa source
  · branché / en charge   ← états pluggedIn, charging du Thing
  · puissance             ← état currentPower du Thing
  · mode                  ← GetChargingInfos + notification ChargingInfoChanged

Absent = « non publié », jamais zéro. Un mode que l'app ne connaît pas se lit « mode non
reconnu (X) » plutôt que d'être deviné, et aucun bouton n'est allumé tant que la box n'a
pas dit ce qu'elle porte.

L'écriture restitue le refus, sinon elle n'existe pas. Les pastilles de 7 à 10 px qui
avalaient les codes d'erreur de setChargingInfo passent à 44 px, montrent l'attente, et
affichent le refus tel que la box le nomme. Et l'app ne tient plus d'état de mode local :
après une écriture acceptée, elle RELIT GetChargingInfos. Elle se souvenait de ce qu'elle
avait demandé, ce qui n'est pas ce que la borne porte.

La fiction perd sa porte d'entrée. chargingMode, chargingPower (3,6 kW) et
solarSourcePercent (82 %) sont SUPPRIMÉS de EnergyData : trois valeurs par défaut que
_updateEnergyData ne touchait jamais. Ce que la box publie sur une borne vit désormais
dans EvChargerLive, champ par champ, nullable.

⚠️ Et le Sankey du tableau de bord en tirait un flux « Voiture » permanent à 3,6 kW,
voiture débranchée. Il est branché sur la mesure de la borne — « non lu » quand il n'y en
a pas. Mais la même vue invente encore PAC = 38 % de la maison, Eau chaude = 18 %,
Autres = 22 %, dont un seul marqué « estimé » : entrée 🔴 au TODO, avec la source réelle
(GetLoadTelemetry publie measuredW par charge : 1500 W et 800 W ce midi). Brancher le
Sankey dessus est une refonte de l'écran d'accueil, pas un correctif — à décider.

Deux attentes tombent, une reste (relevé Integrations.GetThings sur .75, cet après-midi)
  · currentL1/L2/L3 est DÉPLOYÉ (5,86 / 5,94 / 6,02 A) — powerL1/L2/L3 a disparu
  · phaseCount EXISTE sur la Trydan (3) — la note qui le disait absent était en retard
  · sessionEnergy existe (4,229 kWh) MAIS vaut exactement chargeEnergy : c'est ce qu'on
    attend d'une session sans interruption, donc ça ne prouve rien. LM-1009 §C1 reste
    entière tant qu'une remise à zéro de chargeEnergy n'a pas été vue avec un
    sessionEnergy qui continue.

Le brief moteur dit désormais du masquage WRITE_FAILED que ce n'est pas « ne se produit
pas » mais « ne s'est pas encore produit » : il attend une charge variable qui perd son
Thing, et le banc n'a pas eu ce cas.

154 tests (8 nouveaux sur la tuile, dont les cibles tactiles et la restitution du refus),
flutter analyze à 27 remarques, aucune nouvelle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
2026-08-28 12:49:28 +02:00

161 lines
6.1 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/energy_data.dart' show ChargingMode;
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,
Future<String?> Function(ChargingMode)? onSetMode,
}) =>
MaterialApp(
home: Scaffold(
body: EvFavoriteTile(
live: live,
onSetMode: onSetMode ?? (_) async => null,
),
),
);
/// 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('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('l\'écriture du mode restitue le refus', () {
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('Boost'));
await tester.pumpAndSettle();
expect(appels, 1);
expect(find.textContaining('a refusé'), findsNothing);
// La sélection n'a pas bougé toute seule : la tuile affiche le mode que la box
// portait à l'ouverture, et attend `ChargingInfoChanged` pour changer d'avis.
expect(find.text('Mode Boost'), findsOneWidget);
});
testWidgets('les cibles tactiles font au moins 44 px', (tester) async {
// Les pastilles précédentes faisaient 7 à 10 px, et écrivaient pour de vrai.
await tester.pumpWidget(_harness(live: _v2c()));
for (final label in ['PV', 'Min+PV', 'Boost']) {
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');
expect(taille.width, greaterThanOrEqualTo(44));
}
});
});
}