TF-2953 Set up Patrol tests

This commit is contained in:
DatDang
2024-10-11 11:58:09 +07:00
committed by Dat H. Pham
parent e8e709d63a
commit 25437ba92e
18 changed files with 437 additions and 18 deletions
+1
View File
@@ -120,3 +120,4 @@ app.*.symbols
*.g.dart
messages_*.dart
*.mocks.dart
integration_test/test_bundle.dart
+12 -1
View File
@@ -23,7 +23,7 @@ if (flutterVersionName == null) {
def flutterMinSdkVersion = localProperties.getProperty('flutter.minSdkVersion')
if (flutterMinSdkVersion == null) {
flutterMinSdkVersion = '19'
flutterMinSdkVersion = '21'
}
apply plugin: 'com.android.application'
@@ -54,6 +54,8 @@ android {
manifestPlaceholders = [
'appAuthRedirectScheme': 'teammail.mobile'
]
testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: "true"
}
compileOptions {
@@ -77,7 +79,15 @@ android {
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.release
}
debug {
minifyEnabled false
}
}
testOptions {
execution "ANDROIDX_TEST_ORCHESTRATOR"
}
}
flutter {
@@ -90,4 +100,5 @@ dependencies {
implementation 'androidx.work:work-runtime-ktx:2.7.0'
implementation 'com.android.support:multidex:1.0.3'
implementation 'androidx.window:window:1.0.0'
androidTestUtil "androidx.test:orchestrator:1.5.0"
}
@@ -0,0 +1,33 @@
package com.linagora.android.tmail;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import pl.leancode.patrol.PatrolJUnitRunner;
@RunWith(Parameterized.class)
public class MainActivityTest {
@Parameters(name = "{0}")
public static Object[] testCases() {
PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
// replace "MainActivity.class" with "io.flutter.embedding.android.FlutterActivity.class"
// if your AndroidManifest is using: android:name="io.flutter.embedding.android.FlutterActivity"
instrumentation.setUp(MainActivity.class);
instrumentation.waitForPatrolAppService();
return instrumentation.listDartTests();
}
public MainActivityTest(String dartTestName) {
this.dartTestName = dartTestName;
}
private final String dartTestName;
@Test
public void runDartTest() {
PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
instrumentation.runDartTest(dartTestName);
}
}
+36
View File
@@ -0,0 +1,36 @@
# 52. Patrol integration test
Date: 2024-04-10
## Status
Accepted
## Context
- A need for integration testing for Twake Mail mobile arised.
- The testing tool must be able to handle native UI and webview.
## Decision
- Patrol was chosen to write and test Twake Mail.
## Consequences
- Developers are now able to integration test Twake Mail
- Set up:
- Run `dart pub global activate patrol_cli` to enable Patrol CLI
- Install ngrok and jq
- Open docker and Android emulator/connect Android device
- Remember to use `dart-define` neccessary for each test command
- Test individual test locally by edit `scripts/patrol-local-integration-test-with-docker.sh`
- Replace `patrol test -v` with `patrol test -v -t path/to/test/file`
- Run the `scripts/patrol-local-integration-test-with-docker.sh`
- Test every tests locally by running `scripts/patrol-local-integration-test-with-docker.sh` script
- Read more about Patrol in [Patrol homepage](https://patrol.leancode.co/)
## Limitations
- Backend docker container is initiated before Patrol tests run, and close after all tests have run. This lead to no data isolation between tests
- Patrol gives no way of accessing Docker from system, due to when it runs, it bundle all tests into a single apk, and apk cannot access the system's terminal
- Tried https://github.com/testcontainers/testcontainers-java but also failed, with the same reason as Patrol.
+9
View File
@@ -0,0 +1,9 @@
import 'package:patrol/patrol.dart';
abstract class BaseScenario {
final PatrolIntegrationTester $;
const BaseScenario(this.$);
Future<void> execute();
}
+15
View File
@@ -0,0 +1,15 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
abstract class CoreRobot {
final PatrolIntegrationTester $;
CoreRobot(this.$);
Future<void> ensureViewVisible(PatrolFinder patrolFinder) async {
await $.waitUntilVisible(patrolFinder);
expect(patrolFinder, findsWidgets);
}
dynamic ignoreException() => $.tester.takeException();
}
+31
View File
@@ -0,0 +1,31 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
import 'package:tmail_ui_user/main.dart' as app;
class TestBase {
void runPatrolTest({
required String description,
required Function(PatrolIntegrationTester $) test,
}) {
patrolTest(
description,
config: const PatrolTesterConfig(
settlePolicy: SettlePolicy.trySettle,
visibleTimeout: Duration(minutes: 1)),
nativeAutomatorConfig: const NativeAutomatorConfig(
findTimeout: Duration(seconds: 10),
),
framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive,
($) async {
await app.runTmail();
// https://github.com/leancodepl/patrol/issues/1602#issuecomment-1665317814
final originalOnError = FlutterError.onError!;
FlutterError.onError = (FlutterErrorDetails details) {
originalOnError(details);
};
await test($);
});
}
}
@@ -0,0 +1,62 @@
import 'package:core/presentation/resources/image_paths.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:model/email/prefix_email_address.dart';
import 'package:rich_text_composer/rich_text_composer.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart';
import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_editor_view.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart';
import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart';
import '../base/core_robot.dart';
class ComposerRobot extends CoreRobot {
ComposerRobot(super.$);
Future<void> addRecipient(String email) async {
await $(RecipientComposerWidget)
.which<RecipientComposerWidget>((widget) => widget.prefix == PrefixEmailAddress.to)
.enterText(email);
await $(RecipientSuggestionItemWidget)
.which<RecipientSuggestionItemWidget>((widget) => widget.emailAddress.email?.contains(email) ?? false)
.tap();
}
Future<void> addSubject(String subject) async {
await $(SubjectComposerWidget).enterText(subject);
}
Future<void> addContent(String content) async {
ComposerController? composerController;
await $(ComposerView)
.which<ComposerView>((widget) {
composerController = widget.controller;
return true;
})
.$(MobileEditorView).$(HtmlEditor).$(InAppWebView).tap();
await composerController?.htmlEditorApi?.requestFocusLastChild();
await composerController!.htmlEditorApi!.insertHtml('$content <br><br>');
}
Future<void> sendEmail() async {
await $(AppBarComposerWidget)
.$(TMailButtonWidget)
.which<TMailButtonWidget>((widget) => widget.icon == ImagePaths().icSendMobile)
.tap();
}
Future<void> expectSendEmailSuccessToast() async {
expect($('Message has been sent successfully'), findsOneWidget);
}
Future<void> grantContactPermission() async {
if (await $.native.isPermissionDialogVisible(timeout: const Duration(seconds: 5))) {
await $.native.grantPermissionWhenInUse();
}
}
}
+43
View File
@@ -0,0 +1,43 @@
import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart';
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/login/domain/model/recent_login_username.dart';
import 'package:tmail_ui_user/features/login/presentation/login_view.dart';
import 'package:tmail_ui_user/features/login/presentation/widgets/login_text_input_builder.dart';
import '../base/core_robot.dart';
class LoginRobot extends CoreRobot {
LoginRobot(super.$);
Future<void> expectLoginViewVisible() => ensureViewVisible($(LoginView));
Future<void> enterEmail(String email) async {
final finder = $(LoginView).$(TextField);
await finder.enterText(email);
await $('Next').tap();
}
Future<void> enterHostUrl(String url) async {
final finder = $(LoginView).$(TextField);
await finder.enterText(url);
await $('Next').tap();
}
Future<void> enterBasicAuthEmail(String email) async {
await $(LoginView)
.$(TypeAheadFormFieldBuilder<RecentLoginUsername>)
.$(TextField)
.enterText(email);
}
Future<void> enterBasicAuthPassword(String password) async {
await $(LoginView)
.$(LoginTextInputBuilder)
.$(TextField)
.enterText(password);
}
Future<void> loginBasicAuth() async {
await $(Container).$(ElevatedButton).tap();
}
}
+18
View File
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart';
import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart';
import '../base/core_robot.dart';
class ThreadRobot extends CoreRobot {
ThreadRobot(super.$);
Future<void> expectThreadViewVisible() => ensureViewVisible($(ThreadView));
Future<void> openComposer() async {
await $(ComposeFloatingButton).$(InkWell).tap();
}
Future<void> expectComposerViewVisible() => ensureViewVisible($(ComposerView));
}
@@ -0,0 +1,39 @@
import '../base/base_scenario.dart';
import '../robots/login_robot.dart';
import '../robots/thread_robot.dart';
import '../utils/scenario_utils_mixin.dart';
class LoginWithBasicAuthScenario extends BaseScenario with ScenarioUtilsMixin {
const LoginWithBasicAuthScenario(
super.$,
{
required this.username,
required this.hostUrl,
required this.email,
required this.password,
}
);
final String username;
final String hostUrl;
final String email;
final String password;
@override
Future<void> execute() async {
final loginRobot = LoginRobot($);
final threadRobot = ThreadRobot($);
await loginRobot.expectLoginViewVisible();
await loginRobot.enterEmail(username);
await loginRobot.enterHostUrl(hostUrl);
await loginRobot.enterBasicAuthEmail(email);
await loginRobot.enterBasicAuthPassword(password);
await loginRobot.loginBasicAuth();
await grantNotificationPermission($.native);
await threadRobot.expectThreadViewVisible();
}
}
@@ -0,0 +1,41 @@
import '../base/base_scenario.dart';
import '../robots/composer_robot.dart';
import '../robots/thread_robot.dart';
import 'login_with_basic_auth_scenario.dart';
class SendEmailScenario extends BaseScenario {
const SendEmailScenario(
super.$,
{
required this.loginWithBasicAuthScenario,
required this.additionalRecipient,
required this.subject,
required this.content
}
);
final LoginWithBasicAuthScenario loginWithBasicAuthScenario;
final String additionalRecipient;
final String subject;
final String content;
@override
Future<void> execute() async {
final threadRobot = ThreadRobot($);
final composerRobot = ComposerRobot($);
await loginWithBasicAuthScenario.execute();
await threadRobot.openComposer();
await threadRobot.expectComposerViewVisible();
await composerRobot.grantContactPermission();
await composerRobot.addRecipient(loginWithBasicAuthScenario.email);
await composerRobot.addRecipient(additionalRecipient);
await composerRobot.addSubject(subject);
await composerRobot.addContent(content);
await composerRobot.sendEmail();
await composerRobot.expectSendEmailSuccessToast();
}
}
@@ -0,0 +1,24 @@
import '../../base/test_base.dart';
import '../../scenarios/login_with_basic_auth_scenario.dart';
import '../../scenarios/send_email_scenario.dart';
void main() {
TestBase().runPatrolTest(
description: 'Should see success toast when send email successfully',
test: ($) async {
final loginWithBasicAuthScenario = LoginWithBasicAuthScenario($,
username: const String.fromEnvironment('USERNAME'),
hostUrl: const String.fromEnvironment('BASIC_AUTH_URL'),
email: const String.fromEnvironment('BASIC_AUTH_EMAIL'),
password: const String.fromEnvironment('PASSWORD'),
);
final sendEmailScenario = SendEmailScenario($,
loginWithBasicAuthScenario: loginWithBasicAuthScenario,
additionalRecipient: const String.fromEnvironment('ADDITIONAL_MAIL_RECIPIENT'),
subject: 'Test subject',
content: 'Test content');
await sendEmailScenario.execute();
},
);
}
@@ -0,0 +1,18 @@
import '../../base/test_base.dart';
import '../../scenarios/login_with_basic_auth_scenario.dart';
void main() {
TestBase().runPatrolTest(
description: 'Should see thread view when login with basic auth successfully',
test: ($) async {
final loginWithBasicAuthScenario = LoginWithBasicAuthScenario($,
username: const String.fromEnvironment('USERNAME'),
hostUrl: const String.fromEnvironment('BASIC_AUTH_URL'),
email: const String.fromEnvironment('BASIC_AUTH_EMAIL'),
password: const String.fromEnvironment('PASSWORD'),
);
await loginWithBasicAuthScenario.execute();
}
);
}
@@ -0,0 +1,9 @@
import 'package:patrol/patrol.dart';
mixin ScenarioUtilsMixin {
Future<void> grantNotificationPermission(NativeAutomator nativeAutomator) async {
if (await nativeAutomator.isPermissionDialogVisible(timeout: const Duration(seconds: 5))) {
await nativeAutomator.grantPermissionWhenInUse();
}
}
}
+18 -14
View File
@@ -15,25 +15,29 @@ import 'package:tmail_ui_user/main/utils/app_utils.dart';
import 'package:url_strategy/url_strategy.dart';
import 'package:worker_manager/worker_manager.dart';
void main() async {
Future<void> main() async {
initLogger(() async {
WidgetsFlutterBinding.ensureInitialized();
ThemeUtils.setSystemLightUIStyle();
await Future.wait([
MainBindings().dependencies(),
HiveCacheConfig.instance.setUp(),
Executor().warmUp(log: BuildUtils.isDebugMode),
AppUtils.loadEnvFile()
]);
await HiveCacheConfig.instance.initializeEncryptionKey();
setPathUrlStrategy();
runApp(const TMailApp());
await runTmail();
});
}
Future<void> runTmail() async {
ThemeUtils.setSystemLightUIStyle();
await Future.wait([
MainBindings().dependencies(),
HiveCacheConfig.instance.setUp(),
Executor().warmUp(log: BuildUtils.isDebugMode),
AppUtils.loadEnvFile()
]);
await HiveCacheConfig.instance.initializeEncryptionKey();
setPathUrlStrategy();
runApp(const TMailApp());
}
class TMailApp extends StatelessWidget {
const TMailApp({Key? key}) : super(key: key);
+19 -3
View File
@@ -1252,10 +1252,10 @@ packages:
dependency: "direct main"
description:
name: json_annotation
sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
url: "https://pub.dev"
source: hosted
version: "4.8.0"
version: "4.8.1"
json_serializable:
dependency: "direct dev"
description:
@@ -1480,6 +1480,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.1"
patrol:
dependency: "direct dev"
description:
name: patrol
sha256: ef07b0022f6eabee77655a3cde2364ff57cf22c29018d524476e972a5476724f
url: "https://pub.dev"
source: hosted
version: "3.11.1"
patrol_finders:
dependency: transitive
description:
name: patrol_finders
sha256: "6bf2c3093fbccd02f80f73fafc1bd021d76410cbab6e329be220b5e3bc58f072"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
pattern_formatter:
dependency: transitive
description:
@@ -2201,4 +2217,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.3.0 <4.0.0"
flutter: ">=3.20.0-7.0.pre.48"
flutter: ">=3.22.0"
+9
View File
@@ -270,6 +270,8 @@ dev_dependencies:
http_mock_adapter: 0.4.2
patrol: 3.11.1
plugin_platform_interface: 2.1.8
dependency_overrides:
@@ -282,6 +284,8 @@ dependency_overrides:
url: https://github.com/linagora/flutter_file_picker
ref: email_supported_5.3.1
json_annotation: 4.8.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -355,3 +359,8 @@ flutter_native_splash:
cider:
link_template:
tag: https://github.com/linagora/tmail-flutter/releases/tag/v%tag% # initial release link template
patrol:
app_name: Twake Mail
android:
package_name: com.linagora.android.teammail