diff --git a/.github/workflows/deploy_app_staging.yaml b/.github/workflows/deploy_app_staging.yaml index 24cfbf0..230e6b0 100644 --- a/.github/workflows/deploy_app_staging.yaml +++ b/.github/workflows/deploy_app_staging.yaml @@ -14,7 +14,7 @@ jobs: flutter-version: "3.16.2" channel: "stable" - run: flutter packages get - - run: flutter build web -t lib/main_development.dart --dart-define SHARING_ENABLED=true + - run: flutter build web -t lib/main_production.dart --dart-define SHARING_ENABLED=true --dart-define="AI_API_URL=${{ secrets.AI_API_URL }}" - uses: FirebaseExtended/action-hosting-deploy@v0 with: repoToken: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.gitignore b/.gitignore index 9190715..bd8e325 100644 --- a/.gitignore +++ b/.gitignore @@ -105,7 +105,6 @@ app.*.map.json !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/dev/ci/**/Gemfile.lock !.vscode/extensions.json -!.vscode/launch.json !.idea/codeStyles/ !.idea/dictionaries/ !.idea/runConfigurations/ diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index f0752cd..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Launch development", - "request": "launch", - "type": "dart", - "program": "lib/main_development.dart", - "args": [ - "--flavor", - "development", - "--target", - "lib/main_development.dart" - ] - }, - { - "name": "Launch production", - "request": "launch", - "type": "dart", - "program": "lib/main_production.dart", - "args": [ - "--flavor", - "production", - "--target", - "lib/main_production.dart" - ] - } - ] -} \ No newline at end of file diff --git a/lib/home/bloc/home_bloc.dart b/lib/home/bloc/home_bloc.dart index a92d0be..411a840 100644 --- a/lib/home/bloc/home_bloc.dart +++ b/lib/home/bloc/home_bloc.dart @@ -10,41 +10,31 @@ part 'home_state.dart'; class HomeBloc extends Bloc { HomeBloc(this._questionsRepository) : super(const HomeState()) { - on(_onFromWelcomeToQuestion); - on(_onQuestion); - on(_queryUpdated); - on(_questionAsked); - on(_questionAskedAgain); - on(_onResults); - on(_onSeeSourceAnswersRequested); - on(_onSeeSourceAnswers); - on(_onAddAnswerFeedback); - on(_navigateSourceAnswers); - on(_onBackToAiSummaryTapped); + on(_onHomeNavigated); + on(_onHomeQueryUpdated); + on(_onHomeQuestionAsked); + on(_onHomeQuestionAskedAgain); + on(_onHomeSeeSourceAnswersRequested); + on(_onHomeAnswerFeedbackAdded); + on(_onHomeSourceAnswersNavigated); + on(_onHomeBackToAiSummaryTapped); } final QuestionsRepository _questionsRepository; - void _onFromWelcomeToQuestion( - FromWelcomeToQuestion event, + void _onHomeNavigated( + HomeNavigated event, Emitter emit, ) { - emit(state.copyWith(status: Status.welcomeToAskQuestion)); + emit(state.copyWith(status: event.status)); } - void _onQuestion( - AskQuestion event, - Emitter emit, - ) { - emit(state.copyWith(status: Status.askQuestion)); - } - - void _queryUpdated(QueryUpdated event, Emitter emit) { + void _onHomeQueryUpdated(HomeQueryUpdated event, Emitter emit) { emit(state.copyWith(query: event.query)); } - Future _questionAsked( - QuestionAsked event, + Future _onHomeQuestionAsked( + HomeQuestionAsked event, Emitter emit, ) async { emit( @@ -56,8 +46,8 @@ class HomeBloc extends Bloc { await _emitVertexResponse(emit); } - Future _questionAskedAgain( - QuestionAskedAgain event, + Future _onHomeQuestionAskedAgain( + HomeQuestionAskedAgain event, Emitter emit, ) async { emit( @@ -79,15 +69,8 @@ class HomeBloc extends Bloc { ); } - void _onResults( - Results event, - Emitter emit, - ) { - emit(state.copyWith(status: Status.results)); - } - - void _onSeeSourceAnswersRequested( - SeeSourceAnswersRequested event, + void _onHomeSeeSourceAnswersRequested( + HomeSeeSourceAnswersRequested event, Emitter emit, ) { final indexParsed = event.index != null ? _getIndex(event.index!) : 0; @@ -99,15 +82,8 @@ class HomeBloc extends Bloc { ); } - void _onSeeSourceAnswers( - SeeResultsSourceAnswers event, - Emitter emit, - ) { - emit(state.copyWith(status: Status.seeSourceAnswers)); - } - - void _onAddAnswerFeedback( - AddAnswerFeedback event, + void _onHomeAnswerFeedbackAdded( + HomeAnswerFeedbackAdded event, Emitter emit, ) { emit( @@ -120,8 +96,8 @@ class HomeBloc extends Bloc { ); } - FutureOr _navigateSourceAnswers( - NavigateSourceAnswers event, + FutureOr _onHomeSourceAnswersNavigated( + HomeSourceAnswersNavigated event, Emitter emit, ) { final indexParsed = _getIndex(event.index); @@ -132,8 +108,8 @@ class HomeBloc extends Bloc { ); } - void _onBackToAiSummaryTapped( - BackToAiSummaryTapped event, + void _onHomeBackToAiSummaryTapped( + HomeBackToAiSummaryTapped event, Emitter emit, ) { emit(state.copyWith(status: Status.sourceAnswersBackToResults)); diff --git a/lib/home/bloc/home_event.dart b/lib/home/bloc/home_event.dart index 0af2b10..21f24df 100644 --- a/lib/home/bloc/home_event.dart +++ b/lib/home/bloc/home_event.dart @@ -2,29 +2,27 @@ part of 'home_bloc.dart'; abstract class HomeEvent extends Equatable { const HomeEvent(); - - @override - List get props => []; } -class FromWelcomeToQuestion extends HomeEvent { - const FromWelcomeToQuestion(); -} +class HomeNavigated extends HomeEvent { + const HomeNavigated(this.status); + + final Status status; -class AskQuestion extends HomeEvent { - const AskQuestion(); + @override + List get props => [status]; } -class QueryUpdated extends HomeEvent { - const QueryUpdated({required this.query}); +class HomeQueryUpdated extends HomeEvent { + const HomeQueryUpdated({required this.query}); final String query; @override List get props => [query]; } -class QuestionAsked extends HomeEvent { - const QuestionAsked(this.submittedQuery); +class HomeQuestionAsked extends HomeEvent { + const HomeQuestionAsked(this.submittedQuery); final String submittedQuery; @@ -32,8 +30,8 @@ class QuestionAsked extends HomeEvent { List get props => [submittedQuery]; } -class QuestionAskedAgain extends HomeEvent { - const QuestionAskedAgain(this.submittedQuery); +class HomeQuestionAskedAgain extends HomeEvent { + const HomeQuestionAskedAgain(this.submittedQuery); final String submittedQuery; @@ -41,22 +39,17 @@ class QuestionAskedAgain extends HomeEvent { List get props => [submittedQuery]; } -class Results extends HomeEvent { - const Results(); -} - -class SeeSourceAnswersRequested extends HomeEvent { - const SeeSourceAnswersRequested(this.index); +class HomeSeeSourceAnswersRequested extends HomeEvent { + const HomeSeeSourceAnswersRequested(this.index); final String? index; -} -class SeeResultsSourceAnswers extends HomeEvent { - const SeeResultsSourceAnswers(); + @override + List get props => [index]; } -class AddAnswerFeedback extends HomeEvent { - const AddAnswerFeedback(this.answerFeedback); +class HomeAnswerFeedbackAdded extends HomeEvent { + const HomeAnswerFeedbackAdded(this.answerFeedback); final AnswerFeedback answerFeedback; @@ -64,12 +57,18 @@ class AddAnswerFeedback extends HomeEvent { List get props => [answerFeedback]; } -class NavigateSourceAnswers extends HomeEvent { - const NavigateSourceAnswers(this.index); +class HomeSourceAnswersNavigated extends HomeEvent { + const HomeSourceAnswersNavigated(this.index); final String index; + + @override + List get props => [index]; } -class BackToAiSummaryTapped extends HomeEvent { - const BackToAiSummaryTapped(); +class HomeBackToAiSummaryTapped extends HomeEvent { + const HomeBackToAiSummaryTapped(); + + @override + List get props => []; } diff --git a/lib/home/widgets/question_view.dart b/lib/home/widgets/question_view.dart index 5bce510..aee92e0 100644 --- a/lib/home/widgets/question_view.dart +++ b/lib/home/widgets/question_view.dart @@ -31,7 +31,7 @@ class QuestionViewState extends State duration: const Duration(milliseconds: 1500), )..addStatusListener((status) { if (status == AnimationStatus.completed) { - context.read().add(const AskQuestion()); + context.read().add(const HomeNavigated(Status.askQuestion)); } }); diff --git a/lib/home/widgets/results_view.dart b/lib/home/widgets/results_view.dart index 4e6047e..633aac9 100644 --- a/lib/home/widgets/results_view.dart +++ b/lib/home/widgets/results_view.dart @@ -203,7 +203,7 @@ class BlueContainerState extends State duration: _cardEnterDuration, )..addStatusListener((status) { if (status == AnimationStatus.completed) { - context.read().add(const Results()); + context.read().add(const HomeNavigated(Status.results)); } }); @@ -215,12 +215,16 @@ class BlueContainerState extends State if (status == AnimationStatus.completed && state.status == Status.resultsToSourceAnswers) { - context.read().add(const SeeResultsSourceAnswers()); + context.read().add( + const HomeNavigated( + Status.seeSourceAnswers, + ), + ); } if (status == AnimationStatus.dismissed && state.status == Status.sourceAnswersBackToResults) { - context.read().add(const Results()); + context.read().add(const HomeNavigated(Status.results)); } }); } @@ -416,14 +420,14 @@ class _AiResponseState extends State<_AiResponse> FeedbackButtons( onLike: () { context.read().add( - const AddAnswerFeedback( + const HomeAnswerFeedbackAdded( AnswerFeedback.good, ), ); }, onDislike: () { context.read().add( - const AddAnswerFeedback( + const HomeAnswerFeedbackAdded( AnswerFeedback.bad, ), ); @@ -514,13 +518,13 @@ class _SummaryViewState extends State Status.seeSourceAnswers; if (isOnSeeSourceAnswers) { context.read().add( - NavigateSourceAnswers( + HomeSourceAnswersNavigated( element.text, ), ); } else { context.read().add( - SeeSourceAnswersRequested( + HomeSeeSourceAnswersRequested( element.text, ), ); @@ -697,7 +701,7 @@ class _BackToAnswerButtonState extends State label: l10n.backToAIAnswer, icon: vertexIcons.arrowBack.image(color: VertexColors.white), onPressed: () { - context.read().add(const BackToAiSummaryTapped()); + context.read().add(const HomeBackToAiSummaryTapped()); }, ), ), @@ -760,7 +764,7 @@ class _SeeSourceAnswersButtonState extends State ), onPressed: () => context .read() - .add(const SeeSourceAnswersRequested(null)), + .add(const HomeSeeSourceAnswersRequested(null)), ), ), ); diff --git a/lib/home/widgets/search_box.dart b/lib/home/widgets/search_box.dart index 170aaae..45b5636 100644 --- a/lib/home/widgets/search_box.dart +++ b/lib/home/widgets/search_box.dart @@ -28,12 +28,12 @@ class SearchBox extends StatelessWidget { hint: l10n.questionHint, actionText: l10n.ask, onTextUpdated: (String query) => - context.read().add(QueryUpdated(query: query)), + context.read().add(HomeQueryUpdated(query: query)), onActionPressed: () { if (askAgain) { - context.read().add(QuestionAskedAgain(searchQuery)); + context.read().add(HomeQuestionAskedAgain(searchQuery)); } else { - context.read().add(QuestionAsked(searchQuery)); + context.read().add(HomeQuestionAsked(searchQuery)); } }, text: searchQuery.isEmpty ? null : searchQuery, diff --git a/lib/home/widgets/sources_carousel_view.dart b/lib/home/widgets/sources_carousel_view.dart index b809df7..8e9a623 100644 --- a/lib/home/widgets/sources_carousel_view.dart +++ b/lib/home/widgets/sources_carousel_view.dart @@ -32,7 +32,10 @@ class _SourcesCarouselViewState extends State List animatedBoxes = []; List documents = []; bool isAnimating = false; + bool isResetingList = false; + bool isResetingListBackwards = false; + bool goingForward = true; int remainingAnimationCount = 0; static const fastAnimationDuration = Duration(milliseconds: 250); @@ -71,27 +74,43 @@ class _SourcesCarouselViewState extends State @override void didUpdateWidget(covariant SourcesCarouselView oldWidget) { super.didUpdateWidget(oldWidget); + if (oldWidget.previouslySelectedIndex != widget.previouslySelectedIndex) { - final diff = + final distance = widget.previouslySelectedIndex - oldWidget.previouslySelectedIndex; - remainingAnimationCount = diff; - if (remainingAnimationCount > 0) { + + if (oldWidget.previouslySelectedIndex == widget.documents.length - 1 && + widget.previouslySelectedIndex == 0) { + // We are at the end and we need to go to the beginning + remainingAnimationCount = 1; + animateForward(); + isResetingList = true; + } else if (oldWidget.previouslySelectedIndex == 0 && + widget.previouslySelectedIndex == widget.documents.length - 1) { + // We are at the beginning and we need to go to the end + remainingAnimationCount = -1; + animateBack(); + isResetingListBackwards = true; + } else if (widget.previouslySelectedIndex >= + oldWidget.previouslySelectedIndex) { // We animate normal + remainingAnimationCount = distance; animateForward(); } else { // We need to animate "back" + remainingAnimationCount = distance; animateBack(); } - // } } void animateForward() { nextAnimationController - ..duration = remainingAnimationCount > 1 + ..duration = remainingAnimationCount.abs() > 1 ? fastAnimationDuration : slowAnimationDuration ..forward(from: 0); + setState(() { remainingAnimationCount = remainingAnimationCount - 1; }); @@ -99,10 +118,11 @@ class _SourcesCarouselViewState extends State void animateBack() { backAnimationController - ..duration = remainingAnimationCount > 1 + ..duration = remainingAnimationCount.abs() > 1 ? fastAnimationDuration : slowAnimationDuration ..forward(from: 0); + setState(() { remainingAnimationCount = remainingAnimationCount + 1; }); @@ -130,7 +150,13 @@ class _SourcesCarouselViewState extends State }); if (status == AnimationStatus.completed) { setState(() { - moveDocumentForward(); + if (!isResetingList) { + moveDocumentForward(); + } else { + documents = List.from(widget.documents); + isResetingList = false; + moveDocumentBackwards(); + } setupAnimatedBoxes(); if (remainingAnimationCount != 0) { animateForward(); @@ -142,7 +168,20 @@ class _SourcesCarouselViewState extends State backAnimationController.addStatusListener((status) { if (status == AnimationStatus.completed) { setState(() { - moveDocumentBackwards(); + if (!isResetingListBackwards) { + moveDocumentBackwards(); + } else { + final newList = + List.from(widget.documents).toList(); + final lastElement = newList.removeLast(); + final previousToLastElement = newList.removeLast(); + documents = [ + previousToLastElement, + lastElement, + ...newList, + ]; + isResetingListBackwards = false; + } setupAnimatedBoxes(); if (remainingAnimationCount != 0) { animateBack(); @@ -335,13 +374,13 @@ class _SourcesCarouselViewState extends State onBackPressed: () { final nextIndex = getDocumentIndex(documents[0]); context.read().add( - NavigateSourceAnswers('[$nextIndex]'), + HomeSourceAnswersNavigated('[$nextIndex]'), ); }, onNextPressed: () { final nextIndex = getDocumentIndex(documents[2]); context.read().add( - NavigateSourceAnswers('[$nextIndex]'), + HomeSourceAnswersNavigated('[$nextIndex]'), ); }, ), diff --git a/lib/home/widgets/welcome_view.dart b/lib/home/widgets/welcome_view.dart index 2cbfe26..0f5d7e2 100644 --- a/lib/home/widgets/welcome_view.dart +++ b/lib/home/widgets/welcome_view.dart @@ -98,9 +98,9 @@ class WelcomeViewState extends State color: VertexColors.googleBlue, ), label: l10n.startAsking, - onPressed: () => context - .read() - .add(const FromWelcomeToQuestion()), + onPressed: () => context.read().add( + const HomeNavigated(Status.welcomeToAskQuestion), + ), ), ), ), diff --git a/lib/main_production.dart b/lib/main_production.dart index 7016e31..ca78926 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -8,7 +8,8 @@ void main() async { await bootstrap( () async { final apiClient = ApiClient( - baseUrl: 'http://production', + realApiEnabled: true, + baseUrl: const String.fromEnvironment('AI_API_URL'), ); final questionsRepository = diff --git a/packages/api_client/lib/src/api_client.dart b/packages/api_client/lib/src/api_client.dart index ef67032..8de72dd 100644 --- a/packages/api_client/lib/src/api_client.dart +++ b/packages/api_client/lib/src/api_client.dart @@ -32,26 +32,6 @@ typedef PostCall = Future Function( Map? headers, }); -/// Definition of a patch call used by this client. -typedef PatchCall = Future Function( - Uri, { - Object? body, - Map? headers, -}); - -/// Definition of a put call used by this client. -typedef PutCall = Future Function( - Uri, { - Object? body, - Map? headers, -}); - -/// Definition of a get call used by this client. -typedef GetCall = Future Function( - Uri, { - Map? headers, -}); - /// {@template api_client} /// Client to access the api. /// {@endtemplate} @@ -60,22 +40,13 @@ class ApiClient { ApiClient({ required String baseUrl, PostCall postCall = http.post, - PutCall putCall = http.put, - PatchCall patchCall = http.patch, - GetCall getCall = http.get, bool realApiEnabled = false, }) : _base = Uri.parse(baseUrl), _post = postCall, - _put = putCall, - _patch = patchCall, - _get = getCall, _realApiEnabled = realApiEnabled; final Uri _base; final PostCall _post; - final PostCall _put; - final PatchCall _patch; - final GetCall _get; final bool _realApiEnabled; Map get _headers => {}; @@ -86,71 +57,18 @@ class ApiClient { realApiEnabled: _realApiEnabled, ); - /// Sends a POST request to the specified [path] with the given [body]. - Future post( - String path, { + /// Sends a POST request with the given [body]. + Future post({ Object? body, - Map? queryParameters, }) async { final response = await _post( - _base.replace( - path: path, - queryParameters: queryParameters, - ), - body: body, - headers: _headers..addContentTypeJson(), - ); - - return response; - } - - /// Sends a PATCH request to the specified [path] with the given [body]. - Future patch( - String path, { - Object? body, - Map? queryParameters, - }) async { - final response = await _patch( - _base.replace( - path: path, - queryParameters: queryParameters, - ), + _base, body: body, headers: _headers..addContentTypeJson(), ); return response; } - - /// Sends a PUT request to the specified [path] with the given [body]. - Future put( - String path, { - Object? body, - }) async { - final response = await _put( - _base.replace(path: path), - body: body, - headers: _headers..addContentTypeJson(), - ); - - return response; - } - - /// Sends a GET request to the specified [path]. - Future get( - String path, { - Map? queryParameters, - }) async { - final response = await _get( - _base.replace( - path: path, - queryParameters: queryParameters, - ), - headers: _headers, - ); - - return response; - } } extension on Map { diff --git a/packages/api_client/lib/src/resources/questions_resource.dart b/packages/api_client/lib/src/resources/questions_resource.dart index ec177f6..1a571e3 100644 --- a/packages/api_client/lib/src/resources/questions_resource.dart +++ b/packages/api_client/lib/src/resources/questions_resource.dart @@ -22,22 +22,13 @@ class QuestionsResource { Future getVertexResponse(String query) async { String body; if (_realApiEnabled) { - final response = await _apiClient.get( - // TODO(oscar): update with real API once is enabled - // and add possible failures. - 'google.es', - queryParameters: { - 'query': query, - }, + final response = await _apiClient.post( + body: jsonEncode( + { + 'search_term': query, + }, + ), ); - if (response.statusCode != 200) { - throw ApiClientError( - 'GET getVertexResponse with query=$query ' - 'returned status ${response.statusCode} ' - 'with the following response: "${response.body}"', - StackTrace.current, - ); - } body = response.body; } else { await Future.delayed(const Duration(seconds: 2)); diff --git a/packages/api_client/test/src/api_client_test.dart b/packages/api_client/test/src/api_client_test.dart index ac71c37..ed38337 100644 --- a/packages/api_client/test/src/api_client_test.dart +++ b/packages/api_client/test/src/api_client_test.dart @@ -73,10 +73,7 @@ void main() { apiClient = ApiClient( baseUrl: baseUrl, - getCall: httpClient.get, postCall: httpClient.post, - patchCall: httpClient.patch, - putCall: httpClient.put, ); }); @@ -93,44 +90,9 @@ void main() { expect(apiClient.questionsResource, isA()); }); - group('get', () { - setUp(() { - when( - () => httpClient.get( - any(), - headers: any(named: 'headers'), - ), - ).thenAnswer((_) async => expectedResponse); - }); - - test('returns the response', () async { - final response = await apiClient.get('/'); - - expect(response.statusCode, equals(expectedResponse.statusCode)); - expect(response.body, equals(expectedResponse.body)); - }); - - test('sends the request correctly', () async { - await apiClient.get( - '/path/to/endpoint', - queryParameters: { - 'param1': 'value1', - 'param2': 'value2', - }, - ); - - verify( - () => httpClient.get( - Uri.parse('$baseUrl/path/to/endpoint?param1=value1¶m2=value2'), - headers: {}, - ), - ).called(1); - }); - }); - group('post', () { test('returns the response', () async { - final response = await apiClient.post('/'); + final response = await apiClient.post(); expect(response.statusCode, equals(expectedResponse.statusCode)); expect(response.body, equals(expectedResponse.body)); @@ -138,63 +100,12 @@ void main() { test('sends the request correctly', () async { await apiClient.post( - '/path/to/endpoint', - queryParameters: {'param1': 'value1', 'param2': 'value2'}, body: 'BODY_CONTENT', ); verify( () => httpClient.post( - Uri.parse('$baseUrl/path/to/endpoint?param1=value1¶m2=value2'), - body: 'BODY_CONTENT', - headers: {HttpHeaders.contentTypeHeader: ContentType.json.value}, - ), - ).called(1); - }); - }); - - group('patch', () { - test('returns the response', () async { - final response = await apiClient.patch('/'); - - expect(response.statusCode, equals(expectedResponse.statusCode)); - expect(response.body, equals(expectedResponse.body)); - }); - - test('sends the request correctly', () async { - await apiClient.patch( - '/path/to/endpoint', - queryParameters: {'param1': 'value1', 'param2': 'value2'}, - body: 'BODY_CONTENT', - ); - - verify( - () => httpClient.patch( - Uri.parse('$baseUrl/path/to/endpoint?param1=value1¶m2=value2'), - body: 'BODY_CONTENT', - headers: {HttpHeaders.contentTypeHeader: ContentType.json.value}, - ), - ).called(1); - }); - }); - - group('put', () { - test('returns the response', () async { - final response = await apiClient.put('/'); - - expect(response.statusCode, equals(expectedResponse.statusCode)); - expect(response.body, equals(expectedResponse.body)); - }); - - test('sends the request correctly', () async { - await apiClient.put( - '/path/to/endpoint', - body: 'BODY_CONTENT', - ); - - verify( - () => httpClient.put( - Uri.parse('$baseUrl/path/to/endpoint'), + Uri.parse(baseUrl), body: 'BODY_CONTENT', headers: {HttpHeaders.contentTypeHeader: ContentType.json.value}, ), diff --git a/packages/api_client/test/src/resources/questions_resource_test.dart b/packages/api_client/test/src/resources/questions_resource_test.dart index 15b13db..1c3fa96 100644 --- a/packages/api_client/test/src/resources/questions_resource_test.dart +++ b/packages/api_client/test/src/resources/questions_resource_test.dart @@ -23,9 +23,8 @@ void main() { QuestionsResource(apiClient: apiClient, realApiEnabled: true); questionsResourceApiNotEnabled = QuestionsResource(apiClient: apiClient); when( - () => apiClient.get( - any(), - queryParameters: any(named: 'queryParameters'), + () => apiClient.post( + body: any(named: 'body'), ), ).thenAnswer((_) async => response); }); diff --git a/test/home/bloc/home_bloc_test.dart b/test/home/bloc/home_bloc_test.dart index e205c86..e98d916 100644 --- a/test/home/bloc/home_bloc_test.dart +++ b/test/home/bloc/home_bloc_test.dart @@ -19,48 +19,18 @@ void main() { return HomeBloc(questionsRepository); } - group('FromWelcomeToQuestion', () { - blocTest( - 'emits [welcomeToAskQuestion]', - build: buildBloc, - act: (bloc) => bloc.add(FromWelcomeToQuestion()), - expect: () => [ - isA().having( - (element) => element.status, - 'status', - Status.welcomeToAskQuestion, - ), - ], - ); - }); - - group('AskQuestion', () { - blocTest( - 'emits [askQuestion]', - build: buildBloc, - act: (bloc) => bloc.add(AskQuestion()), - expect: () => [ - isA().having( - (element) => element.status, - 'status', - Status.askQuestion, - ), - ], - ); - }); - - group('QueryUpdated', () { + group('HomeQueryUpdated', () { blocTest( 'emits query updated', build: buildBloc, - act: (bloc) => bloc.add(QueryUpdated(query: 'new query')), + act: (bloc) => bloc.add(HomeQueryUpdated(query: 'new query')), expect: () => [ HomeState(query: 'new query'), ], ); }); - group('QuestionAsked', () { + group('HomeQuestionAsked', () { blocTest( 'emits [Status.askQuestionToThinking, Status.thinkingToResults] ' 'with vertex response from _questionsRepository.getVertexResponse', @@ -69,7 +39,7 @@ void main() { .thenAnswer((_) async => VertexResponse.empty()); }, build: buildBloc, - act: (bloc) => bloc.add(QuestionAsked('query')), + act: (bloc) => bloc.add(HomeQuestionAsked('query')), expect: () => [ HomeState( status: Status.askQuestionToThinking, @@ -84,7 +54,18 @@ void main() { ); }); - group('QuestionAskedAgain', () { + group('HomeNavigated', () { + blocTest( + 'emits the new status', + build: buildBloc, + act: (bloc) => bloc.add(HomeNavigated(Status.askQuestion)), + expect: () => [ + HomeState(status: Status.askQuestion), + ], + ); + }); + + group('HomeQuestionAskedAgain', () { blocTest( 'emits [Status.resultsToThinking, Status.thinkingToResults] ' 'with vertex response from _questionsRepository.getVertexResponse', @@ -93,7 +74,7 @@ void main() { .thenAnswer((_) async => VertexResponse.empty()); }, build: buildBloc, - act: (bloc) => bloc.add(QuestionAskedAgain('query')), + act: (bloc) => bloc.add(HomeQuestionAskedAgain('query')), expect: () => [ HomeState( status: Status.resultsToThinking, @@ -108,55 +89,33 @@ void main() { ); }); - group('Results', () { - blocTest( - 'emits Status.results', - build: buildBloc, - act: (bloc) => bloc.add(Results()), - expect: () => [ - HomeState(status: Status.results), - ], - ); - }); - - group('SeeSourceAnswersRequested', () { + group('HomeSeeSourceAnswersRequested', () { blocTest( 'emits Status.resultsToSourceAnswers', build: buildBloc, - act: (bloc) => bloc.add(SeeSourceAnswersRequested('[1]')), + act: (bloc) => bloc.add(HomeSeeSourceAnswersRequested('[1]')), expect: () => [ HomeState(status: Status.resultsToSourceAnswers), ], ); }); - group('SeeResultsSourceAnswers', () { - blocTest( - 'emits Status.seeSourceAnswers', - build: buildBloc, - act: (bloc) => bloc.add(SeeResultsSourceAnswers()), - expect: () => [ - HomeState(status: Status.seeSourceAnswers), - ], - ); - }); - group('BackToAiResultsTapped', () { blocTest( 'emits Status.sourceAnswersBackToResults', build: buildBloc, - act: (bloc) => bloc.add(BackToAiSummaryTapped()), + act: (bloc) => bloc.add(HomeBackToAiSummaryTapped()), expect: () => [ HomeState(status: Status.sourceAnswersBackToResults), ], ); }); - group('AddAnswerFeedback', () { + group('HomeAnswerFeedbackAdded', () { blocTest( 'emits the new feedback', build: buildBloc, - act: (bloc) => bloc.add(AddAnswerFeedback(AnswerFeedback.good)), + act: (bloc) => bloc.add(HomeAnswerFeedbackAdded(AnswerFeedback.good)), expect: () => [ HomeState( answerFeedbacks: const [AnswerFeedback.good], @@ -165,11 +124,11 @@ void main() { ); }); - group('NavigateSourceAnswers', () { + group('HomeSourceAnswersNavigated', () { blocTest( 'emits updated selectedIndex', build: buildBloc, - act: (bloc) => bloc.add(NavigateSourceAnswers('[2]')), + act: (bloc) => bloc.add(HomeSourceAnswersNavigated('[2]')), expect: () => [ HomeState( selectedIndex: 1, diff --git a/test/home/bloc/home_event_test.dart b/test/home/bloc/home_event_test.dart index 91fd7ef..76c677f 100644 --- a/test/home/bloc/home_event_test.dart +++ b/test/home/bloc/home_event_test.dart @@ -3,66 +3,52 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('HomeEvent', () { - test('FromWelcomeToQuestion supports value equality', () { + test('HomeQueryUpdated supports value equality', () { expect( - FromWelcomeToQuestion(), - equals(FromWelcomeToQuestion()), + HomeQueryUpdated(query: 'query'), + equals(HomeQueryUpdated(query: 'query')), ); }); - test('AskQuestion supports value equality', () { + test('HomeQuestionAsked supports value equality', () { expect( - AskQuestion(), - equals(AskQuestion()), + HomeQuestionAsked('query'), + equals(HomeQuestionAsked('query')), ); }); - test('QueryUpdated supports value equality', () { + test('HomeQuestionAskedAgain supports value equality', () { expect( - QueryUpdated(query: 'query'), - equals(QueryUpdated(query: 'query')), + HomeQuestionAskedAgain('query'), + equals(HomeQuestionAskedAgain('query')), ); }); - test('QuestionAsked supports value equality', () { + test('HomeSeeSourceAnswersRequested supports value equality', () { expect( - QuestionAsked('query'), - equals(QuestionAsked('query')), + HomeSeeSourceAnswersRequested('[1]'), + equals(HomeSeeSourceAnswersRequested('[1]')), ); }); - test('QuestionAskedAgain supports value equality', () { + test('HomeAnswerFeedbackAdded supports value equality', () { expect( - QuestionAskedAgain('query'), - equals(QuestionAskedAgain('query')), + HomeAnswerFeedbackAdded(AnswerFeedback.good), + equals(HomeAnswerFeedbackAdded(AnswerFeedback.good)), ); }); - test('Results supports value equality', () { + test('HomeNavigated supports value equality', () { expect( - Results(), - equals(Results()), + HomeNavigated(Status.askQuestion), + equals(HomeNavigated(Status.askQuestion)), ); }); - test('SeeSourceAnswersRequested supports value equality', () { + test('HomeBackToAiSummaryTapped supports value equality', () { expect( - SeeSourceAnswersRequested('[1]'), - equals(SeeSourceAnswersRequested('[1]')), - ); - }); - - test('SeeResultsSourceAnswers supports value equality', () { - expect( - SeeResultsSourceAnswers(), - equals(SeeResultsSourceAnswers()), - ); - }); - - test('AddAnswerFeedback supports value equality', () { - expect( - AddAnswerFeedback(AnswerFeedback.good), - equals(AddAnswerFeedback(AnswerFeedback.good)), + HomeBackToAiSummaryTapped(), + equals(HomeBackToAiSummaryTapped()), ); }); }); diff --git a/test/home/widgets/question_view_test.dart b/test/home/widgets/question_view_test.dart index 3334d54..8bc66a5 100644 --- a/test/home/widgets/question_view_test.dart +++ b/test/home/widgets/question_view_test.dart @@ -59,17 +59,17 @@ void main() { (WidgetTester tester) async { await tester.pumpApp(bootstrap()); await tester.pumpAndSettle(); - verify(() => homeBloc.add(AskQuestion())).called(1); + verify(() => homeBloc.add(HomeNavigated(Status.askQuestion))).called(1); }, ); testWidgets( - 'calls QueryUpdated writing on the TextField', + 'calls HomeQueryUpdated writing on the TextField', (WidgetTester tester) async { await tester.pumpApp(bootstrap()); const newText = 'text'; await tester.enterText(find.byType(TextField), newText); - verify(() => homeBloc.add(QueryUpdated(query: newText))).called(1); + verify(() => homeBloc.add(HomeQueryUpdated(query: newText))).called(1); }, ); }); diff --git a/test/home/widgets/results_view_test.dart b/test/home/widgets/results_view_test.dart index 404aa4c..7f3d58f 100644 --- a/test/home/widgets/results_view_test.dart +++ b/test/home/widgets/results_view_test.dart @@ -91,7 +91,7 @@ void main() { expect(find.byType(CarouselView), findsOneWidget); }); - testWidgets('calls BackToAiSummaryTapped on backToAiResults tapped', + testWidgets('calls HomeBackToAiSummaryTapped on backToAiResults tapped', (tester) async { when(() => homeBloc.state).thenReturn( HomeState(vertexResponse: response, status: Status.seeSourceAnswers), @@ -101,7 +101,7 @@ void main() { final button = tester.widget(find.byKey(Key('backToAnswerButtonKey'))); button.onPressed!(); - verify(() => homeBloc.add(const BackToAiSummaryTapped())).called(1); + verify(() => homeBloc.add(const HomeBackToAiSummaryTapped())).called(1); }); testWidgets('animates in search box when enter', (tester) async { @@ -152,7 +152,7 @@ void main() { }); testWidgets( - 'calls SeeResultsSourceAnswers on exit', + 'calls HomeNavigated(seeSourceAnswers) on exit', (WidgetTester tester) async { final controller = StreamController(); whenListen( @@ -172,7 +172,8 @@ void main() { ); await tester.pumpAndSettle(); - verify(() => homeBloc.add(SeeResultsSourceAnswers())).called(1); + verify(() => homeBloc.add(HomeNavigated(Status.seeSourceAnswers))) + .called(1); }, ); @@ -208,7 +209,7 @@ void main() { ); await tester.pumpAndSettle(); - verify(() => homeBloc.add(Results())).called(2); + verify(() => homeBloc.add(HomeNavigated(Status.results))).called(2); }, ); @@ -236,7 +237,7 @@ void main() { }); testWidgets( - 'calls SeeSourceAnswersRequested on SeeSourceAnswersButton tapped', + 'calls HomeSeeSourceAnswersRequested on SeeSourceAnswersButton tapped', (tester) async { await tester.pumpApp(bootstrap()); @@ -253,7 +254,7 @@ void main() { .onPressed ?.call(); - verify(() => homeBloc.add(const SeeSourceAnswersRequested(null))) + verify(() => homeBloc.add(const HomeSeeSourceAnswersRequested(null))) .called(1); }); @@ -268,7 +269,7 @@ void main() { verify( () => homeBloc.add( - const AddAnswerFeedback(AnswerFeedback.good), + const HomeAnswerFeedbackAdded(AnswerFeedback.good), ), ).called(1); }); @@ -284,7 +285,7 @@ void main() { verify( () => homeBloc.add( - const AddAnswerFeedback(AnswerFeedback.bad), + const HomeAnswerFeedbackAdded(AnswerFeedback.bad), ), ).called(1); }); @@ -348,7 +349,7 @@ void main() { }); testWidgets( - 'adds NavigateSourceAnswers tapping on the link if state ' + 'adds HomeSourceAnswersNavigated tapping on the link if state ' 'Status.seeSourceAnswers', (WidgetTester tester) async { when(() => homeBloc.state).thenReturn( @@ -357,12 +358,12 @@ void main() { await tester.pumpApp(bootstrap()); final widget = tester.widget(find.byType(InkWell).first); widget.onTap?.call(); - verify(() => homeBloc.add(NavigateSourceAnswers('[1]'))).called(1); + verify(() => homeBloc.add(HomeSourceAnswersNavigated('[1]'))).called(1); }, ); testWidgets( - 'adds SeeSourceAnswersRequested tapping on the link if state ' + 'adds HomeSeeSourceAnswersRequested tapping on the link if state ' 'is not Status.seeSourceAnswers', (WidgetTester tester) async { when(() => homeBloc.state).thenReturn( @@ -371,7 +372,8 @@ void main() { await tester.pumpApp(bootstrap()); final widget = tester.widget(find.byType(InkWell).first); widget.onTap?.call(); - verify(() => homeBloc.add(SeeSourceAnswersRequested('[1]'))).called(1); + verify(() => homeBloc.add(HomeSeeSourceAnswersRequested('[1]'))) + .called(1); }, ); }); diff --git a/test/home/widgets/search_box_test.dart b/test/home/widgets/search_box_test.dart index 8d2329d..7f7aca7 100644 --- a/test/home/widgets/search_box_test.dart +++ b/test/home/widgets/search_box_test.dart @@ -35,32 +35,32 @@ void main() { }); testWidgets( - 'calls QueryUpdated writing on the TextField', + 'calls HomeQueryUpdated writing on the TextField', (WidgetTester tester) async { await tester.pumpApp(bootstrap()); const newText = 'text'; await tester.enterText(find.byType(TextField), newText); - verify(() => homeBloc.add(QueryUpdated(query: newText))).called(1); + verify(() => homeBloc.add(HomeQueryUpdated(query: newText))).called(1); }, ); testWidgets( - 'calls QuestionAsked clicking on the PrimaryCTA ' + 'calls HomeQuestionAsked clicking on the PrimaryCTA ' 'when askAgain is false', (WidgetTester tester) async { await tester.pumpApp(bootstrap()); await tester.tap(find.byType(PrimaryCTA)); - verify(() => homeBloc.add(QuestionAsked(''))).called(1); + verify(() => homeBloc.add(HomeQuestionAsked(''))).called(1); }, ); testWidgets( - 'calls QuestionAskedAgain clicking on the PrimaryCTA ' + 'calls HomeQuestionAskedAgain clicking on the PrimaryCTA ' 'when askAgain is true', (WidgetTester tester) async { await tester.pumpApp(bootstrap(askAgain: true)); await tester.tap(find.byType(PrimaryCTA)); - verify(() => homeBloc.add(QuestionAskedAgain(''))).called(1); + verify(() => homeBloc.add(HomeQuestionAskedAgain(''))).called(1); }, ); }); diff --git a/test/home/widgets/sources_carousel_view_test.dart b/test/home/widgets/sources_carousel_view_test.dart index 64e5db3..af2e045 100644 --- a/test/home/widgets/sources_carousel_view_test.dart +++ b/test/home/widgets/sources_carousel_view_test.dart @@ -69,7 +69,7 @@ void main() { ); testWidgets( - 'calls NavigateSourceAnswers taps on GoPreviousButton', + 'calls HomeSourceAnswersNavigated taps on GoPreviousButton', (WidgetTester tester) async { await tester.pumpApp( BlocProvider.value( @@ -85,12 +85,12 @@ void main() { await tester.pumpAndSettle(); await tester.tap(finder); await tester.pumpAndSettle(); - verify(() => homeBloc.add(NavigateSourceAnswers('[1]'))).called(1); + verify(() => homeBloc.add(HomeSourceAnswersNavigated('[1]'))).called(1); }, ); testWidgets( - 'calls NavigateSourceAnswers taps on GoNextButton', + 'calls HomeSourceAnswersNavigated taps on GoNextButton', (WidgetTester tester) async { await tester.pumpApp( BlocProvider.value( @@ -106,7 +106,7 @@ void main() { await tester.pumpAndSettle(); await tester.tap(finder); await tester.pumpAndSettle(); - verify(() => homeBloc.add(NavigateSourceAnswers('[2]'))).called(1); + verify(() => homeBloc.add(HomeSourceAnswersNavigated('[2]'))).called(1); }, ); @@ -145,7 +145,7 @@ void main() { ElevatedButton( onPressed: () { setState(() { - index = 2; + index = 1; }); }, child: Text('Click me'), @@ -164,7 +164,7 @@ void main() { await tester.tap(finder); await tester.pumpAndSettle(); - expect(find.text('3/4'), findsOneWidget); + expect(find.text('2/4'), findsOneWidget); }, ); @@ -173,6 +173,88 @@ void main() { 'in only both directions', (WidgetTester tester) async { var index = 2; + await tester.pumpApp( + StatefulBuilder( + builder: (context, setState) { + return BlocProvider.value( + value: homeBloc, + child: Stack( + children: [ + SourcesCarouselView( + documents: documents, + previouslySelectedIndex: index, + ), + ElevatedButton( + onPressed: () { + setState(() { + index = 1; + }); + }, + child: Text('Click me'), + ), + ], + ), + ); + }, + ), + ); + final finder = find.byType(ElevatedButton); + await tester.ensureVisible(finder); + await tester.pumpAndSettle(); + expect(find.text('3/4'), findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + + expect(find.text('2/4'), findsOneWidget); + }, + ); + + testWidgets( + 'navigates back when on the first index and previouslySelectedIndex gets ' + 'updated', + (WidgetTester tester) async { + var index = 0; + await tester.pumpApp( + StatefulBuilder( + builder: (context, setState) { + return BlocProvider.value( + value: homeBloc, + child: Stack( + children: [ + SourcesCarouselView( + documents: documents, + previouslySelectedIndex: index, + ), + ElevatedButton( + onPressed: () { + setState(() { + index = documents.length - 1; + }); + }, + child: Text('Click me'), + ), + ], + ), + ); + }, + ), + ); + final finder = find.byType(ElevatedButton); + await tester.ensureVisible(finder); + await tester.pumpAndSettle(); + expect(find.text('1/4'), findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + + expect(find.text('4/4'), findsOneWidget); + }, + ); + + testWidgets( + 'navigates forward when on the last index and previouslySelectedIndex ' + 'gets updated', + (WidgetTester tester) async { + var index = documents.length - 1; await tester.pumpApp( StatefulBuilder( builder: (context, setState) { @@ -201,12 +283,93 @@ void main() { final finder = find.byType(ElevatedButton); await tester.ensureVisible(finder); await tester.pumpAndSettle(); + expect(find.text('4/4'), findsOneWidget); await tester.tap(finder); await tester.pumpAndSettle(); expect(find.text('1/4'), findsOneWidget); }, ); + + testWidgets( + 'navigates forward multiples indexes', + (WidgetTester tester) async { + var index = 0; + await tester.pumpApp( + StatefulBuilder( + builder: (context, setState) { + return BlocProvider.value( + value: homeBloc, + child: Stack( + children: [ + SourcesCarouselView( + documents: documents, + previouslySelectedIndex: index, + ), + ElevatedButton( + onPressed: () { + setState(() { + index = 2; + }); + }, + child: Text('Click me'), + ), + ], + ), + ); + }, + ), + ); + final finder = find.byType(ElevatedButton); + await tester.ensureVisible(finder); + await tester.pumpAndSettle(); + expect(find.text('1/4'), findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + + expect(find.text('3/4'), findsOneWidget); + }, + ); + + testWidgets( + 'navigates forward multiples indexes backwards', + (WidgetTester tester) async { + var index = documents.length - 1; + await tester.pumpApp( + StatefulBuilder( + builder: (context, setState) { + return BlocProvider.value( + value: homeBloc, + child: Stack( + children: [ + SourcesCarouselView( + documents: documents, + previouslySelectedIndex: index, + ), + ElevatedButton( + onPressed: () { + setState(() { + index = 1; + }); + }, + child: Text('Click me'), + ), + ], + ), + ); + }, + ), + ); + final finder = find.byType(ElevatedButton); + await tester.ensureVisible(finder); + await tester.pumpAndSettle(); + expect(find.text('4/4'), findsOneWidget); + await tester.tap(finder); + await tester.pumpAndSettle(); + + expect(find.text('2/4'), findsOneWidget); + }, + ); }); group('SourceCard', () { diff --git a/test/home/widgets/welcome_view_test.dart b/test/home/widgets/welcome_view_test.dart index 2fdce0f..72794cb 100644 --- a/test/home/widgets/welcome_view_test.dart +++ b/test/home/widgets/welcome_view_test.dart @@ -64,19 +64,26 @@ void main() { expect(forwardExitStatuses, equals([Status.welcomeToAskQuestion])); }); - testWidgets('calls FromWelcomeToQuestion on PrimaryCTA tapped', - (tester) async { - await tester.pumpApp( - BlocProvider.value( - value: homeBloc, - child: WelcomeView(), - ), - ); - - await tester.pumpAndSettle(); - await tester.tap(find.byType(PrimaryIconCTA)); - - verify(() => homeBloc.add(const FromWelcomeToQuestion())).called(1); - }); + testWidgets( + 'calls HomeNavigated(welcomeToAskQuestion) on PrimaryCTA ' + 'tapped', + (tester) async { + await tester.pumpApp( + BlocProvider.value( + value: homeBloc, + child: WelcomeView(), + ), + ); + + await tester.pumpAndSettle(); + await tester.tap(find.byType(PrimaryIconCTA)); + + verify( + () => homeBloc.add( + const HomeNavigated(Status.welcomeToAskQuestion), + ), + ).called(1); + }, + ); }); }