-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add end-to-end tests placeholder * Lower flutter version
- Loading branch information
Showing
8 changed files
with
358 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import 'package:example/src/common/widget/app.dart'; | ||
import 'package:example/src/feature/initialization/widget/inherited_dependencies.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter_test/flutter_test.dart'; | ||
import 'package:integration_test/integration_test.dart'; | ||
|
||
import 'src/fake/fake_dependencies.dart'; | ||
import 'src/util/tester_extension.dart'; | ||
|
||
void main() { | ||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); | ||
group('end-to-end', () { | ||
late final Widget app; | ||
|
||
setUpAll(() async { | ||
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; | ||
final dependencies = await $initializeFakeDependencies(); | ||
app = InheritedDependencies( | ||
dependencies: dependencies, | ||
child: const App(), | ||
); | ||
}); | ||
|
||
testWidgets('app', (tester) async { | ||
await tester.pumpWidget(app); | ||
await tester.pumpAndSettle(); | ||
expect(find.byType(InheritedDependencies), findsOneWidget); | ||
expect(find.byType(App), findsOneWidget); | ||
expect(find.byType(MaterialApp), findsOneWidget); | ||
}); | ||
|
||
testWidgets('sign-in', (tester) async { | ||
await tester.pumpWidget(app); | ||
await tester.pumpAndSettle(); | ||
expect(find.text('Sign-In'), findsAtLeastNWidgets(1)); | ||
await tester.tap(find.descendant( | ||
of: find.byType(InkWell), | ||
matching: find.text('Sign-Up'), | ||
)); | ||
await tester.pumpAndPause(); | ||
await tester.tap(find.descendant( | ||
of: find.byType(InkWell), | ||
matching: find.text('Cancel'), | ||
)); | ||
await tester.pumpAndPause(); | ||
await tester.enterText( | ||
find.ancestor( | ||
of: find.text('Username'), | ||
matching: find.byType(TextField), | ||
), | ||
'app-test@gmail.com'); | ||
await tester.enterText( | ||
find.ancestor( | ||
of: find.text('Password'), | ||
matching: find.byType(TextField), | ||
), | ||
'Password123'); | ||
await tester.tap(find.ancestor( | ||
of: find.byIcon(Icons.visibility), | ||
matching: find.byType(IconButton), | ||
)); | ||
await tester.pumpAndPause(); | ||
await tester.tap(find.ancestor( | ||
of: find.byIcon(Icons.visibility_off), | ||
matching: find.byType(IconButton), | ||
)); | ||
await tester.pumpAndPause(); | ||
await tester.tap(find.descendant( | ||
of: find.byType(InkWell), | ||
matching: find.text('Sign-In'), | ||
)); | ||
await tester.pumpAndPause(const Duration(seconds: 1)); | ||
expect(find.text('Sign-In'), findsNothing); | ||
expect(find.text('Home'), findsAtLeastNWidgets(1)); | ||
}); | ||
}); | ||
} |
48 changes: 48 additions & 0 deletions
48
example/integration_test/src/fake/fake_authentication.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:example/src/feature/authentication/data/authentication_repository.dart'; | ||
import 'package:example/src/feature/authentication/model/sign_in_data.dart'; | ||
import 'package:example/src/feature/authentication/model/user.dart'; | ||
|
||
class FakeIAuthenticationRepositoryImpl implements IAuthenticationRepository { | ||
FakeIAuthenticationRepositoryImpl(); | ||
|
||
static const String _sessionKey = 'authentication.session'; | ||
final Map<String, Object?> _sharedPreferences = <String, Object?>{}; | ||
final StreamController<User> _userController = | ||
StreamController<User>.broadcast(); | ||
User _user = const User.unauthenticated(); | ||
|
||
@override | ||
FutureOr<User> getUser() => _user; | ||
|
||
@override | ||
Stream<User> userChanges() => _userController.stream; | ||
|
||
@override | ||
Future<void> signIn(SignInData data) async { | ||
final user = User.authenticated(id: data.username); | ||
_sharedPreferences[_sessionKey] = user.toJson(); | ||
_userController.add(_user = user); | ||
} | ||
|
||
@override | ||
Future<void> restore() async { | ||
final session = _sharedPreferences[_sessionKey]; | ||
if (session == null) return; | ||
final json = session; | ||
if (json case Map<String, Object?> jsonMap) { | ||
final user = User.fromJson(jsonMap); | ||
_userController.add(_user = user); | ||
} | ||
} | ||
|
||
@override | ||
Future<void> signOut() => Future<void>.sync( | ||
() { | ||
const user = User.unauthenticated(); | ||
_sharedPreferences.remove(_sessionKey); | ||
_userController.add(_user = user); | ||
}, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import 'package:example/src/common/model/dependencies.dart'; | ||
import 'package:example/src/feature/authentication/controller/authentication_controller.dart'; | ||
import 'package:example/src/feature/shop/controller/favorite_controller.dart'; | ||
import 'package:example/src/feature/shop/controller/shop_controller.dart'; | ||
import 'package:flutter/widgets.dart'; | ||
import 'package:shared_preferences/shared_preferences.dart'; | ||
|
||
import 'fake_authentication.dart'; | ||
import 'fake_product.dart'; | ||
|
||
Future<FakeDependencies> $initializeFakeDependencies() async { | ||
SharedPreferences.setMockInitialValues(<String, String>{}); | ||
final fakeProductRepository = FakeProductRepository(); | ||
final dependencies = FakeDependencies() | ||
..sharedPreferences = await SharedPreferences.getInstance() | ||
..authenticationController = AuthenticationController( | ||
repository: FakeIAuthenticationRepositoryImpl(), | ||
) | ||
..shopController = ShopController( | ||
repository: fakeProductRepository, | ||
) | ||
..favoriteController = FavoriteController( | ||
repository: fakeProductRepository, | ||
); | ||
return dependencies; | ||
} | ||
|
||
/// Fake Dependencies | ||
class FakeDependencies implements Dependencies { | ||
FakeDependencies(); | ||
|
||
/// The state from the closest instance of this class. | ||
static Dependencies of(BuildContext context) => Dependencies.of(context); | ||
|
||
/// Shared preferences | ||
@override | ||
late final SharedPreferences sharedPreferences; | ||
|
||
/// Authentication controller | ||
@override | ||
late final AuthenticationController authenticationController; | ||
|
||
/// Shop controller | ||
@override | ||
late final ShopController shopController; | ||
|
||
/// Favorite controller | ||
@override | ||
late final FavoriteController favoriteController; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import 'dart:convert'; | ||
|
||
import 'package:example/src/common/constant/assets.gen.dart' as assets; | ||
import 'package:example/src/feature/shop/data/product_repository.dart'; | ||
import 'package:example/src/feature/shop/model/category.dart'; | ||
import 'package:example/src/feature/shop/model/product.dart'; | ||
import 'package:flutter/foundation.dart'; | ||
import 'package:flutter/services.dart'; | ||
|
||
class FakeProductRepository implements IProductRepository { | ||
FakeProductRepository(); | ||
|
||
static const String _favoriteProductsKey = 'shop.products.favorite'; | ||
|
||
final Map<String, Object?> _sharedPreferences = <String, Object?>{}; | ||
|
||
Set<ProductID>? _favoritesCache; | ||
|
||
@override | ||
Stream<CategoryEntity> fetchCategories() async* { | ||
final json = await rootBundle.loadString(assets.Assets.data.categories); | ||
final categories = await compute<String, List<Map<String, Object?>>>( | ||
_extractCollection, json); | ||
for (final category in categories) { | ||
yield CategoryEntity.fromJson(category); | ||
} | ||
} | ||
|
||
@override | ||
Stream<ProductEntity> fetchProducts() async* { | ||
final json = await rootBundle.loadString(assets.Assets.data.products); | ||
final products = await compute<String, List<Map<String, Object?>>>( | ||
_extractCollection, json); | ||
for (final product in products) { | ||
yield ProductEntity.fromJson(product); | ||
} | ||
} | ||
|
||
@override | ||
Future<Set<ProductID>> fetchFavoriteProducts() async { | ||
if (_favoritesCache case Set<ProductID> cache) | ||
return Set<ProductID>.of(cache); | ||
final set = _sharedPreferences[_favoriteProductsKey]; | ||
if (set is! Iterable<String>) return <ProductID>{}; | ||
return Set<ProductID>.of(_favoritesCache = | ||
set.map<int?>(int.tryParse).whereType<ProductID>().toSet()); | ||
} | ||
|
||
@override | ||
Future<void> addFavoriteProduct(ProductID id) async { | ||
final set = await fetchFavoriteProducts(); | ||
if (!set.add(id)) return; | ||
_favoritesCache = set; | ||
_sharedPreferences[_favoriteProductsKey] = <String>[ | ||
...set.map<String>((e) => e.toString()), | ||
id.toString(), | ||
]; | ||
} | ||
|
||
@override | ||
Future<void> removeFavoriteProduct(ProductID id) async { | ||
final set = await fetchFavoriteProducts(); | ||
if (!set.remove(id)) return; | ||
_favoritesCache = set; | ||
_sharedPreferences[_favoriteProductsKey] = <String>[ | ||
for (final e in set) e.toString(), | ||
]; | ||
} | ||
|
||
static List<Map<String, Object?>> _extractCollection(String json) => | ||
(jsonDecode(json) as Map<String, Object?>) | ||
.values | ||
.whereType<Iterable<Object?>>() | ||
.reduce((v, e) => <Object?>[...v, ...e]) | ||
.whereType<Map<String, Object?>>() | ||
.toList(growable: false); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import 'dart:async'; | ||
import 'dart:io' as io; | ||
import 'dart:typed_data' as td; | ||
import 'dart:ui' as ui; | ||
|
||
import 'package:flutter/rendering.dart' show RenderRepaintBoundary; | ||
import 'package:flutter/widgets.dart' show WidgetsApp; | ||
import 'package:flutter_test/flutter_test.dart'; | ||
import 'package:integration_test/integration_test.dart'; | ||
|
||
extension WidgetTesterExtension on WidgetTester { | ||
/// Sleep for `duration`. | ||
Future<void> sleep([Duration duration = const Duration(milliseconds: 500)]) => | ||
Future<void>.delayed(duration); | ||
|
||
/// Pump the widget tree, wait and then pumps a frame again. | ||
Future<void> pumpAndPause([ | ||
Duration duration = const Duration(milliseconds: 500), | ||
]) async { | ||
await pump(); | ||
await sleep(duration); | ||
await pump(); | ||
} | ||
|
||
/// Try to pump and find some widget few times. | ||
Future<Finder> asyncFinder({ | ||
required Finder Function() finder, | ||
Duration limit = const Duration(milliseconds: 15000), | ||
}) async { | ||
final stopwatch = Stopwatch()..start(); | ||
var result = finder(); | ||
try { | ||
while (stopwatch.elapsed <= limit) { | ||
await pumpAndSettle(const Duration(milliseconds: 100)) | ||
.timeout(limit - stopwatch.elapsed); | ||
result = finder(); | ||
if (result.evaluate().isNotEmpty) return result; | ||
} | ||
return result; | ||
} on TimeoutException { | ||
return result; | ||
} on Object { | ||
rethrow; | ||
} finally { | ||
stopwatch.stop(); | ||
} | ||
} | ||
|
||
/// Returns a function that takes a screenshot of the current state of the app. | ||
Future<List<int>> Function([String? name]) screenshot({ | ||
/// The [_$pixelRatio] describes the scale between the logical pixels and the | ||
/// size of the output image. It is independent of the | ||
/// [dart:ui.FlutterView.devicePixelRatio] for the device, so specifying 1.0 | ||
/// (the default) will give you a 1:1 mapping between logical pixels and the | ||
/// output pixels in the image. | ||
double pixelRatio = 1, | ||
|
||
/// If provided, the screenshot will be get | ||
/// with standard [IntegrationTestWidgetsFlutterBinding.takeScreenshot] on | ||
/// Android and iOS devices. | ||
IntegrationTestWidgetsFlutterBinding? binding, | ||
}) => | ||
([name]) async { | ||
await pump(); | ||
if (binding != null && | ||
name != null && | ||
(io.Platform.isAndroid || io.Platform.isIOS)) { | ||
if (io.Platform.isAndroid) | ||
await binding.convertFlutterSurfaceToImage(); | ||
return await binding.takeScreenshot(name); | ||
} else { | ||
final element = firstElement(find.byType(WidgetsApp)); | ||
RenderRepaintBoundary? boundary; | ||
element.visitAncestorElements((element) { | ||
final renderObject = element.renderObject; | ||
if (renderObject is RenderRepaintBoundary) boundary = renderObject; | ||
return true; | ||
}); | ||
if (boundary == null) | ||
throw StateError('No RenderRepaintBoundary found'); | ||
final image = await boundary!.toImage(pixelRatio: pixelRatio); | ||
final bytes = await image.toByteData(format: ui.ImageByteFormat.png); | ||
if (bytes is! td.ByteData) | ||
throw StateError('Error converting image to bytes'); | ||
return bytes.buffer.asUint8List(); | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters