Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Philipp Hasper <vcs@hasper.info>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.test

import android.view.View
import android.widget.TextView
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.TypeSafeMatcher

fun withSelectedText(expected: String): Matcher<View> = object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("with selected text \"$expected\"")
}

@Suppress("ReturnCount")
override fun matchesSafely(view: View): Boolean {
if (view !is TextView) return false
val text = view.text?.toString() ?: ""
val s = view.selectionStart
val e = view.selectionEnd
@Suppress("ComplexCondition")
if (s < 0 || e < 0 || s > e || e > text.length) return false
return text.substring(s, e) == expected
}
}
12 changes: 8 additions & 4 deletions app/src/androidTest/java/com/owncloud/android/AbstractIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -233,17 +235,19 @@ protected Account[] getAllAccounts() {
return AccountManager.get(targetContext).getAccounts();
}

protected static void createDummyFiles() throws IOException {
protected static List<File> createDummyFiles() throws IOException {
File tempPath = new File(FileStorageUtils.getTemporalPath(account.name));
if (!tempPath.exists()) {
assertTrue(tempPath.mkdirs());
}

assertTrue(tempPath.exists());

createFile("empty.txt", 0);
createFile("nonEmpty.txt", 100);
createFile("chunkedFile.txt", 500000);
return Arrays.asList(
createFile("empty.txt", 0),
createFile("nonEmpty.txt", 100),
createFile("chunkedFile.txt", 500000)
);
}

protected static File getDummyFile(String name) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,76 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Philipp Hasper <vcs@hasper.info>
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2022 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.owncloud.android.ui.activity

import android.content.Intent
import android.net.Uri
import android.view.KeyEvent
import androidx.test.core.app.launchActivity
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import com.facebook.testing.screenshot.internal.TestNameDetector
import com.nextcloud.client.preferences.AppPreferencesImpl
import com.nextcloud.test.GrantStoragePermissionRule
import com.nextcloud.test.withSelectedText
import com.nextcloud.utils.extensions.removeFileExtension
import com.owncloud.android.AbstractIT
import com.owncloud.android.R
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.utils.ScreenshotTest
import org.hamcrest.Matchers.not
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import java.io.File

class ReceiveExternalFilesActivityIT : AbstractIT() {
private val testClassName = "com.owncloud.android.ui.activity.ReceiveExternalFilesActivityIT"

@get:Rule
var storagePermissionRule: TestRule = GrantStoragePermissionRule.grant()

lateinit var mainFolder: OCFile
lateinit var subFolder: OCFile
lateinit var existingImageFile: OCFile

@Before
fun setupFolderAndFileStructure() {
// Create folders with the necessary permissions and another test file
mainFolder = OCFile("/folder/").apply {
permissions = OCFile.PERMISSION_CAN_CREATE_FILE_AND_FOLDER
setFolder()
fileDataStorageManager.saveNewFile(this)
}
subFolder = OCFile("${mainFolder.remotePath}sub folder/").apply {
permissions = OCFile.PERMISSION_CAN_CREATE_FILE_AND_FOLDER
setFolder()
fileDataStorageManager.saveNewFile(this)
}
existingImageFile = OCFile("${mainFolder.remotePath}Existing Image File.jpg").apply {
fileDataStorageManager.saveNewFile(this)
}
}

@Test
@ScreenshotTest
fun open() {
// Screenshot name must be constructed outside of the scenario, otherwise it will not be reliably detected
val screenShotName = TestNameDetector.getTestClass() + "_" + TestNameDetector.getTestName()
launchActivity<ReceiveExternalFilesActivity>().use { scenario ->
val screenShotName = createName(testClassName + "_" + "open", "")
onView(isRoot()).check(matches(isDisplayed()))

scenario.onActivity { sut ->
Expand All @@ -40,4 +86,161 @@ class ReceiveExternalFilesActivityIT : AbstractIT() {
open()
removeAccount(secondAccount)
}

fun createSendIntent(file: File): Intent = Intent(targetContext, ReceiveExternalFilesActivity::class.java).apply {
action = Intent.ACTION_SEND
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file))
}

fun createSendIntent(files: Iterable<File>): Intent =
Intent(targetContext, ReceiveExternalFilesActivity::class.java).apply {
action = Intent.ACTION_SEND_MULTIPLE
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(files.map { Uri.fromFile(it) }))
}

@Test
fun renameSingleFileUpload() {
val imageFile = getDummyFile("image.jpg")
val intent = createSendIntent(imageFile)

// Store the folder in preferences, so the activity starts from there.
@Suppress("DEPRECATION")
val preferences = AppPreferencesImpl.fromContext(targetContext)
preferences.setLastUploadPath(mainFolder.remotePath)

launchActivity<ReceiveExternalFilesActivity>(intent).use {
val expectedMainFolderTitle = (getCurrentActivity() as ToolbarActivity).getActionBarTitle(mainFolder, false)
// Verify that the test starts in the expected folder. If this fails, change the setup calls above
onView(withId(R.id.toolbar))
.check(matches(hasDescendant(withText(expectedMainFolderTitle))))

onView(withText(R.string.uploader_btn_upload_text))
.check(matches(isDisplayed()))
.check(matches(isEnabled()))

// Test the pre-selection behavior (filename, but without extension, shall be selected)
onView(withId(R.id.user_input))
.check(matches(withText(imageFile.name)))
.perform(ViewActions.click())
.check(matches(withSelectedText(imageFile.name.removeFileExtension())))

// Set a new file name
val secondFileName = "New filename.jpg"
onView(withId(R.id.user_input))
.perform(ViewActions.typeTextIntoFocusedView(secondFileName.removeFileExtension()))
.check(matches(withText(secondFileName)))
// Leave the field and come back to verify the pre-selection behavior correctly handles the new name
.perform(ViewActions.pressKey(KeyEvent.KEYCODE_TAB))
.perform(ViewActions.click())
.check(matches(withSelectedText(secondFileName.removeFileExtension())))
onView(withText(R.string.uploader_btn_upload_text))
.check(matches(isDisplayed()))
.check(matches(isEnabled()))

// Set a file name without file extension
val thirdFileName = "No extension"
onView(withId(R.id.user_input))
.perform(ViewActions.clearText())
.perform(ViewActions.typeTextIntoFocusedView(thirdFileName))
.check(matches(withText(thirdFileName)))
// Leave the field and come back to verify the pre-selection behavior correctly handles the new name
.perform(ViewActions.pressKey(KeyEvent.KEYCODE_TAB))
.perform(ViewActions.click())
.check(matches(withSelectedText(thirdFileName)))
onView(withText(R.string.uploader_btn_upload_text))
.check(matches(isDisplayed()))
.check(matches(isEnabled()))

// Test an invalid filename. Note: as the user is null, the capabilities are also null, so the name checker
// will not reject any special characters like '/'. So we only test empty and an existing file name
onView(withId(R.id.user_input))
.perform(ViewActions.clearText())
.check(matches(withText("")))
onView(withText(R.string.uploader_btn_upload_text))
.check(matches(isDisplayed()))
.check(matches(not(isEnabled())))
onView(withId(R.id.user_input))
.perform(ViewActions.click())
.perform(ViewActions.typeTextIntoFocusedView(existingImageFile.fileName))
.check(matches(withText(existingImageFile.fileName)))
onView(withText(R.string.uploader_btn_upload_text))
.check(matches(isDisplayed()))
.check(matches(not(isEnabled())))

val fourthFileName = "New file name.jpg"
onView(withId(R.id.user_input))
.perform(ViewActions.click())
.perform(ViewActions.clearText())
.perform(ViewActions.typeTextIntoFocusedView(fourthFileName))
.check(matches(withText(fourthFileName)))
onView(withText(R.string.uploader_btn_upload_text))
.check(matches(isDisplayed()))
.check(matches(isEnabled()))

// Enter the subfolder and verify that the text stays intact
val expectedSubFolderTitle = (getCurrentActivity() as ToolbarActivity).getActionBarTitle(subFolder, false)
onView(withText(expectedSubFolderTitle))
.perform(ViewActions.click())
onView(withId(R.id.toolbar))
.check(matches(hasDescendant(withText(expectedSubFolderTitle))))
onView(withId(R.id.user_input))
.check(matches(withText(fourthFileName)))
.perform(ViewActions.click())
.check(matches(withSelectedText(fourthFileName.removeFileExtension())))

// Set a new, shorter file name
val fifthFileName = "short.jpg"
onView(withId(R.id.user_input))
.perform(ViewActions.typeTextIntoFocusedView(fifthFileName.removeFileExtension()))
.check(matches(withText(fifthFileName)))

// Start the upload, so the folder is stored in the preferences.
// Even though the upload is expected to fail because the backend is not mocked (yet?)
onView(withText(R.string.uploader_btn_upload_text))
.check(matches(isDisplayed()))
.check(matches(isEnabled()))
.perform(ViewActions.click())
}

// Start a new file receive flow. Should now start in the sub folder, but with the original filename again
launchActivity<ReceiveExternalFilesActivity>(intent).use {
val expectedMainFolderTitle = (getCurrentActivity() as ToolbarActivity).getActionBarTitle(subFolder, false)
onView(withId(R.id.toolbar))
.check(matches(hasDescendant(withText(expectedMainFolderTitle))))

onView(withText(R.string.uploader_btn_upload_text))
.check(matches(isDisplayed()))
.check(matches(isEnabled()))

onView(withId(R.id.user_input))
.check(matches(withText(imageFile.name)))
}
}

@Test
fun noRenameForMultiUpload() {
val testFiles = createDummyFiles()
val intent = createSendIntent(testFiles)

// Store the folder in preferences, so the activity starts from there.
@Suppress("DEPRECATION")
val preferences = AppPreferencesImpl.fromContext(targetContext)
preferences.setLastUploadPath(mainFolder.remotePath)

launchActivity<ReceiveExternalFilesActivity>(intent).use {
val expectedMainFolderTitle = (getCurrentActivity() as ToolbarActivity).getActionBarTitle(mainFolder, false)
// Verify that the test starts in the expected folder. If this fails, change the setup calls above
onView(withId(R.id.toolbar))
.check(matches(hasDescendant(withText(expectedMainFolderTitle))))

onView(withText(R.string.uploader_btn_upload_text))
.check(matches(isDisplayed()))
.check(matches(isEnabled()))

onView(withId(R.id.user_input))
.check(matches(not(isDisplayed())))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,6 @@ import com.owncloud.android.lib.resources.status.OwnCloudVersion
import com.owncloud.android.lib.resources.users.Status
import com.owncloud.android.lib.resources.users.StatusType
import com.owncloud.android.ui.activity.FileDisplayActivity
import com.owncloud.android.ui.dialog.LoadingDialog.Companion.newInstance
import com.owncloud.android.ui.dialog.RenameFileDialogFragment.Companion.newInstance
import com.owncloud.android.ui.dialog.SharePasswordDialogFragment.Companion.newInstance
import com.owncloud.android.ui.dialog.SslUntrustedCertDialog.Companion.newInstanceForEmptySslError
import com.owncloud.android.ui.fragment.OCFileListBottomSheetActions
import com.owncloud.android.ui.fragment.OCFileListBottomSheetDialog
import com.owncloud.android.ui.fragment.ProfileBottomSheetDialog
Expand Down Expand Up @@ -104,7 +100,7 @@ class DialogFragmentIT : AbstractIT() {
Looper.prepare()
}

newInstance(
RenameFileDialogFragment.newInstance(
OCFile("/Test/"),
OCFile("/")
).run {
Expand All @@ -115,7 +111,7 @@ class DialogFragmentIT : AbstractIT() {
@Test
@ScreenshotTest
fun testLoadingDialog() {
newInstance("Wait…").run {
LoadingDialog.newInstance("Wait…").run {
showDialog(this)
}
}
Expand Down Expand Up @@ -240,7 +236,7 @@ class DialogFragmentIT : AbstractIT() {
if (Looper.myLooper() == null) {
Looper.prepare()
}
val sut = newInstance(OCFile("/"), true, false)
val sut = SharePasswordDialogFragment.newInstance(OCFile("/"), createShare = true, askForPassword = false)
showDialog(sut)
}

Expand All @@ -250,7 +246,7 @@ class DialogFragmentIT : AbstractIT() {
if (Looper.myLooper() == null) {
Looper.prepare()
}
val sut = newInstance(OCFile("/"), true, true)
val sut = SharePasswordDialogFragment.newInstance(OCFile("/"), createShare = true, askForPassword = true)
showDialog(sut)
}

Expand Down Expand Up @@ -634,7 +630,7 @@ class DialogFragmentIT : AbstractIT() {

val handler = mockk<SslErrorHandler>(relaxed = true)

newInstanceForEmptySslError(sslError, handler).run {
SslUntrustedCertDialog.newInstanceForEmptySslError(sslError, handler).run {
showDialog(this)
}
}
Expand Down
Loading
Loading