Skip to content

Commit b14f0e5

Browse files
committed
Testing slideshow deletion for localOnly
Testing the case of localOnly files. As the deletion logic differs per scenario, tests for the online scenario will also need to be added. Signed-off-by: Philipp Hasper <vcs@hasper.info>
1 parent cbf0c0e commit b14f0e5

3 files changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Philipp Hasper <vcs@hasper.info>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.nextcloud.test
8+
9+
import com.nextcloud.client.network.Connectivity
10+
import com.nextcloud.client.network.ConnectivityService
11+
12+
/** A mocked connectivity service returning that the device is offline **/
13+
class ConnectivityServiceOfflineMock : ConnectivityService {
14+
override fun isNetworkAndServerAvailable(callback: ConnectivityService.GenericCallback<Boolean>) {
15+
callback.onComplete(false)
16+
}
17+
18+
override fun isConnected(): Boolean = false
19+
20+
override fun isInternetWalled(): Boolean = false
21+
22+
override fun getConnectivity(): Connectivity = Connectivity.CONNECTED_WIFI
23+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Philipp Hasper <vcs@hasper.info>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.nextcloud.test
8+
9+
import android.content.Context
10+
import android.view.View
11+
import androidx.test.espresso.FailureHandler
12+
import androidx.test.espresso.base.DefaultFailureHandler
13+
import org.hamcrest.Matcher
14+
15+
/**
16+
* When testing inside of a loop, test failures are hard to attribute. For that, wrap them in an outer
17+
* exception detailing more about the context.
18+
*
19+
* Set the failure handler via
20+
* ```
21+
* Espresso.setFailureHandler(
22+
* LoopFailureHandler(targetContext, "Test failed in iteration $yourTestIterationCounter")
23+
* )
24+
* ```
25+
* and set it back to the default afterwards via
26+
* ```
27+
* Espresso.setFailureHandler(DefaultFailureHandler(targetContext))
28+
* ```
29+
*/
30+
class LoopFailureHandler(targetContext: Context, private val loopMessage: String) : FailureHandler {
31+
private val delegate: FailureHandler = DefaultFailureHandler(targetContext)
32+
33+
override fun handle(error: Throwable?, viewMatcher: Matcher<View?>?) {
34+
// Wrap in additional Exception
35+
delegate.handle(Exception(loopMessage, error), viewMatcher)
36+
}
37+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2025 Philipp Hasper <vcs@hasper.info>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
package com.owncloud.android.ui.preview
8+
9+
import androidx.appcompat.widget.ActionBarContainer
10+
import androidx.test.core.app.launchActivity
11+
import androidx.test.espresso.Espresso
12+
import androidx.test.espresso.Espresso.onView
13+
import androidx.test.espresso.IdlingRegistry
14+
import androidx.test.espresso.action.ViewActions
15+
import androidx.test.espresso.assertion.ViewAssertions.matches
16+
import androidx.test.espresso.base.DefaultFailureHandler
17+
import androidx.test.espresso.matcher.RootMatchers.isDialog
18+
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
19+
import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
20+
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
21+
import androidx.test.espresso.matcher.ViewMatchers.isRoot
22+
import androidx.test.espresso.matcher.ViewMatchers.withId
23+
import androidx.test.espresso.matcher.ViewMatchers.withText
24+
import com.nextcloud.test.ConnectivityServiceOfflineMock
25+
import com.nextcloud.test.LoopFailureHandler
26+
import com.owncloud.android.AbstractOnServerIT
27+
import com.owncloud.android.R
28+
import com.owncloud.android.datamodel.OCFile
29+
import org.hamcrest.Matchers.allOf
30+
import org.junit.Test
31+
import java.io.File
32+
33+
class PreviewImageActivityIT : AbstractOnServerIT() {
34+
lateinit var testFiles: List<OCFile>
35+
36+
fun createMockedImageFiles(count: Int, localOnly: Boolean) {
37+
val srcPngFile = getFile("imageFile.png")
38+
testFiles = (0 until count).map { i ->
39+
val pngFile = File(srcPngFile.parent ?: ".", "image$i.png")
40+
srcPngFile.copyTo(pngFile, overwrite = true)
41+
42+
OCFile("/${pngFile.name}").apply {
43+
storagePath = pngFile.absolutePath
44+
mimeType = "image/png"
45+
modificationTimestamp = 1000000
46+
permissions = "D" // OCFile.PERMISSION_CAN_DELETE_OR_LEAVE_SHARE. Required for deletion button to show
47+
remoteId = if (localOnly) null else "abc-mocked-remote-id" // mocking the file to be on the server
48+
}.also {
49+
storageManager.saveNewFile(it)
50+
}
51+
}
52+
}
53+
54+
fun veryImageThenDelete(index: Int) {
55+
val currentFileName = testFiles[index].fileName
56+
Espresso.setFailureHandler(
57+
LoopFailureHandler(targetContext, "Test failed with image file index $index, $currentFileName")
58+
)
59+
60+
onView(withId(R.id.image))
61+
.check(matches(isDisplayed()))
62+
63+
// Check that the Action Bar shows the file name as title
64+
onView(
65+
allOf(
66+
isDescendantOfA(isAssignableFrom(ActionBarContainer::class.java)),
67+
withText(currentFileName)
68+
)
69+
).check(matches(isDisplayed()))
70+
71+
// Open the Action Bar's overflow menu.
72+
// The official way would be:
73+
// openActionBarOverflowOrOptionsMenu(targetContext)
74+
// But this doesn't find the view. Presumably because Espresso.OVERFLOW_BUTTON_MATCHER looks for the description
75+
// "More options", whereas it actually says "More menu".
76+
// selecting by this would also work:
77+
// onView(withContentDescription("More menu")).perform(ViewActions.click())
78+
// For now, we identify it by the ID we know it to be
79+
onView(withId(R.id.custom_menu_placeholder_item)).perform(ViewActions.click())
80+
81+
// Click the "Remove" button
82+
onView(withText(R.string.common_remove)).perform(ViewActions.click())
83+
84+
// Check confirmation dialog and then confirm the deletion by clicking the main button of the dialog
85+
val expectedText = targetContext.getString(R.string.confirmation_remove_file_alert, currentFileName)
86+
onView(withId(android.R.id.message))
87+
.inRoot(isDialog())
88+
.check(matches(withText(expectedText)))
89+
90+
onView(withId(android.R.id.button1))
91+
.inRoot(isDialog())
92+
.check(matches(withText(R.string.file_delete)))
93+
.perform(ViewActions.click())
94+
95+
Espresso.setFailureHandler(DefaultFailureHandler(targetContext))
96+
}
97+
98+
@Test
99+
fun deleteFromSlideshow_localOnly_online() {
100+
// Prepare local test data
101+
val imageCount = 5
102+
createMockedImageFiles(imageCount, localOnly = true)
103+
104+
// Launch the activity with the first image
105+
val intent = PreviewImageActivity.previewFileIntent(targetContext, user, testFiles[0])
106+
launchActivity<PreviewImageActivity>(intent).use {
107+
onView(isRoot()).check(matches(isDisplayed()))
108+
109+
for (i in 0 until imageCount) {
110+
veryImageThenDelete(i)
111+
}
112+
}
113+
}
114+
115+
@Test
116+
fun deleteFromSlideshow_localOnly_offline() {
117+
// Prepare local test data
118+
val imageCount = 5
119+
createMockedImageFiles(imageCount, localOnly = true)
120+
121+
// Launch the activity with the first image
122+
val intent = PreviewImageActivity.previewFileIntent(targetContext, user, testFiles[0])
123+
launchActivity<PreviewImageActivity>(intent).use { scenario ->
124+
scenario.onActivity { activity ->
125+
activity.connectivityService = ConnectivityServiceOfflineMock()
126+
}
127+
onView(isRoot()).check(matches(isDisplayed()))
128+
129+
for (i in 0 until imageCount) {
130+
veryImageThenDelete(i)
131+
}
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)