diff --git a/build-logic/plugins/build.gradle.kts b/build-logic/plugins/build.gradle.kts index 52f8e226940..f2eb84faa8e 100644 --- a/build-logic/plugins/build.gradle.kts +++ b/build-logic/plugins/build.gradle.kts @@ -63,5 +63,9 @@ gradlePlugin { id = libs.plugins.wire.versionizer.get().pluginId implementationClass = "AppVersionPlugin" } + register("androidTestLibraryConventionPlugin") { + id = libs.plugins.wire.android.test.library.get().pluginId + implementationClass = "AndroidTestLibraryConventionPlugin" + } } } diff --git a/build-logic/plugins/src/main/kotlin/AndroidTestLibraryConventionPlugin.kt b/build-logic/plugins/src/main/kotlin/AndroidTestLibraryConventionPlugin.kt new file mode 100644 index 00000000000..a5f2dd29ed1 --- /dev/null +++ b/build-logic/plugins/src/main/kotlin/AndroidTestLibraryConventionPlugin.kt @@ -0,0 +1,76 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +import com.android.build.api.dsl.AndroidSourceSet +import com.android.build.gradle.LibraryExtension +import com.wire.android.gradle.configureCompose +import com.wire.android.gradle.configureKotlinAndroid +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get + +class AndroidTestLibraryConventionPlugin : Plugin { + override fun apply(target: Project): Unit = with(target) { + with(pluginManager) { + apply("com.android.library") + apply("org.jetbrains.kotlin.android") + } + + extensions.configure { + namespace = "com.wire.android.tests.${target.name.replace("-", "_")}" + + configureKotlinAndroid(this) + defaultConfig.targetSdk = AndroidSdk.target + configureCompose(this) + + defaultConfig { + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments.putAll( + mapOf( + "clearPackageData" to "true", + "force-queryable" to "true" + ) + ) + } + + // This enables us to share some code between UI and Unit tests! + fun AndroidSourceSet.includeCommonTestSourceDir() = java { + srcDir("src/commonTest/kotlin") + } + sourceSets["test"].includeCommonTestSourceDir() + sourceSets["androidTest"].includeCommonTestSourceDir() + + testOptions { + execution = "ANDROIDX_TEST_ORCHESTRATOR" + animationsDisabled = true + unitTests.isReturnDefaultValues = true + unitTests.isIncludeAndroidResources = true + } + } + + buildTypes { + // submodules using this plugin can skip minification, since the app will do it + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + } + } +} diff --git a/build-logic/plugins/src/test/resources/version.txt b/build-logic/plugins/src/test/resources/version.txt index a2f6e245a16..91a230bf99d 100644 --- a/build-logic/plugins/src/test/resources/version.txt +++ b/build-logic/plugins/src/test/resources/version.txt @@ -2,4 +2,4 @@ VersionCode: 100018802 VersionName: 4.8.2-18802 Revision: e53642019 Buildtime: 2024-08-21 19:32:06 -Application-name: com.wire \ No newline at end of file +Application-name: com.wire diff --git a/docs/adr/0006-enterprise-login-supporting-both-flows.md b/docs/adr/0006-enterprise-login-supporting-both-flows.md index 946acb2b802..9e141262daf 100644 --- a/docs/adr/0006-enterprise-login-supporting-both-flows.md +++ b/docs/adr/0006-enterprise-login-supporting-both-flows.md @@ -1,4 +1,4 @@ -# 5. Simplified enterprise login +# 6. Simplified enterprise login Date: 2025-01-23 diff --git a/docs/adr/0007-introducing-uiautomator.md b/docs/adr/0007-introducing-uiautomator.md new file mode 100644 index 00000000000..dd73a49f505 --- /dev/null +++ b/docs/adr/0007-introducing-uiautomator.md @@ -0,0 +1,36 @@ +# 7. Introducing UIAutomator for integrated testing + +Date: 2025-05-06 + +## Status + +Accepted + +## Context + +QA wants to migrate from Appium to a new framework to run tests and being closed to the source code that we are testing. +The tests nowadays are running on an emulator with an apk. After meeting the QA we evaluated options, Espresso and UIAutomator. + +## Decision + +UIAutomator seems to be the best option for us, as it is a framework that is already in the Android SDK and it is more flexible than Espresso, this last one is more limited to the app under test because of mocks and stubs. +We will create a new module(s) for testing purposes, and we will use the UIAutomator framework to run the tests, common logic can be extracted and shared between tests modules in case we want to parallelize the tests in the future. + + +## Consequences + +The new structure of the project will be as follows: +``` +wire-android +├── app +│ ├── ... +├── core +│ ├── ... +├── features +│ ├── ... +├── tests +│ ├── ... +│ ├── testsCore +│ ├── testsSupport +└── build.gradle.kts +``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 952a4592cb0..f95acbb5047 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -125,6 +125,7 @@ screenshot = { id = "com.android.compose.screenshot", version.ref = "screenshot" # Home-made convention plugins defined in build-logic wire-android-application = { id = "com.wire.android.application" } wire-android-library = { id = "com.wire.android.library" } +wire-android-test-library = { id = "com.wire.android.test.library" } wire-hilt = { id = "com.wire.android.hilt" } wire-kover = { id = "com.wire.android.kover" } wire-versionizer = { id = "com.wire.android.versionizer" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 4b57446af8f..07a301459a7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -24,7 +24,7 @@ pluginManagement { } // Include all the existent modules in the project -val basePathModules = setOf("features", "core") +val basePathModules = setOf("features", "core", "tests") val ignorableModules = setOf("buildSrc", "kalium") rootDir .walk() diff --git a/tests/testsCore/.gitignore b/tests/testsCore/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/tests/testsCore/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/tests/testsCore/build.gradle.kts b/tests/testsCore/build.gradle.kts new file mode 100644 index 00000000000..8840bbc72e0 --- /dev/null +++ b/tests/testsCore/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id(libs.plugins.wire.android.test.library.get().pluginId) +} + +android { + sourceSets { + getByName("androidTest") { + kotlin.srcDirs("src/androidTest/kotlin") + kotlin.srcDirs(project(":tests:testsSupport").file("src/androidTest/kotlin")) + } + } +} + +dependencies { + val composeBom = platform(libs.compose.bom) + implementation(composeBom) + implementation(libs.compose.ui) + + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.extJunit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.test.uiAutomator) + androidTestImplementation(project(":tests:testsSupport")) +} diff --git a/tests/testsCore/consumer-rules.pro b/tests/testsCore/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/testsCore/lint-baseline.xml b/tests/testsCore/lint-baseline.xml new file mode 100644 index 00000000000..98cd24e06c5 --- /dev/null +++ b/tests/testsCore/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/testsCore/proguard-rules.pro b/tests/testsCore/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/tests/testsCore/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/login/LoginTest.kt b/tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/login/LoginTest.kt new file mode 100644 index 00000000000..37086d844d2 --- /dev/null +++ b/tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/login/LoginTest.kt @@ -0,0 +1,50 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.tests.core.login + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.uiautomator.UiDevice +import com.wire.android.tests.core.login.pages.LoginPage +import com.wire.android.tests.support.UiAutomatorSetup +import com.wire.android.tests.support.suite.RC +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/* +This test works on the following conditions: +1) The dev/staging app is installed on the device/emulator. +*/ +@RunWith(AndroidJUnit4::class) +@RC +class LoginTest { + private lateinit var device: UiDevice + + @Before + fun setUp() { + device = UiAutomatorSetup.start(UiAutomatorSetup.APP_DEV) + } + + @Test + fun openTheAppAndShouldSeeEmailFieldAndLoginNextButtonWhenValid() { + LoginPage(device) + .tapOnEmailField() + .typeEmail("tester@wire.com") + .shouldEnableTheLoginButtonWhenValid() + } +} diff --git a/tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/login/pages/LoginPage.kt b/tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/login/pages/LoginPage.kt new file mode 100644 index 00000000000..18f33f33e03 --- /dev/null +++ b/tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/login/pages/LoginPage.kt @@ -0,0 +1,48 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.tests.core.login.pages + +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiObject +import androidx.test.uiautomator.UiSelector +import com.wire.android.tests.support.TIMEOUT_IN_MILLISECONDS +import org.junit.Assert.assertTrue + +data class LoginPage(private val device: UiDevice) { + + fun tapOnEmailField(): LoginPage { + val emailSsoCodeField = device.findObject(UiSelector().resourceId("userIdentifierInput")) + emailSsoCodeField.waitForExists(TIMEOUT_IN_MILLISECONDS) + emailSsoCodeField.click() + return this + } + + fun typeEmail(email: String): LoginPage { + val emailSsoCodeField: UiObject = device.findObject(UiSelector().resourceId("userIdentifierInput")) + emailSsoCodeField.waitForExists(TIMEOUT_IN_MILLISECONDS) + emailSsoCodeField.setText(email) + return this + } + + fun shouldEnableTheLoginButtonWhenValid(): LoginPage { + val loginButton = device.findObject(UiSelector().resourceId("loginButton")) + loginButton.waitForExists(TIMEOUT_IN_MILLISECONDS) + assertTrue("LoginButton not found or not enabled", loginButton.isClickable) + return this + } +} diff --git a/tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/registration/RegistrationTest.kt b/tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/registration/RegistrationTest.kt new file mode 100644 index 00000000000..b5a850c079b --- /dev/null +++ b/tests/testsCore/src/androidTest/kotlin/com/wire/android/tests/core/registration/RegistrationTest.kt @@ -0,0 +1,20 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.tests.core.registration + +// TODO: Add tests for registration diff --git a/tests/testsCore/src/main/AndroidManifest.xml b/tests/testsCore/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..95e75b5368e --- /dev/null +++ b/tests/testsCore/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/tests/testsSupport/.gitignore b/tests/testsSupport/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/tests/testsSupport/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/tests/testsSupport/build.gradle.kts b/tests/testsSupport/build.gradle.kts new file mode 100644 index 00000000000..a4c419e74c5 --- /dev/null +++ b/tests/testsSupport/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id(libs.plugins.wire.android.test.library.get().pluginId) +} + +dependencies { + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.extJunit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.test.uiAutomator) +} diff --git a/tests/testsSupport/consumer-rules.pro b/tests/testsSupport/consumer-rules.pro new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/testsSupport/lint-baseline.xml b/tests/testsSupport/lint-baseline.xml new file mode 100644 index 00000000000..98cd24e06c5 --- /dev/null +++ b/tests/testsSupport/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/testsSupport/proguard-rules.pro b/tests/testsSupport/proguard-rules.pro new file mode 100644 index 00000000000..481bb434814 --- /dev/null +++ b/tests/testsSupport/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/tests/testsSupport/src/androidTest/kotlin/com/wire/android/tests/support/UiAutomatorSetup.kt b/tests/testsSupport/src/androidTest/kotlin/com/wire/android/tests/support/UiAutomatorSetup.kt new file mode 100644 index 00000000000..e24150bc741 --- /dev/null +++ b/tests/testsSupport/src/androidTest/kotlin/com/wire/android/tests/support/UiAutomatorSetup.kt @@ -0,0 +1,96 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.tests.support + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert.assertThat + +const val TIMEOUT_IN_MILLISECONDS = 20_000L + +object UiAutomatorSetup { + + const val APP_DEV: String = "com.waz.zclient.dev.debug" + const val APP_BETA: String = "com.wire.android.internal" + const val APP_PROD: String = "com.wire" + lateinit var appPackage: String + + fun start(appPackage: String, clearData: Boolean = true): UiDevice { + this.appPackage = appPackage + + val device = getDevice() + + if (clearData) { + device.executeShellCommand("pm clear $appPackage") + } + + device.executeShellCommand("settings put secure show_ime_with_hard_keyboard 0") + device.executeShellCommand("settings put global window_animation_scale 0") + device.executeShellCommand("settings put global transition_animation_scale 0") + device.executeShellCommand("settings put global animator_duration_scale 0") + device.pressHome() + + waitForLauncher(device) + startApp() + waitAppStart(device) + + return device + } + + fun getDevice(): UiDevice { + return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + } + + private fun waitAppStart(device: UiDevice) { + device.wait(Until.hasObject(By.pkg(appPackage).depth(0)), TIMEOUT_IN_MILLISECONDS) + } + + private fun startApp() { + val context: Context = getApplicationContext() + val intent = context.packageManager.getLaunchIntentForPackage(appPackage) + try { + intent!!.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + } catch (e: Exception) { + throw RuntimeException("You must have the Wire app installed to run this test.") + } + context.startActivity(intent) + } + + private fun waitForLauncher(device: UiDevice) { + val launcherPackage = getLauncherPackageName() + assertThat(launcherPackage, CoreMatchers.notNullValue()) + device.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), TIMEOUT_IN_MILLISECONDS) + } + + private fun getLauncherPackageName(): String { + val intent = Intent(Intent.ACTION_MAIN) + intent.addCategory(Intent.CATEGORY_HOME) + + val context: Context = getApplicationContext() + val packageManager = context.packageManager + val resolveInfo = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) + return resolveInfo!!.activityInfo.packageName + } +} diff --git a/tests/testsSupport/src/androidTest/kotlin/com/wire/android/tests/support/suite/RC.kt b/tests/testsSupport/src/androidTest/kotlin/com/wire/android/tests/support/suite/RC.kt new file mode 100644 index 00000000000..9168412ca99 --- /dev/null +++ b/tests/testsSupport/src/androidTest/kotlin/com/wire/android/tests/support/suite/RC.kt @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.tests.support.suite + +/** + * Suite for running scoped tests for release candidate. + */ +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class RC diff --git a/tests/testsSupport/src/androidTest/kotlin/com/wire/android/tests/support/suite/Regression.kt b/tests/testsSupport/src/androidTest/kotlin/com/wire/android/tests/support/suite/Regression.kt new file mode 100644 index 00000000000..2143e3d7d8d --- /dev/null +++ b/tests/testsSupport/src/androidTest/kotlin/com/wire/android/tests/support/suite/Regression.kt @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2025 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.tests.support.suite + +/** + * Suite for running scoped tests for regression tests. + */ +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class Regression diff --git a/tests/testsSupport/src/main/AndroidManifest.xml b/tests/testsSupport/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..7d150eedbc3 --- /dev/null +++ b/tests/testsSupport/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + +