Compass App: Integration tests and image error handling (#2389)

This PR goes on top of PR #2385 adding integration test using the
`integration_test` package.

Adds `integration_test` folder with two test suits:
- Local test: Uses the local dependency config that pulls data from the
assets folder and has no login logic.
- Remote test: Starts the dart server in the background and uses the
remote dependency config, pulls data from the server and performs
login/logout.

To run the tests:

```
flutter test integration_test/app_server_data_test.dart
```
or
```
flutter test integration_test/app_local_data_test.dart
```

Running both at once with `flutter test integration_test` will likely
fail, seems this issue is related:
https://github.com/flutter/flutter/issues/101031

Also, this PR fixes exceptions being thrown by the network image
library, now instead they get logged using the app `Logger`.

## Pre-launch Checklist

- [x] I read the [Flutter Style Guide] _recently_, and have followed its
advice.
- [x] I signed the [CLA].
- [x] I read the [Contributors Guide].
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-devrel
channel on [Discord].

<!-- Links -->
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md
[CLA]: https://cla.developers.google.com/
[Discord]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md
[Contributors Guide]:
https://github.com/flutter/samples/blob/main/CONTRIBUTING.md
pull/2444/head
Miguel Beltran 3 months ago committed by GitHub
parent bb58c63be5
commit 56bf31fa21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,3 +1,22 @@
# compass_app # compass_app
A new Flutter project. A new Flutter project.
## Integration Tests
Run separately with:
**Integration tests with local data**
```
flutter test integration_test/app_local_data_test.dart
```
**Integration tests with background server and remote data**
```
flutter test integration_test/app_server_data_test.dart
```
Running the tests together with `flutter test integration_test` will fail.
See: https://github.com/flutter/flutter/issues/101031

@ -0,0 +1,96 @@
import 'package:compass_app/config/dependencies.dart';
import 'package:compass_app/main.dart';
import 'package:compass_app/ui/activities/widgets/activities_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/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_screen.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';
/// This Integration Test launches the Compass-App with the local configuration.
/// The app uses data from the assets folder to create a booking.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end-to-end test with local data', () {
testWidgets('should load app', (tester) async {
// Load app widget.
await tester.pumpWidget(
MultiProvider(
providers: providersLocal,
child: const MainApp(),
),
);
});
testWidgets('Create booking', (tester) async {
// Load app widget with local configuration
await tester.pumpWidget(
MultiProvider(
providers: providersLocal,
child: const MainApp(),
),
);
await tester.pumpAndSettle();
// Search destinations screen
expect(find.byType(SearchFormScreen), findsOneWidget);
// Select Europe because it is always the first result
await tester.tap(find.text('Europe'), warnIfMissed: false);
// Select dates
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;
// Select first and last widget that matches today number
//and tomorrow number, sort of ensures a valid range
await tester.tap(find.text(tomorrow.toString()).first);
await tester.tap(find.text(nextDay.toString()).last);
await tester.pumpAndSettle();
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
// Select guests
await tester.tap(find.byKey(const ValueKey('add_guests')),
warnIfMissed: false);
// Refresh screen state
await tester.pumpAndSettle();
// Perform search and navigate to next screen
await tester.tap(find.byKey(const ValueKey('submit_button')));
await tester.pumpAndSettle(const Duration(seconds: 2));
// Results Screen
expect(find.byType(ResultsScreen), findsOneWidget);
// Amalfi Coast should be the first result for Europe
// Tap and navigate to activities screen
await tester.tap(find.byType(ResultCard).first);
await tester.pumpAndSettle(const Duration(seconds: 2));
// 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);
// Submit selection
await tester.tap(find.byKey(const ValueKey('confirm-button')));
await tester.pumpAndSettle(const Duration(seconds: 2));
// Should be at booking screen
expect(find.byType(BookingScreen), findsOneWidget);
expect(find.text('Amalfi Coast'), findsOneWidget);
});
});
}

@ -0,0 +1,147 @@
import 'dart:io';
import 'package:compass_app/config/dependencies.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/auth/logout/widgets/logout_button.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/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_screen.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';
/// This Integration Test starts the Dart server
/// before launching the Compass-App with the remote configuration.
/// The app connects to its endpoints to perform login and create a booking.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end-to-end test with remote data', () {
final port = '8080';
late Process p;
setUpAll(() async {
// Clear any stored shared preferences
final sharedPreferences = await SharedPreferences.getInstance();
await sharedPreferences.clear();
// Start the dart server
p = await Process.start(
'dart',
['run', 'bin/compass_server.dart'],
environment: {'PORT': port},
// Relative to the app/ folder
workingDirectory: '../server',
);
// Wait for server to start and print to stdout.
await p.stdout.first;
});
tearDownAll(() => p.kill());
testWidgets('should load app', (tester) async {
// Load app widget.
await tester.pumpWidget(
MultiProvider(
providers: providersRemote,
child: const MainApp(),
),
);
await tester.pumpAndSettle();
// Login screen because logget out
expect(find.byType(LoginScreen), findsOneWidget);
});
testWidgets('Create booking', (tester) async {
// Load app widget with local configuration
await tester.pumpWidget(
MultiProvider(
providers: providersRemote,
child: const MainApp(),
),
);
await tester.pumpAndSettle();
// Login screen because logget out
expect(find.byType(LoginScreen), findsOneWidget);
// Perform login (credentials are prefilled)
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
// Search destinations screen
expect(find.byType(SearchFormScreen), findsOneWidget);
// Select Europe because it is always the first result
await tester.tap(find.text('Europe'), warnIfMissed: false);
// Select dates
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;
// Select first and last widget that matches today number
//and tomorrow number, sort of ensures a valid range
await tester.tap(find.text(tomorrow.toString()).first);
await tester.tap(find.text(nextDay.toString()).last);
await tester.pumpAndSettle();
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
// Select guests
await tester.tap(find.byKey(const ValueKey('add_guests')),
warnIfMissed: false);
// Refresh screen state
await tester.pumpAndSettle();
// Perform search and navigate to next screen
await tester.tap(find.byKey(const ValueKey('submit_button')));
await tester.pumpAndSettle(const Duration(seconds: 2));
// Results Screen
expect(find.byType(ResultsScreen), findsOneWidget);
// Amalfi Coast should be the first result for Europe
// Tap and navigate to activities screen
await tester.tap(find.byType(ResultCard).first);
await tester.pumpAndSettle(const Duration(seconds: 2));
// 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);
// Submit selection
await tester.tap(find.byKey(const ValueKey('confirm-button')));
await tester.pumpAndSettle(const Duration(seconds: 2));
// Should be at booking screen
expect(find.byType(BookingScreen), findsOneWidget);
expect(find.text('Amalfi Coast'), findsOneWidget);
// Navigate back to home
await tester.tap(find.byType(HomeButton).first);
await tester.pumpAndSettle();
expect(find.byType(SearchFormScreen), findsOneWidget);
// Perform logout
await tester.tap(find.byType(LogoutButton).first);
await tester.pumpAndSettle();
expect(find.byType(LoginScreen), findsOneWidget);
});
});
}

@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:compass_model/model.dart'; import 'package:compass_model/model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../utils/image_error_listener.dart';
import '../../core/ui/custom_checkbox.dart'; import '../../core/ui/custom_checkbox.dart';
class ActivityEntry extends StatelessWidget { class ActivityEntry extends StatelessWidget {
@ -28,6 +29,7 @@ class ActivityEntry extends StatelessWidget {
imageUrl: activity.imageUrl, imageUrl: activity.imageUrl,
height: 80, height: 80,
width: 80, width: 80,
errorListener: imageErrorListener,
), ),
), ),
const SizedBox(width: 20), const SizedBox(width: 20),

@ -2,6 +2,8 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import '../../../../utils/image_error_listener.dart';
class TiltedCards extends StatelessWidget { class TiltedCards extends StatelessWidget {
const TiltedCards({super.key}); const TiltedCards({super.key});
@ -79,6 +81,7 @@ class _Card extends StatelessWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
color: showTitle ? Colors.black.withOpacity(0.5) : null, color: showTitle ? Colors.black.withOpacity(0.5) : null,
colorBlendMode: showTitle ? BlendMode.darken : null, colorBlendMode: showTitle ? BlendMode.darken : null,
errorListener: imageErrorListener,
), ),
if (showTitle) Center(child: SvgPicture.asset('assets/logo.svg')), if (showTitle) Center(child: SvgPicture.asset('assets/logo.svg')),
], ],

@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:compass_model/model.dart'; import 'package:compass_model/model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../utils/image_error_listener.dart';
import '../../core/themes/dimens.dart'; import '../../core/themes/dimens.dart';
import '../view_models/booking_viewmodel.dart'; import '../view_models/booking_viewmodel.dart';
import 'booking_header.dart'; import 'booking_header.dart';
@ -64,6 +65,7 @@ class _Activity extends StatelessWidget {
imageUrl: activity.imageUrl, imageUrl: activity.imageUrl,
height: 80, height: 80,
width: 80, width: 80,
errorListener: imageErrorListener,
), ),
), ),
const SizedBox(width: 20), const SizedBox(width: 20),

@ -3,6 +3,7 @@ import 'package:compass_model/model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../utils/image_error_listener.dart';
import '../../core/localization/applocalization.dart'; import '../../core/localization/applocalization.dart';
import '../../core/themes/colors.dart'; import '../../core/themes/colors.dart';
import '../../core/themes/dimens.dart'; import '../../core/themes/dimens.dart';
@ -173,6 +174,7 @@ class _HeaderImage extends StatelessWidget {
return CachedNetworkImage( return CachedNetworkImage(
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
imageUrl: booking.destination.imageUrl, imageUrl: booking.destination.imageUrl,
errorListener: imageErrorListener,
); );
} }
} }

@ -2,6 +2,7 @@ import 'package:compass_model/model.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../utils/image_error_listener.dart';
import '../../core/themes/text_styles.dart'; import '../../core/themes/text_styles.dart';
import '../../core/ui/tag_chip.dart'; import '../../core/ui/tag_chip.dart';
@ -26,6 +27,7 @@ class ResultCard extends StatelessWidget {
imageUrl: destination.imageUrl, imageUrl: destination.imageUrl,
fit: BoxFit.fitHeight, fit: BoxFit.fitHeight,
errorWidget: (context, url, error) => const Icon(Icons.error), errorWidget: (context, url, error) => const Icon(Icons.error),
errorListener: imageErrorListener,
), ),
Positioned( Positioned(
bottom: 12.0, bottom: 12.0,

@ -3,6 +3,7 @@ import 'package:compass_model/model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../../utils/image_error_listener.dart';
import '../../core/localization/applocalization.dart'; import '../../core/localization/applocalization.dart';
import '../../core/themes/colors.dart'; import '../../core/themes/colors.dart';
import '../../core/themes/dimens.dart'; import '../../core/themes/dimens.dart';
@ -100,6 +101,7 @@ class _CarouselItem extends StatelessWidget {
CachedNetworkImage( CachedNetworkImage(
imageUrl: imageUrl, imageUrl: imageUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
errorListener: imageErrorListener,
errorWidget: (context, url, error) { errorWidget: (context, url, error) {
// NOTE: Getting "invalid image data" error for some of the images // NOTE: Getting "invalid image data" error for some of the images
// e.g. https://rstr.in/google/tripedia/jlbgFDrSUVE // e.g. https://rstr.in/google/tripedia/jlbgFDrSUVE

@ -0,0 +1,7 @@
import 'package:logging/logging.dart';
final _log = Logger('ImageErrorListener');
void imageErrorListener(Object error) {
_log.warning('Failed to load image', error);
}

@ -29,6 +29,8 @@ dev_dependencies:
flutter_lints: ^4.0.0 flutter_lints: ^4.0.0
mocktail_image_network: ^1.2.0 mocktail_image_network: ^1.2.0
mocktail: ^1.0.4 mocktail: ^1.0.4
integration_test:
sdk: flutter
flutter: flutter:
uses-material-design: true uses-material-design: true

Loading…
Cancel
Save