diff --git a/lib/screens/energy/load_mechanism_screen.dart b/lib/screens/energy/load_mechanism_screen.dart index 88ec4dd..18165b2 100644 --- a/lib/screens/energy/load_mechanism_screen.dart +++ b/lib/screens/energy/load_mechanism_screen.dart @@ -322,7 +322,8 @@ class _Mecanisme extends StatelessWidget { 'branchée, et changent avec elle — les figer ici en ferait une seconde source ' 'de vérité, fausse dès le prochain véhicule.\n\n' 'Ce qui se règle depuis cet écran : le libellé, le domaine, le rang. Le mode de ' - 'recharge, lui, se règle depuis la carte véhicule du tableau de bord.', + 'recharge (PV, Min+PV, Boost) se règle depuis la tuile « Borne EV » des ' + 'favoris — c\'est le seul endroit de l\'app qui écrit SetChargingInfo.', style: TextStyle( fontSize: 12, height: 1.4, color: EtmTokens.mutedOf(context)), ), @@ -1511,9 +1512,30 @@ class _CompteurEtSonde extends StatelessWidget { 'elle n\'est qu\'une décomposition. L\'y ajouter compterait deux fois la ' 'même énergie.', ), + if (entry.isEvCharger) ...[ + const SizedBox(height: 6), + _Aide( + 'Sur une borne, aucun compteur séparé n\'est nécessaire : elle publie ' + 'elle-même sa puissance. La désigner ICI comme son propre compteur est ' + 'ce qui fait apparaître « mesuré » à côté de « alloué » sur sa carte — ' + 'sans quoi le moteur n\'a rien à comparer à ce qu\'il commande.', + ), + ], ], ), ), + // ── Pas de sonde sur une BORNE ──────────────────────────────────── + // + // `sensorThingId` est un champ générique de `LoadConfig` : le schéma l'accepte + // sur n'importe quelle charge, y compris une borne. Mais le moteur ne le lit que + // pour en publier une **température** (`attachMeasurements`, LM-1103), et une + // borne de recharge ne rend aucun service thermique : le champ ne mènerait nulle + // part. Proposé sur cet écran, il faisait chercher à quoi il sert. + // + // Le compteur, lui, RESTE : c'est le seul chemin par lequel `measuredW` apparaît + // pour cette charge dans la télémétrie — et sur une borne qui publie + // `currentPower`, on peut la désigner elle-même. + if (!entry.isEvCharger) ...[ const SizedBox(height: 14), _Carte( titre: 'Sonde de température', @@ -1536,6 +1558,7 @@ class _CompteurEtSonde extends StatelessWidget { ), ), ), + ], ], ); } @@ -1658,6 +1681,17 @@ class _BarreEnregistrement extends StatelessWidget { } } } + } else if (entry.isEvCharger) { + // ⚠️ RIEN à valider sur une borne, et surtout pas une puissance nominale. + // + // Elle tombait dans la branche `else` avec les charges modulables, et l'écran + // refusait donc d'enregistrer un simple renommage : « La puissance nominale est + // requise » — sur la carte qui explique deux blocs plus haut qu'une borne n'en porte + // pas. Constaté sur téléphone le 2026-08-28, contre `.75`. + // + // Le contrat le confirme : l'entrée d'une borne porte `adapter`, `mode`, `domain`, + // `label`, `priority`, `enabled` — aucune charge utile de mécanisme. Ses limites + // viennent de la borne et du véhicule branché. } else { if (entry.maxPowerW <= 0) { erreurs.add( diff --git a/test/load_mechanism_ev_test.dart b/test/load_mechanism_ev_test.dart new file mode 100644 index 0000000..88be7f9 --- /dev/null +++ b/test/load_mechanism_ev_test.dart @@ -0,0 +1,129 @@ +/// L'écran « Configurer » d'une BORNE — deux défauts trouvés sur téléphone, pas en test. +/// +/// Le 2026-08-28, app installée en release contre `.75` : +/// +/// 1. l'écran **refusait d'enregistrer** une borne — « La puissance nominale est requise et +/// doit être supérieure à 0 W » — alors que sa propre carte « Mécanisme » explique deux +/// blocs plus haut qu'une borne n'en porte pas. `_valider()` faisait tomber +/// `adapter: evcharger` dans la branche des charges modulables ; +/// 2. il proposait une **sonde de température** sur une borne de recharge. Le champ est +/// générique au schéma (`sensorThingId` s'écrit sur n'importe quelle charge), mais le +/// moteur ne le lit que pour publier une température — sur une borne, il ne mène nulle +/// part. +/// +/// Aucun des 164 tests ne pouvait les voir : le premier vit dans une méthode privée de +/// widget, le second est une question de sens, pas de valeur. +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:etm_powersync_app/l10n/app_localizations.dart'; +import 'package:etm_powersync_app/providers/load_config_provider.dart'; +import 'package:etm_powersync_app/providers/load_telemetry_provider.dart'; +import 'package:etm_powersync_app/providers/schema_provider.dart'; +import 'package:etm_powersync_app/screens/energy/load_mechanism_screen.dart'; +import 'package:etm_powersync_app/services/nymea_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +/// Configuration réelle du banc : chauffe-eau (relais), PAC (SG-Ready), trois bornes. +List> _banc() { + final raw = jsonDecode( + File('test/fixtures/loadconfig_hems75_v2c.json').readAsStringSync()); + return ((raw as Map)['loadConfigs'] as List) + .cast>(); +} + +const kV2c = '{4d1adafd-cafc-4813-93b1-ca8933ce71ab}'; + +Widget _harness(String loadId, {List>? config}) => + MultiProvider( + providers: [ + ChangeNotifierProvider.value( + value: LoadConfigProvider()..adoptForTest(config ?? _banc())), + ChangeNotifierProvider.value( + value: LoadTelemetryProvider() + ..adoptForTest({'degradedMode': false, 'loads': const []})), + ChangeNotifierProvider.value(value: SchemaProvider()), + ChangeNotifierProvider.value(value: NymeaService()), + ], + // Les délégués de localisation sont indispensables : `_Identite` lit `context.l10n` + // dès sa construction, et sans eux l'écran explose avant d'afficher quoi que ce soit. + child: MaterialApp( + locale: const Locale('fr'), + localizationsDelegates: L10n.localizationsDelegates, + supportedLocales: L10n.supportedLocales, + home: LoadMechanismScreen(loadId: loadId), + ), + ); + +/// Grande surface : l'écran est un `ListView`, qui ne construit QUE ce qui entre dans son +/// viewport. Sur 600 px de haut, le compteur, la sonde et la barre d'enregistrement ne +/// sont jamais bâtis — et `find` ne trouve rien, ce qui se lit comme un succès quand on +/// cherche une absence. C'est le piège exact que ce fichier éprouve ailleurs. +Future _poser(WidgetTester tester, Widget w) async { + await tester.binding.setSurfaceSize(const Size(1000, 4000)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + await tester.pumpWidget(w); + await tester.pump(); +} + +void main() { + testWidgets('une BORNE ne réclame aucune puissance nominale', (tester) async { + await _poser(tester, _harness(kV2c)); + + // Le blocage qui empêchait d'enregistrer un simple renommage. + expect(find.textContaining('puissance nominale est requise'), findsNothing); + // Et l'écran dit toujours POURQUOI elle n'en a pas. + expect(find.textContaining('ni contacts, ni paliers'), findsOneWidget); + }); + + testWidgets('une charge MODULABLE, elle, la réclame toujours', (tester) async { + // Contrôle négatif : la règle n'est pas supprimée, elle est rendue à son périmètre. + final cfg = [ + for (final c in _banc()) + if (c['id'] == 'chauffe-eau') + { + ...c, + 'adapter': 'etmvariableload', + 'relays': const [], + 'maxPowerW': 0, + } + else + c, + ]; + await _poser(tester, _harness('chauffe-eau', config: cfg)); + + expect(find.textContaining('puissance nominale est requise'), findsOneWidget); + }); + + testWidgets('pas de sonde de température sur une borne — le compteur reste', + (tester) async { + await _poser(tester, _harness(kV2c)); + + expect(find.text('Sonde de température'), findsNothing); + // Le compteur, lui, garde un sens : c'est le seul chemin par lequel `measuredW` + // apparaît, et une borne peut se désigner elle-même (elle publie `currentPower`). + expect(find.text('Compteur dédié'), findsOneWidget); + expect(find.textContaining('aucun compteur séparé n\'est nécessaire'), + findsOneWidget); + }); + + testWidgets('sur une charge thermique, la sonde est bien là', (tester) async { + await _poser(tester, _harness('chauffe-eau')); + + expect(find.text('Sonde de température'), findsOneWidget); + }); + + testWidgets('l\'écran n\'envoie plus vers une carte véhicule supprimée', + (tester) async { + // Elle renvoyait au « tableau de bord », où la carte a été supprimée le 2026-08-28 : + // l'installateur y aurait cherché un réglage qui n'y est pas. + await _poser(tester, _harness(kV2c)); + + expect(find.textContaining('tableau de bord'), findsNothing); + expect(find.textContaining('tuile « Borne EV » des favoris'), findsOneWidget); + }); +}