Geste de mise en service, signalé par le moteur : toute borne détectée reçoit d'office ChargingModeNormal, c'est-à-dire pleine puissance HORS arbitrage. Une borne neuve n'est donc jamais pilotée tant que personne n'a posé un mode éco, et rien ne le disait — l'installateur pose une borne, elle charge à fond, et il cherche du côté du HEMS un défaut qui n'existe pas. La note apparaît aux deux endroits qui comptent, mot pour mot (kEvUnmanagedNote) : · la carte de configuration, celle qu'on ouvre à la mise en service ; · la tuile des favoris, où les trois boutons de mode sont juste à côté. ⚠️ Elle nomme l'ÉTAT, pas une négligence. La box publie le même chargingMode pour un boost délibéré et pour une borne jamais réglée : l'app ne peut pas les distinguer, donc elle dit « pleine puissance, hors arbitrage » et rappelle que c'est aussi le mode d'office. Même règle que la note de rangs ex æquo — reproductible n'est pas choisi, et on ne fait pas dire à un écran ce que la donnée ne porte pas. Un mode NON LU ne déclenche rien non plus : on ne conclut pas d'une absence. Le piège qui aurait rendu la note invisible est couvert par un test : GetLoadConfig rend l'id de charge AVEC accolades ({4d1adafd-…}), GetChargingInfos rend l'evChargerId SANS. Comparer les deux tels quels ne rend jamais rien — en silence : l'écran aurait conclu « mode non lu » sur une borne parfaitement lue. chargingModeOf() réconcilie les deux. Six tests montés (trois par écran), dont les deux contrôles négatifs. Et la réserve sessionEnergy est documentée à sa juste place : le champ est CONSTRUIT par le plugin V2C 1.15.2+etm13 — il accumule les incréments de chargeEnergy, détecte les remises à zéro, ne repart qu'à l'ouverture d'une session — donc l'égalité observée hier s'explique d'elle-même. La réserve tient jusqu'à la trace que la campagne produira (chargeEnergy à zéro, sessionEnergy qui continue) : aucun avancement affiché d'ici là. Noté aussi, pour la suite : sessionEnergy n'est PAS historisé (suggestLogging écrasé par things.conf sur un Thing déjà appairé ; la V2C n'y a que connected, charging, power). La valeur courante est juste, il n'y aura pas de courbe — le choix des états enregistrés est côté app, entrée au TODO. 164 tests, flutter analyze inchangé à 27 remarques. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
192 lines
7.5 KiB
Dart
192 lines
7.5 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('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('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));
|
|
}
|
|
});
|
|
});
|
|
}
|