From 2acae4df9d7fe3f5ce55d22b75c5ec36d04d5105 Mon Sep 17 00:00:00 2001 From: Alejandro Santiago Date: Mon, 8 Apr 2024 15:22:41 +0100 Subject: [PATCH 1/2] feat: update WelcomePage (#220) * refactor: use new WelcomePage * feat: remove old code and update WelcomeBloc test * test: remove board info * test: add WelcomeBloc tests * test:s support value equality test --- lib/game_intro/bloc/game_intro_bloc.dart | 24 +-- lib/game_intro/bloc/game_intro_event.dart | 7 - lib/game_intro/bloc/game_intro_state.dart | 8 - lib/game_intro/view/game_intro_page.dart | 9 +- lib/game_intro/view/view.dart | 1 - lib/game_intro/view/welcome_view.dart | 83 ---------- lib/welcome/bloc/welcome_bloc.dart | 39 +++++ lib/welcome/bloc/welcome_event.dart | 13 ++ lib/welcome/bloc/welcome_state.dart | 45 ++++++ lib/welcome/view/welcome_page.dart | 36 ++++- lib/welcome/welcome.dart | 1 + .../game_intro/bloc/game_intro_bloc_test.dart | 36 ----- .../bloc/game_intro_event_test.dart | 9 -- .../bloc/game_intro_state_test.dart | 14 -- .../game_intro/view/game_intro_page_test.dart | 5 +- test/game_intro/view/welcome_view_test.dart | 153 ------------------ test/welcome/bloc/welcome_bloc_test.dart | 52 ++++++ test/welcome/bloc/welcome_event_test.dart | 16 ++ test/welcome/bloc/welcome_state_test.dart | 42 +++++ test/welcome/view/welcome_page_test.dart | 70 +++++--- 20 files changed, 293 insertions(+), 370 deletions(-) delete mode 100644 lib/game_intro/view/welcome_view.dart create mode 100644 lib/welcome/bloc/welcome_bloc.dart create mode 100644 lib/welcome/bloc/welcome_event.dart create mode 100644 lib/welcome/bloc/welcome_state.dart delete mode 100644 test/game_intro/view/welcome_view_test.dart create mode 100644 test/welcome/bloc/welcome_bloc_test.dart create mode 100644 test/welcome/bloc/welcome_event_test.dart create mode 100644 test/welcome/bloc/welcome_state_test.dart diff --git a/lib/game_intro/bloc/game_intro_bloc.dart b/lib/game_intro/bloc/game_intro_bloc.dart index c082c43d4..6e11e2ec0 100644 --- a/lib/game_intro/bloc/game_intro_bloc.dart +++ b/lib/game_intro/bloc/game_intro_bloc.dart @@ -1,6 +1,5 @@ import 'package:api_client/api_client.dart'; import 'package:bloc/bloc.dart'; -import 'package:board_info_repository/board_info_repository.dart'; import 'package:equatable/equatable.dart'; import 'package:game_domain/game_domain.dart'; @@ -9,13 +8,10 @@ part 'game_intro_state.dart'; class GameIntroBloc extends Bloc { GameIntroBloc({ - required BoardInfoRepository boardInfoRepository, required LeaderboardResource leaderboardResource, - }) : _boardInfoRepository = boardInfoRepository, - _leaderboardResource = leaderboardResource, + }) : _leaderboardResource = leaderboardResource, super(const GameIntroState()) { on(_onBlacklistRequested); - on(_onBoardProgressRequested); on(_onWelcomeCompleted); on(_onMascotUpdated); on(_onMascotSubmitted); @@ -23,7 +19,6 @@ class GameIntroBloc extends Bloc { on(_onInitialsSubmitted); } - final BoardInfoRepository _boardInfoRepository; final LeaderboardResource _leaderboardResource; final initialsRegex = RegExp('[A-Z]{3}'); @@ -39,23 +34,6 @@ class GameIntroBloc extends Bloc { } } - Future _onBoardProgressRequested( - BoardProgressRequested event, - Emitter emit, - ) async { - final [solved, total] = await Future.wait([ - _boardInfoRepository.getSolvedWordsCount(), - _boardInfoRepository.getTotalWordsCount(), - ]); - - emit( - state.copyWith( - solvedWords: solved, - totalWords: total, - ), - ); - } - void _onWelcomeCompleted( WelcomeCompleted event, Emitter emit, diff --git a/lib/game_intro/bloc/game_intro_event.dart b/lib/game_intro/bloc/game_intro_event.dart index 48354fcb5..85a043b3c 100644 --- a/lib/game_intro/bloc/game_intro_event.dart +++ b/lib/game_intro/bloc/game_intro_event.dart @@ -4,13 +4,6 @@ sealed class GameIntroEvent extends Equatable { const GameIntroEvent(); } -class BoardProgressRequested extends GameIntroEvent { - const BoardProgressRequested(); - - @override - List get props => []; -} - class BlacklistRequested extends GameIntroEvent { const BlacklistRequested(); diff --git a/lib/game_intro/bloc/game_intro_state.dart b/lib/game_intro/bloc/game_intro_state.dart index f16f0284a..4c64164e6 100644 --- a/lib/game_intro/bloc/game_intro_state.dart +++ b/lib/game_intro/bloc/game_intro_state.dart @@ -19,8 +19,6 @@ class GameIntroState extends Equatable { const GameIntroState({ this.status = GameIntroStatus.welcome, this.isIntroCompleted = false, - this.solvedWords = 0, - this.totalWords = 0, this.selectedMascot = Mascots.dash, this.initials = const ['', '', ''], this.initialsBlacklist = const [], @@ -29,8 +27,6 @@ class GameIntroState extends Equatable { final GameIntroStatus status; final bool isIntroCompleted; - final int solvedWords; - final int totalWords; final Mascots selectedMascot; final List initials; final List initialsBlacklist; @@ -49,8 +45,6 @@ class GameIntroState extends Equatable { return GameIntroState( status: status ?? this.status, isIntroCompleted: isIntroCompleted ?? this.isIntroCompleted, - solvedWords: solvedWords ?? this.solvedWords, - totalWords: totalWords ?? this.totalWords, selectedMascot: selectedMascot ?? this.selectedMascot, initials: initials ?? this.initials, initialsBlacklist: initialsBlacklist ?? this.initialsBlacklist, @@ -62,8 +56,6 @@ class GameIntroState extends Equatable { List get props => [ status, isIntroCompleted, - solvedWords, - totalWords, selectedMascot, initials, initialsBlacklist, diff --git a/lib/game_intro/view/game_intro_page.dart b/lib/game_intro/view/game_intro_page.dart index 6559d51f4..f3f236255 100644 --- a/lib/game_intro/view/game_intro_page.dart +++ b/lib/game_intro/view/game_intro_page.dart @@ -1,11 +1,11 @@ import 'package:api_client/api_client.dart'; -import 'package:board_info_repository/board_info_repository.dart'; import 'package:flow_builder/flow_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:io_crossword/about/about.dart'; import 'package:io_crossword/crossword/crossword.dart'; import 'package:io_crossword/game_intro/game_intro.dart'; +import 'package:io_crossword/welcome/view/welcome_page.dart'; class GameIntroPage extends StatelessWidget { const GameIntroPage({super.key}); @@ -14,11 +14,8 @@ class GameIntroPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => GameIntroBloc( - boardInfoRepository: context.read(), leaderboardResource: context.read(), - ) - ..add(const BoardProgressRequested()) - ..add(const BlacklistRequested()), + )..add(const BlacklistRequested()), child: const GameIntroView(), ); } @@ -60,7 +57,7 @@ List> onGenerateGameIntroPages( List> pages, ) { return switch (state.status) { - GameIntroStatus.welcome => [WelcomeView.page()], + GameIntroStatus.welcome => [WelcomePage.page()], GameIntroStatus.mascotSelection => [MascotSelectionView.page()], GameIntroStatus.initialsInput => [InitialsInputView.page()], }; diff --git a/lib/game_intro/view/view.dart b/lib/game_intro/view/view.dart index 7195acb11..765b079bc 100644 --- a/lib/game_intro/view/view.dart +++ b/lib/game_intro/view/view.dart @@ -2,4 +2,3 @@ export 'game_intro_page.dart'; export 'initials_form_view.dart'; export 'initials_input_view.dart'; export 'mascot_selection_view.dart'; -export 'welcome_view.dart'; diff --git a/lib/game_intro/view/welcome_view.dart b/lib/game_intro/view/welcome_view.dart deleted file mode 100644 index 0d9218816..000000000 --- a/lib/game_intro/view/welcome_view.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart'; -import 'package:io_crossword/game_intro/game_intro.dart'; -import 'package:io_crossword/l10n/l10n.dart'; -import 'package:io_crossword_ui/io_crossword_ui.dart'; - -class WelcomeView extends StatelessWidget { - const WelcomeView({super.key}); - - static Page page() { - return const MaterialPage(child: WelcomeView()); - } - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - return CardScrollableContentWithButton( - buttonLabel: l10n.getStarted, - onPressed: () { - context.read().add(const WelcomeCompleted()); - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Placeholder(fallbackHeight: 150), - const SizedBox(height: IoCrosswordSpacing.xlg), - Text( - l10n.welcome, - style: IoCrosswordTextStyles.bodyLG, - ), - const SizedBox(height: IoCrosswordSpacing.sm), - Text( - l10n.welcomeSubtitle, - style: IoCrosswordTextStyles.bodyLG, - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const Spacer(), - const RecordProgress(), - const SizedBox(height: IoCrosswordSpacing.xxlg), - ], - ), - ); - } -} - -class RecordProgress extends StatelessWidget { - @visibleForTesting - const RecordProgress({super.key}); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - final solvedWords = - context.select((GameIntroBloc bloc) => bloc.state.solvedWords); - final totalWords = - context.select((GameIntroBloc bloc) => bloc.state.totalWords); - - final f = NumberFormat.decimalPattern(l10n.localeName); - - return Column( - children: [ - Text( - l10n.wordsToBreakRecord, - style: IoCrosswordTextStyles.bodyLG, - ), - const SizedBox(height: IoCrosswordSpacing.sm), - IoLinearProgressIndicator( - value: totalWords == 0 ? 0 : solvedWords / totalWords, - ), - const SizedBox(height: IoCrosswordSpacing.sm), - Text( - '${f.format(solvedWords)} / ${f.format(totalWords)}', - style: IoCrosswordTextStyles.bodyLG.medium, - ), - ], - ); - } -} diff --git a/lib/welcome/bloc/welcome_bloc.dart b/lib/welcome/bloc/welcome_bloc.dart new file mode 100644 index 000000000..74c423642 --- /dev/null +++ b/lib/welcome/bloc/welcome_bloc.dart @@ -0,0 +1,39 @@ +import 'package:bloc/bloc.dart'; +import 'package:board_info_repository/board_info_repository.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +part 'welcome_event.dart'; +part 'welcome_state.dart'; + +class WelcomeBloc extends Bloc { + WelcomeBloc({ + required BoardInfoRepository boardInfoRepository, + }) : _boardInfoRepository = boardInfoRepository, + super(const WelcomeState.initial()) { + on(_onDataRequested); + } + + final BoardInfoRepository _boardInfoRepository; + + Future _onDataRequested( + WelcomeDataRequested event, + Emitter emit, + ) async { + try { + final [solved, total] = await Future.wait([ + _boardInfoRepository.getSolvedWordsCount(), + _boardInfoRepository.getTotalWordsCount(), + ]); + + emit( + state.copyWith( + solvedWords: solved, + totalWords: total, + ), + ); + } catch (error, stackTrace) { + addError(error, stackTrace); + } + } +} diff --git a/lib/welcome/bloc/welcome_event.dart b/lib/welcome/bloc/welcome_event.dart new file mode 100644 index 000000000..d043ef577 --- /dev/null +++ b/lib/welcome/bloc/welcome_event.dart @@ -0,0 +1,13 @@ +part of 'welcome_bloc.dart'; + +sealed class WelcomeEvent extends Equatable { + const WelcomeEvent(); + + @override + List get props => []; +} + +/// Requests the data needed to welcome the user. +class WelcomeDataRequested extends WelcomeEvent { + const WelcomeDataRequested(); +} diff --git a/lib/welcome/bloc/welcome_state.dart b/lib/welcome/bloc/welcome_state.dart new file mode 100644 index 000000000..e6e527d4c --- /dev/null +++ b/lib/welcome/bloc/welcome_state.dart @@ -0,0 +1,45 @@ +part of 'welcome_bloc.dart'; + +class WelcomeState extends Equatable { + const WelcomeState({ + required this.solvedWords, + required this.totalWords, + }); + + /// Creates a [WelcomeState] with the initial status. + const WelcomeState.initial({ + this.solvedWords = fallbackSolvedWords, + this.totalWords = fallbackTotalWords, + }); + + @visibleForTesting + static const fallbackTotalWords = 66666; + + @visibleForTesting + static const fallbackSolvedWords = 0; + + /// The current solved words in the challenge. + /// + /// If the loading of the solved words is yet to be done or fails this will + /// default to [fallbackSolvedWords]. + final int solvedWords; + + /// The total words to complete the challenge. + /// + /// If the loading of the total words is yet to be done or fails this will + /// default to [fallbackTotalWords]. + final int totalWords; + + WelcomeState copyWith({ + int? solvedWords, + int? totalWords, + }) { + return WelcomeState( + solvedWords: solvedWords ?? this.solvedWords, + totalWords: totalWords ?? this.totalWords, + ); + } + + @override + List get props => [solvedWords, totalWords]; +} diff --git a/lib/welcome/view/welcome_page.dart b/lib/welcome/view/welcome_page.dart index 53131b80f..daad1b2c2 100644 --- a/lib/welcome/view/welcome_page.dart +++ b/lib/welcome/view/welcome_page.dart @@ -1,19 +1,40 @@ +import 'package:flow_builder/flow_builder.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:io_crossword/game_intro/game_intro.dart'; import 'package:io_crossword/l10n/l10n.dart'; import 'package:io_crossword/welcome/welcome.dart'; class WelcomePage extends StatelessWidget { const WelcomePage({super.key}); + static Page page() { + return const MaterialPage(child: WelcomePage()); + } + @override Widget build(BuildContext context) { - return const WelcomeView(); + return BlocProvider( + create: (context) => WelcomeBloc( + boardInfoRepository: context.read(), + )..add(const WelcomeDataRequested()), + child: const WelcomeView(), + ); } } +@visibleForTesting class WelcomeView extends StatelessWidget { const WelcomeView({super.key}); + void _onGetStarted(BuildContext context) { + context.flow().update( + (status) => status.copyWith( + status: GameIntroStatus.mascotSelection, + ), + ); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -45,13 +66,18 @@ class WelcomeView extends StatelessWidget { textAlign: TextAlign.center, ), const SizedBox(height: 48), - const ChallengeProgress( - solvedWords: 25, - totalWords: 100, + BlocSelector( + selector: (state) => (state.solvedWords, state.totalWords), + builder: (context, words) { + return ChallengeProgress( + solvedWords: words.$1, + totalWords: words.$2, + ); + }, ), const SizedBox(height: 48), OutlinedButton( - onPressed: () {}, + onPressed: () => _onGetStarted(context), child: Text( l10n.getStarted, style: theme.textTheme.bodyMedium, diff --git a/lib/welcome/welcome.dart b/lib/welcome/welcome.dart index 930a6ff13..bcf8ea309 100644 --- a/lib/welcome/welcome.dart +++ b/lib/welcome/welcome.dart @@ -6,5 +6,6 @@ /// of all the players. library; +export 'bloc/welcome_bloc.dart'; export 'view/view.dart'; export 'widgets/widgets.dart'; diff --git a/test/game_intro/bloc/game_intro_bloc_test.dart b/test/game_intro/bloc/game_intro_bloc_test.dart index 93ff5daa9..9e62aeb7d 100644 --- a/test/game_intro/bloc/game_intro_bloc_test.dart +++ b/test/game_intro/bloc/game_intro_bloc_test.dart @@ -3,23 +3,18 @@ import 'package:api_client/api_client.dart'; import 'package:bloc_test/bloc_test.dart'; -import 'package:board_info_repository/board_info_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:game_domain/game_domain.dart'; import 'package:io_crossword/game_intro/bloc/game_intro_bloc.dart'; import 'package:io_crossword/game_intro/game_intro.dart'; import 'package:mocktail/mocktail.dart'; -class _MockBoardInfoRepository extends Mock implements BoardInfoRepository {} - class _MockLeaderboardResource extends Mock implements LeaderboardResource {} void main() { - late BoardInfoRepository boardInfoRepository; late LeaderboardResource leaderboardResource; setUp(() { - boardInfoRepository = _MockBoardInfoRepository(); leaderboardResource = _MockLeaderboardResource(); }); @@ -30,7 +25,6 @@ void main() { .thenAnswer((_) => Future.value(['TST'])); }, build: () => GameIntroBloc( - boardInfoRepository: boardInfoRepository, leaderboardResource: leaderboardResource, ), act: (bloc) => bloc.add(BlacklistRequested()), @@ -41,32 +35,9 @@ void main() { ], ); - blocTest( - 'emits state with updated board progress data ' - 'when BoardProgressRequested is added', - build: () => GameIntroBloc( - boardInfoRepository: boardInfoRepository, - leaderboardResource: leaderboardResource, - ), - setUp: () { - when(() => boardInfoRepository.getSolvedWordsCount()) - .thenAnswer((_) => Future.value(123)); - when(() => boardInfoRepository.getTotalWordsCount()) - .thenAnswer((_) => Future.value(8900)); - }, - act: (bloc) => bloc.add(BoardProgressRequested()), - expect: () => [ - GameIntroState( - solvedWords: 123, - totalWords: 8900, - ), - ], - ); - blocTest( 'emits state with mascot selection status when WelcomeCompleted is added', build: () => GameIntroBloc( - boardInfoRepository: boardInfoRepository, leaderboardResource: leaderboardResource, ), act: (bloc) => bloc.add(WelcomeCompleted()), @@ -78,7 +49,6 @@ void main() { blocTest( 'emits state with updated selected mascot when MascotUpdated is added', build: () => GameIntroBloc( - boardInfoRepository: boardInfoRepository, leaderboardResource: leaderboardResource, ), act: (bloc) => bloc.add(MascotUpdated(Mascots.dino)), @@ -90,7 +60,6 @@ void main() { blocTest( 'emits state with initials input status when MascotSubmitted is added', build: () => GameIntroBloc( - boardInfoRepository: boardInfoRepository, leaderboardResource: leaderboardResource, ), act: (bloc) => bloc.add(MascotSubmitted()), @@ -102,7 +71,6 @@ void main() { blocTest( 'emits state with updated initials when InitialsUpdated is added', build: () => GameIntroBloc( - boardInfoRepository: boardInfoRepository, leaderboardResource: leaderboardResource, ), act: (bloc) => bloc @@ -118,7 +86,6 @@ void main() { 'emits state with initials status invalid when InitialsSubmitted is added ' 'and initials are not valid', build: () => GameIntroBloc( - boardInfoRepository: boardInfoRepository, leaderboardResource: leaderboardResource, ), seed: () => GameIntroState(initials: ['A', 'B', '2']), @@ -135,7 +102,6 @@ void main() { 'emits state with initials status blacklisted when InitialsSubmitted ' 'is added and initials are blacklisted', build: () => GameIntroBloc( - boardInfoRepository: boardInfoRepository, leaderboardResource: leaderboardResource, ), seed: () => GameIntroState( @@ -164,7 +130,6 @@ void main() { ).thenAnswer((_) => Future.value()); }, build: () => GameIntroBloc( - boardInfoRepository: boardInfoRepository, leaderboardResource: leaderboardResource, ), seed: () => GameIntroState( @@ -205,7 +170,6 @@ void main() { ).thenThrow(Exception('Oops')); }, build: () => GameIntroBloc( - boardInfoRepository: boardInfoRepository, leaderboardResource: leaderboardResource, ), seed: () => GameIntroState( diff --git a/test/game_intro/bloc/game_intro_event_test.dart b/test/game_intro/bloc/game_intro_event_test.dart index afebd4f7f..523337ca9 100644 --- a/test/game_intro/bloc/game_intro_event_test.dart +++ b/test/game_intro/bloc/game_intro_event_test.dart @@ -6,15 +6,6 @@ import 'package:io_crossword/game_intro/game_intro.dart'; void main() { group('GameIntroEvent', () { - group('BoardProgressRequested', () { - test('supports value comparisons', () { - expect( - BoardProgressRequested(), - equals(BoardProgressRequested()), - ); - }); - }); - group('BlacklistRequested', () { test('supports value comparisons', () { expect( diff --git a/test/game_intro/bloc/game_intro_state_test.dart b/test/game_intro/bloc/game_intro_state_test.dart index adb7416ad..e84d21f03 100644 --- a/test/game_intro/bloc/game_intro_state_test.dart +++ b/test/game_intro/bloc/game_intro_state_test.dart @@ -29,20 +29,6 @@ void main() { ); }); - test('updates solvedWords', () { - expect( - GameIntroState().copyWith(solvedWords: 987), - equals(GameIntroState(solvedWords: 987)), - ); - }); - - test('updates totalWords', () { - expect( - GameIntroState().copyWith(totalWords: 1234), - equals(GameIntroState(totalWords: 1234)), - ); - }); - test('updates selectedMascot', () { expect( GameIntroState().copyWith(selectedMascot: Mascots.android), diff --git a/test/game_intro/view/game_intro_page_test.dart b/test/game_intro/view/game_intro_page_test.dart index 50ab2b52c..89c7ce78f 100644 --- a/test/game_intro/view/game_intro_page_test.dart +++ b/test/game_intro/view/game_intro_page_test.dart @@ -9,6 +9,7 @@ import 'package:game_domain/game_domain.dart'; import 'package:io_crossword/crossword/bloc/crossword_bloc.dart'; import 'package:io_crossword/crossword/crossword.dart'; import 'package:io_crossword/game_intro/game_intro.dart'; +import 'package:io_crossword/welcome/view/welcome_page.dart'; import 'package:mocktail/mocktail.dart'; import '../../helpers/helpers.dart'; @@ -47,14 +48,14 @@ void main() { }); testWidgets( - 'renders the welcome view with the default state', + 'renders the $WelcomePage with the default state', (tester) async { when(() => gameIntroBloc.state).thenReturn( const GameIntroState(), ); await tester.pumpApp(child); - expect(find.byType(WelcomeView), findsOneWidget); + expect(find.byType(WelcomePage), findsOneWidget); }, ); diff --git a/test/game_intro/view/welcome_view_test.dart b/test/game_intro/view/welcome_view_test.dart deleted file mode 100644 index 9ad394d26..000000000 --- a/test/game_intro/view/welcome_view_test.dart +++ /dev/null @@ -1,153 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:io_crossword/game_intro/game_intro.dart'; -import 'package:io_crossword/l10n/l10n.dart'; -import 'package:io_crossword_ui/io_crossword_ui.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../helpers/helpers.dart'; - -class _MockGameIntroBloc extends MockBloc - implements GameIntroBloc {} - -void main() { - group('WelcomeView', () { - late GameIntroBloc bloc; - late Widget child; - - setUp(() { - bloc = _MockGameIntroBloc(); - - child = BlocProvider.value( - value: bloc, - child: const IoCrosswordCard(child: WelcomeView()), - ); - }); - - testWidgets( - 'renders the record progress', - (tester) async { - when(() => bloc.state).thenReturn(const GameIntroState()); - await tester.pumpApp(child); - - expect(find.byType(RecordProgress), findsOneWidget); - }, - ); - - testWidgets( - 'adds WelcomeCompleted event when tapping button', - (tester) async { - when(() => bloc.state).thenReturn(const GameIntroState()); - await tester.pumpApp(child); - - await tester.tap(find.byType(PrimaryButton)); - - verify(() => bloc.add(const WelcomeCompleted())).called(1); - }, - ); - }); - - group('RecordProgress', () { - late GameIntroBloc bloc; - late Widget child; - - late AppLocalizations l10n; - - setUpAll(() async { - l10n = await AppLocalizations.delegate.load(Locale('en')); - }); - - setUp(() { - bloc = _MockGameIntroBloc(); - - child = BlocProvider.value( - value: bloc, - child: RecordProgress(), - ); - }); - - testWidgets( - 'renders IoLinearProgressIndicator', - (tester) async { - when(() => bloc.state).thenReturn(const GameIntroState()); - - await tester.pumpApp(child); - - expect(find.byType(IoLinearProgressIndicator), findsOneWidget); - }, - ); - - testWidgets( - 'renders IoLinearProgressIndicator value 0 with initial state', - (tester) async { - when(() => bloc.state).thenReturn(GameIntroState()); - - await tester.pumpApp(child); - - expect( - tester - .widget( - find.byType(IoLinearProgressIndicator), - ) - .value, - equals(0), - ); - }, - ); - - testWidgets( - 'renders IoLinearProgressIndicator value 0.5 when the solved words are ' - 'the half of the total words', - (tester) async { - when(() => bloc.state).thenReturn( - GameIntroState( - solvedWords: 20, - totalWords: 40, - ), - ); - - await tester.pumpApp(child); - - expect( - tester - .widget( - find.byType(IoLinearProgressIndicator), - ) - .value, - equals(0.5), - ); - }, - ); - - testWidgets( - 'displays wordsToBreakRecord', - (tester) async { - when(() => bloc.state).thenReturn(GameIntroState()); - - await tester.pumpApp(child); - - expect(find.text(l10n.wordsToBreakRecord), findsOneWidget); - }, - ); - - testWidgets( - 'displays words solved and total words', - (tester) async { - when(() => bloc.state).thenReturn( - GameIntroState( - solvedWords: 10, - totalWords: 50, - ), - ); - - await tester.pumpApp(child); - - expect(find.text('10 / 50'), findsOneWidget); - }, - ); - }); -} diff --git a/test/welcome/bloc/welcome_bloc_test.dart b/test/welcome/bloc/welcome_bloc_test.dart new file mode 100644 index 000000000..aeb97b4d0 --- /dev/null +++ b/test/welcome/bloc/welcome_bloc_test.dart @@ -0,0 +1,52 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:board_info_repository/board_info_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:io_crossword/welcome/welcome.dart'; +import 'package:mocktail/mocktail.dart'; + +class _MockBoardInfoRepository extends Mock implements BoardInfoRepository {} + +void main() { + group('$WelcomeBloc', () { + late BoardInfoRepository boardInfoRepository; + + setUp(() { + boardInfoRepository = _MockBoardInfoRepository(); + }); + + test('initial state is WelcomeState.initial', () { + expect( + WelcomeBloc(boardInfoRepository: boardInfoRepository).state, + equals(const WelcomeState.initial()), + ); + }); + + blocTest( + 'remains the same when retrieval fails', + build: () => WelcomeBloc(boardInfoRepository: boardInfoRepository), + act: (bloc) { + when(() => boardInfoRepository.getSolvedWordsCount()) + .thenThrow(Exception('oops')); + when(() => boardInfoRepository.getTotalWordsCount()) + .thenThrow(Exception('oops')); + bloc.add(const WelcomeDataRequested()); + }, + expect: () => [], + ); + + blocTest( + 'emits WelcomeState with updated values when data is requested', + build: () => WelcomeBloc(boardInfoRepository: boardInfoRepository), + act: (bloc) { + when(() => boardInfoRepository.getSolvedWordsCount()) + .thenAnswer((_) => Future.value(1)); + when(() => boardInfoRepository.getTotalWordsCount()) + .thenAnswer((_) => Future.value(2)); + bloc.add(const WelcomeDataRequested()); + }, + expect: () => [ + const WelcomeState(solvedWords: 1, totalWords: 2), + ], + ); + }); +} diff --git a/test/welcome/bloc/welcome_event_test.dart b/test/welcome/bloc/welcome_event_test.dart new file mode 100644 index 000000000..daca8be0d --- /dev/null +++ b/test/welcome/bloc/welcome_event_test.dart @@ -0,0 +1,16 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:io_crossword/welcome/welcome.dart'; + +void main() { + group('$WelcomeDataRequested', () { + test('can be instantiated', () { + expect(WelcomeDataRequested(), isA()); + }); + + test('supports value equality', () { + expect(WelcomeDataRequested(), WelcomeDataRequested()); + }); + }); +} diff --git a/test/welcome/bloc/welcome_state_test.dart b/test/welcome/bloc/welcome_state_test.dart new file mode 100644 index 000000000..8d33b2c39 --- /dev/null +++ b/test/welcome/bloc/welcome_state_test.dart @@ -0,0 +1,42 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:io_crossword/welcome/welcome.dart'; + +void main() { + group('$WelcomeState', () { + test('initials uses fallback values', () { + const state = WelcomeState.initial(); + + expect(state.solvedWords, WelcomeState.fallbackSolvedWords); + expect(state.totalWords, WelcomeState.fallbackTotalWords); + }); + + test('supports value equality', () { + final state1 = WelcomeState(solvedWords: 1, totalWords: 2); + final state2 = WelcomeState(solvedWords: 1, totalWords: 2); + final state3 = WelcomeState(solvedWords: 2, totalWords: 2); + + expect(state1, equals(state2)); + expect(state1, isNot(equals(state3))); + }); + + group('copyWith', () { + test('does nothing when no parameters are specified', () { + final state = WelcomeState(solvedWords: 1, totalWords: 2); + expect(state.copyWith(), equals(state)); + }); + + test('copies with new values', () { + final state = WelcomeState(solvedWords: 1, totalWords: 2); + final newState = state.copyWith(solvedWords: 3, totalWords: 4); + final copiedState = state.copyWith( + solvedWords: newState.solvedWords, + totalWords: newState.totalWords, + ); + + expect(copiedState, equals(newState)); + }); + }); + }); +} diff --git a/test/welcome/view/welcome_page_test.dart b/test/welcome/view/welcome_page_test.dart index f34519735..1b3912d05 100644 --- a/test/welcome/view/welcome_page_test.dart +++ b/test/welcome/view/welcome_page_test.dart @@ -1,12 +1,22 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flow_builder/flow_builder.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:io_crossword/game_intro/bloc/game_intro_bloc.dart'; +import 'package:io_crossword/game_intro/game_intro.dart'; import 'package:io_crossword/l10n/l10n.dart'; import 'package:io_crossword/welcome/welcome.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../helpers/helpers.dart'; + +class _MockWelcomeBloc extends Mock implements WelcomeBloc {} void main() { group('$WelcomePage', () { testWidgets('displays a $WelcomeView', (tester) async { - await tester.pumpSubject(const WelcomePage()); + await tester.pumpApp(const WelcomePage()); expect(find.byType(WelcomeView), findsOneWidget); }); @@ -14,21 +24,32 @@ void main() { group('$WelcomeView', () { testWidgets( - 'does nothing when getting started button is pressed', + 'updates flow when pressed', (tester) async { - await tester.pumpSubject(const WelcomeView()); + final flowController = FlowController(const GameIntroState()); + addTearDown(flowController.dispose); + + await tester.pumpSubject( + FlowBuilder( + controller: flowController, + onGeneratePages: (_, __) => [ + const MaterialPage(child: WelcomeView()), + ], + ), + ); final outlinedButtonFinder = find.byType(OutlinedButton); await tester.ensureVisible(outlinedButtonFinder); await tester.pumpAndSettle(); await tester.tap(outlinedButtonFinder); - await tester.pumpAndSettle(); + await tester.pump(); expect( - find.byType(WelcomeView), - findsOneWidget, - reason: 'No navigation occurs when the button is pressed.', + flowController.state, + equals( + const GameIntroState(status: GameIntroStatus.mascotSelection), + ), ); }, ); @@ -102,22 +123,25 @@ void main() { extension on WidgetTester { /// Pumps the test subject with all its required ancestors. - Future pumpSubject(Widget child) => pumpWidget(_Subject(child: child)); -} - -class _Subject extends StatelessWidget { - const _Subject({ - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - return MaterialApp( - locale: const Locale('en'), - localizationsDelegates: AppLocalizations.localizationsDelegates, - home: child, + Future pumpSubject( + Widget child, { + WelcomeBloc? welcomeBloc, + }) { + final bloc = welcomeBloc ?? _MockWelcomeBloc(); + if (welcomeBloc == null) { + whenListen( + bloc, + const Stream.empty(), + initialState: const WelcomeState.initial(), + ); + when(bloc.close).thenAnswer((_) => Future.value()); + } + + return pumpApp( + BlocProvider( + create: (_) => bloc, + child: child, + ), ); } } From d356e30711e4c06fea5c72b99772615713064104 Mon Sep 17 00:00:00 2001 From: Hugo Walbecq Date: Mon, 8 Apr 2024 16:43:22 +0200 Subject: [PATCH 2/2] feat: add provider for user (#221) --- lib/app/view/app.dart | 4 ++++ lib/main_debug.dart | 1 + lib/main_development.dart | 1 + lib/main_local.dart | 1 + lib/main_production.dart | 1 + lib/main_staging.dart | 1 + test/app/view/app_test.dart | 4 ++++ 7 files changed, 13 insertions(+) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index f629075fa..c3281b489 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -1,4 +1,5 @@ import 'package:api_client/api_client.dart'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:board_info_repository/board_info_repository.dart'; import 'package:crossword_repository/crossword_repository.dart'; import 'package:flutter/material.dart'; @@ -15,10 +16,12 @@ class App extends StatelessWidget { required this.apiClient, required this.crosswordRepository, required this.boardInfoRepository, + required this.user, super.key, }); final ApiClient apiClient; + final User user; final CrosswordRepository crosswordRepository; final BoardInfoRepository boardInfoRepository; @@ -29,6 +32,7 @@ class App extends StatelessWidget { return MultiProvider( providers: [ Provider.value(value: apiClient.leaderboardResource), + Provider.value(value: user), Provider.value(value: crosswordResource), Provider.value(value: crosswordRepository), Provider.value(value: boardInfoRepository), diff --git a/lib/main_debug.dart b/lib/main_debug.dart index 3b0ca0417..65c11af11 100644 --- a/lib/main_debug.dart +++ b/lib/main_debug.dart @@ -59,6 +59,7 @@ void main() async { apiClient: apiClient, crosswordRepository: CrosswordRepository(db: firestore), boardInfoRepository: BoardInfoRepository(firestore: firestore), + user: await authenticationRepository.user.first, ); }, ), diff --git a/lib/main_development.dart b/lib/main_development.dart index 6ba325d91..9abea05db 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -56,6 +56,7 @@ void main() async { apiClient: apiClient, crosswordRepository: CrosswordRepository(db: firestore), boardInfoRepository: BoardInfoRepository(firestore: firestore), + user: await authenticationRepository.user.first, ); }, ), diff --git a/lib/main_local.dart b/lib/main_local.dart index 62919b5b2..ed61702b3 100644 --- a/lib/main_local.dart +++ b/lib/main_local.dart @@ -53,6 +53,7 @@ void main() async { apiClient: apiClient, crosswordRepository: CrosswordRepository(db: firestore), boardInfoRepository: BoardInfoRepository(firestore: firestore), + user: await authenticationRepository.user.first, ); }, ), diff --git a/lib/main_production.dart b/lib/main_production.dart index 495d6195d..363473047 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -47,6 +47,7 @@ void main() async { apiClient: apiClient, crosswordRepository: CrosswordRepository(db: firestore), boardInfoRepository: BoardInfoRepository(firestore: firestore), + user: await authenticationRepository.user.first, ); }, ), diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 1912d7401..97226e4dd 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -56,6 +56,7 @@ void main() async { apiClient: apiClient, crosswordRepository: CrosswordRepository(db: firestore), boardInfoRepository: BoardInfoRepository(firestore: firestore), + user: await authenticationRepository.user.first, ); }, ), diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index 0d543b224..daa5f92c5 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: prefer_const_constructors import 'package:api_client/api_client.dart'; +import 'package:authentication_repository/authentication_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:board_info_repository/board_info_repository.dart'; import 'package:crossword_repository/crossword_repository.dart'; @@ -25,6 +26,8 @@ class _MockCrosswordResource extends Mock implements CrosswordResource {} class _MockCrosswordBloc extends Mock implements CrosswordBloc {} +class _MockUser extends Mock implements User {} + void main() { group('App', () { late ApiClient apiClient; @@ -59,6 +62,7 @@ void main() { apiClient: apiClient, crosswordRepository: crosswordRepository, boardInfoRepository: boardInfoRepository, + user: _MockUser(), ), );