MO-Fuel-Tax-Back/test/main_shell_test.dart
2026-08-15 19:23:03 -05:00

251 lines
9.5 KiB
Dart

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<Text>(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<AppState> 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 step 1 (Receipts) automatically on first launch', (tester) async {
await pumpShellWithFreshTour(tester);
expect(find.text('Add Receipts Here'), findsOneWidget);
expect(find.text('Step 1 of 4'), findsOneWidget);
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('Add Receipts Here'), findsNothing);
});
testWidgets('tapping the dimmed barrier does not dismiss the tour or reach the highlighted button',
(tester) async {
await pumpShellWithFreshTour(tester);
// 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 four steps, switching tabs and pushing Settings > Data',
(tester) async {
await pumpShellWithFreshTour(tester);
expect(find.text('Add Receipts Here'), 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 2 of 4'), 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.widgetWithText(AppBar, 'Fuel Report'), findsOneWidget);
await tester.tap(find.widgetWithText(FilledButton, 'Next'));
await tester.pumpAndSettle();
expect(find.text('Back Up to the Cloud'), findsOneWidget);
expect(find.text('Step 4 of 4'), 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 < 3; 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);
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('Add Receipts Here'), findsNothing);
expect(appState.hasSeenOnboardingTour, isTrue);
expect(find.widgetWithText(AppBar, 'Receipts'), findsOneWidget,
reason: 'stayed on the tab it started on');
});
testWidgets('Skip Tour from the last step still pops the tour-pushed Data screen',
(tester) async {
await pumpShellWithFreshTour(tester);
for (var i = 0; i < 3; 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);
});
});
}