264 lines
8.1 KiB
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')),
|
|
);
|
|
}
|
|
}
|