Skip to content
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ configurations.configureEach {
useVersion(libs.versions.jacoco.get())
} else if (requested.group == "commons-logging" && requested.name == "commons-logging") {
useTarget(libs.slfj)
} else if (requested.group == "org.hamcrest") {
useVersion("2.2")
because(
"Align hamcrest on compile and runtime. 1.3 (via junit) exposes fixed-arity " +
"anyOf/allOf overloads that 2.2 (via androidx.test) removed, causing NoSuchMethodError."
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ class FileDisplayActivityScreenshotIT : AbstractIT() {

@get:Rule
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.POST_NOTIFICATIONS
Manifest.permission.WRITE_EXTERNAL_STORAGE
)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
package com.nextcloud.client

import android.content.Intent
import android.os.Looper
import androidx.test.core.app.launchActivity
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
Expand Down Expand Up @@ -61,12 +60,8 @@ class SettingsActivityIT : AbstractIT() {
}
}

@Suppress("DEPRECATION")
@Test
fun showMnemonic() {
if (Looper.myLooper() == null) {
Looper.prepare()
}
val intent = Intent().apply {
putExtra(RequestCredentialsActivity.KEY_CHECK_RESULT, RequestCredentialsActivity.KEY_CHECK_RESULT_TRUE)
}
Expand All @@ -76,13 +71,10 @@ class SettingsActivityIT : AbstractIT() {
}

launchActivity<SettingsActivity>().use { scenario ->
onView(isRoot()).check(matches(isDisplayed()))

scenario.onActivity { sut ->
sut.handleMnemonicRequest(intent)
}

Looper.myLooper()?.quitSafely()
Assert.assertTrue(true)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,13 @@ class AssistantRepositoryTests : AbstractOnServerIT() {

testCreateTask()

sleep(120)

runBlocking {
val taskList = sut?.getTaskList("assistant")
assertTrue(taskList != null)

sleep(120)

assert((taskList?.size ?: 0) > 0)
val taskId = taskList?.firstOrNull()?.id ?: return@runBlocking

val result = sut?.deleteTask(taskList!!.first().id)
val result = sut?.deleteTask(taskId)
assertTrue(result?.isSuccess == true)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.test

import android.os.ParcelFileDescriptor
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

/**
* Disables system animations for the duration of the test.
*
* Espresso requires that no view keeps requesting layout while it waits for the root window. Animated components such
* as [com.google.android.material.snackbar.Snackbar] keep animating unless the global animation scales are set to zero.
* The Gradle `animationsDisabled` option only passes `--no-window-animation`, which leaves `animator_duration_scale`
* untouched, so animations still run on CI. This rule zeroes all scales directly via a shell command.
*/
class DisableAnimationsRule : TestRule {

override fun apply(base: Statement, description: Description): Statement = object : Statement() {
override fun evaluate() {
setAnimationScale("0.0")
try {
base.evaluate()
} finally {
setAnimationScale("1.0")
}
}
}

private fun setAnimationScale(scale: String) {
val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
SCALE_SETTINGS.forEach { setting ->
val descriptor = uiAutomation.executeShellCommand("settings put global $setting $scale")
ParcelFileDescriptor.AutoCloseInputStream(descriptor).use { it.readBytes() }
}
}

companion object {
private val SCALE_SETTINGS = listOf(
"window_animation_scale",
"transition_animation_scale",
"animator_duration_scale"
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.test

import android.Manifest
import android.os.Build
import androidx.test.rule.GrantPermissionRule

/**
* Grants the runtime permissions UI tests need. POST_NOTIFICATIONS only exists on API 33+, so
* granting it on lower API levels fails with "Unknown permission" and it is therefore requested only
* when supported.
*/
object GrantTestPermissionRule {

fun grantStorageAndNotification(): GrantPermissionRule = if (Build.VERSION.SDK_INT >=
Build.VERSION_CODES.TIRAMISU
) {
GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.POST_NOTIFICATIONS
)
} else {
GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ import org.junit.Test
class SetOnlineStatusBottomSheetIT : AbstractIT() {

@get:Rule
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.POST_NOTIFICATIONS
)
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)

@Test
fun open() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
*/
package com.nextcloud.ui

import android.Manifest
import androidx.test.core.app.launchActivity
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.rule.GrantPermissionRule
import com.nextcloud.test.GrantTestPermissionRule
import com.owncloud.android.AbstractIT
import com.owncloud.android.R
import com.owncloud.android.lib.resources.users.ClearAt
Expand All @@ -28,10 +28,7 @@ import org.junit.Test

class SetStatusMessageBottomSheetIT : AbstractIT() {
@get:Rule
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.POST_NOTIFICATIONS
)
val permissionRule: GrantPermissionRule = GrantTestPermissionRule.grantStorageAndNotification()

@Test
fun open() {
Expand Down
34 changes: 11 additions & 23 deletions app/src/androidTest/java/com/nmc/android/ui/LauncherActivityIT.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,12 @@
*/
package com.nmc.android.ui

import android.view.View
import androidx.test.core.app.launchActivity
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.owncloud.android.AbstractIT
import com.owncloud.android.R
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith

Expand All @@ -24,29 +20,21 @@ class LauncherActivityIT : AbstractIT() {

@Test
fun testSplashScreenWithEmptyTitlesShouldHideTitles() {
launchActivity<LauncherActivity>().use { scenario ->
onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed()))
onView(
withId(R.id.splashScreenBold)
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
onView(
withId(R.id.splashScreenNormal)
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
launchActivity<LauncherActivity>().onActivity { activity ->
assertEquals(View.VISIBLE, activity.findViewById<View>(R.id.ivSplash).visibility)
assertEquals(View.GONE, activity.findViewById<View>(R.id.splashScreenBold).visibility)
assertEquals(View.GONE, activity.findViewById<View>(R.id.splashScreenNormal).visibility)
}
}

@Test
fun testSplashScreenWithTitlesShouldShowTitles() {
launchActivity<LauncherActivity>().use { scenario ->
onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed()))
launchActivity<LauncherActivity>().onActivity { activity ->
activity.setSplashTitles("Example", "Cloud")

scenario.onActivity {
it.setSplashTitles("Example", "Cloud")
}

val onePercentArea = ViewMatchers.isDisplayingAtLeast(1)
onView(withId(R.id.splashScreenBold)).check(matches(onePercentArea))
onView(withId(R.id.splashScreenNormal)).check(matches(onePercentArea))
assertEquals(View.VISIBLE, activity.findViewById<View>(R.id.ivSplash).visibility)
assertEquals(View.VISIBLE, activity.findViewById<View>(R.id.splashScreenBold).visibility)
assertEquals(View.VISIBLE, activity.findViewById<View>(R.id.splashScreenNormal).visibility)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ class ScreenshotsIT : AbstractIT() {

@get:Rule
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.POST_NOTIFICATIONS
Manifest.permission.WRITE_EXTERNAL_STORAGE
)

@Test
Expand Down
2 changes: 2 additions & 0 deletions app/src/androidTest/java/com/owncloud/android/ui/LoginIT.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.owncloud.android.authentication.AuthenticatorActivity
import org.junit.AfterClass
import org.junit.Assert
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test

Expand All @@ -53,6 +54,7 @@ class LoginIT : AbstractIT() {
@Throws(InterruptedException::class)
@Suppress("MagicNumber", "SwallowedException")
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Ignore("Login no longer uses an in-app WebView, so espresso-web (Espresso.onWebView) cannot drive it. ")
fun login() {
val arguments = InstrumentationRegistry.getArguments()
val baseUrl = arguments.getString("TEST_SERVER_URL")!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/
package com.owncloud.android.ui.activity

import android.Manifest
import android.accounts.Account
import android.accounts.AccountManager
import android.net.Uri
Expand All @@ -21,6 +20,7 @@ import androidx.test.rule.GrantPermissionRule
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.account.UserAccountManagerImpl
import com.nextcloud.test.GrantTestPermissionRule
import com.nextcloud.test.RetryTestRule
import com.owncloud.android.AbstractIT
import com.owncloud.android.MainApp
Expand All @@ -39,36 +39,36 @@ class DrawerActivityIT : AbstractIT() {
val retryTestRule = RetryTestRule()

@get:Rule
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.POST_NOTIFICATIONS
)
val permissionRule: GrantPermissionRule = GrantTestPermissionRule.grantStorageAndNotification()

@Test
fun switchAccountViaAccountList() {
launchActivity<FileDisplayActivity>().use { scenario ->
var sut: FileDisplayActivity? = null
scenario.onActivity { activity ->
sut = activity
}
// Switching accounts finishes and relaunches FileDisplayActivity (see
// FileDisplayActivity.handleRestartIntent). That self-relaunch is incompatible with
// ActivityScenario#close(), which cannot drive its tracked instance to DESTROYED, so the
// scenario is launched without auto-closing.
val scenario = launchActivity<FileDisplayActivity>()
lateinit var sut: FileDisplayActivity
scenario.onActivity { activity ->
sut = activity
}

Assert.assertEquals(account1, sut!!.user.get().toPlatformAccount())
Assert.assertEquals(account1, sut.user.get().toPlatformAccount())

onView(ViewMatchers.withId(R.id.switch_account_button)).perform(ViewActions.click())
onView(
Matchers.anyOf(
ViewMatchers.withText(account2Name),
ViewMatchers.withText(
account2DisplayName
)
onView(ViewMatchers.withId(R.id.switch_account_button)).perform(ViewActions.click())
onView(
Matchers.anyOf(
ViewMatchers.withText(account2Name),
ViewMatchers.withText(
account2DisplayName
)
).perform(ViewActions.click())
)
).perform(ViewActions.click())

Assert.assertEquals(account2, sut.user.get().toPlatformAccount())
Assert.assertEquals(account2, sut.user.get().toPlatformAccount())

onView(ViewMatchers.withId(R.id.switch_account_button)).perform(ViewActions.click())
onView(ViewMatchers.withText(account1?.name)).perform(ViewActions.click())
}
onView(ViewMatchers.withId(R.id.switch_account_button)).perform(ViewActions.click())
onView(ViewMatchers.withText(account1?.name)).perform(ViewActions.click())
}

companion object {
Expand Down
Loading
Loading