MO-Fuel-Tax-Back/test/user_agreement_screen_test.dart

211 lines
8.3 KiB
Dart

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<AppState> 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.ensureVisible(find.text('See the FAQ for more'));
await tester.pumpAndSettle();
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 = <MethodCall>[];
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<void> 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<AppState> 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);
});
});
}