worldhopper/test/navigation/tab_retap_pops_to_root_test.dart

264 lines
8.1 KiB
Dart

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<NavigatorState>] 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<NavigatorState> homeNavKey;
late GlobalKey<NavigatorState> settingsNavKey;
late GoRouter router;
setUp(() {
homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'home');
settingsNavKey = GlobalKey<NavigatorState>(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<GlobalKey<NavigatorState>> 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')),
);
}
}