diff --git a/lib/results/view/results_view.dart b/lib/results/view/results_view.dart index 8234a22..3de14d5 100644 --- a/lib/results/view/results_view.dart +++ b/lib/results/view/results_view.dart @@ -1,8 +1,6 @@ import 'package:api_client/api_client.dart'; import 'package:app_ui/app_ui.dart'; import 'package:dash_ai_search/home/home.dart'; -import 'package:dash_ai_search/l10n/l10n.dart'; -import 'package:dash_ai_search/question/question.dart'; import 'package:dash_ai_search/results/results.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -104,67 +102,6 @@ class _ResultsView extends StatelessWidget { } } -class SearchBoxView extends StatefulWidget { - @visibleForTesting - const SearchBoxView({super.key}); - - @override - State createState() => SearchBoxViewState(); -} - -class SearchBoxViewState extends State - with TickerProviderStateMixin, TransitionScreenMixin { - late Animation _offset; - late Animation _opacity; - - @override - List get forwardEnterStatuses => [Status.thinkingToResults]; - - @override - void initializeTransitionController() { - super.initializeTransitionController(); - - enterTransitionController = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); - - exitTransitionController = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); - } - - @override - void initState() { - super.initState(); - - _offset = Tween(begin: const Offset(0, 1), end: Offset.zero) - .animate(enterTransitionController); - - _opacity = - Tween(begin: 0, end: 1).animate(enterTransitionController); - } - - @override - Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints( - maxWidth: 659, - ), - child: SlideTransition( - position: _offset, - child: FadeTransition( - opacity: _opacity, - child: const SearchBox( - askAgain: true, - ), - ), - ), - ); - } -} - class BlueContainer extends StatefulWidget { @visibleForTesting const BlueContainer({ @@ -451,126 +388,6 @@ class _AiResponseState extends State<_AiResponse> } } -class SummaryView extends StatefulWidget { - const SummaryView({ - super.key, - }); - - @override - State createState() => _SummaryViewState(); -} - -class _SummaryViewState extends State - with TickerProviderStateMixin, TransitionScreenMixin { - late Animation _width; - - @override - List get forwardExitStatuses => [Status.resultsToSourceAnswers]; - - @override - List get backEnterStatuses => [Status.sourceAnswersBackToResults]; - - @override - void initializeTransitionController() { - super.initializeTransitionController(); - - enterTransitionController = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); - - exitTransitionController = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); - } - - @override - void initState() { - super.initState(); - - _width = Tween(begin: 563, end: 659).animate( - CurvedAnimation( - parent: exitTransitionController, - curve: Curves.decelerate, - ), - ); - } - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - final parsed = context.select((HomeBloc bloc) => bloc.state.parsedSummary); - - return AnimatedBuilder( - animation: _width, - builder: (context, child) { - return SizedBox( - width: _width.value, - child: RichText( - text: TextSpan( - children: [ - for (final element in parsed.elements) - if (element.isLink) - WidgetSpan( - child: InkWell( - onTap: () { - final isOnSeeSourceAnswers = - context.read().state.status == - Status.seeSourceAnswers; - if (isOnSeeSourceAnswers) { - context.read().add( - HomeSourceAnswersNavigated( - element.text, - ), - ); - } else { - context.read().add( - HomeSeeSourceAnswersRequested( - element.text, - ), - ); - } - }, - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: 2, - ), - padding: const EdgeInsets.symmetric( - vertical: 4, - horizontal: 12, - ), - decoration: const BoxDecoration( - color: VertexColors.white, - borderRadius: BorderRadius.all( - Radius.circular(100), - ), - ), - child: Text( - element.text, - style: textTheme.labelLarge?.copyWith( - color: VertexColors.googleBlue, - ), - ), - ), - ), - ) - else - TextSpan( - text: element.text, - style: textTheme.headlineLarge?.copyWith( - color: VertexColors.white, - ), - ), - ], - ), - ), - ); - }, - ); - } -} - class CarouselView extends StatefulWidget { @visibleForTesting const CarouselView({ @@ -645,130 +462,3 @@ class CarouselViewState extends State ); } } - -class BackToAnswerButton extends StatefulWidget { - @visibleForTesting - const BackToAnswerButton({super.key}); - - @override - State createState() => _BackToAnswerButtonState(); -} - -class _BackToAnswerButtonState extends State - with TickerProviderStateMixin, TransitionScreenMixin { - late Animation _sizeExitIn; - - @override - List get forwardExitStatuses => [Status.resultsToSourceAnswers]; - - @override - List get backEnterStatuses => [Status.sourceAnswersBackToResults]; - - @override - void initializeTransitionController() { - super.initializeTransitionController(); - - enterTransitionController = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); - - exitTransitionController = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); - } - - @override - void initState() { - super.initState(); - - _sizeExitIn = CurvedAnimation( - parent: exitTransitionController, - curve: Curves.decelerate, - ); - } - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return SizeTransition( - sizeFactor: _sizeExitIn, - axis: Axis.horizontal, - child: Align( - alignment: Alignment.topLeft, - child: TertiaryCTA( - key: const Key('backToAnswerButtonKey'), - label: l10n.backToAIAnswer, - icon: vertexIcons.arrowBack.image(color: VertexColors.white), - onPressed: () { - context.read().add(const HomeBackToAiSummaryTapped()); - }, - ), - ), - ); - } -} - -class SeeSourceAnswersButton extends StatefulWidget { - @visibleForTesting - const SeeSourceAnswersButton({super.key}); - - @override - State createState() => _SeeSourceAnswersButtonState(); -} - -class _SeeSourceAnswersButtonState extends State - with TickerProviderStateMixin, TransitionScreenMixin { - late Animation _opacityExitOut; - - @override - List get forwardExitStatuses => [Status.resultsToSourceAnswers]; - - @override - List get backEnterStatuses => [Status.sourceAnswersBackToResults]; - - @override - void initializeTransitionController() { - super.initializeTransitionController(); - - enterTransitionController = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); - - exitTransitionController = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - ); - } - - @override - void initState() { - super.initState(); - _opacityExitOut = - Tween(begin: 1, end: 0).animate(exitTransitionController); - } - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return FadeTransition( - opacity: _opacityExitOut, - child: Align( - alignment: Alignment.bottomRight, - child: TertiaryCTA( - label: l10n.seeSourceAnswers, - icon: vertexIcons.arrowForward.image( - color: VertexColors.white, - ), - onPressed: () => context - .read() - .add(const HomeSeeSourceAnswersRequested(null)), - ), - ), - ); - } -} diff --git a/lib/results/widgets/back_to_answer_button.dart b/lib/results/widgets/back_to_answer_button.dart new file mode 100644 index 0000000..b9a96dd --- /dev/null +++ b/lib/results/widgets/back_to_answer_button.dart @@ -0,0 +1,69 @@ +import 'package:app_ui/app_ui.dart'; +import 'package:dash_ai_search/home/home.dart'; +import 'package:dash_ai_search/l10n/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class BackToAnswerButton extends StatefulWidget { + const BackToAnswerButton({super.key}); + + @override + State createState() => _BackToAnswerButtonState(); +} + +class _BackToAnswerButtonState extends State + with TickerProviderStateMixin, TransitionScreenMixin { + late Animation _sizeExitIn; + + @override + List get forwardExitStatuses => [Status.resultsToSourceAnswers]; + + @override + List get backEnterStatuses => [Status.sourceAnswersBackToResults]; + + @override + void initializeTransitionController() { + super.initializeTransitionController(); + + enterTransitionController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + + exitTransitionController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + } + + @override + void initState() { + super.initState(); + + _sizeExitIn = CurvedAnimation( + parent: exitTransitionController, + curve: Curves.decelerate, + ); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return SizeTransition( + sizeFactor: _sizeExitIn, + axis: Axis.horizontal, + child: Align( + alignment: Alignment.topLeft, + child: TertiaryCTA( + key: const Key('backToAnswerButtonKey'), + label: l10n.backToAIAnswer, + icon: vertexIcons.arrowBack.image(color: VertexColors.white), + onPressed: () { + context.read().add(const HomeBackToAiSummaryTapped()); + }, + ), + ), + ); + } +} diff --git a/lib/results/widgets/search_box_view.dart b/lib/results/widgets/search_box_view.dart new file mode 100644 index 0000000..48d3faf --- /dev/null +++ b/lib/results/widgets/search_box_view.dart @@ -0,0 +1,63 @@ +import 'package:dash_ai_search/home/home.dart'; +import 'package:dash_ai_search/question/question.dart'; +import 'package:flutter/material.dart'; + +class SearchBoxView extends StatefulWidget { + const SearchBoxView({super.key}); + + @override + State createState() => SearchBoxViewState(); +} + +class SearchBoxViewState extends State + with TickerProviderStateMixin, TransitionScreenMixin { + late Animation _offset; + late Animation _opacity; + + @override + List get forwardEnterStatuses => [Status.thinkingToResults]; + + @override + void initializeTransitionController() { + super.initializeTransitionController(); + + enterTransitionController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + + exitTransitionController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + } + + @override + void initState() { + super.initState(); + + _offset = Tween(begin: const Offset(0, 1), end: Offset.zero) + .animate(enterTransitionController); + + _opacity = + Tween(begin: 0, end: 1).animate(enterTransitionController); + } + + @override + Widget build(BuildContext context) { + return Container( + constraints: const BoxConstraints( + maxWidth: 659, + ), + child: SlideTransition( + position: _offset, + child: FadeTransition( + opacity: _opacity, + child: const SearchBox( + askAgain: true, + ), + ), + ), + ); + } +} diff --git a/lib/results/widgets/see_source_answers_button.dart b/lib/results/widgets/see_source_answers_button.dart new file mode 100644 index 0000000..a46ca73 --- /dev/null +++ b/lib/results/widgets/see_source_answers_button.dart @@ -0,0 +1,66 @@ +import 'package:app_ui/app_ui.dart'; +import 'package:dash_ai_search/home/home.dart'; +import 'package:dash_ai_search/l10n/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SeeSourceAnswersButton extends StatefulWidget { + const SeeSourceAnswersButton({super.key}); + + @override + State createState() => _SeeSourceAnswersButtonState(); +} + +class _SeeSourceAnswersButtonState extends State + with TickerProviderStateMixin, TransitionScreenMixin { + late Animation _opacityExitOut; + + @override + List get forwardExitStatuses => [Status.resultsToSourceAnswers]; + + @override + List get backEnterStatuses => [Status.sourceAnswersBackToResults]; + + @override + void initializeTransitionController() { + super.initializeTransitionController(); + + enterTransitionController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + + exitTransitionController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + } + + @override + void initState() { + super.initState(); + _opacityExitOut = + Tween(begin: 1, end: 0).animate(exitTransitionController); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + return FadeTransition( + opacity: _opacityExitOut, + child: Align( + alignment: Alignment.bottomRight, + child: TertiaryCTA( + label: l10n.seeSourceAnswers, + icon: vertexIcons.arrowForward.image( + color: VertexColors.white, + ), + onPressed: () => context + .read() + .add(const HomeSeeSourceAnswersRequested(null)), + ), + ), + ); + } +} diff --git a/lib/results/widgets/summary_view.dart b/lib/results/widgets/summary_view.dart new file mode 100644 index 0000000..190125f --- /dev/null +++ b/lib/results/widgets/summary_view.dart @@ -0,0 +1,124 @@ +import 'package:app_ui/app_ui.dart'; +import 'package:dash_ai_search/home/home.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SummaryView extends StatefulWidget { + const SummaryView({ + super.key, + }); + + @override + State createState() => _SummaryViewState(); +} + +class _SummaryViewState extends State + with TickerProviderStateMixin, TransitionScreenMixin { + late Animation _width; + + @override + List get forwardExitStatuses => [Status.resultsToSourceAnswers]; + + @override + List get backEnterStatuses => [Status.sourceAnswersBackToResults]; + + @override + void initializeTransitionController() { + super.initializeTransitionController(); + + enterTransitionController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + + exitTransitionController = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + } + + @override + void initState() { + super.initState(); + + _width = Tween(begin: 563, end: 659).animate( + CurvedAnimation( + parent: exitTransitionController, + curve: Curves.decelerate, + ), + ); + } + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final parsed = context.select((HomeBloc bloc) => bloc.state.parsedSummary); + + return AnimatedBuilder( + animation: _width, + builder: (context, child) { + return SizedBox( + width: _width.value, + child: RichText( + text: TextSpan( + children: [ + for (final element in parsed.elements) + if (element.isLink) + WidgetSpan( + child: InkWell( + onTap: () { + final isOnSeeSourceAnswers = + context.read().state.status == + Status.seeSourceAnswers; + if (isOnSeeSourceAnswers) { + context.read().add( + HomeSourceAnswersNavigated( + element.text, + ), + ); + } else { + context.read().add( + HomeSeeSourceAnswersRequested( + element.text, + ), + ); + } + }, + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 2, + ), + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 12, + ), + decoration: const BoxDecoration( + color: VertexColors.white, + borderRadius: BorderRadius.all( + Radius.circular(100), + ), + ), + child: Text( + element.text, + style: textTheme.labelLarge?.copyWith( + color: VertexColors.googleBlue, + ), + ), + ), + ), + ) + else + TextSpan( + text: element.text, + style: textTheme.headlineLarge?.copyWith( + color: VertexColors.white, + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/results/widgets/widgets.dart b/lib/results/widgets/widgets.dart index 4d49ac7..498eb76 100644 --- a/lib/results/widgets/widgets.dart +++ b/lib/results/widgets/widgets.dart @@ -1,2 +1,6 @@ +export 'back_to_answer_button.dart'; export 'emoji_bubbles.dart'; +export 'search_box_view.dart'; +export 'see_source_answers_button.dart'; export 'sources_carousel_view.dart'; +export 'summary_view.dart'; diff --git a/lib/thinking/thinking.dart b/lib/thinking/thinking.dart index 00ffcf9..3b46d13 100644 --- a/lib/thinking/thinking.dart +++ b/lib/thinking/thinking.dart @@ -1 +1,2 @@ export 'view/view.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/thinking/view/thinking_view.dart b/lib/thinking/view/thinking_view.dart index 04c94bb..4d80bd3 100644 --- a/lib/thinking/view/thinking_view.dart +++ b/lib/thinking/view/thinking_view.dart @@ -1,6 +1,7 @@ import 'package:app_ui/app_ui.dart'; import 'package:dash_ai_search/home/home.dart'; import 'package:dash_ai_search/l10n/l10n.dart'; +import 'package:dash_ai_search/thinking/thinking.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -102,91 +103,6 @@ class ThinkingViewState extends State } } -class PulseAnimationView extends StatefulWidget { - const PulseAnimationView({super.key}); - - @override - State createState() => _PulseAnimationViewState(); -} - -const _pulseDuration = Duration(milliseconds: 2000); - -class _PulseAnimationViewState extends State - with SingleTickerProviderStateMixin { - late AnimationController pulseTransitionController; - late Animation _scale; - - @override - void initState() { - super.initState(); - - pulseTransitionController = AnimationController( - vsync: this, - duration: _pulseDuration, - ); - - _scale = - Tween(begin: 1.05, end: .6).animate(pulseTransitionController); - pulseTransitionController.repeat(reverse: true); - } - - @override - void dispose() { - pulseTransitionController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - const backgroundColor = Colors.transparent; - const borderColor = VertexColors.googleBlue; - - return Align( - child: LayoutBuilder( - builder: (context, constraints) { - final viewport = constraints.maxHeight < constraints.maxWidth - ? constraints.maxHeight - : constraints.maxWidth; - - final bigCircleRadius = viewport / 2; - final mediumCircleRadius = bigCircleRadius * .59; - final smallCircleRadius = bigCircleRadius * .27; - - return SizedBox( - width: viewport, - height: viewport, - child: ScaleTransition( - scale: _scale, - child: Circle( - dotted: true, - backgroundColor: backgroundColor, - borderColor: borderColor, - radius: bigCircleRadius, - child: Center( - child: Circle( - dotted: true, - backgroundColor: backgroundColor, - borderColor: borderColor, - radius: mediumCircleRadius, - child: Center( - child: Circle( - dotted: true, - backgroundColor: backgroundColor, - borderColor: borderColor, - radius: smallCircleRadius, - ), - ), - ), - ), - ), - ), - ); - }, - ), - ); - } -} - class TextArea extends StatelessWidget { @visibleForTesting const TextArea({required this.query, super.key}); diff --git a/lib/thinking/widgets/pulse_animation_view.dart b/lib/thinking/widgets/pulse_animation_view.dart new file mode 100644 index 0000000..46b6a64 --- /dev/null +++ b/lib/thinking/widgets/pulse_animation_view.dart @@ -0,0 +1,88 @@ +import 'package:app_ui/app_ui.dart'; +import 'package:dash_ai_search/home/home.dart'; +import 'package:flutter/material.dart'; + +class PulseAnimationView extends StatefulWidget { + const PulseAnimationView({super.key}); + + @override + State createState() => _PulseAnimationViewState(); +} + +const _pulseDuration = Duration(milliseconds: 2000); + +class _PulseAnimationViewState extends State + with SingleTickerProviderStateMixin { + late AnimationController pulseTransitionController; + late Animation _scale; + + @override + void initState() { + super.initState(); + + pulseTransitionController = AnimationController( + vsync: this, + duration: _pulseDuration, + ); + + _scale = + Tween(begin: 1.05, end: .6).animate(pulseTransitionController); + pulseTransitionController.repeat(reverse: true); + } + + @override + void dispose() { + pulseTransitionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + const backgroundColor = Colors.transparent; + const borderColor = VertexColors.googleBlue; + + return Align( + child: LayoutBuilder( + builder: (context, constraints) { + final viewport = constraints.maxHeight < constraints.maxWidth + ? constraints.maxHeight + : constraints.maxWidth; + + final bigCircleRadius = viewport / 2; + final mediumCircleRadius = bigCircleRadius * .59; + final smallCircleRadius = bigCircleRadius * .27; + + return SizedBox( + width: viewport, + height: viewport, + child: ScaleTransition( + scale: _scale, + child: Circle( + dotted: true, + backgroundColor: backgroundColor, + borderColor: borderColor, + radius: bigCircleRadius, + child: Center( + child: Circle( + dotted: true, + backgroundColor: backgroundColor, + borderColor: borderColor, + radius: mediumCircleRadius, + child: Center( + child: Circle( + dotted: true, + backgroundColor: backgroundColor, + borderColor: borderColor, + radius: smallCircleRadius, + ), + ), + ), + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/thinking/widgets/widgets.dart b/lib/thinking/widgets/widgets.dart new file mode 100644 index 0000000..29e6395 --- /dev/null +++ b/lib/thinking/widgets/widgets.dart @@ -0,0 +1 @@ +export 'pulse_animation_view.dart'; diff --git a/test/results/view/results_view_test.dart b/test/results/view/results_view_test.dart index 36d1200..90b83b0 100644 --- a/test/results/view/results_view_test.dart +++ b/test/results/view/results_view_test.dart @@ -292,91 +292,4 @@ void main() { ).called(1); }); }); - - group('SummaryView', () { - late HomeBloc homeBloc; - const response = VertexResponse( - summary: 'Flutter is a free and open source software development ' - "kit (SDK) from Google [1]. It's used to create beautiful, " - 'fast user experiences, ,,,for mobile, web, and desktop ' - 'applications [1]. Flutter works with existing code and is used ' - 'by developers and organizations around the world [1]. [3]. ' - 'Flutter is a fully open source project [3].', - documents: [ - VertexDocument( - id: '1', - metadata: VertexMetadata( - url: 'url', - title: 'title', - description: 'description', - ), - ), - VertexDocument( - id: '2', - metadata: VertexMetadata( - url: 'url', - title: 'title', - description: 'description', - ), - ), - VertexDocument( - id: '3', - metadata: VertexMetadata( - url: 'url', - title: 'title', - description: 'description', - ), - ), - VertexDocument( - id: '4', - metadata: VertexMetadata( - url: 'url', - title: 'title', - description: 'description', - ), - ), - ], - ); - - Widget bootstrap() => BlocProvider.value( - value: homeBloc, - child: Material(child: SummaryView()), - ); - - setUp(() { - homeBloc = _MockHomeBloc(); - when(() => homeBloc.state).thenReturn( - HomeState(vertexResponse: response), - ); - }); - - testWidgets( - 'adds HomeSourceAnswersNavigated tapping on the link if state ' - 'Status.seeSourceAnswers', - (WidgetTester tester) async { - when(() => homeBloc.state).thenReturn( - HomeState(vertexResponse: response, status: Status.seeSourceAnswers), - ); - await tester.pumpApp(bootstrap()); - final widget = tester.widget(find.byType(InkWell).first); - widget.onTap?.call(); - verify(() => homeBloc.add(HomeSourceAnswersNavigated('[1]'))).called(1); - }, - ); - - testWidgets( - 'adds HomeSeeSourceAnswersRequested tapping on the link if state ' - 'is not Status.seeSourceAnswers', - (WidgetTester tester) async { - when(() => homeBloc.state).thenReturn( - HomeState(vertexResponse: response, status: Status.results), - ); - await tester.pumpApp(bootstrap()); - final widget = tester.widget(find.byType(InkWell).first); - widget.onTap?.call(); - verify(() => homeBloc.add(HomeSeeSourceAnswersRequested('[1]'))) - .called(1); - }, - ); - }); } diff --git a/test/results/widgets/summary_view_test.dart b/test/results/widgets/summary_view_test.dart new file mode 100644 index 0000000..6c00004 --- /dev/null +++ b/test/results/widgets/summary_view_test.dart @@ -0,0 +1,102 @@ +import 'package:api_client/api_client.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:dash_ai_search/home/home.dart'; +import 'package:dash_ai_search/results/results.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../helpers/helpers.dart'; + +class _MockHomeBloc extends MockBloc + implements HomeBloc {} + +void main() { + group('SummaryView', () { + late HomeBloc homeBloc; + const response = VertexResponse( + summary: 'Flutter is a free and open source software development ' + "kit (SDK) from Google [1]. It's used to create beautiful, " + 'fast user experiences, ,,,for mobile, web, and desktop ' + 'applications [1]. Flutter works with existing code and is used ' + 'by developers and organizations around the world [1]. [3]. ' + 'Flutter is a fully open source project [3].', + documents: [ + VertexDocument( + id: '1', + metadata: VertexMetadata( + url: 'url', + title: 'title', + description: 'description', + ), + ), + VertexDocument( + id: '2', + metadata: VertexMetadata( + url: 'url', + title: 'title', + description: 'description', + ), + ), + VertexDocument( + id: '3', + metadata: VertexMetadata( + url: 'url', + title: 'title', + description: 'description', + ), + ), + VertexDocument( + id: '4', + metadata: VertexMetadata( + url: 'url', + title: 'title', + description: 'description', + ), + ), + ], + ); + + Widget bootstrap() => BlocProvider.value( + value: homeBloc, + child: Material(child: SummaryView()), + ); + + setUp(() { + homeBloc = _MockHomeBloc(); + when(() => homeBloc.state).thenReturn( + HomeState(vertexResponse: response), + ); + }); + + testWidgets( + 'adds HomeSourceAnswersNavigated tapping on the link if state ' + 'Status.seeSourceAnswers', + (WidgetTester tester) async { + when(() => homeBloc.state).thenReturn( + HomeState(vertexResponse: response, status: Status.seeSourceAnswers), + ); + await tester.pumpApp(bootstrap()); + final widget = tester.widget(find.byType(InkWell).first); + widget.onTap?.call(); + verify(() => homeBloc.add(HomeSourceAnswersNavigated('[1]'))).called(1); + }, + ); + + testWidgets( + 'adds HomeSeeSourceAnswersRequested tapping on the link if state ' + 'is not Status.seeSourceAnswers', + (WidgetTester tester) async { + when(() => homeBloc.state).thenReturn( + HomeState(vertexResponse: response, status: Status.results), + ); + await tester.pumpApp(bootstrap()); + final widget = tester.widget(find.byType(InkWell).first); + widget.onTap?.call(); + verify(() => homeBloc.add(HomeSeeSourceAnswersRequested('[1]'))) + .called(1); + }, + ); + }); +} diff --git a/test/thinking/view/thinking_view_test.dart b/test/thinking/view/thinking_view_test.dart index eb51212..dc3a391 100644 --- a/test/thinking/view/thinking_view_test.dart +++ b/test/thinking/view/thinking_view_test.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:bloc_test/bloc_test.dart'; import 'package:dash_ai_search/home/home.dart'; import 'package:dash_ai_search/thinking/thinking.dart'; @@ -67,33 +65,5 @@ void main() { expect(forwardExitStatuses, equals([Status.thinkingToResults])); }); - - group('PulseAnimationView', () { - Widget bootstrap() => BlocProvider.value( - value: homeBloc, - child: Material( - child: PulseAnimationView(), - ), - ); - - testWidgets( - 'animation changes correctly', - (tester) async { - final streamController = StreamController(); - whenListen( - homeBloc, - streamController.stream, - initialState: const HomeState(), - ); - - await tester.pumpApp(bootstrap()); - - streamController.add( - const HomeState(status: Status.thinkingToResults), - ); - await tester.pump(); - }, - ); - }); }); } diff --git a/test/thinking/widgets/pulse_animation_view_test.dart b/test/thinking/widgets/pulse_animation_view_test.dart new file mode 100644 index 0000000..e5eacbc --- /dev/null +++ b/test/thinking/widgets/pulse_animation_view_test.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:dash_ai_search/home/home.dart'; +import 'package:dash_ai_search/thinking/thinking.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../helpers/helpers.dart'; + +class _MockHomeBloc extends MockBloc + implements HomeBloc {} + +void main() { + group('PulseAnimationView', () { + late HomeBloc homeBloc; + + setUp(() { + homeBloc = _MockHomeBloc(); + when(() => homeBloc.state).thenReturn(HomeState()); + }); + + Widget bootstrap() => BlocProvider.value( + value: homeBloc, + child: Material( + child: PulseAnimationView(), + ), + ); + + testWidgets( + 'animation changes correctly', + (tester) async { + final streamController = StreamController(); + whenListen( + homeBloc, + streamController.stream, + initialState: const HomeState(), + ); + + await tester.pumpApp(bootstrap()); + + streamController.add( + const HomeState(status: Status.thinkingToResults), + ); + await tester.pump(); + }, + ); + }); +}