Skip to content

Commit 3a9f9d7

Browse files
Merge pull request #17268 from nextcloud/ciTests
Fix CI tests
2 parents 983d6b5 + 3b483ea commit 3a9f9d7

29 files changed

Lines changed: 533 additions & 752 deletions

app/build.gradle.kts

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Nextcloud - Android Client
33
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
45
* SPDX-FileCopyrightText: 2025 Jimly Asshiddiqy <jimly.asshiddiqy@accenture.com>
56
* SPDX-License-Identifier: AGPL-3.0-or-later
67
*/
@@ -46,13 +47,24 @@ configurations.configureEach {
4647
force(libs.objenesis)
4748

4849
eachDependency {
49-
if (requested.group == "org.checkerframework" && requested.name != "checker-compat-qual") {
50-
useVersion(libs.versions.checker.get())
51-
because("https://github.com/google/ExoPlayer/issues/10007")
52-
} else if (requested.group == "org.jacoco") {
53-
useVersion(libs.versions.jacoco.get())
54-
} else if (requested.group == "commons-logging" && requested.name == "commons-logging") {
55-
useTarget(libs.slfj)
50+
when (requested.group) {
51+
"org.checkerframework" if requested.name != "checker-compat-qual" -> {
52+
useVersion(libs.versions.checker.get())
53+
because("https://github.com/google/ExoPlayer/issues/10007")
54+
}
55+
"org.jacoco" -> {
56+
useVersion(libs.versions.jacoco.get())
57+
}
58+
"commons-logging" if requested.name == "commons-logging" -> {
59+
useTarget(libs.slfj)
60+
}
61+
"org.hamcrest" -> {
62+
useVersion("2.2")
63+
because(
64+
"Align hamcrest on compile and runtime. 1.3 (via junit) exposes fixed-arity " +
65+
"anyOf/allOf overloads that 2.2 (via androidx.test) removed, causing NoSuchMethodError."
66+
)
67+
}
5668
}
5769
}
5870
}
@@ -266,6 +278,7 @@ spotbugs {
266278
}
267279

268280
tasks.register<Checkstyle>("checkstyle") {
281+
description = "Runs Checkstyle static analysis on the Java sources to enforce the project's coding style."
269282
configFile = file("${rootProject.projectDir}/checkstyle.xml")
270283
setConfigProperties(
271284
"checkstyleSuppressionsPath" to file("${rootProject.rootDir}/suppressions.xml").absolutePath
@@ -277,6 +290,7 @@ tasks.register<Checkstyle>("checkstyle") {
277290
}
278291

279292
tasks.register<Pmd>("pmd") {
293+
description = "Runs PMD static analysis on the Java sources to detect common programming flaws and bad practices."
280294
ruleSetFiles = files("${rootProject.rootDir}/ruleset.xml")
281295
ignoreFailures = true // should continue checking
282296
ruleSets = emptyList()
@@ -332,6 +346,17 @@ tasks.named("check").configure {
332346
dependsOn("checkstyle", "spotbugsGplayDebug", "pmd", "lint", "spotlessKotlinCheck", "detekt")
333347
}
334348

349+
val kspConfiguration = "ksp"
350+
val kspAndroidTestConfiguration = "kspAndroidTest"
351+
val gplayImplementationConfiguration = "gplayImplementation"
352+
val huaweiImplementationConfiguration = "huaweiImplementation"
353+
val qaImplementationConfiguration = "qaImplementation"
354+
val appScanConfigurations = listOf(
355+
gplayImplementationConfiguration,
356+
huaweiImplementationConfiguration,
357+
qaImplementationConfiguration
358+
)
359+
335360
dependencies {
336361
// region Nextcloud library
337362
implementation(libs.android.library) {
@@ -429,10 +454,10 @@ dependencies {
429454
// endregion
430455

431456
// region AppScan, document scanner not available on FDroid (generic) due to OpenCV binaries
432-
// To enable the feature for another variant, add it here.
433-
"gplayImplementation"(project(":appscan"))
434-
"huaweiImplementation"(project(":appscan"))
435-
"qaImplementation"(project(":appscan"))
457+
// To enable the feature for another variant, add its "<variant>Implementation" here.
458+
appScanConfigurations.forEach { configuration ->
459+
add(configuration, project(":appscan"))
460+
}
436461
// endregion
437462

438463
// region SpotBugs
@@ -444,9 +469,10 @@ dependencies {
444469
implementation(libs.dagger)
445470
implementation(libs.dagger.android)
446471
implementation(libs.dagger.android.support)
447-
ksp(libs.dagger.compiler)
448472
ksp(libs.dagger.processor)
449-
kspAndroidTest(libs.dagger.compiler)
473+
listOf(kspConfiguration, kspAndroidTestConfiguration).forEach { configuration ->
474+
add(configuration, libs.dagger.compiler)
475+
}
450476
// endregion
451477

452478
// region Crypto
@@ -509,7 +535,7 @@ dependencies {
509535
// endregion
510536

511537
// region Google Play dependencies, upon each update first test: new registration, receive push
512-
"gplayImplementation"(libs.bundles.gplay)
538+
add(gplayImplementationConfiguration, libs.bundles.gplay)
513539
// endregion
514540

515541
// region common

app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityScreenshotIT.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ class FileDisplayActivityScreenshotIT : AbstractIT() {
3333

3434
@get:Rule
3535
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
36-
Manifest.permission.WRITE_EXTERNAL_STORAGE,
37-
Manifest.permission.POST_NOTIFICATIONS
36+
Manifest.permission.WRITE_EXTERNAL_STORAGE
3837
)
3938

4039
companion object {

app/src/androidTest/java/com/nextcloud/client/SettingsActivityIT.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
package com.nextcloud.client
1010

1111
import android.content.Intent
12-
import android.os.Looper
1312
import androidx.test.core.app.launchActivity
1413
import androidx.test.espresso.Espresso.onView
1514
import androidx.test.espresso.assertion.ViewAssertions.matches
@@ -61,12 +60,8 @@ class SettingsActivityIT : AbstractIT() {
6160
}
6261
}
6362

64-
@Suppress("DEPRECATION")
6563
@Test
6664
fun showMnemonic() {
67-
if (Looper.myLooper() == null) {
68-
Looper.prepare()
69-
}
7065
val intent = Intent().apply {
7166
putExtra(RequestCredentialsActivity.KEY_CHECK_RESULT, RequestCredentialsActivity.KEY_CHECK_RESULT_TRUE)
7267
}
@@ -76,13 +71,10 @@ class SettingsActivityIT : AbstractIT() {
7671
}
7772

7873
launchActivity<SettingsActivity>().use { scenario ->
79-
onView(isRoot()).check(matches(isDisplayed()))
80-
8174
scenario.onActivity { sut ->
8275
sut.handleMnemonicRequest(intent)
8376
}
8477

85-
Looper.myLooper()?.quitSafely()
8678
Assert.assertTrue(true)
8779
}
8880
}

app/src/androidTest/java/com/nextcloud/client/assistant/AssistantRepositoryTests.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,13 @@ class AssistantRepositoryTests : AbstractOnServerIT() {
8787

8888
testCreateTask()
8989

90-
sleep(120)
91-
9290
runBlocking {
9391
val taskList = sut?.getTaskList("assistant")
9492
assertTrue(taskList != null)
9593

96-
sleep(120)
97-
98-
assert((taskList?.size ?: 0) > 0)
94+
val taskId = taskList?.firstOrNull()?.id ?: return@runBlocking
9995

100-
val result = sut?.deleteTask(taskList!!.first().id)
96+
val result = sut?.deleteTask(taskId)
10197
assertTrue(result?.isSuccess == true)
10298
}
10399
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.nextcloud.test
8+
9+
import android.os.ParcelFileDescriptor
10+
import androidx.test.platform.app.InstrumentationRegistry
11+
import org.junit.rules.TestRule
12+
import org.junit.runner.Description
13+
import org.junit.runners.model.Statement
14+
15+
/**
16+
* Disables system animations for the duration of the test.
17+
*
18+
* Espresso requires that no view keeps requesting layout while it waits for the root window. Animated components such
19+
* as [com.google.android.material.snackbar.Snackbar] keep animating unless the global animation scales are set to zero.
20+
* The Gradle `animationsDisabled` option only passes `--no-window-animation`, which leaves `animator_duration_scale`
21+
* untouched, so animations still run on CI. This rule zeroes all scales directly via a shell command.
22+
*/
23+
class DisableAnimationsRule : TestRule {
24+
25+
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
26+
override fun evaluate() {
27+
setAnimationScale("0.0")
28+
try {
29+
base.evaluate()
30+
} finally {
31+
setAnimationScale("1.0")
32+
}
33+
}
34+
}
35+
36+
private fun setAnimationScale(scale: String) {
37+
val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
38+
SCALE_SETTINGS.forEach { setting ->
39+
val descriptor = uiAutomation.executeShellCommand("settings put global $setting $scale")
40+
ParcelFileDescriptor.AutoCloseInputStream(descriptor).use { it.readBytes() }
41+
}
42+
}
43+
44+
companion object {
45+
private val SCALE_SETTINGS = listOf(
46+
"window_animation_scale",
47+
"transition_animation_scale",
48+
"animator_duration_scale"
49+
)
50+
}
51+
}

app/src/androidTest/java/com/nextcloud/test/FileRemovedIdlingResource.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ package com.nextcloud.test
99
import androidx.test.espresso.IdlingResource
1010
import com.owncloud.android.datamodel.FileDataStorageManager
1111
import com.owncloud.android.datamodel.OCFile
12-
import java.util.concurrent.atomic.AtomicLong
1312
import java.util.concurrent.atomic.AtomicReference
1413

1514
/**
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.nextcloud.test
8+
9+
import android.Manifest
10+
import android.os.Build
11+
import androidx.test.rule.GrantPermissionRule
12+
13+
/**
14+
* Grants the runtime permissions UI tests need, matching what the manifest actually declares:
15+
*
16+
* - WRITE_EXTERNAL_STORAGE is capped at maxSdkVersion="29", so on API 30+ the app never requests it
17+
* and granting it throws "has not requested permission WRITE_EXTERNAL_STORAGE". It is therefore
18+
* granted only when SDK_INT <= 29.
19+
* - POST_NOTIFICATIONS only exists on API 33+, so granting it on lower API levels fails with
20+
* "Unknown permission" and it is granted only from TIRAMISU onwards.
21+
*/
22+
object GrantTestPermissionRule {
23+
24+
fun grantStorageAndNotification(): GrantPermissionRule {
25+
val permissions = buildList {
26+
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
27+
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
28+
}
29+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
30+
add(Manifest.permission.POST_NOTIFICATIONS)
31+
}
32+
}
33+
@Suppress("SpreadOperator")
34+
return GrantPermissionRule.grant(*permissions.toTypedArray())
35+
}
36+
}

app/src/androidTest/java/com/nextcloud/ui/SetOnlineStatusBottomSheetIT.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ import org.junit.Test
2727
class SetOnlineStatusBottomSheetIT : AbstractIT() {
2828

2929
@get:Rule
30-
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
31-
Manifest.permission.WRITE_EXTERNAL_STORAGE,
32-
Manifest.permission.POST_NOTIFICATIONS
33-
)
30+
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
3431

3532
@Test
3633
fun open() {

app/src/androidTest/java/com/nextcloud/ui/SetStatusMessageBottomSheetIT.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
*/
99
package com.nextcloud.ui
1010

11-
import android.Manifest
1211
import androidx.test.core.app.launchActivity
1312
import androidx.test.espresso.Espresso.onView
1413
import androidx.test.espresso.assertion.ViewAssertions.matches
1514
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
1615
import androidx.test.espresso.matcher.ViewMatchers.isRoot
1716
import androidx.test.espresso.matcher.ViewMatchers.withId
1817
import androidx.test.rule.GrantPermissionRule
18+
import com.nextcloud.test.GrantTestPermissionRule
1919
import com.owncloud.android.AbstractIT
2020
import com.owncloud.android.R
2121
import com.owncloud.android.lib.resources.users.ClearAt
@@ -28,10 +28,7 @@ import org.junit.Test
2828

2929
class SetStatusMessageBottomSheetIT : AbstractIT() {
3030
@get:Rule
31-
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
32-
Manifest.permission.WRITE_EXTERNAL_STORAGE,
33-
Manifest.permission.POST_NOTIFICATIONS
34-
)
31+
val permissionRule: GrantPermissionRule = GrantTestPermissionRule.grantStorageAndNotification()
3532

3633
@Test
3734
fun open() {

app/src/androidTest/java/com/nmc/android/ui/LauncherActivityIT.kt

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@
66
*/
77
package com.nmc.android.ui
88

9+
import android.view.View
910
import androidx.test.core.app.launchActivity
10-
import androidx.test.espresso.Espresso.onView
11-
import androidx.test.espresso.assertion.ViewAssertions.matches
12-
import androidx.test.espresso.matcher.ViewMatchers
13-
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
14-
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
15-
import androidx.test.espresso.matcher.ViewMatchers.withId
1611
import androidx.test.ext.junit.runners.AndroidJUnit4
1712
import com.owncloud.android.AbstractIT
1813
import com.owncloud.android.R
14+
import org.junit.Assert.assertEquals
1915
import org.junit.Test
2016
import org.junit.runner.RunWith
2117

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

2521
@Test
2622
fun testSplashScreenWithEmptyTitlesShouldHideTitles() {
27-
launchActivity<LauncherActivity>().use { scenario ->
28-
onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed()))
29-
onView(
30-
withId(R.id.splashScreenBold)
31-
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
32-
onView(
33-
withId(R.id.splashScreenNormal)
34-
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
23+
launchActivity<LauncherActivity>().onActivity { activity ->
24+
assertEquals(View.VISIBLE, activity.findViewById<View>(R.id.ivSplash).visibility)
25+
assertEquals(View.GONE, activity.findViewById<View>(R.id.splashScreenBold).visibility)
26+
assertEquals(View.GONE, activity.findViewById<View>(R.id.splashScreenNormal).visibility)
3527
}
3628
}
3729

3830
@Test
3931
fun testSplashScreenWithTitlesShouldShowTitles() {
40-
launchActivity<LauncherActivity>().use { scenario ->
41-
onView(withId(R.id.ivSplash)).check(matches(isCompletelyDisplayed()))
32+
launchActivity<LauncherActivity>().onActivity { activity ->
33+
activity.setSplashTitles("Example", "Cloud")
4234

43-
scenario.onActivity {
44-
it.setSplashTitles("Example", "Cloud")
45-
}
46-
47-
val onePercentArea = ViewMatchers.isDisplayingAtLeast(1)
48-
onView(withId(R.id.splashScreenBold)).check(matches(onePercentArea))
49-
onView(withId(R.id.splashScreenNormal)).check(matches(onePercentArea))
35+
assertEquals(View.VISIBLE, activity.findViewById<View>(R.id.ivSplash).visibility)
36+
assertEquals(View.VISIBLE, activity.findViewById<View>(R.id.splashScreenBold).visibility)
37+
assertEquals(View.VISIBLE, activity.findViewById<View>(R.id.splashScreenNormal).visibility)
5038
}
5139
}
5240
}

0 commit comments

Comments
 (0)