From 51989b92b37e8c07b4fbf73d8d05a5eb32698985 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Tue, 5 Dec 2023 11:32:25 -0300 Subject: [PATCH] final details --- lib/home/widgets/emoji_bubbles.dart | 110 ++++++++++------- test/home/widgets/emoji_bubbles_test.dart | 142 ++++++++++++++++++++++ 2 files changed, 210 insertions(+), 42 deletions(-) create mode 100644 test/home/widgets/emoji_bubbles_test.dart diff --git a/lib/home/widgets/emoji_bubbles.dart b/lib/home/widgets/emoji_bubbles.dart index 916d14d..cf0fc62 100644 --- a/lib/home/widgets/emoji_bubbles.dart +++ b/lib/home/widgets/emoji_bubbles.dart @@ -25,17 +25,35 @@ class Bubble { class EmojiBubbles extends StatefulWidget { const EmojiBubbles({super.key}); + static const cellebrateImages = [ + 'assets/rating-assets/confetti.png', + 'assets/rating-assets/heart.png', + 'assets/rating-assets/star.png', + 'assets/rating-assets/thumbs-up.png', + ]; + static const beDepressedImages = [ + 'assets/rating-assets/rain.png', + 'assets/rating-assets/sad.png', + 'assets/rating-assets/thumbs-down.png', + ]; @override - State createState() => _EmojiBubblesState(); + State createState() => EmojiBubblesState(); } -class _EmojiBubblesState extends State +@visibleForTesting +class EmojiBubblesState extends State with SingleTickerProviderStateMixin { - late final _controller = AnimationController( - vsync: this, - duration: Duration(milliseconds: (1000 / 60).round()), - )..addListener(_runLoop); + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: (1000 / 60).round()), + )..addListener(_runLoop); + } DateTime? _lastUpdate; late final List _bubbles = []; @@ -49,19 +67,8 @@ class _EmojiBubblesState extends State ? VertexColors.deepArctic : VertexColors.white; - const cellebrateImages = [ - 'assets/rating-assets/confetti.png', - 'assets/rating-assets/heart.png', - 'assets/rating-assets/star.png', - 'assets/rating-assets/thumbs-up.png', - ]; - const beDepressedImages = [ - 'assets/rating-assets/rain.png', - 'assets/rating-assets/sad.png', - 'assets/rating-assets/thumbs-down.png', - ]; - - final emojis = happy ? cellebrateImages : beDepressedImages; + final emojis = + happy ? EmojiBubbles.cellebrateImages : EmojiBubbles.beDepressedImages; const numberOfBubbles = 15; @@ -99,9 +106,11 @@ class _EmojiBubblesState extends State @override void dispose() { - super.dispose(); + _controller + ..stop() + ..dispose(); - _controller.dispose(); + super.dispose(); } void _initAnimation(bool happy) { @@ -111,6 +120,7 @@ class _EmojiBubblesState extends State void _stopAnimation() { _controller.stop(); + setState(() {}); } void _runLoop() { @@ -120,8 +130,10 @@ class _EmojiBubblesState extends State : now.difference(_lastUpdate!).inMilliseconds / 1000; _lastUpdate = now; - _update(dt); - setState(() {}); + update(dt); + if (mounted) { + setState(() {}); + } } double _interpolatePosition(Bubble buble, double dt) { @@ -134,7 +146,7 @@ class _EmojiBubblesState extends State return buble.position.y - speed * dt; } - void _update(double dt) { + void update(double dt) { for (final bubble in _bubbles) { bubble.position.y = _interpolatePosition(bubble, dt); @@ -166,24 +178,7 @@ class _EmojiBubblesState extends State Positioned( left: bubble.position.x, top: bubble.position.y, - child: DecoratedBox( - decoration: BoxDecoration( - color: bubble.color, - shape: BoxShape.circle, - ), - child: SizedBox( - width: bubble.size, - height: bubble.size, - child: Center( - child: Padding( - padding: EdgeInsets.all(bubble.size / 4), - child: Image.asset( - bubble.emoji, - ), - ), - ), - ), - ), + child: EmojiBubble(bubble: bubble), ), ], ), @@ -191,3 +186,34 @@ class _EmojiBubblesState extends State ); } } + +class EmojiBubble extends StatelessWidget { + const EmojiBubble({ + required this.bubble, + super.key, + }); + + final Bubble bubble; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: bubble.color, + shape: BoxShape.circle, + ), + child: SizedBox( + width: bubble.size, + height: bubble.size, + child: Center( + child: Padding( + padding: EdgeInsets.all(bubble.size / 4), + child: Image.asset( + bubble.emoji, + ), + ), + ), + ), + ); + } +} diff --git a/test/home/widgets/emoji_bubbles_test.dart b/test/home/widgets/emoji_bubbles_test.dart new file mode 100644 index 0000000..ad35a99 --- /dev/null +++ b/test/home/widgets/emoji_bubbles_test.dart @@ -0,0 +1,142 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:dash_ai_search/home/home.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../helpers/helpers.dart'; + +class _MockHomeBloc extends MockBloc + implements HomeBloc {} + +void main() { + group('EmojiBubbles', () { + late HomeBloc homeBloc; + + setUp(() { + homeBloc = _MockHomeBloc(); + + whenListen( + homeBloc, + Stream.fromIterable([const HomeState()]), + initialState: const HomeState(), + ); + }); + + Widget bootstrap() => BlocProvider.value( + value: homeBloc, + child: const MaterialApp( + home: Scaffold( + body: EmojiBubbles(), + ), + ), + ); + + testWidgets('renders', (tester) async { + await tester.pumpApp(bootstrap()); + + expect(find.byType(EmojiBubbles), findsOneWidget); + }); + + testWidgets( + 'when receiving a positive feedback, spawn emojis with ' + 'cellebration images', + (tester) async { + final controller = StreamController(); + + whenListen( + homeBloc, + controller.stream, + initialState: const HomeState(), + ); + + await tester.pumpApp(bootstrap()); + + controller.add(const HomeState(answerFeedbacks: [AnswerFeedback.good])); + + await tester.pump(); + + expect(find.byType(EmojiBubble), findsWidgets); + + final widgets = tester.widgetList(find.byType(EmojiBubble)).toList(); + + for (final widget in widgets) { + expect(widget, isA()); + expect( + EmojiBubbles.cellebrateImages, + contains((widget as EmojiBubble).bubble.emoji), + ); + } + + for (var i = 0; i < 10; i++) { + await tester.pump(); + } + }, + ); + + testWidgets( + 'when receiving a negative feedback, spawn emojis with ' + 'depressing images', + (tester) async { + final controller = StreamController(); + + whenListen( + homeBloc, + controller.stream, + initialState: const HomeState(), + ); + + await tester.pumpApp(bootstrap()); + + controller.add(const HomeState(answerFeedbacks: [AnswerFeedback.bad])); + + await tester.pump(); + + expect(find.byType(EmojiBubble), findsWidgets); + + final widgets = tester.widgetList(find.byType(EmojiBubble)).toList(); + + for (final widget in widgets) { + expect(widget, isA()); + expect( + EmojiBubbles.beDepressedImages, + contains((widget as EmojiBubble).bubble.emoji), + ); + } + + for (var i = 0; i < 10; i++) { + await tester.pump(); + } + }, + ); + + testWidgets( + 'there are no bubbles anymore when the anymation is over', + (tester) async { + final controller = StreamController(); + + whenListen( + homeBloc, + controller.stream, + initialState: const HomeState(), + ); + + await tester.pumpApp(bootstrap()); + + controller.add(const HomeState(answerFeedbacks: [AnswerFeedback.good])); + + final state = + tester.state(find.byType(EmojiBubbles)); + + for (var i = 0; i < 100; i++) { + state.update(1); + await tester.pump(); + } + + expect(find.byType(EmojiBubble), findsNothing); + }, + ); + }); +}