Search code examples
dartflutterflutter-test

How to test navigation via Navigator in Flutter


Let's say, I have a test for a screen in Flutter using WidgetTester. There is a button, which executes a navigation via Navigator. I would like to test behavior of that button.

Widget/Screen

class MyScreen extends StatefulWidget {

  MyScreen({Key key}) : super(key: key);

  @override
  _MyScreenState createState() => _MyScreenScreenState();
}

class _MyScreenState extends State<MyScreen> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: RaisedButton(
                onPressed: () {
                    Navigator.of(context).pushNamed("/nextscreen");
                },
                child: Text(Strings.traktTvUrl)
            )
        )
    );
  }

}

Test

void main() {

  testWidgets('Button is present and triggers navigation after tapped',
      (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(home: MyScreen()));
    expect(find.byType(RaisedButton), findsOneWidget);
    await tester.tap(find.byType(RaisedButton));
    //how to test navigator?    
  });
}

I there a proper way how to check, that Navigator was called? Or is there a way to mock and replace navigator?

Pleas note, that code above will actually fail with an exception, because there is no named route '/nextscreen' declared in application. That's simple to solve and you don't need to point it out.

My main concern is how to correctly approach this test scenario in Flutter.


Solution

  • In the navigator tests in the flutter repo they use the NavigatorObserver class to observe navigations:

    class TestObserver extends NavigatorObserver {
      OnObservation onPushed;
      OnObservation onPopped;
      OnObservation onRemoved;
      OnObservation onReplaced;
    
      @override
      void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
        if (onPushed != null) {
          onPushed(route, previousRoute);
        }
      }
    
      @override
      void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
        if (onPopped != null) {
          onPopped(route, previousRoute);
        }
      }
    
      @override
      void didRemove(Route<dynamic> route, Route<dynamic> previousRoute) {
        if (onRemoved != null)
          onRemoved(route, previousRoute);
      }
    
      @override
      void didReplace({ Route<dynamic> oldRoute, Route<dynamic> newRoute }) {
        if (onReplaced != null)
          onReplaced(newRoute, oldRoute);
      }
    }
    

    This looks like it should do what you want, however it may only work from the top level (MaterialApp), I'm not sure if you can provide it to just a widget.