From 5edd77a022be7f8fe455cd0401afe8259eb869ae Mon Sep 17 00:00:00 2001 From: SofiaRey Date: Wed, 8 Jan 2025 13:47:01 -0300 Subject: [PATCH] fix: build routes refactor and tests added --- .../lib/app/routes/routes.dart | 74 +++---------------- .../lib/article/bloc/article_bloc.dart | 2 +- .../lib/article/view/article_page.dart | 34 +++++++++ .../lib/article/widgets/article_content.dart | 11 ++- .../lib/feed/widgets/category_feed.dart | 12 +-- .../lib/home/view/home_page.dart | 7 +- .../lib/login/view/login_with_email_page.dart | 8 +- .../view/magic_link_prompt_page.dart | 9 +++ .../lib/network_error/view/network_error.dart | 20 +++-- .../view/notification_preferences_page.dart | 12 +-- .../lib/onboarding/view/onboarding_page.dart | 7 +- .../lib/slideshow/view/slideshow_page.dart | 20 +++-- .../view/manage_subscription_page.dart | 11 +-- .../user_profile/view/user_profile_page.dart | 8 +- .../test/article/view/article_page_test.dart | 18 +++++ .../test/feed/widgets/category_feed_test.dart | 19 +++-- .../test/home/view/home_page_test.dart | 60 ++++++++++++++- .../test/home/view/home_view_test.dart | 37 +++++++++- .../view/login_with_email_page_test.dart | 18 ++++- .../view/magic_link_prompt_page_test.dart | 21 ++++++ .../network_error/network_error_test.dart | 14 ++++ .../notification_preferences_page_test.dart | 25 +++++-- .../onboarding/view/onboarding_page_test.dart | 21 +++++- .../slideshow/view/slideshow_page_test.dart | 28 +++++-- .../view/manage_subscription_page_test.dart | 18 ++++- .../view/user_profile_page_test.dart | 17 ++++- 26 files changed, 385 insertions(+), 146 deletions(-) diff --git a/flutter_news_example/lib/app/routes/routes.dart b/flutter_news_example/lib/app/routes/routes.dart index ac30713fc..47924abca 100644 --- a/flutter_news_example/lib/app/routes/routes.dart +++ b/flutter_news_example/lib/app/routes/routes.dart @@ -1,4 +1,3 @@ -import 'package:flutter/widgets.dart'; import 'package:flutter_news_example/article/article.dart'; import 'package:flutter_news_example/home/home.dart'; import 'package:flutter_news_example/login/login.dart'; @@ -10,107 +9,56 @@ import 'package:flutter_news_example/slideshow/slideshow.dart'; import 'package:flutter_news_example/subscriptions/view/manage_subscription_page.dart'; import 'package:flutter_news_example/user_profile/user_profile.dart'; import 'package:go_router/go_router.dart'; -import 'package:news_blocks/news_blocks.dart'; final GoRouter router = GoRouter( routes: [ GoRoute( path: HomePage.routePath, - builder: (BuildContext context, GoRouterState state) { - return const HomePage(); - }, + builder: HomePage.routeBuilder, routes: [ GoRoute( name: NetworkErrorPage.routePath, path: NetworkErrorPage.routePath, - builder: (BuildContext context, GoRouterState state) { - final onRetry = state.extra as VoidCallback?; - return NetworkError(onRetry: onRetry); - }, + builder: NetworkErrorPage.routeBuilder, ), GoRoute( name: LoginWithEmailPage.routePath, path: LoginWithEmailPage.routePath, - builder: (BuildContext context, GoRouterState state) { - return const LoginWithEmailPage(); - }, + builder: LoginWithEmailPage.routeBuilder, routes: [ GoRoute( name: MagicLinkPromptPage.routePath, path: MagicLinkPromptPage.routePath, - builder: (BuildContext context, GoRouterState state) { - return MagicLinkPromptPage( - email: state.uri.queryParameters['email']!, - ); - }, + builder: MagicLinkPromptPage.routeBuilder, ), ], ), GoRoute( name: ArticlePage.routeName, path: ArticlePage.routePath, - builder: (BuildContext context, GoRouterState state) { - final id = state.pathParameters['id']; - - final isVideoArticle = bool.tryParse( - state.uri.queryParameters['isVideoArticle'] ?? 'false', - ) ?? - false; - final interstitialAdBehavior = - state.uri.queryParameters['interstitialAdBehavior'] != null - ? InterstitialAdBehavior.values.firstWhere( - (e) => - e.toString() == - 'InterstitialAdBehavior.' - // ignore: lines_longer_than_80_chars - '${state.uri.queryParameters['interstitialAdBehavior']}', - ) - : null; - - if (id == null) { - throw Exception('Missing required "id" parameter'); - } - - return ArticlePage( - id: id, - isVideoArticle: isVideoArticle, - interstitialAdBehavior: - interstitialAdBehavior ?? InterstitialAdBehavior.onOpen, - ); - }, + builder: ArticlePage.routeBuilder, routes: [ GoRoute( name: SlideshowPage.routePath, path: SlideshowPage.routePath, - builder: (BuildContext context, GoRouterState state) { - return SlideshowPage( - slideshow: state.extra! as SlideshowBlock, - articleId: state.pathParameters['id']!, - ); - }, + builder: SlideshowPage.routeBuilder, ), ], ), GoRoute( name: UserProfilePage.routePath, path: UserProfilePage.routePath, - builder: (BuildContext context, GoRouterState state) { - return const UserProfilePage(); - }, + builder: UserProfilePage.routeBuilder, routes: [ GoRoute( name: ManageSubscriptionPage.routePath, path: ManageSubscriptionPage.routePath, - builder: (BuildContext context, GoRouterState state) { - return const ManageSubscriptionPage(); - }, + builder: ManageSubscriptionPage.routeBuilder, ), GoRoute( name: NotificationPreferencesPage.routePath, path: NotificationPreferencesPage.routePath, - builder: (BuildContext context, GoRouterState state) { - return const NotificationPreferencesPage(); - }, + builder: NotificationPreferencesPage.routeBuilder, ), ], ), @@ -119,9 +67,7 @@ final GoRouter router = GoRouter( GoRoute( name: OnboardingPage.routePath, path: OnboardingPage.routePath, - builder: (BuildContext context, GoRouterState state) { - return const OnboardingPage(); - }, + builder: OnboardingPage.routeBuilder, ), ], ); diff --git a/flutter_news_example/lib/article/bloc/article_bloc.dart b/flutter_news_example/lib/article/bloc/article_bloc.dart index a7b02dee0..cc66a71c0 100644 --- a/flutter_news_example/lib/article/bloc/article_bloc.dart +++ b/flutter_news_example/lib/article/bloc/article_bloc.dart @@ -10,9 +10,9 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:news_blocks/news_blocks.dart'; import 'package:share_launcher/share_launcher.dart'; +part 'article_bloc.g.dart'; part 'article_event.dart'; part 'article_state.dart'; -part 'article_bloc.g.dart'; class ArticleBloc extends HydratedBloc { ArticleBloc({ diff --git a/flutter_news_example/lib/article/view/article_page.dart b/flutter_news_example/lib/article/view/article_page.dart index 57e24a3bd..9bb0a97ca 100644 --- a/flutter_news_example/lib/article/view/article_page.dart +++ b/flutter_news_example/lib/article/view/article_page.dart @@ -8,6 +8,7 @@ import 'package:flutter_news_example/app/app.dart'; import 'package:flutter_news_example/article/article.dart'; import 'package:flutter_news_example/l10n/l10n.dart'; import 'package:flutter_news_example/subscriptions/subscriptions.dart'; +import 'package:go_router/go_router.dart'; import 'package:news_blocks_ui/news_blocks_ui.dart'; import 'package:share_launcher/share_launcher.dart'; @@ -31,6 +32,39 @@ class ArticlePage extends StatelessWidget { static const routeName = 'article'; static const routePath = 'article/:id'; + static Widget routeBuilder( + BuildContext context, + GoRouterState state, + ) { + final id = state.pathParameters['id']; + + final isVideoArticle = bool.tryParse( + state.uri.queryParameters['isVideoArticle'] ?? 'false', + ) ?? + false; + final interstitialAdBehavior = + state.uri.queryParameters['interstitialAdBehavior'] != null + ? InterstitialAdBehavior.values.firstWhere( + (e) => + e.toString() == + 'InterstitialAdBehavior.' + // ignore: lines_longer_than_80_chars + '${state.uri.queryParameters['interstitialAdBehavior']}', + ) + : null; + + if (id == null) { + throw Exception('Missing required "id" parameter'); + } + + return ArticlePage( + id: id, + isVideoArticle: isVideoArticle, + interstitialAdBehavior: + interstitialAdBehavior ?? InterstitialAdBehavior.onOpen, + ); + } + /// The id of the requested article. final String id; diff --git a/flutter_news_example/lib/article/widgets/article_content.dart b/flutter_news_example/lib/article/widgets/article_content.dart index 2b770c477..6d0d2d4b9 100644 --- a/flutter_news_example/lib/article/widgets/article_content.dart +++ b/flutter_news_example/lib/article/widgets/article_content.dart @@ -35,15 +35,14 @@ class ArticleContent extends StatelessWidget { return ArticleContentSeenListener( child: BlocListener( - listener: (context, state) { + listener: (context, state) async { if (state.status == ArticleStatus.failure && state.content.isEmpty) { - context.goNamed( + await context.pushNamed( NetworkErrorPage.routePath, - extra: () { - context.read().add(const ArticleRequested()); - Navigator.of(context).pop(); - }, ); + if (context.mounted) { + context.read().add(const ArticleRequested()); + } } else if (state.status == ArticleStatus.shareFailure) { _handleShareFailure(context); } diff --git a/flutter_news_example/lib/feed/widgets/category_feed.dart b/flutter_news_example/lib/feed/widgets/category_feed.dart index e413275df..ef0458f2d 100644 --- a/flutter_news_example/lib/feed/widgets/category_feed.dart +++ b/flutter_news_example/lib/feed/widgets/category_feed.dart @@ -30,15 +30,15 @@ class CategoryFeed extends StatelessWidget { .select((FeedBloc bloc) => bloc.state.status == FeedStatus.failure); return BlocListener( - listener: (context, state) { + listener: (context, state) async { if (state.status == FeedStatus.failure && state.feed.isEmpty) { - context.goNamed( + await context.pushNamed( NetworkErrorPage.routePath, - extra: () { - context.read().add(FeedRequested(category: category)); - Navigator.of(context).pop(); - }, ); + // TODO: check if this implementation works (tests) + if (context.mounted) { + context.read().add(FeedRequested(category: category)); + } } }, child: RefreshIndicator( diff --git a/flutter_news_example/lib/home/view/home_page.dart b/flutter_news_example/lib/home/view/home_page.dart index 192c6a09e..815d8cd88 100644 --- a/flutter_news_example/lib/home/view/home_page.dart +++ b/flutter_news_example/lib/home/view/home_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_example/feed/feed.dart'; import 'package:flutter_news_example/home/home.dart'; +import 'package:go_router/go_router.dart'; import 'package:news_repository/news_repository.dart'; class HomePage extends StatelessWidget { @@ -9,7 +10,11 @@ class HomePage extends StatelessWidget { static const routePath = '/'; - static Page page() => const MaterialPage(child: HomePage()); + static Widget routeBuilder( + BuildContext context, + GoRouterState state, + ) => + const HomePage(); @override Widget build(BuildContext context) { diff --git a/flutter_news_example/lib/login/view/login_with_email_page.dart b/flutter_news_example/lib/login/view/login_with_email_page.dart index a3f064072..76f2e2f23 100644 --- a/flutter_news_example/lib/login/view/login_with_email_page.dart +++ b/flutter_news_example/lib/login/view/login_with_email_page.dart @@ -2,6 +2,7 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_example/login/login.dart'; +import 'package:go_router/go_router.dart'; import 'package:user_repository/user_repository.dart'; class LoginWithEmailPage extends StatelessWidget { @@ -9,8 +10,11 @@ class LoginWithEmailPage extends StatelessWidget { static const routePath = 'login-with-email'; - static Route route() => - MaterialPageRoute(builder: (_) => const LoginWithEmailPage()); + static Widget routeBuilder( + BuildContext context, + GoRouterState state, + ) => + const LoginWithEmailPage(); @override Widget build(BuildContext context) { diff --git a/flutter_news_example/lib/magic_link_prompt/view/magic_link_prompt_page.dart b/flutter_news_example/lib/magic_link_prompt/view/magic_link_prompt_page.dart index db4d1941f..1d64746a7 100644 --- a/flutter_news_example/lib/magic_link_prompt/view/magic_link_prompt_page.dart +++ b/flutter_news_example/lib/magic_link_prompt/view/magic_link_prompt_page.dart @@ -2,6 +2,7 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_news_example/login/login.dart'; import 'package:flutter_news_example/magic_link_prompt/magic_link_prompt.dart'; +import 'package:go_router/go_router.dart'; class MagicLinkPromptPage extends StatelessWidget { const MagicLinkPromptPage({required this.email, super.key}); @@ -10,6 +11,14 @@ class MagicLinkPromptPage extends StatelessWidget { final String email; + static Widget routeBuilder( + BuildContext context, + GoRouterState state, + ) { + final email = state.uri.queryParameters['email']!; + return MagicLinkPromptPage(email: email); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/flutter_news_example/lib/network_error/view/network_error.dart b/flutter_news_example/lib/network_error/view/network_error.dart index 675b4634d..1ee668da7 100644 --- a/flutter_news_example/lib/network_error/view/network_error.dart +++ b/flutter_news_example/lib/network_error/view/network_error.dart @@ -1,26 +1,32 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_news_example/l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; /// {@template network_error} /// A network error alert page. /// {@endtemplate} class NetworkErrorPage extends StatelessWidget { /// {@macro network_error} - const NetworkErrorPage({super.key, this.onRetry}); - - /// An optional callback which is invoked when the retry button is pressed. - final VoidCallback? onRetry; + const NetworkErrorPage({ + super.key, + }); static const routePath = 'network-error'; + static Widget routeBuilder( + BuildContext context, + GoRouterState state, + ) => + const NetworkErrorPage(); + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, appBar: AppBar(leading: const AppBackButton()), - body: Center( - child: NetworkError(onRetry: onRetry), + body: const Center( + child: NetworkError(), ), ); } @@ -59,7 +65,7 @@ class NetworkError extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xxxlg), child: AppButton.darkAqua( - onPressed: onRetry, + onPressed: onRetry ?? context.pop, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/flutter_news_example/lib/notification_preferences/view/notification_preferences_page.dart b/flutter_news_example/lib/notification_preferences/view/notification_preferences_page.dart index f9f9ab855..6d309d8e7 100644 --- a/flutter_news_example/lib/notification_preferences/view/notification_preferences_page.dart +++ b/flutter_news_example/lib/notification_preferences/view/notification_preferences_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_example/l10n/l10n.dart'; import 'package:flutter_news_example/notification_preferences/notification_preferences.dart'; +import 'package:go_router/go_router.dart'; import 'package:news_repository/news_repository.dart'; import 'package:notifications_repository/notifications_repository.dart'; @@ -10,11 +11,12 @@ class NotificationPreferencesPage extends StatelessWidget { const NotificationPreferencesPage({super.key}); static const routePath = 'notification-preferences'; - static MaterialPageRoute route() { - return MaterialPageRoute( - builder: (_) => const NotificationPreferencesPage(), - ); - } + + static Widget routeBuilder( + BuildContext context, + GoRouterState state, + ) => + const NotificationPreferencesPage(); @override Widget build(BuildContext context) { diff --git a/flutter_news_example/lib/onboarding/view/onboarding_page.dart b/flutter_news_example/lib/onboarding/view/onboarding_page.dart index bee971a34..efa8f3457 100644 --- a/flutter_news_example/lib/onboarding/view/onboarding_page.dart +++ b/flutter_news_example/lib/onboarding/view/onboarding_page.dart @@ -3,6 +3,7 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_example/onboarding/onboarding.dart'; +import 'package:go_router/go_router.dart'; import 'package:notifications_repository/notifications_repository.dart'; class OnboardingPage extends StatelessWidget { @@ -10,7 +11,11 @@ class OnboardingPage extends StatelessWidget { static const routePath = '/onboarding'; - static Page page() => const MaterialPage(child: OnboardingPage()); + static Widget routeBuilder( + BuildContext context, + GoRouterState state, + ) => + const OnboardingPage(); @override Widget build(BuildContext context) { diff --git a/flutter_news_example/lib/slideshow/view/slideshow_page.dart b/flutter_news_example/lib/slideshow/view/slideshow_page.dart index 8026d8413..d0fa00581 100644 --- a/flutter_news_example/lib/slideshow/view/slideshow_page.dart +++ b/flutter_news_example/lib/slideshow/view/slideshow_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_example/article/article.dart'; import 'package:flutter_news_example/slideshow/slideshow.dart'; +import 'package:go_router/go_router.dart'; import 'package:news_blocks/news_blocks.dart'; import 'package:share_launcher/share_launcher.dart'; @@ -15,17 +16,14 @@ class SlideshowPage extends StatelessWidget { static const String routePath = 'slideshow'; - static Route route({ - required SlideshowBlock slideshow, - required String articleId, - }) { - return MaterialPageRoute( - builder: (_) => SlideshowPage( - slideshow: slideshow, - articleId: articleId, - ), - ); - } + static Widget routeBuilder( + BuildContext context, + GoRouterState state, + ) => + SlideshowPage( + slideshow: state.extra! as SlideshowBlock, + articleId: state.pathParameters['id']!, + ); final SlideshowBlock slideshow; final String articleId; diff --git a/flutter_news_example/lib/subscriptions/view/manage_subscription_page.dart b/flutter_news_example/lib/subscriptions/view/manage_subscription_page.dart index 5a5389c77..3be6bc68d 100644 --- a/flutter_news_example/lib/subscriptions/view/manage_subscription_page.dart +++ b/flutter_news_example/lib/subscriptions/view/manage_subscription_page.dart @@ -1,17 +1,18 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_news_example/l10n/l10n.dart'; +import 'package:go_router/go_router.dart'; class ManageSubscriptionPage extends StatelessWidget { const ManageSubscriptionPage({super.key}); static const routePath = 'manage-subscription'; - static MaterialPageRoute route() { - return MaterialPageRoute( - builder: (_) => const ManageSubscriptionPage(), - ); - } + static Widget routeBuilder( + BuildContext context, + GoRouterState state, + ) => + const ManageSubscriptionPage(); @override Widget build(BuildContext context) { diff --git a/flutter_news_example/lib/user_profile/view/user_profile_page.dart b/flutter_news_example/lib/user_profile/view/user_profile_page.dart index b4ac85bf5..1883058a4 100644 --- a/flutter_news_example/lib/user_profile/view/user_profile_page.dart +++ b/flutter_news_example/lib/user_profile/view/user_profile_page.dart @@ -18,9 +18,11 @@ class UserProfilePage extends StatelessWidget { static const routePath = 'profile'; - static MaterialPageRoute route() { - return MaterialPageRoute(builder: (_) => const UserProfilePage()); - } + static Widget routeBuilder( + BuildContext context, + GoRouterState state, + ) => + const UserProfilePage(); @override Widget build(BuildContext context) { diff --git a/flutter_news_example/test/article/view/article_page_test.dart b/flutter_news_example/test/article/view/article_page_test.dart index dc9e5a0c2..70d27d2c4 100644 --- a/flutter_news_example/test/article/view/article_page_test.dart +++ b/flutter_news_example/test/article/view/article_page_test.dart @@ -32,8 +32,14 @@ class MockRewardItem extends Mock implements ads.RewardItem {} class MockGoRouter extends Mock implements GoRouter {} +class _MockGoRouterState extends Mock implements GoRouterState {} + +class _MockBuildContext extends Mock implements BuildContext {} + void main() { initMockHydratedStorage(); + late GoRouterState goRouterState; + late BuildContext context; group('ArticlePage', () { late GoRouter goRouter; @@ -49,6 +55,18 @@ void main() { Stream.value(FullScreenAdsState.initial()), initialState: FullScreenAdsState.initial(), ); + goRouterState = _MockGoRouterState(); + context = _MockBuildContext(); + }); + + testWidgets('routeBuilder builds a ArticlePage', (tester) async { + when(() => goRouterState.pathParameters).thenReturn({'id': 'id'}); + when(() => goRouterState.uri) + .thenReturn(Uri(queryParameters: {'isVideoArticle': 'true'})); + + final page = ArticlePage.routeBuilder(context, goRouterState); + + expect(page, isA()); }); testWidgets('renders ArticleView', (tester) async { diff --git a/flutter_news_example/test/feed/widgets/category_feed_test.dart b/flutter_news_example/test/feed/widgets/category_feed_test.dart index c66fc3d51..12ebade08 100644 --- a/flutter_news_example/test/feed/widgets/category_feed_test.dart +++ b/flutter_news_example/test/feed/widgets/category_feed_test.dart @@ -141,11 +141,12 @@ void main() { ); when( - () => goRouter.goNamed( + () => goRouter.pushNamed( NetworkErrorPage.routePath, - extra: any(named: 'extra'), ), - ).thenAnswer((_) async {}); + ).thenAnswer((_) async { + return null; + }); }); Future pumpWidget(WidgetTester tester, Widget child) async { @@ -154,21 +155,25 @@ void main() { value: feedBloc, child: InheritedGoRouter( goRouter: goRouter, - child: CategoryFeed(category: category), + child: child, ), ), ); } - testWidgets('calls goNamed to Network Error page', (tester) async { + testWidgets('navigates to Network Error page and requests feed again', + (tester) async { await pumpWidget(tester, CategoryFeed(category: category)); verify( - () => goRouter.goNamed( + () => goRouter.pushNamed( NetworkErrorPage.routePath, - extra: any(named: 'extra'), ), ).called(1); + + verify( + () => feedBloc.add(FeedRequested(category: category)), + ).called(2); }); }); diff --git a/flutter_news_example/test/home/view/home_page_test.dart b/flutter_news_example/test/home/view/home_page_test.dart index dd3b55c2e..f72e92507 100644 --- a/flutter_news_example/test/home/view/home_page_test.dart +++ b/flutter_news_example/test/home/view/home_page_test.dart @@ -1,12 +1,15 @@ // ignore_for_file: prefer_const_constructors // ignore_for_file: prefer_const_literals_to_create_immutables -import 'package:flutter/material.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_news_example/feed/feed.dart'; import 'package:flutter_news_example/home/home.dart'; +import 'package:flutter_news_example/network_error/network_error.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:news_blocks/news_blocks.dart'; import 'package:news_repository/news_repository.dart'; import '../../helpers/helpers.dart'; @@ -15,15 +18,56 @@ class MockNewsRepository extends Mock implements NewsRepository {} class MockGoRouter extends Mock implements GoRouter {} +class _MockGoRouterState extends Mock implements GoRouterState {} + +class _MockBuildContext extends Mock implements BuildContext {} + +class MockFeedBloc extends MockBloc implements FeedBloc {} + void main() { initMockHydratedStorage(); late NewsRepository newsRepository; + late FeedBloc feedBloc; + + final entertainmentCategory = Category( + id: 'entertainment', + name: 'Entertainment', + ); + final healthCategory = Category(id: 'health', name: 'Health'); + + final feed = >{ + entertainmentCategory.id: [ + SectionHeaderBlock(title: 'Top'), + DividerHorizontalBlock(), + SpacerBlock(spacing: Spacing.medium), + ], + healthCategory.id: [ + SectionHeaderBlock(title: 'Technology'), + DividerHorizontalBlock(), + SpacerBlock(spacing: Spacing.medium), + ], + }; + + initMockHydratedStorage(); late GoRouter router; + late GoRouterState goRouterState; + late BuildContext context; setUp(() { + feedBloc = MockFeedBloc(); + + when(() => feedBloc.state).thenReturn( + FeedState( + feed: feed, + status: FeedStatus.populated, + ), + ); + newsRepository = MockNewsRepository(); router = MockGoRouter(); + goRouterState = _MockGoRouterState(); + context = _MockBuildContext(); final healthCategory = Category(id: 'health', name: 'Health'); when(newsRepository.getCategories).thenAnswer( @@ -31,10 +75,20 @@ void main() { categories: [healthCategory], ), ); + + when( + () => router.pushNamed( + NetworkErrorPage.routePath, + ), + ).thenAnswer((_) async { + return null; + }); }); - test('has a page', () { - expect(HomePage.page(), isA>()); + testWidgets('routeBuilder builds a HomePage', (tester) async { + final page = HomePage.routeBuilder(context, goRouterState); + + expect(page, isA()); }); testWidgets('renders a HomeView', (tester) async { diff --git a/flutter_news_example/test/home/view/home_view_test.dart b/flutter_news_example/test/home/view/home_view_test.dart index 11b5b0d26..656d1d9f8 100644 --- a/flutter_news_example/test/home/view/home_view_test.dart +++ b/flutter_news_example/test/home/view/home_view_test.dart @@ -13,9 +13,11 @@ import 'package:flutter_news_example/feed/feed.dart'; import 'package:flutter_news_example/home/home.dart'; import 'package:flutter_news_example/login/login.dart'; import 'package:flutter_news_example/navigation/navigation.dart'; +import 'package:flutter_news_example/onboarding/onboarding.dart'; import 'package:flutter_news_example/search/search.dart'; import 'package:flutter_news_example/user_profile/user_profile.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; import 'package:news_blocks/news_blocks.dart'; import 'package:news_repository/news_repository.dart'; @@ -33,6 +35,8 @@ class MockNewsRepository extends Mock implements NewsRepository {} class MockAppBloc extends Mock implements AppBloc {} +class MockGoRouter extends Mock implements GoRouter {} + void main() { initMockHydratedStorage(); @@ -41,6 +45,7 @@ void main() { late CategoriesBloc categoriesBloc; late FeedBloc feedBloc; late AppBloc appBloc; + late GoRouter goRouter; final entertainmentCategory = Category( id: 'entertainment', @@ -69,6 +74,7 @@ void main() { feedBloc = MockFeedBloc(); cubit = MockHomeCubit(); appBloc = MockAppBloc(); + goRouter = MockGoRouter(); when(() => appBloc.state).thenReturn( AppState( @@ -186,6 +192,32 @@ void main() { expect(find.byType(LoginModal), findsOneWidget); }); + + testWidgets('navigates to Onboarding page if onboarding is required', + (tester) async { + whenListen( + appBloc, + Stream.fromIterable([ + AppState( + showLoginOverlay: false, + status: AppStatus.onboardingRequired, + ), + ]), + ); + when(() => goRouter.goNamed(OnboardingPage.routePath)).thenAnswer((_) {}); + + await pumpHomeView( + tester: tester, + cubit: cubit, + categoriesBloc: categoriesBloc, + feedBloc: feedBloc, + newsRepository: newsRepository, + appBloc: appBloc, + goRouter: goRouter, + ); + + verify(() => goRouter.goNamed(OnboardingPage.routePath)).called(1); + }); }); group('BottomNavigationBar', () { @@ -285,6 +317,7 @@ Future pumpHomeView({ required FeedBloc feedBloc, required NewsRepository newsRepository, AppBloc? appBloc, + GoRouter? goRouter, }) async { await tester.pumpApp( MultiBlocProvider( @@ -299,7 +332,9 @@ Future pumpHomeView({ value: cubit, ), ], - child: HomeView(), + child: goRouter != null + ? InheritedGoRouter(goRouter: goRouter, child: HomeView()) + : HomeView(), ), newsRepository: newsRepository, appBloc: appBloc, diff --git a/flutter_news_example/test/login/view/login_with_email_page_test.dart b/flutter_news_example/test/login/view/login_with_email_page_test.dart index 578c5f429..1e708d56d 100644 --- a/flutter_news_example/test/login/view/login_with_email_page_test.dart +++ b/flutter_news_example/test/login/view/login_with_email_page_test.dart @@ -2,16 +2,30 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_news_example/login/login.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:mockingjay/mockingjay.dart'; import '../../helpers/helpers.dart'; +class _MockGoRouterState extends Mock implements GoRouterState {} + +class _MockBuildContext extends Mock implements BuildContext {} + void main() { const closeIcon = Key('loginWithEmailPage_closeIcon'); + late GoRouterState goRouterState; + late BuildContext context; + + setUp(() { + goRouterState = _MockGoRouterState(); + context = _MockBuildContext(); + }); group('LoginWithEmailPage', () { - test('has a route', () { - expect(LoginWithEmailPage.route(), isA>()); + testWidgets('routeBuilder builds a LoginWithEmailPage', (tester) async { + final page = LoginWithEmailPage.routeBuilder(context, goRouterState); + + expect(page, isA()); }); testWidgets('renders LoginWithEmailForm', (tester) async { diff --git a/flutter_news_example/test/magic_link_prompt/view/magic_link_prompt_page_test.dart b/flutter_news_example/test/magic_link_prompt/view/magic_link_prompt_page_test.dart index 8964bbb43..f4677ef36 100644 --- a/flutter_news_example/test/magic_link_prompt/view/magic_link_prompt_page_test.dart +++ b/flutter_news_example/test/magic_link_prompt/view/magic_link_prompt_page_test.dart @@ -3,15 +3,36 @@ import 'package:flutter/material.dart'; import 'package:flutter_news_example/magic_link_prompt/magic_link_prompt.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:mockingjay/mockingjay.dart'; import '../../helpers/helpers.dart'; +class _MockGoRouterState extends Mock implements GoRouterState {} + +class _MockBuildContext extends Mock implements BuildContext {} + void main() { const testEmail = 'testEmail@gmail.com'; const magicLinkPromptCloseIconKey = Key('magicLinkPrompt_closeIcon'); + late GoRouterState goRouterState; + late BuildContext context; + + setUp(() { + goRouterState = _MockGoRouterState(); + context = _MockBuildContext(); + }); group('MagicLinkPromptPage', () { + testWidgets('routeBuilder builds a MagicLinkPromptPage', (tester) async { + when(() => goRouterState.uri) + .thenReturn(Uri(queryParameters: {'email': 'email'})); + + final page = MagicLinkPromptPage.routeBuilder(context, goRouterState); + + expect(page, isA()); + }); + testWidgets('renders a MagicLinkPromptView', (tester) async { await tester.pumpApp( const MagicLinkPromptPage(email: testEmail), diff --git a/flutter_news_example/test/network_error/network_error_test.dart b/flutter_news_example/test/network_error/network_error_test.dart index fb4c05a5b..b02a87c45 100644 --- a/flutter_news_example/test/network_error/network_error_test.dart +++ b/flutter_news_example/test/network_error/network_error_test.dart @@ -10,16 +10,30 @@ import '../helpers/helpers.dart'; class MockGoRouter extends Mock implements GoRouter {} +class _MockGoRouterState extends Mock implements GoRouterState {} + +class _MockBuildContext extends Mock implements BuildContext {} + void main() { const tapMeText = 'Tap Me'; late GoRouter goRouter; + late GoRouterState goRouterState; + late BuildContext context; setUpAll(() { goRouter = MockGoRouter(); when(() => goRouter.goNamed(NetworkErrorPage.routePath)).thenAnswer((_) {}); + goRouterState = _MockGoRouterState(); + context = _MockBuildContext(); }); group('NetworkError', () { + testWidgets('builds a NetworkErrorPage', (tester) async { + final page = NetworkErrorPage.routeBuilder(context, goRouterState); + + expect(page, isA()); + }); + testWidgets('renders correctly', (tester) async { await tester.pumpApp(NetworkError()); diff --git a/flutter_news_example/test/notification_preferences/view/notification_preferences_page_test.dart b/flutter_news_example/test/notification_preferences/view/notification_preferences_page_test.dart index c564e04f2..59cadaab1 100644 --- a/flutter_news_example/test/notification_preferences/view/notification_preferences_page_test.dart +++ b/flutter_news_example/test/notification_preferences/view/notification_preferences_page_test.dart @@ -2,11 +2,12 @@ import 'package:app_ui/app_ui.dart'; import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_news_example/categories/categories.dart'; import 'package:flutter_news_example/notification_preferences/notification_preferences.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; import 'package:news_repository/news_repository.dart'; import 'package:notifications_repository/notifications_repository.dart'; @@ -21,11 +22,17 @@ class MockNotificationPreferencesRepository extends Mock class MockCategoriesBloc extends Mock implements CategoriesBloc {} +class _MockGoRouterState extends Mock implements GoRouterState {} + +class _MockBuildContext extends Mock implements BuildContext {} + void main() { final NotificationPreferencesBloc bloc = MockNotificationPreferencesBloc(); final NotificationsRepository repository = MockNotificationPreferencesRepository(); final CategoriesBloc categoryBloc = MockCategoriesBloc(); + late GoRouterState goRouterState; + late BuildContext context; final entertainmentCategory = Category( id: 'entertainment', @@ -33,17 +40,23 @@ void main() { ); final healthCategory = Category(id: 'health', name: 'Health'); + setUp(() { + goRouterState = _MockGoRouterState(); + context = _MockBuildContext(); + }); + group('NotificationPreferencesPage', () { final populatedState = CategoriesState( status: CategoriesStatus.populated, categories: [entertainmentCategory, healthCategory], ); - test('has a route', () { - expect( - NotificationPreferencesPage.route(), - isA>(), - ); + testWidgets('routeBuilder builds a NotificationPreferencesPage', + (tester) async { + final page = + NotificationPreferencesPage.routeBuilder(context, goRouterState); + + expect(page, isA()); }); testWidgets('renders NotificationPreferencesView', (tester) async { diff --git a/flutter_news_example/test/onboarding/view/onboarding_page_test.dart b/flutter_news_example/test/onboarding/view/onboarding_page_test.dart index 122bf057b..91fc457a1 100644 --- a/flutter_news_example/test/onboarding/view/onboarding_page_test.dart +++ b/flutter_news_example/test/onboarding/view/onboarding_page_test.dart @@ -1,19 +1,34 @@ // ignore_for_file: prefer_const_constructors import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_news_example/app/app.dart'; import 'package:flutter_news_example/onboarding/onboarding.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mocktail/mocktail.dart'; import '../../helpers/helpers.dart'; class MockAppBloc extends MockBloc implements AppBloc {} +class _MockGoRouterState extends Mock implements GoRouterState {} + +class _MockBuildContext extends Mock implements BuildContext {} + void main() { + late GoRouterState goRouterState; + late BuildContext context; + + setUp(() { + goRouterState = _MockGoRouterState(); + context = _MockBuildContext(); + }); group('OnboardingPage', () { - test('has a page', () { - expect(OnboardingPage.page(), isA>()); + testWidgets('routeBuilder builds a OnboardingPage', (tester) async { + final page = OnboardingPage.routeBuilder(context, goRouterState); + + expect(page, isA()); }); testWidgets('renders OnboardingView', (tester) async { diff --git a/flutter_news_example/test/slideshow/view/slideshow_page_test.dart b/flutter_news_example/test/slideshow/view/slideshow_page_test.dart index 4ba64e578..0dc713b79 100644 --- a/flutter_news_example/test/slideshow/view/slideshow_page_test.dart +++ b/flutter_news_example/test/slideshow/view/slideshow_page_test.dart @@ -4,14 +4,26 @@ import 'package:app_ui/app_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_news_example/slideshow/slideshow.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:mocktail_image_network/mocktail_image_network.dart'; import 'package:news_blocks/news_blocks.dart'; import '../../helpers/helpers.dart'; +class _MockGoRouterState extends Mock implements GoRouterState {} + +class _MockBuildContext extends Mock implements BuildContext {} + void main() { initMockHydratedStorage(); + late GoRouterState goRouterState; + late BuildContext context; + + setUp(() { + goRouterState = _MockGoRouterState(); + context = _MockBuildContext(); + }); group('SlideshowPage', () { const articleId = 'articleId'; @@ -26,14 +38,14 @@ void main() { ); final slideshow = SlideshowBlock(title: 'title', slides: slides); - test('has a route', () { - expect( - SlideshowPage.route( - slideshow: slideshow, - articleId: articleId, - ), - isA>(), - ); + testWidgets('routeBuilder builds a SlideshowPage', (tester) async { + when(() => goRouterState.pathParameters).thenReturn({'id': 'id'}); + when(() => goRouterState.extra) + .thenReturn(const SlideshowBlock(title: 'title', slides: [])); + + final page = SlideshowPage.routeBuilder(context, goRouterState); + + expect(page, isA()); }); testWidgets('renders a SlideshowView', (tester) async { diff --git a/flutter_news_example/test/subscriptions/view/manage_subscription_page_test.dart b/flutter_news_example/test/subscriptions/view/manage_subscription_page_test.dart index ce6cdea3d..8110007e0 100644 --- a/flutter_news_example/test/subscriptions/view/manage_subscription_page_test.dart +++ b/flutter_news_example/test/subscriptions/view/manage_subscription_page_test.dart @@ -5,16 +5,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_news_example/app/app.dart'; import 'package:flutter_news_example/subscriptions/subscriptions.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; import 'package:mockingjay/mockingjay.dart'; import '../../helpers/helpers.dart'; class MockAppBloc extends MockBloc implements AppBloc {} +class _MockGoRouterState extends Mock implements GoRouterState {} + +class _MockBuildContext extends Mock implements BuildContext {} + void main() { + late GoRouterState goRouterState; + late BuildContext context; + + setUp(() { + goRouterState = _MockGoRouterState(); + context = _MockBuildContext(); + }); group('ManageSubscriptionPage', () { - test('has a route', () { - expect(ManageSubscriptionPage.route(), isA>()); + testWidgets('routeBuilder builds a ManageSubscriptionPage', (tester) async { + final page = ManageSubscriptionPage.routeBuilder(context, goRouterState); + + expect(page, isA()); }); testWidgets('renders ManageSubscriptionView', (tester) async { diff --git a/flutter_news_example/test/user_profile/view/user_profile_page_test.dart b/flutter_news_example/test/user_profile/view/user_profile_page_test.dart index fc71e45ea..bb3957271 100644 --- a/flutter_news_example/test/user_profile/view/user_profile_page_test.dart +++ b/flutter_news_example/test/user_profile/view/user_profile_page_test.dart @@ -28,13 +28,26 @@ class MockAppBloc extends MockBloc implements AppBloc {} class MockGoRouter extends Mock implements GoRouter {} +class _MockGoRouterState extends Mock implements GoRouterState {} + +class _MockBuildContext extends Mock implements BuildContext {} + void main() { const termsOfServiceItemKey = Key('userProfilePage_termsOfServiceItem'); late GoRouter goRouter; + late GoRouterState goRouterState; + late BuildContext context; + + setUp(() { + goRouterState = _MockGoRouterState(); + context = _MockBuildContext(); + }); group('UserProfilePage', () { - test('has a route', () { - expect(UserProfilePage.route(), isA>()); + testWidgets('routeBuilder builds a UserProfilePage', (tester) async { + final page = UserProfilePage.routeBuilder(context, goRouterState); + + expect(page, isA()); }); testWidgets('renders UserProfileView', (tester) async {