Deux défauts vus sur appareil après installation, tous deux d'affichage.
1. « mesuré depuis 16:37 » à 06:15, pour un `measuredSince` de la VEILLE. Le
lecteur comprend 16:37 aujourd'hui — une heure qui n'est pas encore arrivée —
et « 2,5 kWh achetés » juste à côté devient incompréhensible avec elle.
Même défaut que l'heure UTC, d'un cran plus haut : là on perdait le fuseau,
ici le jour. `heureLisible` rend « HH:MM » dans la journée du lecteur,
« hier HH:MM » la veille, « le JJ/MM à HH:MM » au-delà — et la comparaison
porte sur le jour civil, pas sur 24 h : à 00:05, un instant de 23:50 est hier
même s'il a quinze minutes.
2. « Autoconsommation · sans production » rendu à la moitié de la taille
d'« Autonomie » à côté. Cause : un FittedBox reçoit des contraintes de
largeur infinies, donc son enfant tient sur une ligne aussi large qu'il veut
et c'est tout le bloc qui rétrécit — `maxLines: 2` n'y sert à rien.
Rallonger le texte d'un suffixe rapetissait donc le NOM de la grandeur, qui
est ce qu'on lit en premier. Le motif passe sur sa propre ligne.
Les tests qui figeaient « HH:MM » dérivent maintenant de `heureLisible`, la
fonction même de l'écran — un attendu recomposé à la main aurait gelé la forme
d'avant, exactement comme les « 03:42 » en dur de vendredi.
Captures : docs/captures/2026-09-08_taux-sans-production.png
docs/captures/2026-09-08_instant-hier.png
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
211 lines
9.1 KiB
Dart
211 lines
9.1 KiB
Dart
/// **Deux grandeurs, deux phrases, aucune ne ment.**
|
|
///
|
|
/// Les tuiles disent la JOURNÉE — différence de compteurs sur `periodStartedAt → maintenant`.
|
|
/// La ligne des ratios dit ce que la BOX publie, qui part de la **baseline** et non du début
|
|
/// de journée.
|
|
///
|
|
/// Le cas est arrivé de lui-même le 2026-09-05 : `nymead` redémarré à 09:23, sonde à 09:26,
|
|
/// et la box publiait **100 % d'autoconsommation**. Vrai sur trois minutes ; absurde intitulé
|
|
/// « aujourd'hui ». C'est ce que le drapeau rend lisible — et c'est pour ça que la ligne ne
|
|
/// s'affiche QUE lorsque l'origine diffère : quand les deux coïncident, une phrase de plus
|
|
/// serait du bruit, et un texte permanent cesse d'être lu.
|
|
library;
|
|
|
|
import 'package:etm_powersync_app/features/dashboard/widgets/kpi_row.dart';
|
|
import 'package:etm_powersync_app/models/daily_balance.dart';
|
|
import 'package:etm_powersync_app/models/energy_data.dart';
|
|
import 'package:etm_powersync_app/models/energy_ratios_snapshot.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
/// Relevé `.75` du 2026-09-05 09:26, après le redémarrage de 09:23.
|
|
EnergyRatiosSnapshot _releve({int measuredSince = 1788592995}) =>
|
|
EnergyRatiosSnapshot.fromJson({
|
|
'autonomyRate': 28.570199887530475,
|
|
'selfConsumptionRate': 100,
|
|
'periodStartedAt': 1788559200,
|
|
'measuredSince': measuredSince,
|
|
'timeZone': 'Europe/Paris',
|
|
});
|
|
|
|
DailyBalance _bilan() => DailyBalance(
|
|
from: DateTime(2026, 9, 5, 0, 15),
|
|
to: DateTime(2026, 9, 5, 9, 26),
|
|
periodStart: DateTime(2026, 9, 5),
|
|
sampleCount: 37,
|
|
productionKwh: 8.0,
|
|
consumptionKwh: 12.0,
|
|
importKwh: 5.0,
|
|
exportKwh: 1.0,
|
|
);
|
|
|
|
/// La NUIT du 2026-09-07 sur `.75` : consommation réelle, production nulle depuis minuit.
|
|
DailyBalance _bilanDeNuit() => DailyBalance(
|
|
from: DateTime(2026, 9, 7),
|
|
to: DateTime(2026, 9, 7, 2, 30),
|
|
periodStart: DateTime(2026, 9, 7),
|
|
sampleCount: 10,
|
|
productionKwh: 0.0,
|
|
consumptionKwh: 1.4,
|
|
importKwh: 1.4,
|
|
exportKwh: 0.0,
|
|
);
|
|
|
|
Widget _harnessBilan(DailyBalance b) => MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: KpiRow(data: const EnergyData(), balance: b),
|
|
),
|
|
),
|
|
);
|
|
|
|
Widget _harness({EnergyRatiosSnapshot? ratios}) => MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: KpiRow(
|
|
data: const EnergyData(), balance: _bilan(), ratios: ratios),
|
|
),
|
|
),
|
|
);
|
|
|
|
void main() {
|
|
testWidgets('baseline reposée en journée : l\'origine est DITE', (tester) async {
|
|
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
await tester.pumpWidget(_harness(ratios: _releve()));
|
|
|
|
expect(find.textContaining('Instantané depuis 09:23'), findsOneWidget);
|
|
// Le 100 % est affiché — il est vrai — mais jamais sous le mot « aujourd'hui ».
|
|
expect(find.textContaining('autoconsommation 100 %'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('les tuiles restent la JOURNÉE, calculée sur la fenêtre de la box',
|
|
(tester) async {
|
|
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
await tester.pumpWidget(_harness(ratios: _releve()));
|
|
// Les anneaux animent leur valeur : sans laisser l'animation finir, ils affichent 0 —
|
|
// une absence qui se lirait comme un succès si on cherchait justement une absence.
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Aujourd\'hui'), findsOneWidget);
|
|
// 8 kWh produits, 1 exporté -> 87,5 % ; et surtout PAS le 100 % de la box.
|
|
expect(find.textContaining('88'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('origine confondue avec le début de journée : rien de plus',
|
|
(tester) async {
|
|
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
// Le cas normal : la baseline date du début de journée.
|
|
await tester.pumpWidget(_harness(ratios: _releve(measuredSince: 1788559200)));
|
|
expect(find.textContaining('Instantané depuis'), findsNothing);
|
|
});
|
|
|
|
testWidgets('les deux coïncident : la ligne se TAIT — vu sur appareil', (tester) async {
|
|
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
// Relevé du 2026-09-05 16:52 : bilan 51,8 / 54,1 sur 00:00→16:45, box 51,9 / 53,7
|
|
// depuis 09:23. L'origine diffère bien, mais les nombres non — et la ligne écrivait
|
|
// « 52 %, 54 % » juste sous des tuiles qui affichaient déjà 52 % et 54 %.
|
|
final coincide = EnergyRatiosSnapshot.fromJson({
|
|
'autonomyRate': 53.67,
|
|
'selfConsumptionRate': 51.92,
|
|
'periodStartedAt': 1788559200,
|
|
'measuredSince': 1788592995,
|
|
'timeZone': 'Europe/Paris',
|
|
});
|
|
await tester.pumpWidget(MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: KpiRow(
|
|
data: const EnergyData(),
|
|
// 8 kWh produits / 1 exporté -> 87,5 % ; 12 consommés / 5 importés -> 58,3 %.
|
|
// On force ici un bilan proche des valeurs de la box pour éprouver le seuil.
|
|
balance: DailyBalance(
|
|
from: DateTime(2026, 9, 5, 0, 15),
|
|
to: DateTime(2026, 9, 5, 16, 45),
|
|
periodStart: DateTime(2026, 9, 5),
|
|
sampleCount: 72,
|
|
productionKwh: 59.89,
|
|
consumptionKwh: 57.31,
|
|
importKwh: 26.31,
|
|
exportKwh: 28.89,
|
|
),
|
|
ratios: coincide,
|
|
),
|
|
),
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
expect(find.textContaining('Instantané depuis'), findsNothing,
|
|
reason: 'répéter les mêmes nombres oblige le lecteur à raisonner pour '
|
|
'découvrir que les fenêtres diffèrent');
|
|
});
|
|
|
|
testWidgets('box antérieure : aucune ligne, aucune supposition', (tester) async {
|
|
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
await tester.pumpWidget(_harness());
|
|
expect(find.textContaining('Instantané'), findsNothing);
|
|
});
|
|
|
|
|
|
group('un taux sans dénominateur — le « — » est juste, le MOTIF ne l\'était pas', () {
|
|
// Signalé sur appareil le 2026-09-07 : « Autoconsommation — · pas mesurable » de nuit,
|
|
// sur une installation dont les quatre compteurs marchaient. Deux causes distinctes
|
|
// portaient la même phrase, et celle qui s'affichait accusait la mesure.
|
|
testWidgets('sans production, la tuile dit « sans production »', (tester) async {
|
|
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
await tester.pumpWidget(_harnessBilan(_bilanDeNuit()));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Le nom de la grandeur et le motif sont DEUX textes : un suffixe rapetissait le
|
|
// nom (le FittedBox réduit tout le bloc faute de pouvoir aller à la ligne).
|
|
expect(find.text('Autoconsommation'), findsOneWidget);
|
|
expect(find.text('sans production'), findsOneWidget);
|
|
// Le contrôle qui MORD : l'ancien motif ne doit plus être là.
|
|
expect(find.text('pas mesurable'), findsNothing);
|
|
// Et surtout PAS un zéro sur CETTE tuile : « 0 % » affirmerait qu'aucune production
|
|
// n'a été autoconsommée, une phrase sur une production qui n'existe pas. Le tiret y
|
|
// est, et il y est une seule fois — l'autre tuile, elle, a bien un nombre.
|
|
//
|
|
// ⚠️ Ne pas écrire `find.text('0 %'), findsNothing` : le 0 % de l'Autonomie est une
|
|
// MESURE et le contrôle mordrait sur elle. Première rédaction de ce test, corrigée
|
|
// en la voyant échouer.
|
|
expect(find.text('—'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('l\'autonomie, elle, a son dénominateur et vaut 0 %', (tester) async {
|
|
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
await tester.pumpWidget(_harnessBilan(_bilanDeNuit()));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Tout vient du réseau : 0 % est une MESURE, pas un trou. Les deux tuiles sont
|
|
// voisines et leurs deux « rien » sont de natures opposées — c'est le cas qui
|
|
// interdit de traiter les taux ensemble.
|
|
expect(find.text('0 %'), findsOneWidget);
|
|
expect(find.text('sans consommation'), findsNothing);
|
|
});
|
|
|
|
testWidgets('sans bilan du tout, le motif reste « pas mesurable »', (tester) async {
|
|
await tester.binding.setSurfaceSize(const Size(1000, 2000));
|
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
|
await tester.pumpWidget(MaterialApp(
|
|
home: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: KpiRow(data: const EnergyData(), balance: null),
|
|
),
|
|
),
|
|
));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Autoconsommation'), findsOneWidget);
|
|
expect(find.text('Autonomie'), findsOneWidget);
|
|
expect(find.text('pas mesurable'), findsNWidgets(2));
|
|
});
|
|
});
|
|
}
|