From 9788e68cf05ff5708da425f1401627c06afa1127 Mon Sep 17 00:00:00 2001 From: Jaime <52668514+jsgalarraga@users.noreply.github.com> Date: Tue, 23 Apr 2024 19:16:50 +0200 Subject: [PATCH] feat: add incorrect answer text (#368) --- lib/l10n/arb/app_en.arb | 4 ++ .../bloc/word_selection_bloc.dart | 9 +++- .../view/word_solving_view.dart | 41 ++++++++++++++- .../lib/src/widgets/io_word_input.dart | 4 +- .../view/word_solving_view_test.dart | 52 +++++++++++++++++++ 5 files changed, 106 insertions(+), 4 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 6c9857498..f0189d6ee 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -121,6 +121,10 @@ "@typeToAnswer": { "description": "Type to answer label" }, + "incorrectAnswer": "Oops, wrong word!", + "@incorrectAnswer": { + "description": "Incorrect answer label" + }, "wordSolved": "Word solved!", "@wordSolved": { "description": "Word solved label" diff --git a/lib/word_selection/bloc/word_selection_bloc.dart b/lib/word_selection/bloc/word_selection_bloc.dart index 84941919c..ce8b95b54 100644 --- a/lib/word_selection/bloc/word_selection_bloc.dart +++ b/lib/word_selection/bloc/word_selection_bloc.dart @@ -85,8 +85,15 @@ class WordSelectionBloc extends Bloc { return; } + if (state.status == WordSelectionStatus.solving || + state.status == WordSelectionStatus.solved || + state.status == WordSelectionStatus.validating) { + // Can't solve a word if it's already being solved, solved, or validating. + return; + } + emit( - WordSelectionState( + state.copyWith( status: WordSelectionStatus.solving, word: state.word, ), diff --git a/lib/word_selection/view/word_solving_view.dart b/lib/word_selection/view/word_solving_view.dart index b45c4de4d..0ce7a921d 100644 --- a/lib/word_selection/view/word_solving_view.dart +++ b/lib/word_selection/view/word_solving_view.dart @@ -27,11 +27,16 @@ class WordSolvingLargeView extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); final selectedWord = context.select((WordSelectionBloc bloc) => bloc.state.word); if (selectedWord == null) return const SizedBox.shrink(); final isHintsEnabled = context.select((HintBloc bloc) => bloc.state.isHintsEnabled); + final isIncorrectAnswer = context.select( + (WordSelectionBloc bloc) => + bloc.state.status == WordSelectionStatus.incorrect, + ); return Column( children: [ @@ -41,6 +46,12 @@ class WordSolvingLargeView extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ + Text( + isIncorrectAnswer ? context.l10n.incorrectAnswer : '', + style: IoCrosswordTextStyles.bodyMD.medium + ?.copyWith(color: theme.colorScheme.error), + ), + const SizedBox(height: 24), Text( selectedWord.word.clue, style: IoCrosswordTextStyles.titleMD, @@ -85,6 +96,18 @@ class WordSolvingSmallView extends StatefulWidget { class _WordSolvingSmallViewState extends State { final _controller = IoWordInputController(); + String _lastWord = ''; + + @override + void initState() { + super.initState(); + _controller.addListener(() { + if (_lastWord.length > _controller.word.length) { + context.read().add(const WordSolveRequested()); + } + _lastWord = _controller.word; + }); + } @override void dispose() { @@ -94,11 +117,16 @@ class _WordSolvingSmallViewState extends State { @override Widget build(BuildContext context) { + final theme = Theme.of(context); final selectedWord = context.select((WordSelectionBloc bloc) => bloc.state.word); if (selectedWord == null) return const SizedBox.shrink(); final isHintsEnabled = context.select((HintBloc bloc) => bloc.state.isHintsEnabled); + final isIncorrectAnswer = context.select( + (WordSelectionBloc bloc) => + bloc.state.status == WordSelectionStatus.incorrect, + ); return Column( children: [ @@ -108,7 +136,13 @@ class _WordSolvingSmallViewState extends State { length: selectedWord.word.length, controller: _controller, ), - const SizedBox(height: 24), + const SizedBox(height: 16), + Text( + isIncorrectAnswer ? context.l10n.incorrectAnswer : '', + style: IoCrosswordTextStyles.bodyMD.medium + ?.copyWith(color: theme.colorScheme.error), + ), + const SizedBox(height: 16), Text( selectedWord.word.clue, style: IoCrosswordTextStyles.titleMD, @@ -128,7 +162,10 @@ class _WordSolvingSmallViewState extends State { return const Center(child: CircularProgressIndicator()); } - return const HintsSection(); + return const Align( + alignment: Alignment.topCenter, + child: HintsSection(), + ); }, ), ), diff --git a/packages/io_crossword_ui/lib/src/widgets/io_word_input.dart b/packages/io_crossword_ui/lib/src/widgets/io_word_input.dart index 7538720c2..f873f9fc4 100644 --- a/packages/io_crossword_ui/lib/src/widgets/io_word_input.dart +++ b/packages/io_crossword_ui/lib/src/widgets/io_word_input.dart @@ -1,4 +1,5 @@ import 'dart:collection'; +import 'dart:math' as math; import 'dart:ui'; import 'package:equatable/equatable.dart'; @@ -386,8 +387,9 @@ class _IoWordInputState extends State { ? (_) => widget.onSubmit!(_word) : null, onSelectionChanged: (selection, cause) { + final offset = math.min(1, controller.text.length); controller.selection = TextSelection.fromPosition( - const TextPosition(offset: 1), + TextPosition(offset: offset), ); }, ), diff --git a/test/word_focused/view/word_solving_view_test.dart b/test/word_focused/view/word_solving_view_test.dart index e7da84b38..2e7bc25af 100644 --- a/test/word_focused/view/word_solving_view_test.dart +++ b/test/word_focused/view/word_solving_view_test.dart @@ -2,6 +2,7 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:flutter/material.dart' hide Axis; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:game_domain/game_domain.dart'; @@ -148,6 +149,21 @@ void main() { }, ); + testWidgets( + 'incorrectAnswer text when the status is incorrect', + (tester) async { + when(() => wordSelectionBloc.state).thenReturn( + WordSelectionState( + status: WordSelectionStatus.incorrect, + word: selectedWord, + ), + ); + await tester.pumpApp(widget); + + expect(find.text(l10n.incorrectAnswer), findsOneWidget); + }, + ); + testWidgets( 'the $HintsSection when the status is not validating', (tester) async { @@ -236,6 +252,21 @@ void main() { }, ); + testWidgets( + 'incorrectAnswer text when the status is incorrect', + (tester) async { + when(() => wordSelectionBloc.state).thenReturn( + WordSelectionState( + status: WordSelectionStatus.incorrect, + word: selectedWord, + ), + ); + await tester.pumpApp(widget); + + expect(find.text(l10n.incorrectAnswer), findsOneWidget); + }, + ); + testWidgets( 'the $HintsSection when the status is not validating', (tester) async { @@ -304,6 +335,27 @@ void main() { ).called(1); }, ); + + testWidgets( + 'deleting a letter sends $WordSolveRequested', + (tester) async { + await tester.pumpApp(widget); + await tester.pumpAndSettle(); + + final editableTexts = find.byType(EditableText); + await tester.enterText(editableTexts.at(0), 'A'); + await tester.enterText(editableTexts.at(1), 'N'); + await tester.enterText(editableTexts.at(2), 'S'); + await tester.enterText(editableTexts.at(3), '!'); // focus previous cell + await tester.pumpAndSettle(); + + await tester.sendKeyEvent(LogicalKeyboardKey.backspace); + + verify( + () => wordSelectionBloc.add(const WordSolveRequested()), + ).called(1); + }, + ); }); group('$BottomPanel', () {