import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; /// Verifies that re-tapping the active bottom navigation tab pops any /// Navigator-pushed sub-screens back to the branch root. /// /// This exercises the same pattern used by [MainShellScreen]: each /// [StatefulShellBranch] has a [GlobalKey] and the shell /// calls `popUntil((route) => route.isFirst)` when the active tab is /// tapped again. void main() { group('Bottom navigation tab re-tap', () { late GlobalKey homeNavKey; late GlobalKey settingsNavKey; late GoRouter router; setUp(() { homeNavKey = GlobalKey(debugLabel: 'home'); settingsNavKey = GlobalKey(debugLabel: 'settings'); router = GoRouter( initialLocation: '/home', routes: [ StatefulShellRoute.indexedStack( builder: (context, state, shell) { return _TestShell( navigationShell: shell, branchNavigatorKeys: [homeNavKey, settingsNavKey], ); }, branches: [ // Branch 0: Home (dummy, needed to satisfy >= 2 destinations) StatefulShellBranch( navigatorKey: homeNavKey, routes: [ GoRoute( path: '/home', builder: (context, state) => const Scaffold( body: Center(child: Text('Home')), ), ), ], ), // Branch 1: Settings StatefulShellBranch( navigatorKey: settingsNavKey, routes: [ GoRoute( path: '/settings', builder: (context, state) => const _SettingsMainScreen(), ), ], ), ], ), ], ); }); tearDown(() { router.dispose(); }); testWidgets( 'tapping active tab pops Navigator-pushed sub-screen to root', (tester) async { await tester.pumpWidget(MaterialApp.router(routerConfig: router)); await tester.pumpAndSettle(); // Navigate to Settings tab. await tester.tap(find.text('Settings')); await tester.pumpAndSettle(); // Main settings screen is visible. expect(find.text('Settings Main'), findsOneWidget); expect(find.text('Sub Settings'), findsNothing); // Push a sub-settings screen via Navigator (same as the real app). await tester.tap(find.text('Open Sub-Settings')); await tester.pumpAndSettle(); // Sub-settings screen is now visible; main is not. expect(find.text('Sub Settings'), findsOneWidget); expect(find.text('Settings Main'), findsNothing); // Re-tap the Settings tab in the bottom navigation. await tester.tap(find.text('Settings')); await tester.pumpAndSettle(); // We should be back at the main settings screen. expect(find.text('Settings Main'), findsOneWidget); expect(find.text('Sub Settings'), findsNothing); }, ); testWidgets( 'tapping active tab when already at root is a no-op', (tester) async { await tester.pumpWidget(MaterialApp.router(routerConfig: router)); await tester.pumpAndSettle(); // Navigate to Settings tab. await tester.tap(find.text('Settings')); await tester.pumpAndSettle(); expect(find.text('Settings Main'), findsOneWidget); // Re-tap the already-active tab without pushing any sub-screen. await tester.tap(find.text('Settings')); await tester.pumpAndSettle(); // Still on the main settings screen. expect(find.text('Settings Main'), findsOneWidget); }, ); testWidgets( 'tapping active tab pops multiple stacked sub-screens', (tester) async { await tester.pumpWidget(MaterialApp.router(routerConfig: router)); await tester.pumpAndSettle(); // Navigate to Settings tab. await tester.tap(find.text('Settings')); await tester.pumpAndSettle(); // Push first sub-screen. await tester.tap(find.text('Open Sub-Settings')); await tester.pumpAndSettle(); expect(find.text('Sub Settings'), findsOneWidget); // Push a second sub-screen on top. await tester.tap(find.text('Go Deeper')); await tester.pumpAndSettle(); expect(find.text('Deep Settings'), findsOneWidget); // Re-tap the tab — should pop all the way back to root. await tester.tap(find.text('Settings')); await tester.pumpAndSettle(); expect(find.text('Settings Main'), findsOneWidget); expect(find.text('Sub Settings'), findsNothing); expect(find.text('Deep Settings'), findsNothing); }, ); }); } // --------------------------------------------------------------------------- // Test helper widgets // --------------------------------------------------------------------------- /// Minimal shell that replicates [MainShellScreen]'s re-tap behavior. class _TestShell extends StatelessWidget { final StatefulNavigationShell navigationShell; final List> branchNavigatorKeys; const _TestShell({ required this.navigationShell, required this.branchNavigatorKeys, }); @override Widget build(BuildContext context) { return Scaffold( body: navigationShell, bottomNavigationBar: NavigationBar( selectedIndex: navigationShell.currentIndex, onDestinationSelected: (index) { // Same logic as MainShellScreen._onDestinationSelected if (index == navigationShell.currentIndex) { final navigatorKey = branchNavigatorKeys[index]; navigatorKey.currentState?.popUntil((route) => route.isFirst); } navigationShell.goBranch( index, initialLocation: index == navigationShell.currentIndex, ); }, destinations: const [ NavigationDestination( icon: Icon(Icons.home), label: 'Home', ), NavigationDestination( icon: Icon(Icons.settings), label: 'Settings', ), ], ), ); } } /// Main settings screen with a button to push a sub-screen. class _SettingsMainScreen extends StatelessWidget { const _SettingsMainScreen(); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('Settings Main'), ElevatedButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const _SubSettingsScreen(), ), ); }, child: const Text('Open Sub-Settings'), ), ], ), ), ); } } /// Sub-settings screen pushed via Navigator (like AppearanceSettingsScreen). class _SubSettingsScreen extends StatelessWidget { const _SubSettingsScreen(); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text('Sub Settings'), ElevatedButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute( builder: (_) => const _DeepSettingsScreen(), ), ); }, child: const Text('Go Deeper'), ), ], ), ), ); } } /// A second level sub-screen for testing multi-level pop. class _DeepSettingsScreen extends StatelessWidget { const _DeepSettingsScreen(); @override Widget build(BuildContext context) { return const Scaffold( body: Center(child: Text('Deep Settings')), ); } }