auto_route_library
auto_route_library copied to clipboard
How to create Widget tests.
Hi, I'm struggling to find a clever way to write Widget Test with auto_route included. I've searched the entire issues for some help and only managed to work with #745. The solution that was suggested there worked for my login screen but going deeper to the home screen or any other one I tried to test unfortunately failed.
The following assertion was thrown building AutoTabsScaffold:
RouteData operation requested with a context that does not include an RouteData.
The context used to retrieve the RouteData must be that of a widget that is a descendant of a
AutoRoutePage.
Test:
@GenerateMocks([StackRouter])
void main() {
group('Home Screen Test - ', () {
late final StackRouter mockStackRouter;
setUpAll(() {
mockStackRouter = MockStackRouter();
});
testWidgets('Home Screen has BottomNavigation', (tester) async {
await tester.pumpWidget(giveWidgetMaterialAncestor(
router: mockStackRouter,
widgetToTest: const HomeScreen(),
));
expect(find.byType(BottomNavigationBar), findsOneWidget);
});
});
}
HomeScreen:
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
static const List<PageRouteInfo> _homeRoutes = [
ProfileRouter(),
DevicesRouter(),
RecentTransfersRouter(),
];
@override
Widget build(BuildContext context) => AutoTabsScaffold(
navigatorObservers: () => [AutoRouteObserver()],
routes: _homeRoutes,
bottomNavigationBuilder: (context, tabsRouter) =>
_bottomNavigationBarBuilder(context, tabsRouter),
);
BottomNavigationBar _bottomNavigationBarBuilder(
BuildContext context,
TabsRouter tabsRouter,
) =>
BottomNavigationBar(
currentIndex: tabsRouter.activeIndex,
onTap: tabsRouter.setActiveIndex,
items: [
_navigationBarItem(
context,
HomeScreenPageType.profile,
tabsRouter.current.name == ProfileRouter.name,
),
_navigationBarItem(
context,
HomeScreenPageType.devices,
tabsRouter.current.name == DevicesRouter.name,
),
_navigationBarItem(
context,
HomeScreenPageType.transfers,
tabsRouter.current.name == RecentTransfersRouter.name,
),
],
);
BottomNavigationBarItem _navigationBarItem(
BuildContext context,
HomeScreenPageType itemType,
bool isActive,
) =>
BottomNavigationBarItem(
icon: HomeBottomNavigationBarItemProvider.getItemIconFromType(
itemType: itemType,
isActive: isActive,
),
label: HomeBottomNavigationBarItemProvider.getLabelFromType(
itemType: itemType,
context: context,
),
);
}
Helper method for providing MaterialApp
:
Widget giveWidgetMaterialAncestor({
required StackRouter router,
required Widget widgetToTest,
}) =>
MaterialApp(
supportedLocales: LocalizationConfig.supportedLocales,
localizationsDelegates: LocalizationConfig.localizationDelegates,
theme: LightTheme().themeData,
home: StackRouterScope(
controller: router,
stateHash: 0,
child: widgetToTest,
),
);
As mentioned this approach was successful for the Login Screen all tests for it:
@GenerateMocks([
AuthRepository,
StackRouter,
])
void main() {
group('Login Screen Tests - ', () {
late final AuthRepository mockAuthRepository;
late final StackRouter mockStackRouter;
setUpAll(() {
mockAuthRepository = MockAuthRepository();
mockStackRouter = MockStackRouter();
});
testWidgets('Login Failed', (tester) async {
when(mockAuthRepository.signIn()).thenAnswer(
(invocation) async => Result.failure(const UnexpectedFailure()),
);
when(mockAuthRepository.hasValidUserSession).thenAnswer(
(invocation) => false,
);
await tester.pumpWidget(giveWidgetMaterialAncestorWithBloc(
router: mockStackRouter,
widgetToTest: const LoginScreen(),
cubit: LoginCubit(authRepository: mockAuthRepository),
));
await tester.tap(find.byKey(const ValueKey('loginButton')));
await tester.pumpAndSettle();
expect(find.byType(Dialog), findsOneWidget);
});
testWidgets('Login Aborted', (tester) async {
when(mockAuthRepository.signIn()).thenAnswer(
(invocation) async => Result.failure(const SignInAbortedFailure()),
);
when(mockAuthRepository.hasValidUserSession).thenAnswer(
(invocation) => false,
);
await tester.pumpWidget(giveWidgetMaterialAncestorWithBloc(
router: mockStackRouter,
widgetToTest: const LoginScreen(),
cubit: LoginCubit(authRepository: mockAuthRepository),
));
await tester.tap(find.byKey(const ValueKey('loginButton')));
await tester.pumpAndSettle();
expect(find.byType(Dialog), findsNothing);
});
testWidgets('Login Successful', (tester) async {
when(mockAuthRepository.signIn()).thenAnswer(
(invocation) async => Result.success(FakeSessionInfo()),
);
when(mockAuthRepository.hasValidUserSession).thenAnswer(
(invocation) => false,
);
when(mockStackRouter.replace(const HomeRoute())).thenAnswer(
(invocation) async => () {},
);
await tester.pumpWidget(giveWidgetMaterialAncestorWithBloc(
router: mockStackRouter,
widgetToTest: const LoginScreen(),
cubit: LoginCubit(authRepository: mockAuthRepository),
));
await tester.tap(find.byKey(const ValueKey('loginButton')));
await tester.pumpAndSettle();
verify(mockStackRouter.replace(const HomeRoute()));
});
});
}
Mocks created by mockito, I'm strongly looking for some docs/tutorials on how to create widget tests with this package
I'm on the same boat right now, without auto_routes router tests find my widgets, but when I try to use it none of the tests find any widget. If someone has resources on how to set this up greatly appreciated.
+1
I've found a way to mock auto_route using get_it
Call the router like this in your widget
getIt<AppRouter>().push(const ScreenHome()));
// intead of
context.router.push(const ScreenHome()));
main.dart
GetIt getIt = GetIt.instance;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
getIt.registerSingleton<AppRouter>(
AppRouter(
checkIfAuthenticated: CheckIfAuthenticated(),
),
);
runApp(initApp()); // child: MyApp()
}
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
final router = getIt<AppRouter>();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: AutoRouterDelegate(router),
routeInformationParser: router.defaultRouteParser(),
);
}
}
widget_test.dart
import 'package:projectname/routes/router.gr.dart' as router;
import 'package:projectname/screens/log_in.dart';
class AppRouterMock extends Mock implements router.AppRouter {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
WidgetsFlutterBinding.ensureInitialized();
setUpAll(() {
final appRouterMock = AppRouterMock();
getIt.registerSingleton<router.AppRouter>(appRouterMock);
});
testWidgets('Should goto home',
(WidgetTester tester) async {
// Given
when(() => getIt<router.AppRouter>().push(
const router.ScreenHome(),
)).thenAnswer((_) async => {});
// When
await tester.pumpWidget(
const MaterialApp(
home: ScreenLogIn(),
),
);
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
await tester.pump(Duration(seconds: 2));
// Then
verify(() => getIt<router.AppRouter>().push(const router.ScreenHome()));
});
}
Here is a exemple how to use get_it with widget tests : https://github.com/TheSmartMonkey/flutter-http-widget-test
+1
@Maqcel seems i faced the same problem with AutoTabsScaffold, and successfully proceed to the next step with
return MultiProvider(
providers: [
...AppDependencies.getProviders(),
],
child: Portal(
child: MaterialApp.router(
theme: AppTheme.buildTheme(const DesignSystem.light()),
darkTheme: AppTheme.buildTheme(const DesignSystem.dark()),
routeInformationParser: router.defaultRouteParser(),
routerDelegate: router.delegate(),
themeMode: ThemeMode.light,
supportedLocales: I18n.supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
],
),
),
);
but stuck with error, that provider can't find one of my dependencies in child page (one of route from routes list in AutoTabsScaffold)
BlocProvider.of() called with a context that does not contain a Bloc of type
SampleBlocType
What i want to achieve - it's not a track of transition between the pages, my target - golden test of page which contain multiple tabs.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions
The issue with injecting auto_route into DI (get_it) is that you can't pop from a nested route.
Navigating to a nested page (a page within BottomNavigationBar
) with
getIt<AppRouter>().push(const ScreenHome()));
works fine until you want to return from ScreenHome
using
getIt<AppRouter>().pop<bool>(false);
The docs mention the following:
navigating without context is not recommended in nested navigation unless you use navigate instead of push and you provide a full hierarchy. e.g router.navigate(SecondRoute(children: [SubChild2Route()]))
Using navigate instead of push means that you can't return a value. The only solution I can see now is to use callbacks on the child page.