Skip to content

Commit 56bf31f

Browse files
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: flutter/flutter#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
1 parent bb58c63 commit 56bf31f

File tree

11 files changed

+284
-0
lines changed

11 files changed

+284
-0
lines changed

compass_app/app/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
11
# compass_app
22

33
A new Flutter project.
4+
5+
## Integration Tests
6+
7+
Run separately with:
8+
9+
**Integration tests with local data**
10+
11+
```
12+
flutter test integration_test/app_local_data_test.dart
13+
```
14+
15+
**Integration tests with background server and remote data**
16+
17+
```
18+
flutter test integration_test/app_server_data_test.dart
19+
```
20+
21+
Running the tests together with `flutter test integration_test` will fail.
22+
See: https://github.com/flutter/flutter/issues/101031
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import 'package:compass_app/config/dependencies.dart';
2+
import 'package:compass_app/main.dart';
3+
import 'package:compass_app/ui/activities/widgets/activities_screen.dart';
4+
import 'package:compass_app/ui/booking/widgets/booking_screen.dart';
5+
import 'package:compass_app/ui/core/ui/custom_checkbox.dart';
6+
import 'package:compass_app/ui/results/widgets/result_card.dart';
7+
import 'package:compass_app/ui/results/widgets/results_screen.dart';
8+
import 'package:compass_app/ui/search_form/widgets/search_form_screen.dart';
9+
import 'package:flutter/foundation.dart';
10+
import 'package:flutter_test/flutter_test.dart';
11+
import 'package:integration_test/integration_test.dart';
12+
import 'package:provider/provider.dart';
13+
14+
/// This Integration Test launches the Compass-App with the local configuration.
15+
/// The app uses data from the assets folder to create a booking.
16+
void main() {
17+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
18+
19+
group('end-to-end test with local data', () {
20+
testWidgets('should load app', (tester) async {
21+
// Load app widget.
22+
await tester.pumpWidget(
23+
MultiProvider(
24+
providers: providersLocal,
25+
child: const MainApp(),
26+
),
27+
);
28+
});
29+
30+
testWidgets('Create booking', (tester) async {
31+
// Load app widget with local configuration
32+
await tester.pumpWidget(
33+
MultiProvider(
34+
providers: providersLocal,
35+
child: const MainApp(),
36+
),
37+
);
38+
39+
await tester.pumpAndSettle();
40+
41+
// Search destinations screen
42+
expect(find.byType(SearchFormScreen), findsOneWidget);
43+
44+
// Select Europe because it is always the first result
45+
await tester.tap(find.text('Europe'), warnIfMissed: false);
46+
47+
// Select dates
48+
await tester.tap(find.text('Add Dates'));
49+
await tester.pumpAndSettle();
50+
final tomorrow = DateTime.now().add(const Duration(days: 1)).day;
51+
final nextDay = DateTime.now().add(const Duration(days: 2)).day;
52+
// Select first and last widget that matches today number
53+
//and tomorrow number, sort of ensures a valid range
54+
await tester.tap(find.text(tomorrow.toString()).first);
55+
await tester.tap(find.text(nextDay.toString()).last);
56+
await tester.pumpAndSettle();
57+
await tester.tap(find.text('Save'));
58+
await tester.pumpAndSettle();
59+
60+
// Select guests
61+
await tester.tap(find.byKey(const ValueKey('add_guests')),
62+
warnIfMissed: false);
63+
64+
// Refresh screen state
65+
await tester.pumpAndSettle();
66+
67+
// Perform search and navigate to next screen
68+
await tester.tap(find.byKey(const ValueKey('submit_button')));
69+
await tester.pumpAndSettle(const Duration(seconds: 2));
70+
71+
// Results Screen
72+
expect(find.byType(ResultsScreen), findsOneWidget);
73+
74+
// Amalfi Coast should be the first result for Europe
75+
// Tap and navigate to activities screen
76+
await tester.tap(find.byType(ResultCard).first);
77+
await tester.pumpAndSettle(const Duration(seconds: 2));
78+
79+
// Activities Screen
80+
expect(find.byType(ActivitiesScreen), findsOneWidget);
81+
82+
// Select one activity
83+
await tester.tap(find.byType(CustomCheckbox).first);
84+
await tester.pumpAndSettle();
85+
expect(find.text('1 selected'), findsOneWidget);
86+
87+
// Submit selection
88+
await tester.tap(find.byKey(const ValueKey('confirm-button')));
89+
await tester.pumpAndSettle(const Duration(seconds: 2));
90+
91+
// Should be at booking screen
92+
expect(find.byType(BookingScreen), findsOneWidget);
93+
expect(find.text('Amalfi Coast'), findsOneWidget);
94+
});
95+
});
96+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import 'dart:io';
2+
3+
import 'package:compass_app/config/dependencies.dart';
4+
import 'package:compass_app/main.dart';
5+
import 'package:compass_app/ui/activities/widgets/activities_screen.dart';
6+
import 'package:compass_app/ui/auth/login/widgets/login_screen.dart';
7+
import 'package:compass_app/ui/auth/logout/widgets/logout_button.dart';
8+
import 'package:compass_app/ui/booking/widgets/booking_screen.dart';
9+
import 'package:compass_app/ui/core/ui/custom_checkbox.dart';
10+
import 'package:compass_app/ui/core/ui/home_button.dart';
11+
import 'package:compass_app/ui/results/widgets/result_card.dart';
12+
import 'package:compass_app/ui/results/widgets/results_screen.dart';
13+
import 'package:compass_app/ui/search_form/widgets/search_form_screen.dart';
14+
import 'package:flutter/foundation.dart';
15+
import 'package:flutter_test/flutter_test.dart';
16+
import 'package:integration_test/integration_test.dart';
17+
import 'package:provider/provider.dart';
18+
import 'package:shared_preferences/shared_preferences.dart';
19+
20+
/// This Integration Test starts the Dart server
21+
/// before launching the Compass-App with the remote configuration.
22+
/// The app connects to its endpoints to perform login and create a booking.
23+
void main() {
24+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
25+
26+
group('end-to-end test with remote data', () {
27+
final port = '8080';
28+
late Process p;
29+
30+
setUpAll(() async {
31+
// Clear any stored shared preferences
32+
final sharedPreferences = await SharedPreferences.getInstance();
33+
await sharedPreferences.clear();
34+
35+
// Start the dart server
36+
p = await Process.start(
37+
'dart',
38+
['run', 'bin/compass_server.dart'],
39+
environment: {'PORT': port},
40+
// Relative to the app/ folder
41+
workingDirectory: '../server',
42+
);
43+
// Wait for server to start and print to stdout.
44+
await p.stdout.first;
45+
});
46+
47+
tearDownAll(() => p.kill());
48+
49+
testWidgets('should load app', (tester) async {
50+
// Load app widget.
51+
await tester.pumpWidget(
52+
MultiProvider(
53+
providers: providersRemote,
54+
child: const MainApp(),
55+
),
56+
);
57+
58+
await tester.pumpAndSettle();
59+
60+
// Login screen because logget out
61+
expect(find.byType(LoginScreen), findsOneWidget);
62+
});
63+
64+
testWidgets('Create booking', (tester) async {
65+
// Load app widget with local configuration
66+
await tester.pumpWidget(
67+
MultiProvider(
68+
providers: providersRemote,
69+
child: const MainApp(),
70+
),
71+
);
72+
73+
await tester.pumpAndSettle();
74+
75+
// Login screen because logget out
76+
expect(find.byType(LoginScreen), findsOneWidget);
77+
78+
// Perform login (credentials are prefilled)
79+
await tester.tap(find.text('Login'));
80+
await tester.pumpAndSettle();
81+
82+
// Search destinations screen
83+
expect(find.byType(SearchFormScreen), findsOneWidget);
84+
85+
// Select Europe because it is always the first result
86+
await tester.tap(find.text('Europe'), warnIfMissed: false);
87+
88+
// Select dates
89+
await tester.tap(find.text('Add Dates'));
90+
await tester.pumpAndSettle();
91+
final tomorrow = DateTime.now().add(const Duration(days: 1)).day;
92+
final nextDay = DateTime.now().add(const Duration(days: 2)).day;
93+
// Select first and last widget that matches today number
94+
//and tomorrow number, sort of ensures a valid range
95+
await tester.tap(find.text(tomorrow.toString()).first);
96+
await tester.tap(find.text(nextDay.toString()).last);
97+
await tester.pumpAndSettle();
98+
await tester.tap(find.text('Save'));
99+
await tester.pumpAndSettle();
100+
101+
// Select guests
102+
await tester.tap(find.byKey(const ValueKey('add_guests')),
103+
warnIfMissed: false);
104+
105+
// Refresh screen state
106+
await tester.pumpAndSettle();
107+
108+
// Perform search and navigate to next screen
109+
await tester.tap(find.byKey(const ValueKey('submit_button')));
110+
await tester.pumpAndSettle(const Duration(seconds: 2));
111+
112+
// Results Screen
113+
expect(find.byType(ResultsScreen), findsOneWidget);
114+
115+
// Amalfi Coast should be the first result for Europe
116+
// Tap and navigate to activities screen
117+
await tester.tap(find.byType(ResultCard).first);
118+
await tester.pumpAndSettle(const Duration(seconds: 2));
119+
120+
// Activities Screen
121+
expect(find.byType(ActivitiesScreen), findsOneWidget);
122+
123+
// Select one activity
124+
await tester.tap(find.byType(CustomCheckbox).first);
125+
await tester.pumpAndSettle();
126+
expect(find.text('1 selected'), findsOneWidget);
127+
128+
// Submit selection
129+
await tester.tap(find.byKey(const ValueKey('confirm-button')));
130+
await tester.pumpAndSettle(const Duration(seconds: 2));
131+
132+
// Should be at booking screen
133+
expect(find.byType(BookingScreen), findsOneWidget);
134+
expect(find.text('Amalfi Coast'), findsOneWidget);
135+
136+
// Navigate back to home
137+
await tester.tap(find.byType(HomeButton).first);
138+
await tester.pumpAndSettle();
139+
expect(find.byType(SearchFormScreen), findsOneWidget);
140+
141+
// Perform logout
142+
await tester.tap(find.byType(LogoutButton).first);
143+
await tester.pumpAndSettle();
144+
expect(find.byType(LoginScreen), findsOneWidget);
145+
});
146+
});
147+
}

compass_app/app/lib/ui/activities/widgets/activity_entry.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
22
import 'package:compass_model/model.dart';
33
import 'package:flutter/material.dart';
44

5+
import '../../../utils/image_error_listener.dart';
56
import '../../core/ui/custom_checkbox.dart';
67

78
class ActivityEntry extends StatelessWidget {
@@ -28,6 +29,7 @@ class ActivityEntry extends StatelessWidget {
2829
imageUrl: activity.imageUrl,
2930
height: 80,
3031
width: 80,
32+
errorListener: imageErrorListener,
3133
),
3234
),
3335
const SizedBox(width: 20),

compass_app/app/lib/ui/auth/login/widgets/tilted_cards.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import 'package:cached_network_image/cached_network_image.dart';
22
import 'package:flutter/material.dart';
33
import 'package:flutter_svg/svg.dart';
44

5+
import '../../../../utils/image_error_listener.dart';
6+
57
class TiltedCards extends StatelessWidget {
68
const TiltedCards({super.key});
79

@@ -79,6 +81,7 @@ class _Card extends StatelessWidget {
7981
fit: BoxFit.cover,
8082
color: showTitle ? Colors.black.withOpacity(0.5) : null,
8183
colorBlendMode: showTitle ? BlendMode.darken : null,
84+
errorListener: imageErrorListener,
8285
),
8386
if (showTitle) Center(child: SvgPicture.asset('assets/logo.svg')),
8487
],

compass_app/app/lib/ui/booking/widgets/booking_body.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart';
22
import 'package:compass_model/model.dart';
33
import 'package:flutter/material.dart';
44

5+
import '../../../utils/image_error_listener.dart';
56
import '../../core/themes/dimens.dart';
67
import '../view_models/booking_viewmodel.dart';
78
import 'booking_header.dart';
@@ -64,6 +65,7 @@ class _Activity extends StatelessWidget {
6465
imageUrl: activity.imageUrl,
6566
height: 80,
6667
width: 80,
68+
errorListener: imageErrorListener,
6769
),
6870
),
6971
const SizedBox(width: 20),

compass_app/app/lib/ui/booking/widgets/booking_header.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:compass_model/model.dart';
33
import 'package:flutter/material.dart';
44
import 'package:go_router/go_router.dart';
55

6+
import '../../../utils/image_error_listener.dart';
67
import '../../core/localization/applocalization.dart';
78
import '../../core/themes/colors.dart';
89
import '../../core/themes/dimens.dart';
@@ -173,6 +174,7 @@ class _HeaderImage extends StatelessWidget {
173174
return CachedNetworkImage(
174175
fit: BoxFit.fitWidth,
175176
imageUrl: booking.destination.imageUrl,
177+
errorListener: imageErrorListener,
176178
);
177179
}
178180
}

compass_app/app/lib/ui/results/widgets/result_card.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:compass_model/model.dart';
22

33
import 'package:cached_network_image/cached_network_image.dart';
44
import 'package:flutter/material.dart';
5+
import '../../../utils/image_error_listener.dart';
56
import '../../core/themes/text_styles.dart';
67
import '../../core/ui/tag_chip.dart';
78

@@ -26,6 +27,7 @@ class ResultCard extends StatelessWidget {
2627
imageUrl: destination.imageUrl,
2728
fit: BoxFit.fitHeight,
2829
errorWidget: (context, url, error) => const Icon(Icons.error),
30+
errorListener: imageErrorListener,
2931
),
3032
Positioned(
3133
bottom: 12.0,

compass_app/app/lib/ui/search_form/widgets/search_form_continent.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:compass_model/model.dart';
33
import 'package:flutter/material.dart';
44
import 'package:google_fonts/google_fonts.dart';
55

6+
import '../../../utils/image_error_listener.dart';
67
import '../../core/localization/applocalization.dart';
78
import '../../core/themes/colors.dart';
89
import '../../core/themes/dimens.dart';
@@ -100,6 +101,7 @@ class _CarouselItem extends StatelessWidget {
100101
CachedNetworkImage(
101102
imageUrl: imageUrl,
102103
fit: BoxFit.cover,
104+
errorListener: imageErrorListener,
103105
errorWidget: (context, url, error) {
104106
// NOTE: Getting "invalid image data" error for some of the images
105107
// e.g. https://rstr.in/google/tripedia/jlbgFDrSUVE
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import 'package:logging/logging.dart';
2+
3+
final _log = Logger('ImageErrorListener');
4+
5+
void imageErrorListener(Object error) {
6+
_log.warning('Failed to load image', error);
7+
}

compass_app/app/pubspec.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ dev_dependencies:
2929
flutter_lints: ^4.0.0
3030
mocktail_image_network: ^1.2.0
3131
mocktail: ^1.0.4
32+
integration_test:
33+
sdk: flutter
3234

3335
flutter:
3436
uses-material-design: true

0 commit comments

Comments
 (0)