etm-powersync-app/test/garde_brouillon_test.dart
Patrick Schurig 438f261de7 feat(config): quitter avec un brouillon demande, et « Placer en dernier » rejoint « Renommer »
Deux retours d'usage, tous deux sur la même carte.

1. « Placer en dernier » poussait « Configurer » hors de sa place. Il rejoint « Renommer »
   sur une ligne à part : ce sont les deux gestes qui ne touchent pas au mécanisme, et un
   bouton d'action principale ne se fait pas déplacer par un geste secondaire.

2. Quitter l'écran avec des modifications non enregistrées ne demandait rien — et ce
   silence cachait pire qu'une perte : le brouillon vit dans LoadConfigProvider, pas dans
   l'écran, donc il SURVIT à la navigation. On repartait en laissant une configuration
   modifiée invisible, qui réapparaissait au retour sans contexte, ou disparaissait à la
   première relecture de la box (LoadConfigChanged, reconnexion) sans que personne ait
   choisi. Les deux issues sont mauvaises pour la même raison : l'utilisateur ne sait pas
   ce qu'il laisse derrière lui.

GardeBrouillon (PopScope) s'interpose sur les DEUX écrans qui partagent ce brouillon — la
liste des charges et l'écran de mécanisme. L'alerte dit l'état de la BOX, pas seulement
celui de l'écran : « vos changements n'ont pas été envoyés, elle porte toujours la
configuration d'avant ». Deux issues, toutes deux explicites : rester, ou quitter en
abandonnant — un brouillon fantôme qui ressort trois écrans plus loin est pire qu'une
modification perdue qu'on a accepté de perdre.

Aucune écriture n'est proposée depuis la boîte de dialogue : enregistrer depuis une sortie
ferait partir une configuration que personne n'a relue.

Quatre tests, dont le contrôle négatif — sans brouillon, on sort sans être interrogé. Une
alerte qui se déclenche pour rien apprend à passer outre sans lire.

235 tests (+4), flutter analyze inchangé à 27 remarques.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
2026-08-29 12:02:04 +02:00

112 lines
4.4 KiB
Dart

/// **Quitter avec un brouillon en attente** — l'alerte, et ce qu'elle laisse derrière.
///
/// Le brouillon vit dans `LoadConfigProvider`, pas dans l'écran : il survit à la
/// navigation. Sans garde, on quittait en laissant une configuration modifiée invisible,
/// qui réapparaissait au retour — ou disparaissait à la première relecture de la box, sans
/// que personne ait choisi.
library;
import 'package:etm_powersync_app/providers/load_config_provider.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';
List<Map<String, dynamic>> _config() => [
{'id': 'a', 'label': 'A', 'priority': 1, 'domain': 'ecs'},
{'id': 'b', 'label': 'B', 'priority': 2, 'domain': 'ev'},
];
/// Un écran poussé sur une pile, pour que « quitter » veuille dire quelque chose.
Future<void> _ouvrir(WidgetTester tester, LoadConfigProvider config) async {
await tester.pumpWidget(ChangeNotifierProvider.value(
value: config,
child: MaterialApp(
home: Builder(
builder: (accueil) => Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.of(accueil).push(MaterialPageRoute(
builder: (_) => const GardeBrouillon(
child: Scaffold(body: Center(child: Text('écran de config'))),
),
)),
child: const Text('ouvrir'),
),
),
),
),
),
));
await tester.tap(find.text('ouvrir'));
await tester.pumpAndSettle();
expect(find.text('écran de config'), findsOneWidget);
}
void main() {
testWidgets('sans brouillon, on sort sans être interrogé', (tester) async {
// Une alerte qui se déclenche pour rien apprend à passer outre sans lire.
final config = LoadConfigProvider()..adoptForTest(_config());
await _ouvrir(tester, config);
final ctx = tester.element(find.text('écran de config'));
Navigator.of(ctx).maybePop();
await tester.pumpAndSettle();
expect(find.text('Modifications non enregistrées'), findsNothing);
expect(find.text('écran de config'), findsNothing, reason: 'sorti directement');
});
testWidgets('avec un brouillon, l\'alerte s\'interpose', (tester) async {
final config = LoadConfigProvider()..adoptForTest(_config());
config.setLabel('a', 'A modifié');
expect(config.hasPendingChanges, isTrue);
await _ouvrir(tester, config);
final ctx = tester.element(find.text('écran de config'));
Navigator.of(ctx).maybePop();
await tester.pumpAndSettle();
expect(find.text('Modifications non enregistrées'), findsOneWidget);
// Elle dit l'état de la BOX, pas seulement celui de l'écran : c'est ce qui permet de
// décider sans avoir à s'en souvenir.
expect(find.textContaining('n\'ont pas été envoyés à la box'), findsOneWidget);
// Et on est toujours sur l'écran tant qu'on n'a pas tranché.
expect(find.text('écran de config'), findsOneWidget);
});
testWidgets('« Rester » ne quitte pas, et ne touche pas au brouillon',
(tester) async {
final config = LoadConfigProvider()..adoptForTest(_config());
config.setLabel('a', 'A modifié');
await _ouvrir(tester, config);
Navigator.of(tester.element(find.text('écran de config'))).maybePop();
await tester.pumpAndSettle();
await tester.tap(find.text('Rester'));
await tester.pumpAndSettle();
expect(find.text('écran de config'), findsOneWidget);
expect(config.hasPendingChanges, isTrue);
});
testWidgets('« Quitter sans enregistrer » sort ET abandonne le brouillon',
(tester) async {
// Délibérément destructeur : un brouillon fantôme qui ressort trois écrans plus loin
// est pire qu'une modification perdue qu'on a accepté de perdre.
final config = LoadConfigProvider()..adoptForTest(_config());
config.setLabel('a', 'A modifié');
await _ouvrir(tester, config);
Navigator.of(tester.element(find.text('écran de config'))).maybePop();
await tester.pumpAndSettle();
await tester.tap(find.text('Quitter sans enregistrer'));
await tester.pumpAndSettle();
expect(find.text('écran de config'), findsNothing);
expect(config.hasPendingChanges, isFalse);
// La configuration adoptée n'a pas bougé : rien n'est parti à la box.
expect(config.entries.firstWhere((e) => e.id == 'a').label, 'A');
});
}