-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
Use case
canPop is a method that can be used in the build method as it reads the state of the router without changing it.
For example, a custom back button could be implemented as
class GoRouterBackButton extends StatelessWidget {
const GoRouterBackButton();
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: GoRouter.of(context).canPop()
? () {
GoRouter.of(context).pop();
}
null,
icon: const Icon(Icons.arrow_back),
),
}
}This works fine for the production code. The issue is in the test, if I do
test.pumpWidget(const MaterialApp(
home: Scaffold(
body: GoRouterBackButton(),
),
));I'll get the error
No inherited error
00:03 +0: Pump widget
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building Home(dirty):
No GoRouter found in context
'package:go_router/src/router.dart':
Failed assertion: line 300 pos 12: 'inherited != null'
The relevant error-causing widget was:
Home Home:file:///Users/valentin/Perso/Projects/flutter_app_stable/test/widget_test.dart:13:35
When the exception was thrown, this was the stack:
#2 GoRouter.of (package:go_router/src/router.dart:300:12)
#3 Home.build (package:flutter_app_stable/main.dart:39:35)
#4 StatelessElement.build (package:flutter/src/widgets/framework.dart:4949:49)
#5 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4878:15)
#6 Element.rebuild (package:flutter/src/widgets/framework.dart:4604:5)
#7 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4859:5)
#8 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4853:5)
#9 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3863:16)
#10 Element.updateChild (package:flutter/src/widgets/framework.dart:3586:20)
#11 RenderObjectToWidgetElement._rebuild (package:flutter/src/widgets/binding.dart:1195:16)
#12 RenderObjectToWidgetElement.update (package:flutter/src/widgets/binding.dart:1172:5)
#13 RenderObjectToWidgetElement.performRebuild (package:flutter/src/widgets/binding.dart:1186:7)
#14 Element.rebuild (package:flutter/src/widgets/framework.dart:4604:5)
#15 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2667:19)
#16 AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1191:19)
#17 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:378:5)
#18 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1175:15)
#19 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1104:9)
#20 AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:1057:9)
#23 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#24 AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:1043:27)
#25 WidgetTester.pumpWidget.<anonymous closure> (package:flutter_test/src/widget_tester.dart:554:22)
#28 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#29 WidgetTester.pumpWidget (package:flutter_test/src/widget_tester.dart:551:27)
#30 main.<anonymous closure> (file:///Users/valentin/Perso/Projects/flutter_app_stable/test/widget_test.dart:13:18)
#31 testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:171:29)
<asynchronous suspension>
<asynchronous suspension>
(elided 7 frames from class _AssertionError, dart:async, and package:stack_trace)
════════════════════════════════════════════════════════════════════════════════════════════════════
00:03 +0 -1: Counter increments smoke test [E]
Test failed. See exception logs above.
The test description was: Counter increments smoke test
To run this test again: /Users/valentin/flutter/flutter/bin/cache/dart-sdk/bin/dart test /Users/valentin/Perso/Projects/flutter_app_stable/test/widget_test.dart -p vm --plain-name 'Counter increments smoke test'
00:03 +0 -1: Some tests failed.
because the GoRouter.of(context) doesn't find any inherited above in the widget tree.
To fix it I can wrap the widget with InheritedGoRouter:
test.pumpWidget(MaterialApp(
home: Scaffold(
body: InheritedGoRouter(
goRouter: router,
child: const GoRouterBackButton(),
),
),
));But doing so raises another error
No context error
00:03 +0: Pump widget
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following _CastError was thrown building Home(dirty, dependencies: [InheritedGoRouter]):
Null check operator used on a null value
The relevant error-causing widget was:
Home Home:file:///Users/valentin/Perso/Projects/flutter_app_stable/test/widget_test.dart:17:22
When the exception was thrown, this was the stack:
#0 GoRouterDelegate._createNavigatorStateIterator (package:go_router/src/delegate.dart:64:68)
#1 GoRouterDelegate.canPop (package:go_router/src/delegate.dart:102:46)
#2 GoRouter.canPop (package:go_router/src/router.dart:146:36)
#3 Home.build (package:flutter_app_stable/main.dart:39:47)
#4 StatelessElement.build (package:flutter/src/widgets/framework.dart:4949:49)
#5 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4878:15)
#6 Element.rebuild (package:flutter/src/widgets/framework.dart:4604:5)
#7 ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:4859:5)
#8 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4853:5)
... Normal element mounting (7 frames)
#15 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3863:16)
#16 Element.updateChild (package:flutter/src/widgets/framework.dart:3586:20)
#17 RenderObjectToWidgetElement._rebuild (package:flutter/src/widgets/binding.dart:1195:16)
#18 RenderObjectToWidgetElement.update (package:flutter/src/widgets/binding.dart:1172:5)
#19 RenderObjectToWidgetElement.performRebuild (package:flutter/src/widgets/binding.dart:1186:7)
#20 Element.rebuild (package:flutter/src/widgets/framework.dart:4604:5)
#21 BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2667:19)
#22 AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1191:19)
#23 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:378:5)
#24 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1175:15)
#25 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1104:9)
#26 AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:1057:9)
#29 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#30 AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:1043:27)
#31 WidgetTester.pumpWidget.<anonymous closure> (package:flutter_test/src/widget_tester.dart:554:22)
#34 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41)
#35 WidgetTester.pumpWidget (package:flutter_test/src/widget_tester.dart:551:27)
#36 main.<anonymous closure> (file:///Users/valentin/Perso/Projects/flutter_app_stable/test/widget_test.dart:14:18)
#37 testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:171:29)
<asynchronous suspension>
<asynchronous suspension>
(elided 5 frames from dart:async and package:stack_trace)
════════════════════════════════════════════════════════════════════════════════════════════════════
00:03 +0 -1: Pump widget [E]
Test failed. See exception logs above.
The test description was: Pump widget
To run this test again: /Users/valentin/flutter/flutter/bin/cache/dart-sdk/bin/dart test /Users/valentin/Perso/Projects/flutter_app_stable/test/widget_test.dart -p vm --plain-name 'Pump widget'
00:04 +0 -1: Some tests failed.
because the GoRouter router was never given to the MaterialApp.router and never global key or context.
It means I have to create a MaterialApp.router(routerConfig: router) in my test, but doing so forces me to mount the entire app/page and not the specific widget I want to write a test on (and this widget might not be somewhere easily accessible from the page built).
A workaround I found was to use
extension on GoRouter {
bool safeCanPop() {
try {
return canPop();
} catch (_) {
return false;
}
}
}But I feel something better could be done at the package level to not force the user to do that.
Details
Or you can check out
https://github.com/ValentinVignal/flutter_app_stable/tree/go-router/can-pop-throws-errors
Code example
// main.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(const MyApp());
}
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const Home(),
)
],
);
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: GoRouter.of(context).canPop() // Or use router.canPop()
? () {
GoRouter.of(context).pop();
}
: null,
icon: const Icon(Icons.arrow_back),
),
ElevatedButton(
onPressed: () {
GoRouter.of(context).push('/');
},
child: const Text('Push'),
),
],
),
),
);
}
}// test.dart
import 'package:flutter_app_stable/main.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Pump widget', (WidgetTester tester) async {
await tester.pumpWidget(const Home());
// Or
await tester.pumpWidget(
InheritedGoRouter(
goRouter: router,
child: const Home(),
),
);
});
}Proposal
We could wrap the body of canPop in a try/catch (like safeCanPop) so it never throws. Maybe there is something better than that to do instead