import 'package:flutter/material.dart'; import 'package:flutter/services.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/main.dart'; import 'package:fuel_tax_tracker/screens/faq_screen.dart'; import 'package:fuel_tax_tracker/screens/main_shell.dart'; import 'package:fuel_tax_tracker/screens/user_agreement_screen.dart'; import 'package:fuel_tax_tracker/services/app_state.dart'; /// Covers [UserAgreementScreen] — the liability gate `main.dart`'s /// `AppRoot` shows before [MainShell] until [AppState.hasAcceptedUserAgreement] /// is true, so a fresh install sees it before anything else, including the /// onboarding tour. void main() { setUp(() { SharedPreferences.setMockInitialValues({}); }); Future pumpScreen(WidgetTester tester) async { // AppState.hasAcceptedUserAgreement defaults to true precisely so that // ad-hoc AppState() construction in every other screen's tests doesn't // trigger this gate — these tests explicitly flip it to false to // reproduce a real fresh install's state. final appState = AppState() ..isLoading = false ..hasAcceptedUserAgreement = false; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: UserAgreementScreen()), ), ); return appState; } testWidgets('shows the data-loss liability section and both actions', (tester) async { await pumpScreen(tester); await tester.scrollUntilVisible( find.text('We Are Not Responsible for Your Data or Any Data Loss'), 300, scrollable: find.byType(Scrollable)); expect(find.text('We Are Not Responsible for Your Data or Any Data Loss'), findsOneWidget); expect(find.textContaining('developer is not responsible for your data'), findsOneWidget); expect(find.widgetWithText(FilledButton, 'I Agree'), findsOneWidget); expect(find.widgetWithText(OutlinedButton, 'Decline'), findsOneWidget); }); testWidgets('shows the privacy section disclosing no tracking and AdMob-only data collection', (tester) async { await pumpScreen(tester); expect(find.text('Privacy'), findsOneWidget); expect(find.textContaining("don't track or log your data"), findsOneWidget); expect(find.textContaining('Google AdMob'), findsOneWidget); }); testWidgets( 'shows both refund types and a not-tax-advice / consult-a-professional disclaimer', (tester) async { await pumpScreen(tester); await tester.scrollUntilVisible(find.text('Not Tax or Legal Advice'), 300, scrollable: find.byType(Scrollable)); expect(find.text('Not Tax or Legal Advice'), findsOneWidget); expect(find.textContaining('12.5¢ per gallon for highway use'), findsOneWidget); expect(find.textContaining('29.5¢ per gallon for non-highway use'), findsOneWidget); expect(find.textContaining('Nothing here is tax or legal advice'), findsOneWidget); expect(find.textContaining('consult a tax professional'), findsOneWidget); }); testWidgets('See the FAQ for more opens FaqScreen', (tester) async { await pumpScreen(tester); await tester.scrollUntilVisible(find.text('See the FAQ for more'), 200, scrollable: find.byType(Scrollable)); await tester.tap(find.text('See the FAQ for more')); await tester.pumpAndSettle(); expect(find.byType(FaqScreen), findsOneWidget); }); testWidgets('I Agree marks the agreement accepted and persists it', (tester) async { final appState = await pumpScreen(tester); await tester.tap(find.widgetWithText(FilledButton, 'I Agree')); await tester.pumpAndSettle(); expect(appState.hasAcceptedUserAgreement, isTrue); final prefs = await SharedPreferences.getInstance(); expect(prefs.getBool('has_accepted_user_agreement'), isTrue, reason: 'persisted so the agreement never shows again on this device'); }); testWidgets('Decline asks the platform to close the app, without accepting', (tester) async { final appState = await pumpScreen(tester); final calls = []; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(SystemChannels.platform, (call) async { calls.add(call); return null; }); addTearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(SystemChannels.platform, null); }); await tester.tap(find.widgetWithText(OutlinedButton, 'Decline')); await tester.pumpAndSettle(); expect(calls.map((c) => c.method), contains('SystemNavigator.pop')); expect(appState.hasAcceptedUserAgreement, isFalse); }); group('isReview mode (reached from Settings, not the first-launch gate)', () { // Pushed on top of a placeholder route (rather than set as `home` // directly) so there's actually something to pop back to — matching // how it's really reached (pushed from Settings) and letting the // automaticallyImplyLeading back button behave realistically. Future pumpReview(WidgetTester tester) async { final appState = AppState()..isLoading = false; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: MaterialApp( home: Builder( builder: (context) => Scaffold( body: Center( child: ElevatedButton( onPressed: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const UserAgreementScreen(isReview: true)), ), child: const Text('open'), ), ), ), ), ), ), ); await tester.tap(find.text('open')); await tester.pumpAndSettle(); } testWidgets('hides Decline/I Agree and shows the content with a back button instead', (tester) async { await pumpReview(tester); expect(find.widgetWithText(FilledButton, 'I Agree'), findsNothing); expect(find.widgetWithText(OutlinedButton, 'Decline'), findsNothing); expect(find.byType(BackButton), findsOneWidget); expect( find.text('This is the agreement you accepted when you first opened Show Me ' 'The Fuel Refund.'), findsOneWidget, ); // Same legal content still shown, just not gating anything. await tester.scrollUntilVisible( find.text('We Are Not Responsible for Your Data or Any Data Loss'), 300, scrollable: find.byType(Scrollable)); expect(find.text('We Are Not Responsible for Your Data or Any Data Loss'), findsOneWidget); }); }); group('AppRoot gating', () { Future pumpRoot(WidgetTester tester, {required bool hasAcceptedUserAgreement}) async { final appState = AppState() ..isLoading = false ..hasAcceptedUserAgreement = hasAcceptedUserAgreement; await tester.pumpWidget( ChangeNotifierProvider.value( value: appState, child: const MaterialApp(home: AppRoot()), ), ); return appState; } testWidgets('shows UserAgreementScreen instead of MainShell before it is accepted', (tester) async { await pumpRoot(tester, hasAcceptedUserAgreement: false); expect(find.byType(UserAgreementScreen), findsOneWidget); expect(find.byType(MainShell), findsNothing); }); testWidgets('shows MainShell once the agreement is accepted', (tester) async { final appState = await pumpRoot(tester, hasAcceptedUserAgreement: false); await tester.tap(find.widgetWithText(FilledButton, 'I Agree')); await tester.pumpAndSettle(); expect(appState.hasAcceptedUserAgreement, isTrue); expect(find.byType(UserAgreementScreen), findsNothing); expect(find.byType(MainShell), findsOneWidget); }); testWidgets('shows MainShell directly when already accepted (returning user)', (tester) async { await pumpRoot(tester, hasAcceptedUserAgreement: true); expect(find.byType(UserAgreementScreen), findsNothing); expect(find.byType(MainShell), findsOneWidget); }); }); }