Tour
This commit is contained in:
parent
956231048e
commit
606106e5ec
10 changed files with 632 additions and 1 deletions
|
|
@ -1,7 +1,9 @@
|
|||
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';
|
||||
|
||||
|
|
@ -102,4 +104,148 @@ void main() {
|
|||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
132
test/onboarding_tour_overlay_test.dart
Normal file
132
test/onboarding_tour_overlay_test.dart
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:fuel_tax_tracker/widgets/onboarding_tour_overlay.dart';
|
||||
|
||||
/// Unit-level coverage of the tour's presentational overlay widget,
|
||||
/// independent of MainShell's step-sequencing/navigation logic (covered
|
||||
/// in main_shell_test.dart's onboarding-tour group instead).
|
||||
void main() {
|
||||
Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child));
|
||||
|
||||
testWidgets('shows the title, description, and step count', (tester) async {
|
||||
final targetKey = GlobalKey();
|
||||
|
||||
await tester.pumpWidget(wrap(
|
||||
OnboardingTourOverlay(
|
||||
targetKey: targetKey,
|
||||
title: 'Example Title',
|
||||
description: 'Example description text.',
|
||||
stepNumber: 2,
|
||||
totalSteps: 4,
|
||||
onNext: () {},
|
||||
onSkip: () {},
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('Example Title'), findsOneWidget);
|
||||
expect(find.text('Example description text.'), findsOneWidget);
|
||||
expect(find.text('Step 2 of 4'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows "Next" before the last step and "Got It" on the last step', (tester) async {
|
||||
final targetKey = GlobalKey();
|
||||
|
||||
await tester.pumpWidget(wrap(
|
||||
OnboardingTourOverlay(
|
||||
targetKey: targetKey,
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
stepNumber: 1,
|
||||
totalSteps: 3,
|
||||
onNext: () {},
|
||||
onSkip: () {},
|
||||
),
|
||||
));
|
||||
expect(find.widgetWithText(FilledButton, 'Next'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, 'Got It'), findsNothing);
|
||||
|
||||
await tester.pumpWidget(wrap(
|
||||
OnboardingTourOverlay(
|
||||
targetKey: targetKey,
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
stepNumber: 3,
|
||||
totalSteps: 3,
|
||||
onNext: () {},
|
||||
onSkip: () {},
|
||||
),
|
||||
));
|
||||
expect(find.widgetWithText(FilledButton, 'Got It'), findsOneWidget);
|
||||
expect(find.widgetWithText(FilledButton, 'Next'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('invokes onNext/onSkip when their buttons are tapped', (tester) async {
|
||||
var nextTapped = false;
|
||||
var skipTapped = false;
|
||||
final targetKey = GlobalKey();
|
||||
|
||||
await tester.pumpWidget(wrap(
|
||||
OnboardingTourOverlay(
|
||||
targetKey: targetKey,
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
stepNumber: 1,
|
||||
totalSteps: 2,
|
||||
onNext: () => nextTapped = true,
|
||||
onSkip: () => skipTapped = true,
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.widgetWithText(TextButton, 'Skip Tour'));
|
||||
expect(skipTapped, isTrue);
|
||||
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Next'));
|
||||
expect(nextTapped, isTrue);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'renders without crashing when the target key has no RenderObject yet — falls back to a '
|
||||
'centered card', (tester) async {
|
||||
final targetKey = GlobalKey(); // never attached to anything
|
||||
|
||||
await tester.pumpWidget(wrap(
|
||||
OnboardingTourOverlay(
|
||||
targetKey: targetKey,
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
stepNumber: 1,
|
||||
totalSteps: 1,
|
||||
onNext: () {},
|
||||
onSkip: () {},
|
||||
),
|
||||
));
|
||||
|
||||
expect(find.text('T'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tapping the dimmed barrier does not invoke onNext or onSkip', (tester) async {
|
||||
var nextTapped = false;
|
||||
var skipTapped = false;
|
||||
final targetKey = GlobalKey();
|
||||
|
||||
await tester.pumpWidget(wrap(
|
||||
OnboardingTourOverlay(
|
||||
targetKey: targetKey,
|
||||
title: 'T',
|
||||
description: 'D',
|
||||
stepNumber: 1,
|
||||
totalSteps: 1,
|
||||
onNext: () => nextTapped = true,
|
||||
onSkip: () => skipTapped = true,
|
||||
),
|
||||
));
|
||||
|
||||
// Top-left corner — away from the card, which sits mid/lower-screen.
|
||||
await tester.tapAt(const Offset(5, 5));
|
||||
await tester.pump();
|
||||
|
||||
expect(nextTapped, isFalse);
|
||||
expect(skipTapped, isFalse);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue