Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b789df8dac | ||
|
|
25caf6ddf8 | ||
|
|
a9651588bb |
@@ -45,12 +45,33 @@ class GlassContainer extends StatelessWidget {
|
|||||||
final resolvedBorderColor = borderColor ?? defaultBorder;
|
final resolvedBorderColor = borderColor ?? defaultBorder;
|
||||||
final resolvedBg = customBackgroundColor ?? defaultBg;
|
final resolvedBg = customBackgroundColor ?? defaultBg;
|
||||||
|
|
||||||
Widget content = Container(
|
Widget innerContent = Container(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: resolvedBg,
|
color: resolvedBg,
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
border: Border.all(color: resolvedBorderColor, width: 1.2),
|
border: Border.all(color: resolvedBorderColor, width: 1.2),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget content = blur > 0
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
|
||||||
|
child: innerContent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
child: innerContent,
|
||||||
|
);
|
||||||
|
|
||||||
|
content = Container(
|
||||||
|
margin: margin,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
if (glowColor != null)
|
if (glowColor != null)
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -67,28 +88,9 @@ class GlassContainer extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: child,
|
child: content,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (blur > 0) {
|
|
||||||
content = ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
|
||||||
child: BackdropFilter(
|
|
||||||
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
|
|
||||||
child: content,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
content = ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
|
||||||
child: content,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (margin != EdgeInsets.zero) {
|
|
||||||
content = Padding(padding: margin, child: content);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onTap != null || onLongPress != null) {
|
if (onTap != null || onLongPress != null) {
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class _TutorialOverlayState extends State<TutorialOverlay>
|
|||||||
|
|
||||||
int _index = 0;
|
int _index = 0;
|
||||||
Rect? _spotRect;
|
Rect? _spotRect;
|
||||||
bool _ready = false;
|
late bool _ready = widget.steps.isNotEmpty && widget.steps[0].targetKey == null;
|
||||||
|
|
||||||
TutorialStep get _step => widget.steps[_index];
|
TutorialStep get _step => widget.steps[_index];
|
||||||
bool get _isLast => _index == widget.steps.length - 1;
|
bool get _isLast => _index == widget.steps.length - 1;
|
||||||
@@ -63,12 +63,17 @@ class _TutorialOverlayState extends State<TutorialOverlay>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Scrollable.ensureVisible(
|
try {
|
||||||
targetContext,
|
await Scrollable.ensureVisible(
|
||||||
alignment: 0.35,
|
targetContext,
|
||||||
duration: const Duration(milliseconds: 320),
|
alignment: 0.35,
|
||||||
curve: Curves.easeOutCubic,
|
duration: const Duration(milliseconds: 320),
|
||||||
);
|
curve: Curves.easeOutCubic,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// Ignorer si l'élément n'est pas dans un widget scrollable
|
||||||
|
}
|
||||||
|
|
||||||
// Laisse le temps au défilement de se stabiliser avant de mesurer.
|
// Laisse le temps au défilement de se stabiliser avant de mesurer.
|
||||||
await Future<void>.delayed(const Duration(milliseconds: 60));
|
await Future<void>.delayed(const Duration(milliseconds: 60));
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
@@ -80,13 +85,18 @@ class _TutorialOverlayState extends State<TutorialOverlay>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Rect? _measure(BuildContext targetContext, double padding) {
|
Rect? _measure(BuildContext targetContext, double padding) {
|
||||||
final box = targetContext.findRenderObject() as RenderBox?;
|
if (!targetContext.mounted) return null;
|
||||||
if (box == null || !box.hasSize) return null;
|
final renderObject = targetContext.findRenderObject();
|
||||||
final origin = box.localToGlobal(Offset.zero);
|
if (renderObject is! RenderBox || !renderObject.hasSize) return null;
|
||||||
final screen = MediaQuery.of(context).size;
|
try {
|
||||||
return Rect.fromLTWH(origin.dx, origin.dy, box.size.width, box.size.height)
|
final origin = renderObject.localToGlobal(Offset.zero);
|
||||||
.inflate(padding)
|
final screen = MediaQuery.of(context).size;
|
||||||
.intersect(Offset.zero & screen);
|
return Rect.fromLTWH(origin.dx, origin.dy, renderObject.size.width, renderObject.size.height)
|
||||||
|
.inflate(padding)
|
||||||
|
.intersect(Offset.zero & screen);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _next() {
|
void _next() {
|
||||||
@@ -96,7 +106,7 @@ class _TutorialOverlayState extends State<TutorialOverlay>
|
|||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_index++;
|
_index++;
|
||||||
_ready = false;
|
_ready = widget.steps[_index].targetKey == null;
|
||||||
_spotRect = null;
|
_spotRect = null;
|
||||||
});
|
});
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _prepareStep());
|
WidgetsBinding.instance.addPostFrameCallback((_) => _prepareStep());
|
||||||
@@ -182,10 +192,14 @@ class _TutorialOverlayState extends State<TutorialOverlay>
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (spot == null) {
|
if (spot == null) {
|
||||||
return Center(
|
return Positioned.fill(
|
||||||
child: Padding(
|
child: SafeArea(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
child: Padding(
|
||||||
child: card,
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Center(
|
||||||
|
child: card,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -195,11 +209,19 @@ class _TutorialOverlayState extends State<TutorialOverlay>
|
|||||||
// Zone très large (image plein écran) : la bulle flotte en bas de l'écran
|
// Zone très large (image plein écran) : la bulle flotte en bas de l'écran
|
||||||
// pour laisser la main animée visible au centre.
|
// pour laisser la main animée visible au centre.
|
||||||
if (spot.height > screenHeight * 0.55) {
|
if (spot.height > screenHeight * 0.55) {
|
||||||
|
final available = (screenHeight * 0.4).clamp(140.0, screenHeight);
|
||||||
return Positioned(
|
return Positioned(
|
||||||
left: 16,
|
left: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
bottom: media.padding.bottom + 24,
|
bottom: media.padding.bottom + 24,
|
||||||
child: card,
|
child: SafeArea(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxHeight: available),
|
||||||
|
child: Center(
|
||||||
|
child: card,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,12 +229,25 @@ class _TutorialOverlayState extends State<TutorialOverlay>
|
|||||||
final above =
|
final above =
|
||||||
_step.preferTooltipAbove ?? (spot.top > screenHeight - spot.bottom);
|
_step.preferTooltipAbove ?? (spot.top > screenHeight - spot.bottom);
|
||||||
|
|
||||||
|
final available = above
|
||||||
|
? (spot.top - gap - media.padding.top).clamp(140.0, screenHeight)
|
||||||
|
: (screenHeight - spot.bottom - gap - media.padding.bottom).clamp(140.0, screenHeight);
|
||||||
|
|
||||||
return Positioned(
|
return Positioned(
|
||||||
left: 16,
|
left: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
top: above ? null : spot.bottom + gap,
|
top: above ? null : (spot.bottom + gap),
|
||||||
bottom: above ? (screenHeight - spot.top + gap) : null,
|
bottom: above ? (screenHeight - spot.top + gap) : null,
|
||||||
child: card,
|
child: SafeArea(
|
||||||
|
top: !above,
|
||||||
|
bottom: above,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxHeight: available),
|
||||||
|
child: Center(
|
||||||
|
child: card,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,10 +281,9 @@ class _TutorialCard extends StatelessWidget {
|
|||||||
final textSecondary =
|
final textSecondary =
|
||||||
isDark ? AppTheme.darkTextSecondary : AppTheme.lightTextSecondary;
|
isDark ? AppTheme.darkTextSecondary : AppTheme.lightTextSecondary;
|
||||||
|
|
||||||
return Center(
|
return ConstrainedBox(
|
||||||
child: ConstrainedBox(
|
constraints: const BoxConstraints(maxWidth: 460),
|
||||||
constraints: const BoxConstraints(maxWidth: 460),
|
child: Container(
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.fromLTRB(18, 16, 18, 12),
|
padding: const EdgeInsets.fromLTRB(18, 16, 18, 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: surface,
|
color: surface,
|
||||||
@@ -270,7 +304,7 @@ class _TutorialCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -298,6 +332,7 @@ class _TutorialCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'${index + 1}/$total',
|
'${index + 1}/$total',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -339,23 +374,21 @@ class _TutorialCard extends StatelessWidget {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
...List.generate(total, (i) {
|
||||||
children: List.generate(total, (i) {
|
final active = i == index;
|
||||||
final active = i == index;
|
return AnimatedContainer(
|
||||||
return AnimatedContainer(
|
duration: const Duration(milliseconds: 200),
|
||||||
duration: const Duration(milliseconds: 200),
|
margin: const EdgeInsets.only(right: 5),
|
||||||
margin: const EdgeInsets.only(right: 5),
|
width: active ? 18 : 6,
|
||||||
width: active ? 18 : 6,
|
height: 6,
|
||||||
height: 6,
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: active
|
||||||
color: active
|
? primary
|
||||||
? primary
|
: primary.withValues(alpha: 0.28),
|
||||||
: primary.withValues(alpha: 0.28),
|
borderRadius: BorderRadius.circular(3),
|
||||||
borderRadius: BorderRadius.circular(3),
|
),
|
||||||
),
|
);
|
||||||
);
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: onSkip,
|
onPressed: onSkip,
|
||||||
@@ -363,9 +396,9 @@ class _TutorialCard extends StatelessWidget {
|
|||||||
child: const Text('Passer'),
|
child: const Text('Passer'),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
ElevatedButton(
|
FilledButton(
|
||||||
onPressed: onNext,
|
onPressed: onNext,
|
||||||
style: ElevatedButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: primary,
|
backgroundColor: primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@@ -384,8 +417,7 @@ class _TutorialCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _gestureHint(TutorialGesture gesture) {
|
static String _gestureHint(TutorialGesture gesture) {
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
|
|
||||||
|
import 'package:bully/app.dart';
|
||||||
|
import 'package:bully/core/theme/theme_provider.dart';
|
||||||
|
import 'package:bully/data/repositories/session_repository.dart';
|
||||||
|
import 'package:bully/features/session/session_provider.dart';
|
||||||
|
import 'package:bully/features/tutorial/tutorial_provider.dart';
|
||||||
|
import 'package:bully/services/grouping_analyzer_service.dart';
|
||||||
|
import 'package:bully/services/score_calculator_service.dart';
|
||||||
|
import 'package:bully/services/tutorial_service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
setUpAll(() {
|
||||||
|
sqfliteFfiInit();
|
||||||
|
databaseFactory = databaseFactoryFfi;
|
||||||
|
FlutterError.onError = (details) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('CAUGHT_FLUTTER_ERROR: ${details.exceptionAsString()}');
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('CAUGHT_STACK: ${details.stack}');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('BullyApp démarre et affiche HomeScreen + Tutorial sans erreur',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
final repository = SessionRepository();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider<ScoreCalculatorService>(
|
||||||
|
create: (_) => ScoreCalculatorService(),
|
||||||
|
),
|
||||||
|
Provider<GroupingAnalyzerService>(
|
||||||
|
create: (_) => GroupingAnalyzerService(),
|
||||||
|
),
|
||||||
|
Provider<SessionRepository>.value(value: repository),
|
||||||
|
ChangeNotifierProvider<ThemeProvider>(create: (_) => ThemeProvider()),
|
||||||
|
ChangeNotifierProvider<SessionProvider>(
|
||||||
|
create: (_) => SessionProvider()),
|
||||||
|
ChangeNotifierProvider<TutorialProvider>(
|
||||||
|
create: (_) => TutorialProvider(service: TutorialService())),
|
||||||
|
],
|
||||||
|
child: const BullyApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
expect(find.text('BULLY'), findsOneWidget);
|
||||||
|
expect(find.text('Bienvenue dans Bully'), findsOneWidget);
|
||||||
|
|
||||||
|
// Passer à l'étape suivante
|
||||||
|
await tester.tap(find.text('Suivant'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
expect(find.text('Démarrez une session'), findsOneWidget);
|
||||||
|
|
||||||
|
// Passer
|
||||||
|
await tester.tap(find.text('Passer'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 500));
|
||||||
|
|
||||||
|
expect(find.text('Démarrez une session'), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -124,4 +124,62 @@ void main() {
|
|||||||
// Aucune cible : pas de main animée non plus.
|
// Aucune cible : pas de main animée non plus.
|
||||||
expect(find.byType(TutorialHand), findsNothing);
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user