fix(écran): un instant d'un autre jour porte son jour, et le motif ne rapetisse plus la grandeur
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
This commit is contained in:
parent
0c5f0ee30d
commit
0df80b5b9b
BIN
docs/captures/2026-09-08_instant-hier.png
Normal file
BIN
docs/captures/2026-09-08_instant-hier.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 KiB |
BIN
docs/captures/2026-09-08_taux-sans-production.png
Normal file
BIN
docs/captures/2026-09-08_taux-sans-production.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 206 KiB |
@ -362,21 +362,42 @@ class _RingTileState extends State<_RingTile>
|
||||
//
|
||||
// `scaleDown` garde le mot ENTIER en réduisant ce qu'il faut, et ne touche
|
||||
// à rien quand la place suffit — donc aucun effet à échelle normale.
|
||||
// ⚠️ **Le motif va sur SA ligne, pas en suffixe.**
|
||||
//
|
||||
// Un `FittedBox` reçoit des contraintes de largeur infinies : son enfant se
|
||||
// met donc sur une seule ligne, aussi large qu'il veut, et c'est ensuite
|
||||
// tout le bloc qui est réduit pour rentrer. `maxLines: 2` ne sert à rien
|
||||
// là-dedans — le texte n'a jamais de raison de passer à la ligne.
|
||||
//
|
||||
// Vu sur appareil le 2026-09-08 : « Autoconsommation · sans production »
|
||||
// rendu à peu près à la moitié de la taille d'« Autonomie » juste à côté.
|
||||
// Rallonger le texte d'un suffixe, c'est donc rapetisser le nom de la
|
||||
// grandeur — et le nom est ce qu'on lit en premier.
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
widget.value == null
|
||||
? '${widget.label} · ${widget.motifAbsence ?? 'pas mesurable'}'
|
||||
: widget.label,
|
||||
style: EtmTokens.sans(
|
||||
size: 11,
|
||||
color: EtmTokens.mutedOf(context),
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
widget.label,
|
||||
style: EtmTokens.sans(
|
||||
size: 11,
|
||||
color: EtmTokens.mutedOf(context),
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
if (widget.value == null)
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
widget.motifAbsence ?? 'pas mesurable',
|
||||
style: EtmTokens.sans(
|
||||
size: 10,
|
||||
color: EtmTokens.mutedOf(context),
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -336,6 +336,32 @@ String ecoDecisionText(L10n t, LoadDecision d) {
|
||||
/// n'y a **aucun étalement**, donc rien qui puisse cesser. Un booléen à `false` se lit
|
||||
/// spontanément comme rassurant : c'est la nuance qu'un lecteur pressé inversera, et c'est
|
||||
/// pour ça qu'aucune phrase d'ici ne le rend tel quel.
|
||||
/// **Une heure nue ment dès que l'instant n'est pas d'aujourd'hui.**
|
||||
///
|
||||
/// Vu sur appareil le 2026-09-08 à 06:15 : « mesuré depuis 16:37 », pour un `measuredSince`
|
||||
/// du **7 septembre**. Le lecteur comprend 16:37 aujourd'hui — une heure qui n'est pas encore
|
||||
/// arrivée — et la ligne voisine « 2,4 kWh achetés » devient incompréhensible avec elle.
|
||||
///
|
||||
/// C'est le même défaut que l'heure UTC, d'un cran plus haut : là on perdait le fuseau, ici
|
||||
/// on perd le jour. Un instant se rend **entier** ou il se lit de travers ; « HH:MM » n'est
|
||||
/// une abréviation légitime que dans la journée du lecteur.
|
||||
///
|
||||
/// La date est comparée en jours calendaires locaux, pas en écart de 24 h : à 00:05, un
|
||||
/// instant de 23:50 est bien « hier » même s'il a quinze minutes.
|
||||
String heureLisible(DateTime t, {DateTime? maintenant}) {
|
||||
final n = maintenant ?? DateTime.now();
|
||||
final hhmm = '${t.hour.toString().padLeft(2, '0')}:'
|
||||
'${t.minute.toString().padLeft(2, '0')}';
|
||||
final jour = DateTime(t.year, t.month, t.day);
|
||||
final aujourdhui = DateTime(n.year, n.month, n.day);
|
||||
final ecart = aujourdhui.difference(jour).inDays;
|
||||
if (ecart == 0) return hhmm;
|
||||
if (ecart == 1) return 'hier $hhmm';
|
||||
final jj = t.day.toString().padLeft(2, '0');
|
||||
final mm = t.month.toString().padLeft(2, '0');
|
||||
return 'le $jj/$mm à $hhmm';
|
||||
}
|
||||
|
||||
String? ecoProgressText(L10n t, LoadLevel niveau) {
|
||||
final p = niveau.progress;
|
||||
if (p == null) return null;
|
||||
@ -375,8 +401,7 @@ String? ecoProgressText(L10n t, LoadLevel niveau) {
|
||||
// fuseau (cf. `obligationEcheanceHorloge`).
|
||||
final depuis = p.measuredSince;
|
||||
if (depuis == null) return base;
|
||||
final h = '${depuis.hour.toString().padLeft(2, '0')}:'
|
||||
'${depuis.minute.toString().padLeft(2, '0')}';
|
||||
final h = heureLisible(depuis);
|
||||
// La date reste le PREMIER instant mesuré ; l'intermittence se dit à part. Après une
|
||||
// bascule en cours de période, `deliveredWh` ne compte que depuis elle pendant que la cible
|
||||
// reste entière — la paire n'est donc plus une fraction, et c'est cette date qui l'explique.
|
||||
|
||||
@ -19,6 +19,7 @@ library;
|
||||
|
||||
import 'package:etm_powersync_app/l10n/app_localizations.dart';
|
||||
import 'package:etm_powersync_app/models/load_telemetry.dart';
|
||||
import 'package:etm_powersync_app/services/telemetry_text.dart';
|
||||
import 'package:etm_powersync_app/providers/load_telemetry_provider.dart';
|
||||
import 'package:etm_powersync_app/widgets/load_cards.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -140,9 +141,10 @@ void main() {
|
||||
// L'attendu se dérive donc, il ne se recopie pas : le fixer à « 03:42 » gelait
|
||||
// l'heure UTC et validait le défaut trouvé sur appareil le 2026-09-06.
|
||||
final _instant = DateTime.utc(2026, 9, 4, 3, 42);
|
||||
final _local = _instant.toLocal();
|
||||
final _heure = '${_local.hour.toString().padLeft(2, '0')}:'
|
||||
'${_local.minute.toString().padLeft(2, '0')}';
|
||||
// ⚠️ L'attendu se dérive de la MÊME fonction que l'écran — `heureLisible` — et pas
|
||||
// d'un « HH:MM » recomposé à la main : depuis le 2026-09-08 un instant d'un autre jour
|
||||
// porte son jour, et un attendu nu gèlerait la forme d'avant.
|
||||
final _heure = heureLisible(_instant.toLocal());
|
||||
|
||||
Map<String, dynamic> mesure() => _payload(
|
||||
progressEco: {
|
||||
@ -231,6 +233,35 @@ void main() {
|
||||
expect(find.textContaining('mesuré depuis'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('un instant d\'un AUTRE JOUR porte son jour', (tester) async {
|
||||
// Vu sur appareil le 2026-09-08 à 06:15 : « mesuré depuis 16:37 » pour un
|
||||
// `measuredSince` du 7. Le lecteur comprend 16:37 aujourd'hui — une heure qui n'est
|
||||
// pas encore arrivée — et « 2,4 kWh achetés » à côté devient incompréhensible.
|
||||
//
|
||||
// Même défaut que l'heure UTC, d'un cran plus haut : là on perdait le fuseau, ici le
|
||||
// jour. « HH:MM » n'est une abréviation légitime que dans la journée du lecteur.
|
||||
final hier = DateTime.now().subtract(const Duration(days: 1));
|
||||
final u = DateTime(hier.year, hier.month, hier.day, 16, 37).toUtc();
|
||||
await tester.pumpWidget(_carte(_payload(
|
||||
progressEco: {
|
||||
'regime': 'measured', 'targetWh': 2000.0, 'deliveredWh': 0.0,
|
||||
'purchasedWh': 2400.0,
|
||||
'measuredSince': u.toIso8601String(),
|
||||
},
|
||||
decisionEco: {
|
||||
'code': 'ECO_FLOOR_GRID',
|
||||
'params': {
|
||||
'gridW': 350, 'remainingWh': 2000, 'targetWh': 2000,
|
||||
'remainingS': 3600, 'regime': 'measured', 'atMaximum': false,
|
||||
},
|
||||
},
|
||||
)));
|
||||
|
||||
expect(find.textContaining('mesuré depuis hier 16:37'), findsOneWidget);
|
||||
// Le contrôle qui MORD : l'heure nue ne doit plus passer seule.
|
||||
expect(find.textContaining('mesuré depuis 16:37'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('« mesuré depuis » se rend dans le fuseau du LECTEUR',
|
||||
(tester) async {
|
||||
// Trouvé sur appareil le 2026-09-06, pendant que `.75` était volontairement en
|
||||
@ -244,8 +275,7 @@ void main() {
|
||||
// fuseau de la box (vérifié le 2026-09-07, les deux en Europe/Paris).
|
||||
final instant = DateTime.utc(2026, 9, 6, 14, 38);
|
||||
final local = instant.toLocal();
|
||||
final attendu = '${local.hour.toString().padLeft(2, '0')}:'
|
||||
'${local.minute.toString().padLeft(2, '0')}';
|
||||
final attendu = heureLisible(local);
|
||||
|
||||
await tester.pumpWidget(_carte(_payload(
|
||||
progressEco: {
|
||||
|
||||
32
test/heure_lisible_test.dart
Normal file
32
test/heure_lisible_test.dart
Normal file
@ -0,0 +1,32 @@
|
||||
/// **`heureLisible` — une heure nue n'est honnête que dans la journée du lecteur.**
|
||||
library;
|
||||
|
||||
import 'package:etm_powersync_app/services/telemetry_text.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
final n = DateTime(2026, 9, 8, 6, 15);
|
||||
|
||||
test('aujourd\'hui : l\'heure seule suffit', () {
|
||||
expect(heureLisible(DateTime(2026, 9, 8, 4, 33), maintenant: n), '04:33');
|
||||
});
|
||||
|
||||
test('hier : le jour est dit', () {
|
||||
expect(heureLisible(DateTime(2026, 9, 7, 16, 37), maintenant: n), 'hier 16:37');
|
||||
});
|
||||
|
||||
test('plus ancien : la date est dite', () {
|
||||
expect(heureLisible(DateTime(2026, 9, 4, 9, 5), maintenant: n), 'le 04/09 à 09:05');
|
||||
});
|
||||
|
||||
test('la comparaison porte sur le JOUR, pas sur 24 h', () {
|
||||
// À 00:05, un instant de 23:50 a quinze minutes — et c'est quand même hier. Comparer
|
||||
// des durées au lieu de dates calendaires l'aurait rendu « aujourd'hui ».
|
||||
final minuitPasse = DateTime(2026, 9, 8, 0, 5);
|
||||
expect(heureLisible(DateTime(2026, 9, 7, 23, 50), maintenant: minuitPasse),
|
||||
'hier 23:50');
|
||||
// Et l'inverse : 23 h d'écart mais le même jour civil reste une heure nue.
|
||||
expect(heureLisible(DateTime(2026, 9, 8, 0, 1), maintenant: DateTime(2026, 9, 8, 23, 30)),
|
||||
'00:01');
|
||||
});
|
||||
}
|
||||
@ -161,9 +161,12 @@ void main() {
|
||||
await tester.pumpWidget(_harnessBilan(_bilanDeNuit()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Autoconsommation · sans production'), findsOneWidget);
|
||||
// Le contrôle qui MORD : l'ancien motif ne doit plus être là pour cette tuile.
|
||||
expect(find.textContaining('Autoconsommation · pas mesurable'), findsNothing);
|
||||
// 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.
|
||||
@ -184,7 +187,7 @@ void main() {
|
||||
// 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.textContaining('Autonomie · '), findsNothing);
|
||||
expect(find.text('sans consommation'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('sans bilan du tout, le motif reste « pas mesurable »', (tester) async {
|
||||
@ -199,8 +202,9 @@ void main() {
|
||||
));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Autoconsommation · pas mesurable'), findsOneWidget);
|
||||
expect(find.textContaining('Autonomie · pas mesurable'), findsOneWidget);
|
||||
expect(find.text('Autoconsommation'), findsOneWidget);
|
||||
expect(find.text('Autonomie'), findsOneWidget);
|
||||
expect(find.text('pas mesurable'), findsNWidgets(2));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user