Le geste sans repli de cet écran — déplacer une plage — puis le geste inverse laissaient un brouillon qui rend exactement la configuration de la box. L'écran le traitait comme n'importe quel brouillon, et n'offrait plus pour en sortir que des portes mal nommées. Quatre messages, tous faux au même instant : la barre « Modifications en attente » la sortie d'écran « vos changements n'ont pas été envoyés » adopter un rang « enregistrez ou annulez vos modifications » retirer une charge idem Il n'y avait plus rien à enregistrer, ni rien à perdre. `draftIsNoOp` nomme l'état : un brouillon dont les charges utiles brutes, clés triées, sont identiques à celles de la box — ce qui couvre l'ordre ET un patch de champ. `hasSubstantiveChanges` garde désormais les trois portes ; la barre, elle, continue de s'afficher, parce que c'est là qu'est la sortie. Elle dit maintenant « L'ordre est revenu à celui de la box — il n'y a rien à enregistrer » et n'offre qu'un geste, « Terminer ». Pas « Enregistrer » : réécrire tout l'ensemble pour n'y rien changer reconstruirait les charges et réarmerait les verrous, au prix de zéro. ⚠️ Le remède n'est PAS d'effacer le brouillon dès qu'il devient sans effet : la barre disparaîtrait sous le doigt à l'instant où l'utilisateur repose la plage, et « j'ai décidé » redeviendrait « il ne s'est rien passé ». Un état se conclut, il ne s'évapore pas. Un test le fige. Et la note sur le glissement est REPRISE. J'y affirmais un défaut ; le test au doigt montre que le réordonnancement fonctionne, liste dépliée comprise. Le mécanisme du shrinkWrap reste écrit — il est probablement juste — mais comme une LIMITE CONNUE non reproduite, avec le protocole en trois gestes (A témoin, B discriminant, C cause), le contournement (replier les blocs) et le remède nommé. On ne réécrit pas un écran qui marche sur la foi d'une lecture de code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
179 lines
6.8 KiB
Dart
179 lines
6.8 KiB
Dart
/// **Un brouillon revenu à son point de départ doit pouvoir se CONCLURE.**
|
|
///
|
|
/// « Je n'ai rien changé » et « j'ai décidé de ne rien changer » sont deux états
|
|
/// différents. Le second s'atteint par le seul geste de l'écran des charges qui n'a pas de
|
|
/// repli — déplacer une plage — puis en le défaisant.
|
|
///
|
|
/// L'écran traitait les deux pareil, et n'offrait plus pour en sortir que des portes mal
|
|
/// nommées : la barre annonçait « Modifications en attente », la sortie d'écran menaçait
|
|
/// d'abandonner des changements, l'adoption d'un rang et le retrait d'une charge
|
|
/// réclamaient d'« enregistrer ou annuler ». Quatre messages, tous faux au même moment.
|
|
///
|
|
/// ⚠️ Le remède N'EST PAS d'effacer le brouillon dès qu'il devient sans effet : la barre
|
|
/// disparaîtrait sous le doigt à l'instant où l'utilisateur repose la plage, et
|
|
/// « j'ai décidé » redeviendrait « il ne s'est rien passé ».
|
|
library;
|
|
|
|
import 'package:etm_powersync_app/providers/load_config_provider.dart';
|
|
import 'package:etm_powersync_app/widgets/controllable_loads_section.dart';
|
|
import 'package:etm_powersync_app/widgets/garde_brouillon.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
Map<String, dynamic> _charge(String id, String label, String domain, int rang) => {
|
|
'id': id,
|
|
'label': label,
|
|
'domain': domain,
|
|
'priority': rang,
|
|
'adapter': 'relay-router',
|
|
'mode': 'fixed',
|
|
'enabled': true,
|
|
};
|
|
|
|
/// Trois domaines distincts : trois plages, donc trois éléments déplaçables.
|
|
List<Map<String, dynamic>> _config() => [
|
|
_charge('a', 'V2C Trydan', 'ev', 1),
|
|
_charge('b', 'chauffe-eau', 'ecs', 2),
|
|
_charge('c', 'PAC banc', 'heating', 3),
|
|
];
|
|
|
|
LoadConfigProvider _provider() => LoadConfigProvider()..adoptForTest(_config());
|
|
|
|
void main() {
|
|
group('l\'état « j\'ai décidé de ne rien changer »', () {
|
|
test('sans brouillon, il n\'existe pas', () {
|
|
final p = _provider();
|
|
expect(p.hasPendingChanges, isFalse);
|
|
expect(p.draftIsNoOp, isFalse,
|
|
reason: 'pas de brouillon n\'est pas un brouillon sans effet');
|
|
expect(p.hasSubstantiveChanges, isFalse);
|
|
});
|
|
|
|
test('un déplacement réel change vraiment quelque chose', () {
|
|
final p = _provider()..reorderDomains(2, 0);
|
|
expect(p.hasPendingChanges, isTrue);
|
|
expect(p.draftIsNoOp, isFalse);
|
|
expect(p.hasSubstantiveChanges, isTrue);
|
|
});
|
|
|
|
test('déplacer puis remettre laisse un brouillon SANS EFFET', () {
|
|
final p = _provider()
|
|
..reorderDomains(2, 0) // C remonte en tête
|
|
..reorderDomains(0, 3); // et redescend en queue
|
|
|
|
expect(p.hasPendingChanges, isTrue,
|
|
reason: 'le brouillon SURVIT — il ne s\'évapore pas sous le doigt');
|
|
expect(p.draftIsNoOp, isTrue);
|
|
expect(p.hasSubstantiveChanges, isFalse);
|
|
// Et l'ordre affiché est bien celui du départ.
|
|
expect([for (final g in p.groups) ...g.loads.map((e) => e.id)],
|
|
['a', 'b', 'c']);
|
|
});
|
|
});
|
|
|
|
group('les portes de sortie disent la vérité', () {
|
|
testWidgets('la barre ne propose plus d\'enregistrer, elle propose de TERMINER',
|
|
(tester) async {
|
|
final p = _provider()
|
|
..reorderDomains(2, 0)
|
|
..reorderDomains(0, 3);
|
|
|
|
await tester.pumpWidget(MaterialApp(
|
|
home: Scaffold(body: ImpactSaveBar(provider: p)),
|
|
));
|
|
|
|
expect(find.textContaining('rien à enregistrer'), findsOneWidget);
|
|
expect(find.text('Terminer'), findsOneWidget);
|
|
// Les contrôles qui MORDENT : les deux portes mal nommées ont disparu.
|
|
expect(find.text('Enregistrer'), findsNothing,
|
|
reason: 'réécrire tout l\'ensemble pour n\'y rien changer '
|
|
'reconstruirait les charges pour rien');
|
|
expect(find.textContaining('Modifications en attente'), findsNothing);
|
|
|
|
await tester.tap(find.text('Terminer'));
|
|
await tester.pump();
|
|
expect(p.hasPendingChanges, isFalse, reason: 'l\'état s\'est CONCLU');
|
|
});
|
|
|
|
testWidgets('un brouillon réel garde sa barre d\'enregistrement', (tester) async {
|
|
final p = _provider()..reorderDomains(2, 0);
|
|
await tester.pumpWidget(MaterialApp(
|
|
home: Scaffold(body: ImpactSaveBar(provider: p)),
|
|
));
|
|
expect(find.text('Enregistrer'), findsOneWidget);
|
|
expect(find.text('Terminer'), findsNothing);
|
|
});
|
|
|
|
testWidgets('la sortie d\'écran ne menace plus d\'une perte inexistante',
|
|
(tester) async {
|
|
final p = _provider()
|
|
..reorderDomains(2, 0)
|
|
..reorderDomains(0, 3);
|
|
|
|
await tester.pumpWidget(ChangeNotifierProvider<LoadConfigProvider>.value(
|
|
value: p,
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (_) => const GardeBrouillon(
|
|
child: Scaffold(body: Text('écran des charges')),
|
|
),
|
|
)),
|
|
child: const Text('ouvrir'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
|
|
await tester.tap(find.text('ouvrir'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text('écran des charges'), findsOneWidget);
|
|
|
|
// Retour arrière : aucune alerte, l'écran se ferme.
|
|
final nav = tester.state<NavigatorState>(find.byType(Navigator));
|
|
nav.maybePop();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Modifications non enregistrées'), findsNothing,
|
|
reason: 'il n\'y avait rien à perdre');
|
|
expect(find.text('écran des charges'), findsNothing);
|
|
expect(p.hasPendingChanges, isFalse,
|
|
reason: 'le brouillon vide est ramassé, pas laissé traîner');
|
|
});
|
|
|
|
testWidgets('un brouillon RÉEL fait toujours barrage', (tester) async {
|
|
final p = _provider()..reorderDomains(2, 0);
|
|
|
|
await tester.pumpWidget(ChangeNotifierProvider<LoadConfigProvider>.value(
|
|
value: p,
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Builder(
|
|
builder: (context) => ElevatedButton(
|
|
onPressed: () => Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (_) => const GardeBrouillon(
|
|
child: Scaffold(body: Text('écran des charges')),
|
|
),
|
|
)),
|
|
child: const Text('ouvrir'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
|
|
await tester.tap(find.text('ouvrir'));
|
|
await tester.pumpAndSettle();
|
|
tester.state<NavigatorState>(find.byType(Navigator)).maybePop();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Modifications non enregistrées'), findsOneWidget);
|
|
expect(find.text('écran des charges'), findsOneWidget);
|
|
});
|
|
});
|
|
}
|