diff --git a/.gitignore b/.gitignore index af0395a..be5dba8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,8 @@ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +# .vscode/ +.vscode/settings.json # Flutter/Dart/Pub related **/doc/api/ diff --git a/lib/mock_navigator.dart b/lib/mock_navigator.dart index f9db659..3d3125a 100644 --- a/lib/mock_navigator.dart +++ b/lib/mock_navigator.dart @@ -23,7 +23,9 @@ class MockNavigatorProvider extends Navigator { @override NavigatorState createState() { - return _MockNavigatorState(navigator: navigator)..child = child; + // The hack that makes it all work. + // ignore: no_logic_in_create_state + return _MockNavigatorState(navigator).._child = child; } @override @@ -54,18 +56,214 @@ mixin MockNavigatorDiagnosticsMixin on Object { } } +/// Internal class that imitates a [NavigatorState] and maps all the real +/// [NavigatorState] methods to the mock methods for use in testing. class _MockNavigatorState extends NavigatorState { - _MockNavigatorState({required this.navigator}); + _MockNavigatorState(this._navigator); - MockNavigatorBase navigator; - Widget? child; + final MockNavigatorBase _navigator; + Widget? _child; @override - Future push(Route route) => navigator.push(route); + Widget build(BuildContext context) => _child!; @override - void pop([T? result]) => navigator.pop(result); + Future push(Route route) { + return _navigator.push(route); + } + + @override + Future pushNamed( + String routeName, { + Object? arguments, + }) { + return _navigator.pushNamed( + routeName, + arguments: arguments, + ); + } + + @override + Future pushNamedAndRemoveUntil( + String newRouteName, + RoutePredicate predicate, { + Object? arguments, + }) { + return _navigator.pushNamedAndRemoveUntil( + newRouteName, + predicate, + arguments: arguments, + ); + } + + @override + Future pushReplacement( + Route newRoute, { + TO? result, + }) { + return _navigator.pushReplacement( + newRoute, + result: result, + ); + } + + @override + Future pushReplacementNamed( + String routeName, { + TO? result, + Object? arguments, + }) { + return _navigator.pushReplacementNamed( + routeName, + result: result, + arguments: arguments, + ); + } + + @override + void pop([T? result]) { + return _navigator.pop(result); + } + + @override + Future popAndPushNamed( + String routeName, { + TO? result, + Object? arguments, + }) { + return _navigator.popAndPushNamed( + routeName, + result: result, + arguments: arguments, + ); + } + + @override + void popUntil(RoutePredicate predicate) { + return _navigator.popUntil(predicate); + } + + @override + Future pushAndRemoveUntil( + Route newRoute, + RoutePredicate predicate, + ) { + return _navigator.pushAndRemoveUntil( + newRoute, + predicate, + ); + } @override - Widget build(BuildContext context) => child!; + String restorablePopAndPushNamed( + String routeName, { + TO? result, + Object? arguments, + }) { + return _navigator.restorablePopAndPushNamed( + routeName, + result: result, + arguments: arguments, + ); + } + + @override + String restorablePush( + RestorableRouteBuilder routeBuilder, { + Object? arguments, + }) { + return _navigator.restorablePush( + routeBuilder, + arguments: arguments, + ); + } + + @override + String restorablePushAndRemoveUntil( + RestorableRouteBuilder newRouteBuilder, + RoutePredicate predicate, { + Object? arguments, + }) { + return _navigator.restorablePushAndRemoveUntil( + newRouteBuilder, + predicate, + arguments: arguments, + ); + } + + @override + String restorablePushNamed( + String routeName, { + Object? arguments, + }) { + return _navigator.restorablePushNamed( + routeName, + arguments: arguments, + ); + } + + @override + String restorablePushNamedAndRemoveUntil( + String newRouteName, + RoutePredicate predicate, { + Object? arguments, + }) { + return _navigator.restorablePushNamedAndRemoveUntil( + newRouteName, + predicate, + arguments: arguments, + ); + } + + @override + String restorablePushReplacement( + RestorableRouteBuilder routeBuilder, { + TO? result, + Object? arguments, + }) { + return _navigator.restorablePushReplacement( + routeBuilder, + result: result, + arguments: arguments, + ); + } + + @override + String restorablePushReplacementNamed( + String routeName, { + TO? result, + Object? arguments, + }) { + return _navigator.restorablePushReplacementNamed( + routeName, + result: result, + arguments: arguments, + ); + } + + @override + String restorableReplace({ + required Route oldRoute, + required RestorableRouteBuilder newRouteBuilder, + Object? arguments, + }) { + return _navigator.restorableReplace( + oldRoute: oldRoute, + newRouteBuilder: newRouteBuilder, + arguments: arguments, + ); + } + + @override + String restorableReplaceRouteBelow({ + required Route anchorRoute, + required RestorableRouteBuilder newRouteBuilder, + Object? arguments, + }) { + return _navigator.restorableReplaceRouteBelow( + anchorRoute: anchorRoute, + newRouteBuilder: newRouteBuilder, + arguments: arguments, + ); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 11e2dea..bd38dd1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - coverage: 0.15.2 - very_good_analysis: 2.0.3 - mocktail: 0.1.1 + mocktail: ^0.1.4 + very_good_analysis: 2.1.2 diff --git a/test/mock_navigator_test.dart b/test/mock_navigator_test.dart index f9593cc..bb29f75 100644 --- a/test/mock_navigator_test.dart +++ b/test/mock_navigator_test.dart @@ -11,12 +11,20 @@ class MockNavigator extends Mock class FakeRoute extends Fake implements Route {} extension on WidgetTester { - Future pumpTest(Widget widget) async { + Future pumpTest({ + required MockNavigatorBase navigator, + required WidgetBuilder builder, + }) async { await pumpWidget( MaterialApp( title: 'Mock Navigator Test', home: Scaffold( - body: widget, + body: MockNavigatorProvider( + navigator: navigator, + child: Builder( + builder: builder, + ), + ), ), ), ); @@ -27,6 +35,21 @@ void main() { group('MockNavigator', () { late MockNavigatorBase navigator; + const testRouteName = '__test_route__'; + final testRoute = MaterialPageRoute( + settings: const RouteSettings( + name: testRouteName, + ), + builder: (_) => const Text(testRouteName), + ); + bool testRoutePredicate(Route _) => false; + Route restorableTestRouteBuilder( + BuildContext context, + Object? arguments, + ) { + return testRoute; + } + setUpAll(() { registerFallbackValue>(FakeRoute()); }); @@ -36,38 +59,332 @@ void main() { }); testWidgets('mocks .push calls', (tester) async { - var pushCalled = 0; - - when(() => navigator.push(any())).thenAnswer((_) async { - pushCalled++; - return null; - }); - - await tester.pumpTest( - MockNavigatorProvider( - navigator: navigator, - child: Builder( - builder: (context) { - return TextButton( - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => const Text('I should not build.'), - ), - ); - }, - child: const Text('Trigger'), - ); - }, + when(() => navigator.push(any())).thenAnswer((_) async {}); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).push(testRoute), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify(() => navigator.push(testRoute)).called(1); + }); + + testWidgets('mocks .pushNamed calls', (tester) async { + when(() => navigator.pushNamed(any())).thenAnswer((_) async {}); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).pushNamed(testRouteName), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify(() => navigator.pushNamed(testRouteName)).called(1); + }); + + testWidgets('mocks .pushNamedAndRemoveUntil calls', (tester) async { + when(() => navigator.pushNamedAndRemoveUntil(any(), any())) + .thenAnswer((_) async {}); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).pushNamedAndRemoveUntil( + testRouteName, + testRoutePredicate, + ), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify( + () => navigator.pushNamedAndRemoveUntil( + testRouteName, + testRoutePredicate, + ), + ).called(1); + }); + + testWidgets('mocks .pushReplacement calls', (tester) async { + when(() => navigator.pushReplacement(any())).thenAnswer((_) async {}); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).pushReplacement(testRoute), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify(() => navigator.pushReplacement(testRoute)).called(1); + }); + + testWidgets('mocks .pushReplacementNamed calls', (tester) async { + when(() => navigator.pushReplacementNamed(any())) + .thenAnswer((_) async {}); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => + Navigator.of(context).pushReplacementNamed(testRouteName), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify(() => navigator.pushReplacementNamed(testRouteName)).called(1); + }); + + testWidgets('mocks .pop calls', (tester) async { + when(() => navigator.pop(any())).thenAnswer((_) async {}); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).pop(testRoute), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify(() => navigator.pop(testRoute)).called(1); + }); + + testWidgets('mocks .popAndPushNamed calls', (tester) async { + when(() => navigator.popAndPushNamed(any())).thenAnswer((_) async {}); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).popAndPushNamed(testRouteName), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify(() => navigator.popAndPushNamed(testRouteName)).called(1); + }); + + testWidgets('mocks .pushAndRemoveUntil calls', (tester) async { + when(() => navigator.pushAndRemoveUntil(any(), any())) + .thenAnswer((_) async {}); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context) + .pushAndRemoveUntil(testRoute, testRoutePredicate), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify( + () => navigator.pushAndRemoveUntil( + testRoute, + testRoutePredicate, + ), + ).called(1); + }); + + testWidgets('mocks .restorablePopAndPushNamed calls', (tester) async { + when(() => navigator.restorablePopAndPushNamed(any())) + .thenReturn(testRouteName); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => + Navigator.of(context).restorablePopAndPushNamed(testRouteName), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify(() => navigator.restorablePopAndPushNamed(testRouteName)) + .called(1); + }); + + testWidgets('mocks .restorablePush calls', (tester) async { + when(() => navigator.restorablePush(any())).thenReturn(testRouteName); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => + Navigator.of(context).restorablePush(restorableTestRouteBuilder), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify(() => navigator.restorablePush(restorableTestRouteBuilder)) + .called(1); + }); + + testWidgets('mocks .restorablePushAndRemoveUntil calls', (tester) async { + when(() => navigator.restorablePushAndRemoveUntil(any(), any())) + .thenReturn(testRouteName); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).restorablePushAndRemoveUntil( + restorableTestRouteBuilder, + testRoutePredicate, ), + child: const Text('Trigger'), ), ); await tester.tap(find.byType(TextButton)); + verify( + () => navigator.restorablePushAndRemoveUntil( + restorableTestRouteBuilder, + testRoutePredicate, + ), + ).called(1); + }); + + testWidgets('mocks .restorablePushNamed calls', (tester) async { + when(() => navigator.restorablePushNamed(any())) + .thenReturn(testRouteName); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => + Navigator.of(context).restorablePushNamed(testRouteName), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify(() => navigator.restorablePushNamed(testRouteName)).called(1); + }); + + testWidgets('mocks .restorablePushNamedAndRemoveUntil calls', + (tester) async { + when(() => navigator.restorablePushNamedAndRemoveUntil(any(), any())) + .thenReturn(testRouteName); - expect(pushCalled, equals(1)); + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => + Navigator.of(context).restorablePushNamedAndRemoveUntil( + testRouteName, + testRoutePredicate, + ), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify( + () => navigator.restorablePushNamedAndRemoveUntil( + testRouteName, + testRoutePredicate, + ), + ).called(1); }); - // TODO: Add 'mocks .pop calls' test + testWidgets('mocks .restorablePushReplacement calls', (tester) async { + when(() => navigator.restorablePushReplacement(any())) + .thenReturn(testRouteName); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context) + .restorablePushReplacement(restorableTestRouteBuilder), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify( + () => navigator.restorablePushReplacement(restorableTestRouteBuilder), + ).called(1); + }); + + testWidgets('mocks .restorablePushReplacementNamed calls', (tester) async { + when(() => navigator.restorablePushReplacementNamed(any())) + .thenReturn(testRouteName); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context) + .restorablePushReplacementNamed(testRouteName), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify(() => navigator.restorablePushReplacementNamed(testRouteName)) + .called(1); + }); + + testWidgets('mocks .restorableReplace calls', (tester) async { + when(() => navigator.restorableReplace( + oldRoute: any(named: 'oldRoute'), + newRouteBuilder: any(named: 'newRouteBuilder'), + )).thenReturn(testRouteName); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).restorableReplace( + oldRoute: testRoute, + newRouteBuilder: restorableTestRouteBuilder, + ), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify( + () => navigator.restorableReplace( + oldRoute: testRoute, + newRouteBuilder: restorableTestRouteBuilder, + ), + ).called(1); + }); + + testWidgets('mocks .restorableReplaceRouteBelow calls', (tester) async { + when(() => navigator.restorableReplaceRouteBelow( + anchorRoute: any(named: 'anchorRoute'), + newRouteBuilder: any(named: 'newRouteBuilder'), + )).thenReturn(testRouteName); + + await tester.pumpTest( + navigator: navigator, + builder: (context) => TextButton( + onPressed: () => Navigator.of(context).restorableReplaceRouteBelow( + anchorRoute: testRoute, + newRouteBuilder: restorableTestRouteBuilder, + ), + child: const Text('Trigger'), + ), + ); + + await tester.tap(find.byType(TextButton)); + verify( + () => navigator.restorableReplaceRouteBelow( + anchorRoute: testRoute, + newRouteBuilder: restorableTestRouteBuilder, + ), + ).called(1); + }); }); }