import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:fuel_tax_tracker/screens/data_settings_screen.dart'; import 'package:fuel_tax_tracker/screens/main_shell.dart'; import 'package:fuel_tax_tracker/services/app_state.dart'; /// Confirms the bottom nav actually switches between the four tabs and /// that each one builds without crashing against a freshly-constructed, /// not-yet-synced [AppState] — the same "no cloud connected, no vehicles /// yet" state a real fresh install starts in. void main() { testWidgets('bottom nav switches between all four tabs', (tester) async { final appState = AppState()..isLoading = false; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: MainShell()), ), ); // Receipts is the default tab. expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget); await tester.tap(find.widgetWithText(NavigationDestination, 'Vehicles')); await tester.pumpAndSettle(); expect(find.widgetWithText(AppBar, 'Vehicles'), findsOneWidget); await tester.tap(find.widgetWithText(NavigationDestination, 'Reports')); await tester.pumpAndSettle(); expect(find.widgetWithText(AppBar, 'Fuel Report'), findsOneWidget); await tester.tap(find.widgetWithText(NavigationDestination, 'Settings')); await tester.pumpAndSettle(); expect(find.widgetWithText(AppBar, 'Settings'), findsOneWidget); await tester.tap(find.widgetWithText(NavigationDestination, 'Receipts')); await tester.pumpAndSettle(); expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget); }); testWidgets('Receipts tab shows an empty state with no fuel entries yet', (tester) async { final appState = AppState()..isLoading = false; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: MainShell()), ), ); expect(find.textContaining('No fuel entries'), findsOneWidget); }); testWidgets('a right-to-left swipe on the body advances to the next tab', (tester) async { final appState = AppState()..isLoading = false; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: MainShell()), ), ); expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget); await tester.fling(find.byType(PageView), const Offset(-400, 0), 1000); await tester.pumpAndSettle(); expect(find.widgetWithText(AppBar, 'Vehicles'), findsOneWidget); // And a left-to-right swipe goes back. await tester.fling(find.byType(PageView), const Offset(400, 0), 1000); await tester.pumpAndSettle(); expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget); }); testWidgets( "swiping away from Receipts and back preserves its selected date range " '(AutomaticKeepAliveClientMixin keeping the tab alive across the PageView, ' 'not just the old IndexedStack)', (tester) async { final appState = AppState()..isLoading = false; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: MainShell()), ), ); await tester.tap(find.text('Last 3 Months')); await tester.pumpAndSettle(); // Swipe all the way to Settings and back. await tester.tap(find.widgetWithText(NavigationDestination, 'Settings')); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(NavigationDestination, 'Receipts')); await tester.pumpAndSettle(); final selectedLabel = tester.widget(find.text('Last 3 Months')); expect((selectedLabel.style?.fontWeight), FontWeight.w800, reason: 'still the active range'); }); group('first-launch onboarding tour', () { setUp(() { SharedPreferences.setMockInitialValues({}); }); Future pumpShellWithFreshTour(WidgetTester tester) async { // AppState.hasSeenOnboardingTour defaults to true precisely so that // ad-hoc AppState() construction elsewhere (every other test in this // file included) doesn't trigger the tour — these tests explicitly // flip it to false to reproduce a real fresh install's state. final appState = AppState() ..isLoading = false ..hasSeenOnboardingTour = false; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: MainShell()), ), ); await tester.pumpAndSettle(); return appState; } testWidgets('shows the "What This App Does" intro automatically on first launch, with the ' 'refund-rate and lawmakers disclaimer', (tester) async { await pumpShellWithFreshTour(tester); expect(find.text('What This App Does'), findsOneWidget); expect(find.text('Step 1 of 6'), findsOneWidget); expect(find.textContaining('12.5¢'), findsOneWidget); expect(find.textContaining('Disclaimer'), findsOneWidget); expect(find.textContaining('Missouri lawmakers'), findsOneWidget); // No specific element to spotlight for the intro — stays on // whichever tab MainShell defaults to. expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget); }); testWidgets('does not show the tour once hasSeenOnboardingTour is true', (tester) async { final appState = AppState() ..isLoading = false ..hasSeenOnboardingTour = true; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: MainShell()), ), ); await tester.pumpAndSettle(); expect(find.text('What This App Does'), findsNothing); }); testWidgets('tapping the dimmed barrier does not dismiss the tour or reach the highlighted button', (tester) async { await pumpShellWithFreshTour(tester); // Advance to the Receipts step, which is the one with an actual // highlighted, tappable element (the "+") behind the barrier. await tester.tap(find.widgetWithText(FilledButton, 'Next')); await tester.pumpAndSettle(); expect(find.text('Add Receipts Here'), findsOneWidget); // Top-left corner, away from the card and from the highlighted "+" // (which sits top-right in the AppBar). await tester.tapAt(const Offset(10, 10)); await tester.pumpAndSettle(); expect(find.text('Add Receipts Here'), findsOneWidget, reason: 'tour still showing'); expect(find.widgetWithText(AppBar, 'Add Vehicle'), findsNothing, reason: "the receipt '+' — which opens vehicle creation when there are no " "vehicles yet — was not actually reached"); }); testWidgets( 'Next advances through all six steps, switching tabs, then pushing Settings > Data', (tester) async { await pumpShellWithFreshTour(tester); expect(find.text('What This App Does'), findsOneWidget); expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget); await tester.tap(find.widgetWithText(FilledButton, 'Next')); await tester.pumpAndSettle(); expect(find.text('Add Receipts Here'), findsOneWidget); expect(find.text('Step 2 of 6'), findsOneWidget); expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget); await tester.tap(find.widgetWithText(FilledButton, 'Next')); await tester.pumpAndSettle(); expect(find.text('Add Vehicles Here'), findsOneWidget); expect(find.text('Step 3 of 6'), findsOneWidget); expect(find.widgetWithText(AppBar, 'Vehicles'), findsOneWidget); await tester.tap(find.widgetWithText(FilledButton, 'Next')); await tester.pumpAndSettle(); expect(find.text('Choose a Report Date Range'), findsOneWidget); expect(find.text('Step 4 of 6'), findsOneWidget); expect(find.widgetWithText(AppBar, 'Fuel Report'), findsOneWidget); await tester.tap(find.widgetWithText(FilledButton, 'Next')); await tester.pumpAndSettle(); expect(find.text('Find Data Settings'), findsOneWidget); expect(find.text('Step 5 of 6'), findsOneWidget); expect(find.widgetWithText(AppBar, 'Settings'), findsOneWidget); expect(find.byType(DataSettingsScreen), findsNothing, reason: 'still on the Settings landing page, not pushed into Data yet'); await tester.tap(find.widgetWithText(FilledButton, 'Next')); await tester.pumpAndSettle(); expect(find.text('Back Up to the Cloud'), findsOneWidget); expect(find.text('Step 6 of 6'), findsOneWidget); expect(find.byType(DataSettingsScreen), findsOneWidget, reason: 'tour pushed Settings > Data'); expect(find.text('Cloud Storage'), findsOneWidget); expect(find.widgetWithText(FilledButton, 'Got It'), findsOneWidget, reason: 'last step reads "Got It", not "Next"'); }); testWidgets( 'finishing on the last step pops the tour-pushed Data screen and marks the tour seen', (tester) async { final appState = await pumpShellWithFreshTour(tester); for (var i = 0; i < 5; i++) { await tester.tap(find.widgetWithText(FilledButton, 'Next')); await tester.pumpAndSettle(); } expect(find.byType(DataSettingsScreen), findsOneWidget); await tester.tap(find.widgetWithText(FilledButton, 'Got It')); await tester.pumpAndSettle(); expect(find.byType(DataSettingsScreen), findsNothing, reason: 'tour-pushed screen popped'); expect(find.text('Back Up to the Cloud'), findsNothing, reason: 'overlay gone'); expect(appState.hasSeenOnboardingTour, isTrue); expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget, reason: 'returned to Receipts, not left on the Settings tab the tour ended on'); final prefs = await SharedPreferences.getInstance(); expect(prefs.getBool('has_seen_onboarding_tour'), isTrue, reason: 'persisted so the tour never shows again on this device'); }); testWidgets('Skip Tour on the very first step ends the tour immediately and marks it seen', (tester) async { final appState = await pumpShellWithFreshTour(tester); await tester.tap(find.widgetWithText(TextButton, 'Skip Tour')); await tester.pumpAndSettle(); expect(find.text('What This App Does'), findsNothing); expect(appState.hasSeenOnboardingTour, isTrue); expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget); }); testWidgets('Skip Tour from a later step still returns to Receipts, not the tab it was on', (tester) async { await pumpShellWithFreshTour(tester); // Two steps in, the tour has already jumped to the Vehicles tab. await tester.tap(find.widgetWithText(FilledButton, 'Next')); await tester.pumpAndSettle(); await tester.tap(find.widgetWithText(FilledButton, 'Next')); await tester.pumpAndSettle(); expect(find.widgetWithText(AppBar, 'Vehicles'), findsOneWidget); await tester.tap(find.widgetWithText(TextButton, 'Skip Tour')); await tester.pumpAndSettle(); expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget); }); testWidgets('Skip Tour from the last step pops the tour-pushed Data screen and returns to ' 'Receipts', (tester) async { await pumpShellWithFreshTour(tester); for (var i = 0; i < 5; i++) { await tester.tap(find.widgetWithText(FilledButton, 'Next')); await tester.pumpAndSettle(); } expect(find.byType(DataSettingsScreen), findsOneWidget); await tester.tap(find.widgetWithText(TextButton, 'Skip Tour')); await tester.pumpAndSettle(); expect(find.byType(DataSettingsScreen), findsNothing); expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget); }); }); }