From 3a3a2857d9959aa2b3749ec4ea8705a6f8873e91 Mon Sep 17 00:00:00 2001 From: Henry Domingo Jr Date: Fri, 5 Jun 2026 20:14:45 +0800 Subject: [PATCH 1/2] add test --- .../app_booking_flow_test.dart | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 compass_app/app/integration_test/app_booking_flow_test.dart diff --git a/compass_app/app/integration_test/app_booking_flow_test.dart b/compass_app/app/integration_test/app_booking_flow_test.dart new file mode 100644 index 00000000000..7795feb6f15 --- /dev/null +++ b/compass_app/app/integration_test/app_booking_flow_test.dart @@ -0,0 +1,145 @@ +import 'package:compass_app/config/dependencies.dart'; +import 'package:compass_app/data/repositories/auth/auth_repository.dart'; +import 'package:compass_app/main.dart'; +import 'package:compass_app/ui/activities/widgets/activities_screen.dart'; +import 'package:compass_app/ui/auth/login/widgets/login_screen.dart'; +import 'package:compass_app/ui/booking/widgets/booking_screen.dart'; +import 'package:compass_app/ui/core/ui/custom_checkbox.dart'; +import 'package:compass_app/ui/core/ui/home_button.dart'; +import 'package:compass_app/ui/home/widgets/home_screen.dart'; +import 'package:compass_app/ui/results/widgets/result_card.dart'; +import 'package:compass_app/ui/results/widgets/results_screen.dart'; +import 'package:compass_app/ui/search_form/widgets/search_form_guests.dart'; +import 'package:compass_app/ui/search_form/widgets/search_form_screen.dart'; +import 'package:compass_app/ui/search_form/widgets/search_form_submit.dart'; +import 'package:compass_app/utils/result.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +// mocked AuthRepository to control the authentication state in the test and trigger UI changes. +class TestAuthRepository extends AuthRepository { + bool _authenticated = false; + + @override + Future get isAuthenticated => Future.value(_authenticated); + + @override + Future> login({ + required String email, + required String password, + }) async { + _authenticated = true; + notifyListeners(); + return const Result.ok(null); + } + + @override + Future> logout() async { + _authenticated = false; + notifyListeners(); + return const Result.ok(null); + } +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('end-to-end booking flow', () { + testWidgets( + 'starts logged out, creates itinerary, and go back to home screen', + (tester) async { + // clears data to ensure the app starts logged out, then logs in with a test auth repository. + final sharedPreferences = await SharedPreferences.getInstance(); + await sharedPreferences.clear(); + final testAuthRepository = TestAuthRepository(); + + await tester.pumpWidget( + MultiProvider( + providers: [ + ...providersLocal, + ChangeNotifierProvider.value( + value: + testAuthRepository, // override the AuthRepository with our test implementation + ), + ], + child: const MainApp(), + ), + ); + await tester.pumpAndSettle(); + + // Auth-gated entry with local data. + expect(find.byType(LoginScreen), findsOneWidget); + await tester.tap(find.text('Login')); + await tester.pumpAndSettle(); + + expect(find.byType(HomeScreen), findsOneWidget); + + // Select create new booking + await tester.tap(find.byKey(const ValueKey(bookingButtonKey))); + await tester.pumpAndSettle(); + expect(find.byType(SearchFormScreen), findsOneWidget); + + // Search destinations screen + await tester.tap(find.text('Europe'), warnIfMissed: false); + + await tester.tap(find.text('Add Dates')); + await tester.pumpAndSettle(); + final tomorrow = DateTime.now().add(const Duration(days: 1)).day; + final nextDay = DateTime.now().add(const Duration(days: 2)).day; + await tester.tap(find.text(tomorrow.toString()).first); + await tester.pumpAndSettle(); + await tester.tap(find.text(nextDay.toString()).first); + await tester.pumpAndSettle(); + await tester.tap(find.text('Save')); + await tester.pumpAndSettle(); + + // Select guests + await tester.tap(find.byKey(const ValueKey(addGuestsKey))); + await tester.pumpAndSettle(); + + // Perform search and navigate to next screen + await tester.tap(find.byKey(const ValueKey(searchFormSubmitButtonKey))); + await tester.pumpAndSettle(); + + // Results Screen + expect(find.byType(ResultsScreen), findsOneWidget); + + // Select a destination + final firstResultCardFinder = find.byType(ResultCard).first; + expect(firstResultCardFinder, findsOneWidget); + final selectedDestination = tester + .widget(firstResultCardFinder) + .destination; + await tester.tap(firstResultCardFinder); + await tester.pumpAndSettle(); + + // Activities Screen + expect(find.byType(ActivitiesScreen), findsOneWidget); + + // Select one activity + await tester.tap(find.byType(CustomCheckbox).first); + await tester.pumpAndSettle(); + expect(find.text('1 selected'), findsOneWidget); + + await tester.tap(find.byKey(const ValueKey(confirmButtonKey))); + await tester.pumpAndSettle(); + expect(find.byType(BookingScreen), findsOneWidget); + expect(find.text(selectedDestination.name), findsOneWidget); + + // Verify booking is visible on Home. + await tester.tap(find.byType(HomeButton).first); + await tester.pumpAndSettle(); + expect(find.byType(HomeScreen), findsOneWidget); + expect( + find.text( + '${selectedDestination.name}, ${selectedDestination.continent}', + ), + findsOneWidget, + ); + }, + ); + }); +} From df999bd1b3891e311ee6b0b882fd4465e61e722b Mon Sep 17 00:00:00 2001 From: Henry Domingo Jr Date: Fri, 5 Jun 2026 20:14:57 +0800 Subject: [PATCH 2/2] revise README --- compass_app/README.md | 79 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/compass_app/README.md b/compass_app/README.md index 05ed7670aa4..f29692bb1a1 100644 --- a/compass_app/README.md +++ b/compass_app/README.md @@ -54,6 +54,85 @@ cd app $ flutter test integration_test/app_server_data_test.dart ``` +**Integration test with auth-gated local flow** + +```bash +cd app +$ flutter test integration_test/app_booking_flow_test.dart +``` + Running the tests together with `flutter test integration_test` will fail. See: https://github.com/flutter/flutter/issues/101031 +## Part 2 Answers + +### 1) State -> UI trace (user taps one booking item on Home) + +- Tap on a booking row triggers `_Booking.onTap` and navigates via `Routes.bookingWithId`. +- Inside the booking `GoRoute` builder in `router.dart`, the booking ID is parsed, the `BookingViewModel` is created, and `viewModel.loadBooking.execute(id)` is called before rendering `BookingScreen`. +- In `booking_viewmodel.dart`, the load command fetches data through the repository abstraction (`BookingRepository`). +- In the remote implementation (`BookingRepositoryRemote.getBooking`), the repository composes booking details using API calls (destinations and destination activities). +- Back in the view model, `Result` (`Ok`/`Error`) is handled and UI state is updated. +- In `booking_screen.dart`, `ListenableBuilder` reacts to command state, showing loading while running and body content when completed. +- In `booking_body.dart`, once booking data is available, header and activities are rendered. + +### 2) Routing and auth gate + +- In `router.dart`, `GoRouter` is configured with `redirect` and `refreshListenable`. +- `AuthRepository` extends `ChangeNotifier`, so auth changes are observable by the router. +- On `login()` and `logout()`, the auth repo calls `notifyListeners()`. +- That refresh triggers `_redirect()` immediately, which enforces route guards: + - unauthenticated user -> `/login` + - authenticated user on `/login` -> `/home` + - otherwise no redirect + +### 3) Data boundary + +- UI/domain layers depend on repository interfaces (for example `BookingRepository`) rather than direct HTTP/local services. +- Remote path: repositories call `ApiClient` for network requests. +- Local/dummy path: repositories read from `LocalDataService`/memory-backed sources. +- Provider selection happens in `dependencies.dart` (`providersRemote` vs `providersLocal`), and each flavor main entrypoint chooses the provider set. +- This design is testable because view models/use cases receive injected interfaces, so tests can supply fakes/mocks. + +### 4) Possible enhancement + +- Add API-boundary caching for read-heavy GET endpoints. +- Strategy: return cached data first, refresh in background, and invalidate with TTL/auth changes. +- Add metrics/logging to validate hit rate, latency reduction, and stale-data behavior. + +### app_booking_flow_test.dart + +`integration_test/app_booking_flow_test.dart` is an end-to-end integration test that validates: + +- auth-gated entry (starts logged out and lands on Login) +- login action +- full booking creation flow (search -> destination -> activity confirm) +- booking visibility after returning Home + +Approach used: + +- It runs on `providersLocal` for deterministic local data. +- It overrides only `AuthRepository` with a test implementation (`TestAuthRepository`) to simulate logged-out -> logged-in transitions while still exercising the real navigation, view models, and local repositories. + +Run command: + +```bash +cd app +flutter test integration_test/app_booking_flow_test.dart +``` + +## Part 4 Answers + +### 1) What was the hardest part of this codebase to understand, and how did you figure it out? + +- It was the chosen state management. While it wasn't hard to decipher, all of the Flutter projects that I handled were using BLoC/Cubit pattern and so I'm more used to it. I still have yet to figure it out completely but I have a good overview. Two things. First I started with the API calls then tracing them up to the repos then VMs then to widgets and screens. Second, I checked/mapped out the builders in the widgets which is similar to the BlocBuilders in Cubit. + +### 2) If you owned this app for the next year, what's the first thing you'd change? + +- Since this app already has a solid foundation code-wise, I'll prepare for better scaling. It would be a set of changes though and not a single one. Changes would be like implementing CI/CD, more helpful logging (ideally utiilizing Firebase Crashlytics), and API resilience (timeouts, retries, etc.). + +### 3) What three questions would you ask the team before starting? + +- Do we have some technical or feature scope overview documentation? +- How's the current feedback of our app in production (from end-users and key stakeholders)? +- What are the high priority goals for the next couple of months? \ No newline at end of file