Search code examples
fluttertestingbloc

How can I test that Flutter BlocListener navigates and renders an initial Welcome page


I am trying to test that a WelcomePage widget is found when my App is first run and no authentication has yet happened.

No matter what I try, I cannot verify that the widget gets rendered.

The test fails every time. I am using expect(find.byType(WelcomePage), findsOneWidget);

Error:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: exactly one matching node in the widget tree
Actual: _WidgetTypeFinder:<zero widgets with type "WelcomePage" (ignoring offstage widgets)>
Which: means none were found but one was expected

Test Code:

class MockAuthenticationBloc
    extends MockBloc<AuthenticationEvent, AuthenticationState>
    implements AuthenticationBloc {}

void main() {
  group('App', () {
    late AuthenticationBloc authenticationBloc;

    setUp(() {
      authenticationBloc = MockAuthenticationBloc();
    });

    group('Unauthenticated', () {
      testWidgets('displays the WelcomeScreen', (tester) async {
        when(() => authenticationBloc.state)
            .thenReturn(const AuthenticationState.unauthenticated());
        await tester.pumpApp(
          Scaffold(
            body: BlocProvider.value(
              value: authenticationBloc,
              child: const AppView(),
            ),
          ),
        );

        expect(find.byType(WelcomePage), findsOneWidget);
      });
    });
  });
}

app.dart

class App extends StatelessWidget {
  const App({
    Key? key,
    required this.authenticationRepository,
    required this.userRepository,
  }) : super(key: key);

  final AuthenticationRepository authenticationRepository;
  final UserRepository userRepository;

  @override
  Widget build(BuildContext context) {
    return RepositoryProvider.value(
      value: authenticationRepository,
      child: BlocProvider(
        create: (_) => AuthenticationBloc(
          authenticationRepository: authenticationRepository,
          userRepository: userRepository,
        ),
        child: const AppView(),
      ),
    );
  }
}

class AppView extends StatefulWidget {
  const AppView({Key? key}) : super(key: key);

  @override
  // ignore: library_private_types_in_public_api
  _AppViewState createState() => _AppViewState();
}

class _AppViewState extends State<AppView> {
  final _navigatorKey = GlobalKey<NavigatorState>();

  NavigatorState get _navigator => _navigatorKey.currentState!;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: _navigatorKey,
      theme: ThemeData(
        appBarTheme:
            const AppBarTheme(color: Color.fromARGB(255, 214, 39, 185)),
        colorScheme: ColorScheme.fromSwatch(
          accentColor: const Color.fromARGB(255, 241, 136, 255),
        ),
      ),
      localizationsDelegates: const [
        AppLocalizations.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: AppLocalizations.supportedLocales,
      builder: (context, child) {
        return BlocListener<AuthenticationBloc, AuthenticationState>(
          listener: (context, state) {
            switch (state.status) {
              case AuthenticationStatus.authenticated:
                _navigator.pushAndRemoveUntil<void>(
                  HomePage.route(),
                  (route) => false,
                );
                break;
              case AuthenticationStatus.unauthenticated:
                _navigator.pushAndRemoveUntil<void>(
                  WelcomePage.route(),
                  (route) => false,
                );
                break;
              case AuthenticationStatus.unknown:
                break;
            }
          },
          child: child,
        );
      },
      onGenerateRoute: (_) => SplashPage.route(),
    );
  }
}

Solution

  • I think you have to wait for your widget to build for a few frames and settle so add await tester.pumpAndSettle(); :

    ...
            await tester.pumpApp(
              Scaffold(
                body: BlocProvider.value(
                  value: authenticationBloc,
                  child: const AppView(),
                ),
              ),
            );
    // HERE
    await tester.pumpAndSettle();
    // HERE
            expect(find.byType(WelcomePage), findsOneWidget);
          });
        });
      });
    }
    
    

    That way you let flutter build the necessary frames for the blocListener to execute and display the appropiate page.