From 9271c7e172588c54531056fa210a278d9369f33b Mon Sep 17 00:00:00 2001 From: "khaled.njim" Date: Tue, 17 Dec 2024 16:51:54 +0100 Subject: [PATCH 01/10] LA-2185 initial patrol setup for android --- android/app/build.gradle | 6 ++++ .../twake/android/chat/MainActivityTest.java | 34 ++++++++++++++++++ pubspec.lock | 36 +++++++++++++++++-- pubspec.yaml | 8 ++++- 4 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 android/app/src/androidTest/java/app/twake/android/chat/MainActivityTest.java diff --git a/android/app/build.gradle b/android/app/build.gradle index 191d884db1..d2c61d6a86 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,6 +51,8 @@ android { versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true + testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner" + testInstrumentationRunnerArguments clearPackageData: "true" } signingConfigs { @@ -75,6 +77,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + testOptions { + execution "ANDROIDX_TEST_ORCHESTRATOR" + } } flutter { @@ -88,6 +93,7 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' implementation 'androidx.multidex:multidex:2.0.1' + androidTestUtil "androidx.test:orchestrator:1.5.1" } apply plugin: 'com.google.gms.google-services' diff --git a/android/app/src/androidTest/java/app/twake/android/chat/MainActivityTest.java b/android/app/src/androidTest/java/app/twake/android/chat/MainActivityTest.java new file mode 100644 index 0000000000..2f3f1ffcd6 --- /dev/null +++ b/android/app/src/androidTest/java/app/twake/android/chat/MainActivityTest.java @@ -0,0 +1,34 @@ +package app.twake.android.chat; + +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 in AndroidManifest.xml in manifest/application/activity you have + // 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); + } +} diff --git a/pubspec.lock b/pubspec.lock index 4ef7431479..2aba13470c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -563,6 +563,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" + dispose_scope: + dependency: transitive + description: + name: dispose_scope + sha256: "48ec38ca2631c53c4f8fa96b294c801e55c335db5e3fb9f82cede150cfe5a2af" + url: "https://pub.dev" + source: hosted + version: "2.1.0" dynamic_color: dependency: "direct main" description: @@ -2172,6 +2180,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + patrol: + dependency: "direct dev" + description: + name: patrol + sha256: "7e7c346890fa234af948fbdf024e09e80142deef7aef572dead1001446ca853c" + url: "https://pub.dev" + source: hosted + version: "3.13.1" + patrol_finders: + dependency: transitive + description: + name: patrol_finders + sha256: "6dfa07aa951f1d769f0a736f4fd4eb63d99ef32931aaaa39e0d24126ddfe9cd9" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + patrol_log: + dependency: transitive + description: + name: patrol_log + sha256: fb67013a5305cfd30a374cce3c5ea5a99613335d8cf31c250ecb0dda75deec21 + url: "https://pub.dev" + source: hosted + version: "0.2.1" permission_handler: dependency: "direct main" description: @@ -3401,5 +3433,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.0-259.0.dev <4.0.0" - flutter: ">=3.19.3" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index c719deb07e..3236b7dda7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -229,6 +229,7 @@ dev_dependencies: msix: ^3.6.2 translations_cleaner: ^0.0.5 mockito: 5.4.4 + patrol: 3.13.1 flutter_launcher_icons: @@ -325,4 +326,9 @@ dependency_overrides: cider: link_template: - tag: https://github.com/linagora/twake-on-matrix/releases/tag/%tag% # initial release link template \ No newline at end of file + tag: https://github.com/linagora/twake-on-matrix/releases/tag/%tag% # initial release link template + +patrol: + app_name: Twake Chat + android: + package_name: app.twake.android.chat \ No newline at end of file From 494427c3e96bcd0670cf475927767e8a19087b38 Mon Sep 17 00:00:00 2001 From: "khaled.njim" Date: Thu, 19 Dec 2024 00:26:14 +0100 Subject: [PATCH 02/10] LA-2185 added integration test for login --- integration_test/base/base_scenario.dart | 15 ++++++ integration_test/base/test_base.dart | 48 +++++++++++++++++++ .../scenarios/login_scenario.dart | 46 ++++++++++++++++++ integration_test/tests/login/login_test.dart | 19 ++++++++ 4 files changed, 128 insertions(+) create mode 100644 integration_test/base/base_scenario.dart create mode 100644 integration_test/base/test_base.dart create mode 100644 integration_test/scenarios/login_scenario.dart create mode 100644 integration_test/tests/login/login_test.dart diff --git a/integration_test/base/base_scenario.dart b/integration_test/base/base_scenario.dart new file mode 100644 index 0000000000..efe8da4413 --- /dev/null +++ b/integration_test/base/base_scenario.dart @@ -0,0 +1,15 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol/patrol.dart'; + +abstract class BaseScenario { + final PatrolIntegrationTester $; + + const BaseScenario(this.$); + + Future execute(); + + Future expectViewVisible(PatrolFinder patrolFinder) async { + await $.waitUntilVisible(patrolFinder); + expect(patrolFinder, findsWidgets); + } +} diff --git a/integration_test/base/test_base.dart b/integration_test/base/test_base.dart new file mode 100644 index 0000000000..c4f7f57e81 --- /dev/null +++ b/integration_test/base/test_base.dart @@ -0,0 +1,48 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/client_manager.dart'; +import 'package:fluffychat/widgets/twake_app.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:patrol/patrol.dart'; + +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.fullyLive, + ($) async { + await initTwakeChat($); + final originalOnError = FlutterError.onError!; + FlutterError.onError = (FlutterErrorDetails details) { + originalOnError(details); + }; + await test($); + }); + } + + Future initTwakeChat(PatrolIntegrationTester $) async { + MediaKit.ensureInitialized(); + GoRouter.optionURLReflectsImperativeAPIs = true; + await Hive.initFlutter(); + GetItInitializer().setUp(); + final clients = await ClientManager.getClients(); + final firstClient = clients.firstOrNull; + await firstClient?.roomsLoading; + await firstClient?.accountDataLoading; + await $.pumpWidgetAndSettle( + TwakeApp(clients: clients), + ); + } +} diff --git a/integration_test/scenarios/login_scenario.dart b/integration_test/scenarios/login_scenario.dart new file mode 100644 index 0000000000..f23ac80536 --- /dev/null +++ b/integration_test/scenarios/login_scenario.dart @@ -0,0 +1,46 @@ +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; +import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol/patrol.dart'; +import '../base/base_scenario.dart'; + +class LoginScenario extends BaseScenario { + final String username; + + final String serverUrl; + + final String password; + + LoginScenario( + super.$, { + required this.username, + required this.serverUrl, + required this.password, + }); + @override + Future execute() async { + await $.waitUntilVisible($(TwakeWelcome)); + await expectViewVisible($(TwakeWelcome)); + await $('Use your company server').tap(); + await $.waitUntilVisible($(HomeserverPickerView)); + await $.enterText($(HomeserverTextField), serverUrl); + await $.tap($('Continue')); + + await $.native.enterTextByIndex( + username, + index: 0, + ); + await $.native.enterTextByIndex( + password, + index: 1, + ); + await $.native.tap( + Selector( + text: 'Sign in', + instance: 1, + ), + ); + await expectViewVisible($(ChatList)); + } +} diff --git a/integration_test/tests/login/login_test.dart b/integration_test/tests/login/login_test.dart new file mode 100644 index 0000000000..cd33ba0e50 --- /dev/null +++ b/integration_test/tests/login/login_test.dart @@ -0,0 +1,19 @@ +import '../../base/test_base.dart'; +import '../../scenarios/login_scenario.dart'; + +void main() { + TestBase().runPatrolTest( + description: + 'Should see chat list after successful login', + test: ($) async { + final loginScenario = LoginScenario( + $, + username: const String.fromEnvironment('USERNAME'), + serverUrl: const String.fromEnvironment('SERVER_URL'), + password: const String.fromEnvironment('PASSWORD'), + ); + + await loginScenario.execute(); + }, + ); +} From 5e28b61276fdc6f8c38dccbecda328555b53c298 Mon Sep 17 00:00:00 2001 From: KhaledNjim <160496984+KhaledNjim@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:09:36 +0100 Subject: [PATCH 03/10] fixup! LA-2185 added integration test for login --- integration_test/base/core_robot.dart | 9 ++++ integration_test/base/test_base.dart | 29 +++------- integration_test/robots/login_robot.dart | 53 +++++++++++++++++++ .../scenarios/login_scenario.dart | 32 +++++------ integration_test/tests/login/login_test.dart | 3 +- 5 files changed, 85 insertions(+), 41 deletions(-) create mode 100644 integration_test/base/core_robot.dart create mode 100644 integration_test/robots/login_robot.dart diff --git a/integration_test/base/core_robot.dart b/integration_test/base/core_robot.dart new file mode 100644 index 0000000000..7ceee8ff68 --- /dev/null +++ b/integration_test/base/core_robot.dart @@ -0,0 +1,9 @@ +import 'package:patrol/patrol.dart'; + +abstract class CoreRobot { + final PatrolIntegrationTester $; + + CoreRobot(this.$); + + dynamic ignoreException() => $.tester.takeException(); +} diff --git a/integration_test/base/test_base.dart b/integration_test/base/test_base.dart index c4f7f57e81..5bef48cf28 100644 --- a/integration_test/base/test_base.dart +++ b/integration_test/base/test_base.dart @@ -1,12 +1,7 @@ -import 'package:fluffychat/di/global/get_it_initializer.dart'; -import 'package:fluffychat/utils/client_manager.dart'; -import 'package:fluffychat/widgets/twake_app.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:go_router/go_router.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:patrol/patrol.dart'; +import 'package:fluffychat/main.dart' as app; class TestBase { void runPatrolTest({ @@ -15,15 +10,17 @@ class TestBase { }) { patrolTest(description, config: const PatrolTesterConfig( - settlePolicy: SettlePolicy.trySettle, + printLogs: true, visibleTimeout: Duration(minutes: 1), ), nativeAutomatorConfig: const NativeAutomatorConfig( - findTimeout: Duration(seconds: 10), + connectionTimeout: Duration(minutes: 1, seconds: 10), + findTimeout: Duration(seconds: 60), + keyboardBehavior: KeyboardBehavior.alternative, ), framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive, ($) async { - await initTwakeChat($); + await initTwakeChat(); final originalOnError = FlutterError.onError!; FlutterError.onError = (FlutterErrorDetails details) { originalOnError(details); @@ -32,17 +29,7 @@ class TestBase { }); } - Future initTwakeChat(PatrolIntegrationTester $) async { - MediaKit.ensureInitialized(); - GoRouter.optionURLReflectsImperativeAPIs = true; - await Hive.initFlutter(); - GetItInitializer().setUp(); - final clients = await ClientManager.getClients(); - final firstClient = clients.firstOrNull; - await firstClient?.roomsLoading; - await firstClient?.accountDataLoading; - await $.pumpWidgetAndSettle( - TwakeApp(clients: clients), - ); + Future initTwakeChat() async { + app.main(); } } diff --git a/integration_test/robots/login_robot.dart b/integration_test/robots/login_robot.dart new file mode 100644 index 0000000000..4d5aa3f49b --- /dev/null +++ b/integration_test/robots/login_robot.dart @@ -0,0 +1,53 @@ +import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; +import 'package:patrol/patrol.dart'; + +import '../base/core_robot.dart'; + +class LoginRobot extends CoreRobot { + LoginRobot(super.$); + + Future grantNotificationPermission( + NativeAutomator nativeAutomator, + ) async { + if (await nativeAutomator.isPermissionDialogVisible( + timeout: const Duration(seconds: 15), + )) { + await nativeAutomator.grantPermissionWhenInUse(); + } + } + + Future tapOnUseYourCompanyServer() async { + await $('Use your company server').tap(); + } + + Future enterServerUrl(String serverUrl) async { + await $.enterText($(HomeserverTextField), serverUrl); + } + + Future confirmServerUrl() async { + await $.tap($('Continue')); + } + + Future enterUsernameSsoLogin(String username) async { + await $.native.enterText( + Selector(resourceId: 'login'), + text: username, + ); + } + + Future enterPasswordSsoLogin(String password) async { + await $.native.enterText( + Selector(resourceId: 'password'), + text: password, + ); + } + + Future pressSignInSsoLogin() async { + await $.native.tap( + Selector( + text: 'Sign in', + instance: 1, + ), + ); + } +} diff --git a/integration_test/scenarios/login_scenario.dart b/integration_test/scenarios/login_scenario.dart index f23ac80536..0fe6bfeda0 100644 --- a/integration_test/scenarios/login_scenario.dart +++ b/integration_test/scenarios/login_scenario.dart @@ -2,8 +2,8 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:patrol/patrol.dart'; import '../base/base_scenario.dart'; +import '../robots/login_robot.dart'; class LoginScenario extends BaseScenario { final String username; @@ -20,27 +20,23 @@ class LoginScenario extends BaseScenario { }); @override Future execute() async { + final loginRobot = LoginRobot($); await $.waitUntilVisible($(TwakeWelcome)); await expectViewVisible($(TwakeWelcome)); - await $('Use your company server').tap(); - await $.waitUntilVisible($(HomeserverPickerView)); - await $.enterText($(HomeserverTextField), serverUrl); - await $.tap($('Continue')); - - await $.native.enterTextByIndex( - username, - index: 0, - ); - await $.native.enterTextByIndex( - password, - index: 1, + await loginRobot.tapOnUseYourCompanyServer(); + await $.waitUntilVisible( + $(HomeserverPickerView), ); - await $.native.tap( - Selector( - text: 'Sign in', - instance: 1, - ), + await loginRobot.enterServerUrl(serverUrl); + await loginRobot.confirmServerUrl(); + + await loginRobot.enterUsernameSsoLogin(username); + await loginRobot.enterPasswordSsoLogin(password); + await loginRobot.pressSignInSsoLogin(); + await $.waitUntilVisible( + $(HomeserverPickerView), ); + await loginRobot.grantNotificationPermission($.nativeAutomator); await expectViewVisible($(ChatList)); } } diff --git a/integration_test/tests/login/login_test.dart b/integration_test/tests/login/login_test.dart index cd33ba0e50..1fa2f0cc37 100644 --- a/integration_test/tests/login/login_test.dart +++ b/integration_test/tests/login/login_test.dart @@ -3,8 +3,7 @@ import '../../scenarios/login_scenario.dart'; void main() { TestBase().runPatrolTest( - description: - 'Should see chat list after successful login', + description: 'Should see chat list after successful login', test: ($) async { final loginScenario = LoginScenario( $, From 262e97c5da8a1a7401a8804e38a1a6c08584b432 Mon Sep 17 00:00:00 2001 From: "khaled.njim" Date: Tue, 24 Dec 2024 15:41:40 +0100 Subject: [PATCH 04/10] TW-2196 added adr for patrol integration tests --- docs/adr/0025-add-patrol-integration-tests.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/adr/0025-add-patrol-integration-tests.md diff --git a/docs/adr/0025-add-patrol-integration-tests.md b/docs/adr/0025-add-patrol-integration-tests.md new file mode 100644 index 0000000000..5595f70cd9 --- /dev/null +++ b/docs/adr/0025-add-patrol-integration-tests.md @@ -0,0 +1,26 @@ +# 25. Add patrol integration tests + +Date: 2024-12-24 + +## Status + +**Accepted** + +## Context + + - The need for integration tests to handle real scenarios + - The need to handle interactions with native views in tests such as notification popups or webviews + - Mocking matrix's behaviour using mockito causes a lot of unexpected issues + +## Decision + +- Add integration tests using Patrol + +## Consequences + + - Setup patrol locally: + - Run `dart pub global activate patrol_cli` to enable Patrol CLI + - Run tests locally: + - to run tests we use `patrol test -t path/to/test --dart-define=arg1='value' ` + - to run tests in dev mode this will enable hot restarting the tests we use : `patrol test -t path/to/test --dart-define=arg1='value' ` + arguments are passed for each test using `dart-define` as shown above \ No newline at end of file From d88f859e5f655d799f4a96fb924a57cc471100e8 Mon Sep 17 00:00:00 2001 From: "khaled.njim" Date: Tue, 7 Jan 2025 16:55:46 +0100 Subject: [PATCH 05/10] fixup! TW-2196 added adr for patrol integration tests --- docs/adr/0025-add-patrol-integration-tests.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/adr/0025-add-patrol-integration-tests.md b/docs/adr/0025-add-patrol-integration-tests.md index 5595f70cd9..f6334d3a6f 100644 --- a/docs/adr/0025-add-patrol-integration-tests.md +++ b/docs/adr/0025-add-patrol-integration-tests.md @@ -14,13 +14,21 @@ Date: 2024-12-24 ## Decision -- Add integration tests using Patrol +- Add integration tests using Patrol +### Why Patrol? + +- Native Interaction: Patrol offers reliable tools for interacting with native views such as + notification popups, platform alerts, or webviews—functionality that can otherwise be cumbersome + with basic Flutter integration tests. +- Reduced Unpredictability: Since Patrol focuses on real-device or simulator-based tests, it helps + reduce the unpredictability that can arise from heavy mocking with tools like Mockito. ## Consequences - Setup patrol locally: - Run `dart pub global activate patrol_cli` to enable Patrol CLI - Run tests locally: - to run tests we use `patrol test -t path/to/test --dart-define=arg1='value' ` - - to run tests in dev mode this will enable hot restarting the tests we use : `patrol test -t path/to/test --dart-define=arg1='value' ` + - to run tests in dev mode this will enable hot restarting the tests we + use : `patrol develop -t path/to/test --dart-define=arg1='value' ` arguments are passed for each test using `dart-define` as shown above \ No newline at end of file From dcb91f09d263f6037fd5a70cf36538e1267cb75e Mon Sep 17 00:00:00 2001 From: "khaled.njim" Date: Tue, 7 Jan 2025 17:01:38 +0100 Subject: [PATCH 06/10] fixup! fixup! LA-2185 added integration test for login --- integration_test/base/test_base.dart | 8 +++----- integration_test/tests/login/login_test.dart | 7 +++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/integration_test/base/test_base.dart b/integration_test/base/test_base.dart index 5bef48cf28..a40a8469b1 100644 --- a/integration_test/base/test_base.dart +++ b/integration_test/base/test_base.dart @@ -7,17 +7,15 @@ class TestBase { void runPatrolTest({ required String description, required Function(PatrolIntegrationTester $) test, + NativeAutomatorConfig? nativeAutomatorConfig, }) { patrolTest(description, config: const PatrolTesterConfig( printLogs: true, visibleTimeout: Duration(minutes: 1), ), - nativeAutomatorConfig: const NativeAutomatorConfig( - connectionTimeout: Duration(minutes: 1, seconds: 10), - findTimeout: Duration(seconds: 60), - keyboardBehavior: KeyboardBehavior.alternative, - ), + nativeAutomatorConfig: + nativeAutomatorConfig ?? const NativeAutomatorConfig(), framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.fullyLive, ($) async { await initTwakeChat(); diff --git a/integration_test/tests/login/login_test.dart b/integration_test/tests/login/login_test.dart index 1fa2f0cc37..d999b31d93 100644 --- a/integration_test/tests/login/login_test.dart +++ b/integration_test/tests/login/login_test.dart @@ -1,9 +1,16 @@ +import 'package:patrol/patrol.dart'; + import '../../base/test_base.dart'; import '../../scenarios/login_scenario.dart'; void main() { TestBase().runPatrolTest( description: 'Should see chat list after successful login', + nativeAutomatorConfig: const NativeAutomatorConfig( + connectionTimeout: Duration(minutes: 1, seconds: 10), + findTimeout: Duration(seconds: 60), + keyboardBehavior: KeyboardBehavior.alternative, + ), test: ($) async { final loginScenario = LoginScenario( $, From 0e60114a853055d19c4da6e84b175ed4aaf5892d Mon Sep 17 00:00:00 2001 From: "khaled.njim" Date: Mon, 13 Jan 2025 01:46:29 +0100 Subject: [PATCH 07/10] LA-2185 Added github action for integration test --- .github/workflows/integration-tests.yaml | 84 ++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/integration-tests.yaml diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml new file mode 100644 index 0000000000..2e1c0553cc --- /dev/null +++ b/.github/workflows/integration-tests.yaml @@ -0,0 +1,84 @@ +on: + pull_request: + +name: Integration + +jobs: + integration_test: + name: Integration Test + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + actions: write + checks: write + pull-requests: write + + env: + USERNAME: ${{ secrets.USERNAME }} + PASSWORD: ${{ secrets.PASSWORD }} + SERVER_URL: ${{ secrets.SERVER_URL }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + project_id: ${{ secrets.GOOGLE_CLOUD_PROJECT_ID }} + workload_identity_provider: ${{ secrets.GOOGLE_CLOUD_WORKLOAD_IDENTITY_PROVIDER_ID }} + service_account: ${{ secrets.GOOGLE_CLOUD_SERVICE_ACCOUNT }} + + - name: Setup Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + + - name: Setup flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.24.0" + channel: "stable" + cache: true + cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}" + + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: "temurin" + + - uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_KEY }} + + - name: setup project + run: + flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs + + - name: Run Patrol Tests + shell: bash + + env: + USERNAME: ${{ secrets.USERNAME }} + PASSWORD: ${{ secrets.PASSWORD }} + SERVER_URL: ${{ secrets.SERVER_URL }} + run: | + + dart pub global activate patrol_cli + + flutter build apk --config-only + + patrol build android \ + -v \ + --dart-define=USERNAME="$USERNAME" \ + --dart-define=SERVER_URL="$SERVER_URL" \ + --dart-define=PASSWORD="$PASSWORD" + + gcloud firebase test android run \ + --type instrumentation \ + --app build/app/outputs/apk/debug/app-debug.apk \ + --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \ + --device model=MediumPhone.arm,version=34 \ + --timeout 5m \ + --use-orchestrator \ + --environment-variables clearPackageData=true From 77a578c17e64cca34d4f4e010678f8d526284b6f Mon Sep 17 00:00:00 2001 From: "khaled.njim" Date: Mon, 13 Jan 2025 01:50:30 +0100 Subject: [PATCH 08/10] fixup! fixup! fixup! LA-2185 added integration test for login --- integration_test/scenarios/login_scenario.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/integration_test/scenarios/login_scenario.dart b/integration_test/scenarios/login_scenario.dart index 0fe6bfeda0..a67e5391b2 100644 --- a/integration_test/scenarios/login_scenario.dart +++ b/integration_test/scenarios/login_scenario.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol/patrol.dart'; import '../base/base_scenario.dart'; import '../robots/login_robot.dart'; @@ -22,14 +23,14 @@ class LoginScenario extends BaseScenario { Future execute() async { final loginRobot = LoginRobot($); await $.waitUntilVisible($(TwakeWelcome)); - await expectViewVisible($(TwakeWelcome)); await loginRobot.tapOnUseYourCompanyServer(); await $.waitUntilVisible( $(HomeserverPickerView), ); await loginRobot.enterServerUrl(serverUrl); await loginRobot.confirmServerUrl(); - + await $.native.tap(Selector(text: "Use without an account")); + await $.native.waitUntilVisible(Selector(resourceId: 'login')); await loginRobot.enterUsernameSsoLogin(username); await loginRobot.enterPasswordSsoLogin(password); await loginRobot.pressSignInSsoLogin(); From 07cee61ccbf06d16e7c3c39664f1f0fe6e893b19 Mon Sep 17 00:00:00 2001 From: HuyNguyen Date: Mon, 13 Jan 2025 16:25:22 +0700 Subject: [PATCH 09/10] fixup! fixup! fixup! fixup! LA-2185 added integration test for login --- integration_test/app_test.dart | 193 ------------------ integration_test/robots/login_robot.dart | 40 ++-- .../scenarios/login_scenario.dart | 33 ++- 3 files changed, 51 insertions(+), 215 deletions(-) delete mode 100644 integration_test/app_test.dart diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart deleted file mode 100644 index eec6f1b5a1..0000000000 --- a/integration_test/app_test.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'package:fluffychat/config/setting_keys.dart'; -import 'package:fluffychat/pages/chat/chat_view.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_body_view.dart'; -import 'package:fluffychat/pages/chat_list/search_title.dart'; -import 'package:fluffychat/pages/new_group/contacts_selection_view.dart'; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'package:fluffychat/main.dart' as app; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'extensions/default_flows.dart'; -import 'extensions/wait_for.dart'; -import 'users.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group( - 'Integration Test', - () { - setUpAll( - () async { - // this random dialog popping up is super hard to cover in tests - SharedPreferences.setMockInitialValues({ - SettingKeys.showNoGoogle: false, - }); - try { - Hive.deleteFromDisk(); - Hive.initFlutter(); - } catch (_) {} - }, - ); - - testWidgets( - 'Start app, login and logout', - (WidgetTester tester) async { - app.main(); - await tester.ensureAppStartedHomescreen(); - await tester.ensureLoggedOut(); - }, - ); - - testWidgets( - 'Login again', - (WidgetTester tester) async { - app.main(); - await tester.ensureAppStartedHomescreen(); - }, - ); - - testWidgets( - 'Start chat and send message', - (WidgetTester tester) async { - app.main(); - await tester.ensureAppStartedHomescreen(); - await tester.waitFor(find.byType(TextField)); - await tester.enterText(find.byType(TextField), Users.user2.name); - await tester.pumpAndSettle(); - - await tester.scrollUntilVisible( - find.text('Chats').first, - 500, - scrollable: find - .descendant( - of: find.byType(ChatListBodyView), - matching: find.byType(Scrollable), - ) - .first, - ); - await tester.pumpAndSettle(); - await tester.tap(find.text('Chats')); - await tester.pumpAndSettle(); - await tester.waitFor(find.byType(SearchTitle)); - await tester.pumpAndSettle(); - - await tester.scrollUntilVisible( - find.text(Users.user2.name).first, - 500, - scrollable: find - .descendant( - of: find.byType(ChatListBodyView), - matching: find.byType(Scrollable), - ) - .first, - ); - await tester.pumpAndSettle(); - await tester.tap(find.text(Users.user2.name).first); - - try { - await tester.waitFor( - find.byType(ChatView), - timeout: const Duration(seconds: 5), - ); - } catch (_) { - // in case the homeserver sends the username as search result - if (find.byIcon(Icons.send_outlined).evaluate().isNotEmpty) { - await tester.tap(find.byIcon(Icons.send_outlined)); - await tester.pumpAndSettle(); - } - } - - await tester.waitFor(find.byType(ChatView)); - await tester.enterText(find.byType(TextField).last, 'Test'); - await tester.pumpAndSettle(); - try { - await tester.waitFor(find.byIcon(Icons.send_outlined)); - await tester.tap(find.byIcon(Icons.send_outlined)); - } catch (_) { - await tester.testTextInput.receiveAction(TextInputAction.done); - } - await tester.pumpAndSettle(); - await tester.waitFor(find.text('Test')); - await tester.pumpAndSettle(); - }, - ); - - testWidgets('Spaces', (tester) async { - app.main(); - await tester.ensureAppStartedHomescreen(); - - await tester.waitFor(find.byTooltip('Show menu')); - await tester.tap(find.byTooltip('Show menu')); - await tester.pumpAndSettle(); - - await tester.waitFor(find.byIcon(Icons.workspaces_outlined)); - await tester.tap(find.byIcon(Icons.workspaces_outlined)); - await tester.pumpAndSettle(); - - await tester.waitFor(find.byType(TextField)); - await tester.enterText(find.byType(TextField).last, 'Test Space'); - await tester.pumpAndSettle(); - - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - await tester.waitFor(find.text('Invite contact')); - - await tester.tap(find.text('Invite contact')); - await tester.pumpAndSettle(); - - await tester.waitFor( - find.descendant( - of: find.byType(ContactsSelectionView), - matching: find.byType(TextField), - ), - ); - await tester.enterText( - find.descendant( - of: find.byType(ContactsSelectionView), - matching: find.byType(TextField), - ), - Users.user2.name, - ); - - await Future.delayed(const Duration(milliseconds: 250)); - await tester.testTextInput.receiveAction(TextInputAction.done); - - await Future.delayed(const Duration(milliseconds: 1000)); - await tester.pumpAndSettle(); - - await tester.tap( - find - .descendant( - of: find.descendant( - of: find.byType(ContactsSelectionView), - matching: find.byType(ListTile), - ), - matching: find.text(Users.user2.name), - ) - .last, - ); - await tester.pumpAndSettle(); - - await tester.waitFor(find.maybeUppercaseText('Yes')); - await tester.tap(find.maybeUppercaseText('Yes')); - await tester.pumpAndSettle(); - - await tester.tap(find.byTooltip('Back')); - await tester.pumpAndSettle(); - - await tester.waitFor(find.text('Load 2 more participants')); - await tester.tap(find.text('Load 2 more participants')); - await tester.pumpAndSettle(); - - expect(find.text(Users.user2.name), findsOneWidget); - }); - }, - ); -} diff --git a/integration_test/robots/login_robot.dart b/integration_test/robots/login_robot.dart index 4d5aa3f49b..db8e29c93b 100644 --- a/integration_test/robots/login_robot.dart +++ b/integration_test/robots/login_robot.dart @@ -29,25 +29,37 @@ class LoginRobot extends CoreRobot { } Future enterUsernameSsoLogin(String username) async { - await $.native.enterText( - Selector(resourceId: 'login'), - text: username, - ); + try { + await $.native.enterText( + Selector(resourceId: 'login'), + text: username, + ); + } catch (e) { + ignoreException(); + } } Future enterPasswordSsoLogin(String password) async { - await $.native.enterText( - Selector(resourceId: 'password'), - text: password, - ); + try { + await $.native.enterText( + Selector(resourceId: 'password'), + text: password, + ); + } catch (e) { + ignoreException(); + } } Future pressSignInSsoLogin() async { - await $.native.tap( - Selector( - text: 'Sign in', - instance: 1, - ), - ); + try { + await $.native.tap( + Selector( + text: 'Sign in', + instance: 1, + ), + ); + } catch (e) { + ignoreException(); + } } } diff --git a/integration_test/scenarios/login_scenario.dart b/integration_test/scenarios/login_scenario.dart index a67e5391b2..1912fdb1cc 100644 --- a/integration_test/scenarios/login_scenario.dart +++ b/integration_test/scenarios/login_scenario.dart @@ -19,25 +19,42 @@ class LoginScenario extends BaseScenario { required this.serverUrl, required this.password, }); + @override Future execute() async { final loginRobot = LoginRobot($); await $.waitUntilVisible($(TwakeWelcome)); await loginRobot.tapOnUseYourCompanyServer(); - await $.waitUntilVisible( - $(HomeserverPickerView), - ); + await _handleWaitUntilVisibleHomeServerPickerView(loginRobot); await loginRobot.enterServerUrl(serverUrl); await loginRobot.confirmServerUrl(); - await $.native.tap(Selector(text: "Use without an account")); - await $.native.waitUntilVisible(Selector(resourceId: 'login')); + await _handleFirebaseTestLab(loginRobot); await loginRobot.enterUsernameSsoLogin(username); await loginRobot.enterPasswordSsoLogin(password); await loginRobot.pressSignInSsoLogin(); - await $.waitUntilVisible( - $(HomeserverPickerView), - ); + await _handleWaitUntilVisibleHomeServerPickerView(loginRobot); await loginRobot.grantNotificationPermission($.nativeAutomator); await expectViewVisible($(ChatList)); } + + Future _handleFirebaseTestLab(LoginRobot loginRobot) async { + try { + await $.native.tap(Selector(text: "Use without an account")); + await $.native.waitUntilVisible(Selector(resourceId: 'login')); + } catch (e) { + loginRobot.ignoreException(); + } + } + + Future _handleWaitUntilVisibleHomeServerPickerView( + LoginRobot loginRobot, + ) async { + try { + await $.waitUntilVisible( + $(HomeserverPickerView), + ); + } catch (e) { + loginRobot.ignoreException(); + } + } } From c1cb9ec3650580b3c094b42c41a26cd7ce614f69 Mon Sep 17 00:00:00 2001 From: "khaled.njim" Date: Mon, 13 Jan 2025 11:51:53 +0100 Subject: [PATCH 10/10] fixup! LA-2185 Added github action for integration test --- .github/workflows/integration-tests.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 2e1c0553cc..6029890c98 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -1,5 +1,7 @@ on: - pull_request: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" name: Integration @@ -18,6 +20,8 @@ jobs: USERNAME: ${{ secrets.USERNAME }} PASSWORD: ${{ secrets.PASSWORD }} SERVER_URL: ${{ secrets.SERVER_URL }} + FLUTTER_VERSION: "3.24.0" + JAVA_VERSION: 17 steps: - name: Checkout repository @@ -36,7 +40,7 @@ jobs: - name: Setup flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.24.0" + flutter-version: ${{ env.FLUTTER_VERSION }} channel: "stable" cache: true cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:-${{ hashFiles('**/pubspec.lock') }}" @@ -44,7 +48,7 @@ jobs: - name: Set up Java uses: actions/setup-java@v4 with: - java-version: 17 + java-version: ${{ env.JAVA_VERSION }} distribution: "temurin" - uses: webfactory/ssh-agent@v0.9.0