Skip to content

Commit e5ffcc9

Browse files
committed
Testing slideshow deletion for remote files
Signed-off-by: Philipp Hasper <vcs@hasper.info>
1 parent fe4b087 commit e5ffcc9

File tree

2 files changed

+122
-4
lines changed

2 files changed

+122
-4
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 androidx.test.espresso.IdlingResource
10+
import com.owncloud.android.datamodel.FileDataStorageManager
11+
import java.util.concurrent.atomic.AtomicLong
12+
13+
/**
14+
* IdlingResource that can be reused to watch the removal of different file ids sequentially.
15+
*
16+
* Use setFileId(fileId) before triggering the deletion. The resource will call the Espresso callback
17+
* once the file no longer exists. Call unregister from IdlingRegistry in @After.
18+
*/
19+
class FileRemovedIdlingResource(private val storageManager: FileDataStorageManager) : IdlingResource {
20+
private var resourceCallback: IdlingResource.ResourceCallback? = null
21+
22+
// 0 or negative means "no file set"
23+
private val currentFileId = AtomicLong(-1L)
24+
25+
override fun getName(): String = "${this::class.java.simpleName}"
26+
27+
override fun isIdleNow(): Boolean {
28+
val id = currentFileId.get()
29+
// If no file set, consider idle. If file set, idle only if it doesn't exist.
30+
val idle = (id <= 0L) || !storageManager.fileExists(id)
31+
if (idle && id > 0L) {
32+
// if we detect it's already removed, notify and clear
33+
resourceCallback?.onTransitionToIdle()
34+
currentFileId.set(-1L)
35+
}
36+
return idle
37+
}
38+
39+
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
40+
this.resourceCallback = callback
41+
}
42+
43+
/**
44+
* Start watching the given fileId. Call this right before performing the UI action that triggers deletion.
45+
* If fileId <= 0 initial value is treated as "no-op".
46+
*/
47+
fun setFileId(fileId: Long) {
48+
currentFileId.set(fileId)
49+
}
50+
}

app/src/androidTest/java/com/owncloud/android/ui/preview/PreviewImageActivityIT.kt

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,29 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot
2222
import androidx.test.espresso.matcher.ViewMatchers.withId
2323
import androidx.test.espresso.matcher.ViewMatchers.withText
2424
import com.nextcloud.test.ConnectivityServiceOfflineMock
25+
import com.nextcloud.test.FileRemovedIdlingResource
2526
import com.nextcloud.test.LoopFailureHandler
2627
import com.owncloud.android.AbstractOnServerIT
2728
import com.owncloud.android.R
2829
import com.owncloud.android.datamodel.OCFile
30+
import com.owncloud.android.db.OCUpload
31+
import com.owncloud.android.files.services.NameCollisionPolicy
2932
import org.hamcrest.Matchers.allOf
33+
import org.junit.After
34+
import org.junit.Before
3035
import org.junit.Test
3136
import java.io.File
3237

3338
class PreviewImageActivityIT : AbstractOnServerIT() {
39+
companion object {
40+
private const val REMOTE_FOLDER: String = "/PreviewImageActivityIT/"
41+
}
42+
3443
lateinit var testFiles: List<OCFile>
3544

36-
fun createMockedImageFiles(count: Int, localOnly: Boolean) {
45+
var fileRemovedIdlingResource = FileRemovedIdlingResource(storageManager)
46+
47+
fun createLocalMockedImageFiles(count: Int) {
3748
val srcPngFile = getFile("imageFile.png")
3849
testFiles = (0 until count).map { i ->
3950
val pngFile = File(srcPngFile.parent ?: ".", "image$i.png")
@@ -44,13 +55,40 @@ class PreviewImageActivityIT : AbstractOnServerIT() {
4455
mimeType = "image/png"
4556
modificationTimestamp = 1000000
4657
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
4858
}.also {
4959
storageManager.saveNewFile(it)
5060
}
5161
}
5262
}
5363

64+
/**
65+
* Create image files and upload them to the connected server.
66+
*
67+
* This function relies on the images not existing beforehand, as AbstractOnServerIT#deleteAllFilesOnServer()
68+
* should clean up. If it does fail, likely because that clean up didn't work and there are leftovers from
69+
* a previous run
70+
* @param count Number of files to create
71+
* @param folder Parent folder to which to upload. Must start and end with a slash
72+
*/
73+
fun createAndUploadImageFiles(count: Int, folder: String = REMOTE_FOLDER) {
74+
val srcPngFile = getFile("imageFile.png")
75+
testFiles = (0 until count).map { i ->
76+
val pngFile = File(srcPngFile.parent ?: ".", "image$i.png")
77+
srcPngFile.copyTo(pngFile, overwrite = true)
78+
79+
val ocUpload = OCUpload(
80+
pngFile.absolutePath,
81+
folder + pngFile.name,
82+
account.name
83+
).apply {
84+
nameCollisionPolicy = NameCollisionPolicy.OVERWRITE
85+
}
86+
uploadOCUpload(ocUpload)
87+
88+
fileDataStorageManager.getFileByDecryptedRemotePath(folder + pngFile.name)!!
89+
}
90+
}
91+
5492
fun veryImageThenDelete(index: Int) {
5593
val currentFileName = testFiles[index].fileName
5694
Espresso.setFailureHandler(
@@ -92,14 +130,27 @@ class PreviewImageActivityIT : AbstractOnServerIT() {
92130
.check(matches(withText(R.string.file_delete)))
93131
.perform(ViewActions.click())
94132

133+
// Register the idling resource to wait for successful deletion
134+
fileRemovedIdlingResource.setFileId(testFiles[index].fileId)
135+
95136
Espresso.setFailureHandler(DefaultFailureHandler(targetContext))
96137
}
97138

139+
@Before
140+
fun bringUp() {
141+
IdlingRegistry.getInstance().register(fileRemovedIdlingResource)
142+
}
143+
144+
@After
145+
fun tearDown() {
146+
IdlingRegistry.getInstance().unregister(fileRemovedIdlingResource)
147+
}
148+
98149
@Test
99150
fun deleteFromSlideshow_localOnly_online() {
100151
// Prepare local test data
101152
val imageCount = 10
102-
createMockedImageFiles(imageCount, localOnly = true)
153+
createLocalMockedImageFiles(imageCount)
103154

104155
// Launch the activity with the first image
105156
val intent = PreviewImageActivity.previewFileIntent(targetContext, user, testFiles[0])
@@ -116,7 +167,7 @@ class PreviewImageActivityIT : AbstractOnServerIT() {
116167
fun deleteFromSlideshow_localOnly_offline() {
117168
// Prepare local test data
118169
val imageCount = 10
119-
createMockedImageFiles(imageCount, localOnly = true)
170+
createLocalMockedImageFiles(imageCount)
120171

121172
// Launch the activity with the first image
122173
val intent = PreviewImageActivity.previewFileIntent(targetContext, user, testFiles[0])
@@ -131,4 +182,21 @@ class PreviewImageActivityIT : AbstractOnServerIT() {
131182
}
132183
}
133184
}
185+
186+
@Test
187+
fun deleteFromSlideshow_remote_online() {
188+
// Prepare local test data
189+
val imageCount = 10
190+
createAndUploadImageFiles(imageCount)
191+
192+
// Launch the activity with the first image
193+
val intent = PreviewImageActivity.previewFileIntent(targetContext, user, testFiles[0])
194+
launchActivity<PreviewImageActivity>(intent).use {
195+
onView(isRoot()).check(matches(isDisplayed()))
196+
197+
for (i in 0 until imageCount) {
198+
veryImageThenDelete(i)
199+
}
200+
}
201+
}
134202
}

0 commit comments

Comments
 (0)