diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 4774f2faa..a46aa2b86 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -94,6 +94,10 @@ "@submit": { "description": "Submit label" }, + "submitError": "An error ocurred while submitting your answer.\nPlease try again.", + "@submitError": { + "description": "Error message when submitting an answer" + }, "solveIt": "Solve it", "@solveIt": { "description": "Solve it label" diff --git a/lib/word_selection/bloc/word_selection_bloc.dart b/lib/word_selection/bloc/word_selection_bloc.dart index 360504d90..9101b77f6 100644 --- a/lib/word_selection/bloc/word_selection_bloc.dart +++ b/lib/word_selection/bloc/word_selection_bloc.dart @@ -116,27 +116,34 @@ class WordSelectionBloc extends Bloc { state.copyWith(status: WordSelectionStatus.validating), ); - final points = await _crosswordResource.answerWord( - wordId: state.word!.word.id, - answer: event.answer, - ); + try { + final points = await _crosswordResource.answerWord( + wordId: state.word!.word.id, + answer: event.answer, + ); - final isCorrect = points > 0; - if (isCorrect) { - emit( - state.copyWith( - status: WordSelectionStatus.solved, - word: state.word!.copyWith( - word: state.word!.word.copyWith(answer: event.answer), + final isCorrect = points > 0; + if (isCorrect) { + emit( + state.copyWith( + status: WordSelectionStatus.solved, + word: state.word!.copyWith( + word: state.word!.word.copyWith(answer: event.answer), + ), + wordPoints: points, ), - wordPoints: points, - ), - ); - } else { + ); + } else { + emit( + state.copyWith( + status: WordSelectionStatus.incorrect, + ), + ); + } + } catch (error, stackTrace) { + addError(error, stackTrace); emit( - state.copyWith( - status: WordSelectionStatus.incorrect, - ), + state.copyWith(status: WordSelectionStatus.failure), ); } } diff --git a/lib/word_selection/view/word_solving_view.dart b/lib/word_selection/view/word_solving_view.dart index d5f9f0403..4a5c8ded8 100644 --- a/lib/word_selection/view/word_solving_view.dart +++ b/lib/word_selection/view/word_solving_view.dart @@ -65,6 +65,7 @@ class WordSolvingLargeView extends StatelessWidget { ], ), ), + const ErrorSection(), const SizedBox(height: 24), if (isHintsEnabled) ...[ const HintsTitle(), @@ -128,6 +129,7 @@ class WordSolvingSmallView extends StatelessWidget { }, ), ), + const ErrorSection(), const SizedBox(height: 24), if (isHintsEnabled) ...[ const HintsTitle(), @@ -162,6 +164,36 @@ class IncorrectAnswerText extends StatelessWidget { } } +@visibleForTesting +class ErrorSection extends StatelessWidget { + @visibleForTesting + const ErrorSection({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final hasError = context.select( + (WordSelectionBloc bloc) => + bloc.state.status == WordSelectionStatus.failure, + ); + + if (!hasError) return const SizedBox.shrink(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 16), + Text( + context.l10n.submitError, + style: IoCrosswordTextStyles.mobile.body3 + .copyWith(color: theme.colorScheme.error), + textAlign: TextAlign.center, + ), + ], + ); + } +} + @visibleForTesting class BottomPanel extends StatelessWidget { @visibleForTesting diff --git a/test/word_focused/bloc/word_selection_bloc_test.dart b/test/word_focused/bloc/word_selection_bloc_test.dart index 22aa7a681..984d0d1e4 100644 --- a/test/word_focused/bloc/word_selection_bloc_test.dart +++ b/test/word_focused/bloc/word_selection_bloc_test.dart @@ -341,6 +341,39 @@ void main() { ), ], ); + + blocTest( + 'emits a state with failure if validating an answer fails', + build: () => WordSelectionBloc( + crosswordResource: crosswordResource, + ), + setUp: () { + answerWord = _MockWord(); + when(() => selectedWord.word.copyWith(answer: 'correct')) + .thenReturn(answerWord); + when( + () => crosswordResource.answerWord( + wordId: selectedWord.word.id, + answer: 'correct', + ), + ).thenThrow(Exception('Oops')); + }, + seed: () => WordSelectionState( + status: WordSelectionStatus.solving, + word: selectedWord, + ), + act: (bloc) => bloc.add(WordSolveAttempted(answer: 'correct')), + expect: () => [ + WordSelectionState( + status: WordSelectionStatus.validating, + word: selectedWord, + ), + WordSelectionState( + status: WordSelectionStatus.failure, + word: selectedWord, + ), + ], + ); }); }); } diff --git a/test/word_focused/view/word_solving_view_test.dart b/test/word_focused/view/word_solving_view_test.dart index be31da0cc..5c5b6f0d2 100644 --- a/test/word_focused/view/word_solving_view_test.dart +++ b/test/word_focused/view/word_solving_view_test.dart @@ -200,6 +200,15 @@ void main() { }, ); + testWidgets( + 'an $ErrorSection', + (tester) async { + await tester.pumpApp(widget); + + expect(find.byType(ErrorSection), findsOneWidget); + }, + ); + testWidgets( 'a $BottomPanel', (tester) async { @@ -360,6 +369,15 @@ void main() { }, ); + testWidgets( + 'an $ErrorSection', + (tester) async { + await tester.pumpApp(widget); + + expect(find.byType(ErrorSection), findsOneWidget); + }, + ); + testWidgets( 'a $BottomPanel', (tester) async { @@ -420,6 +438,54 @@ void main() { ); }); + group('$ErrorSection', () { + late WordSelectionBloc wordSelectionBloc; + late Widget widget; + + setUp(() { + wordSelectionBloc = _MockWordSolvingBloc(); + + widget = BlocProvider.value( + value: wordSelectionBloc, + child: ErrorSection(), + ); + }); + + group('renders', () { + testWidgets( + 'a $SizedBox when the status is not failure', + (tester) async { + when(() => wordSelectionBloc.state).thenReturn( + WordSelectionState( + status: WordSelectionStatus.solving, + word: selectedWord, + ), + ); + + await tester.pumpApp(widget); + + expect(find.byType(SizedBox), findsOneWidget); + }, + ); + + testWidgets( + 'a submitError text when the status is failure', + (tester) async { + when(() => wordSelectionBloc.state).thenReturn( + WordSelectionState( + status: WordSelectionStatus.failure, + word: selectedWord, + ), + ); + + await tester.pumpApp(widget); + + expect(find.text(l10n.submitError), findsOneWidget); + }, + ); + }); + }); + group('$BottomPanel', () { late WordSelectionBloc wordSelectionBloc; late HintBloc hintBloc;