Origine du problème (BoxConstraints forces an infinite width / RenderPhysicalShape) :
Hauteur non bornée dans le Positioned :
Lorsque la bulle d'aide était positionnée via Positioned(top: ..., bottom: null), Flutter transmettait une contrainte de hauteur infinie (maxHeight: double.infinity).
Le bouton ElevatedButton calcule son ombre et sa forme physique via RenderPhysicalShape en appelant constraints.biggest ; avec une dimension infinie, Flutter générait une contrainte invalide (BoxConstraints(w=Infinity, 50.0<=h<=Infinity)) et crashait le rendu (ce qui provoquait l'écran gris/noir).
Nids de Row imbriqués dans le pied de la carte d'étape.
Solutions apportées :
tutorial_overlay.dart
:
Hauteur maximale bornée : _buildTooltip calcule et transmet désormais la hauteur disponible réelle de l'écran (availableHeight) via ConstrainedBox(maxHeight: available).
Bouton moderne Material 3 : Remplacement par FilledButton pour le bouton d'action principale ("Suivant" / "C'est parti").
Structure simplifiée : Aplatissement de la barre inférieure avec un Row unique utilisant Spacer().
full_app_render_test.dart
:
Ajout d'un test d'intégration complet validant le démarrage de l'application et la navigation étape par étape du didacticiel.
186 lines
5.6 KiB
Dart
186 lines
5.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:bully/features/tutorial/tutorial_step.dart';
|
|
import 'package:bully/features/tutorial/widgets/tutorial_hand.dart';
|
|
import 'package:bully/features/tutorial/widgets/tutorial_overlay.dart';
|
|
|
|
/// Monte l'overlay au-dessus d'un faux écran contenant l'élément visé.
|
|
Future<GlobalKey> pumpOverlay(
|
|
WidgetTester tester, {
|
|
required List<TutorialStep> Function(GlobalKey targetKey) steps,
|
|
required VoidCallback onFinished,
|
|
}) async {
|
|
final targetKey = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: Stack(
|
|
children: [
|
|
Center(
|
|
child: SizedBox(key: targetKey, width: 160, height: 48),
|
|
),
|
|
TutorialOverlay(steps: steps(targetKey), onFinished: onFinished),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Laisse le temps à la mesure du spot (défilement + délai de stabilisation).
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
return targetKey;
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('la visite avance d\'une étape à l\'autre puis se termine',
|
|
(WidgetTester tester) async {
|
|
var finished = false;
|
|
|
|
await pumpOverlay(
|
|
tester,
|
|
onFinished: () => finished = true,
|
|
steps: (targetKey) => [
|
|
TutorialStep(
|
|
targetKey: targetKey,
|
|
title: 'Démarrez une session',
|
|
description: 'Tout part d\'ici.',
|
|
gesture: TutorialGesture.tap,
|
|
),
|
|
TutorialStep(
|
|
targetKey: targetKey,
|
|
title: 'Zoomer sur la cible',
|
|
description: 'Écartez deux doigts.',
|
|
gesture: TutorialGesture.pinch,
|
|
),
|
|
],
|
|
);
|
|
|
|
expect(find.text('Démarrez une session'), findsOneWidget);
|
|
expect(find.text('1/2'), findsOneWidget);
|
|
// La main animée mime le geste attendu, posée sur l'élément mis en avant.
|
|
expect(find.byType(TutorialHand), findsOneWidget);
|
|
expect(find.text('Appuyez une fois'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('Suivant'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
|
|
expect(find.text('Zoomer sur la cible'), findsOneWidget);
|
|
expect(find.text('2/2'), findsOneWidget);
|
|
expect(find.text('Pincez avec deux doigts pour zoomer'), findsOneWidget);
|
|
expect(finished, isFalse);
|
|
|
|
await tester.tap(find.text('C\'est parti'));
|
|
await tester.pump();
|
|
|
|
expect(finished, isTrue);
|
|
});
|
|
|
|
testWidgets('« Passer » interrompt la visite immédiatement',
|
|
(WidgetTester tester) async {
|
|
var finished = false;
|
|
|
|
await pumpOverlay(
|
|
tester,
|
|
onFinished: () => finished = true,
|
|
steps: (targetKey) => [
|
|
TutorialStep(
|
|
targetKey: targetKey,
|
|
title: 'Première étape',
|
|
description: 'Description.',
|
|
),
|
|
const TutorialStep(
|
|
title: 'Seconde étape',
|
|
description: 'Description.',
|
|
),
|
|
],
|
|
);
|
|
|
|
await tester.tap(find.text('Passer'));
|
|
await tester.pump();
|
|
|
|
expect(finished, isTrue);
|
|
expect(find.text('Seconde étape'), findsNothing);
|
|
});
|
|
|
|
testWidgets('une étape sans cible s\'affiche comme carte centrée',
|
|
(WidgetTester tester) async {
|
|
await pumpOverlay(
|
|
tester,
|
|
onFinished: () {},
|
|
steps: (_) => [
|
|
const TutorialStep(
|
|
title: 'Bienvenue dans Bully',
|
|
description: 'Ce guide rapide vous montre l\'essentiel.',
|
|
),
|
|
],
|
|
);
|
|
|
|
expect(find.text('Bienvenue dans Bully'), findsOneWidget);
|
|
expect(find.text('C\'est parti'), findsOneWidget);
|
|
// Aucune cible : pas de main animée non plus.
|
|
expect(find.byType(TutorialHand), findsNothing);
|
|
});
|
|
|
|
testWidgets('l\'overlay se rend correctement dans un Navigator push modal',
|
|
(WidgetTester tester) async {
|
|
final targetKey = GlobalKey();
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Builder(
|
|
builder: (context) => Scaffold(
|
|
body: Column(
|
|
children: [
|
|
SizedBox(key: targetKey, width: 200, height: 60),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).push(
|
|
PageRouteBuilder<void>(
|
|
opaque: false,
|
|
pageBuilder: (routeCtx, _, __) => TutorialOverlay(
|
|
steps: [
|
|
const TutorialStep(
|
|
title: 'Bienvenue',
|
|
description: 'Texte introductif',
|
|
),
|
|
TutorialStep(
|
|
targetKey: targetKey,
|
|
title: 'Bouton',
|
|
description: 'Texte bouton',
|
|
gesture: TutorialGesture.tap,
|
|
),
|
|
],
|
|
onFinished: () => Navigator.of(routeCtx).pop(),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
child: const Text('Ouvrir'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('Ouvrir'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
expect(find.text('Bienvenue'), findsOneWidget);
|
|
|
|
await tester.tap(find.text('Suivant'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 500));
|
|
|
|
expect(find.text('Bouton'), findsOneWidget);
|
|
expect(find.byType(TutorialHand), findsOneWidget);
|
|
});
|
|
}
|
|
|