TF-2953 Set up Patrol tests
This commit is contained in:
@@ -120,3 +120,4 @@ app.*.symbols
|
||||
*.g.dart
|
||||
messages_*.dart
|
||||
*.mocks.dart
|
||||
integration_test/test_bundle.dart
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:patrol/patrol.dart';
|
||||
|
||||
abstract class BaseScenario {
|
||||
final PatrolIntegrationTester $;
|
||||
|
||||
const BaseScenario(this.$);
|
||||
|
||||
Future<void> execute();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user