diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8fd56c295fd5..5e96105fa02c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -130,6 +130,7 @@ android { debug { enableUnitTestCoverage = project.hasProperty("coverage") + enableAndroidTestCoverage = project.hasProperty("coverage") resConfigs("xxxhdpi") } } diff --git a/app/src/androidTest/java/com/nextcloud/ui/SetOnlineStatusBottomSheetIT.kt b/app/src/androidTest/java/com/nextcloud/ui/SetOnlineStatusBottomSheetIT.kt index e0840fd93dbb..c9e07da94cfd 100644 --- a/app/src/androidTest/java/com/nextcloud/ui/SetOnlineStatusBottomSheetIT.kt +++ b/app/src/androidTest/java/com/nextcloud/ui/SetOnlineStatusBottomSheetIT.kt @@ -37,15 +37,14 @@ class SetOnlineStatusBottomSheetIT : AbstractIT() { launchActivity().use { scenario -> onView(isRoot()).check(matches(isDisplayed())) - onView(withId(R.id.clearStatusAfterSpinner)) - .check(matches(isDisplayed())) - scenario.onActivity { activity -> val sut = SetOnlineStatusBottomSheet( Status(StatusType.DND, "Working hard…", "🤖", -1) ) sut.show(activity.supportFragmentManager, "") } + onView(withId(R.id.onlineStatus)) + .check(matches(isDisplayed())) } } } diff --git a/app/src/androidTest/java/com/nextcloud/ui/SetStatusMessageBottomSheetIT.kt b/app/src/androidTest/java/com/nextcloud/ui/SetStatusMessageBottomSheetIT.kt index dbb424284897..b9e9feec6e2e 100644 --- a/app/src/androidTest/java/com/nextcloud/ui/SetStatusMessageBottomSheetIT.kt +++ b/app/src/androidTest/java/com/nextcloud/ui/SetStatusMessageBottomSheetIT.kt @@ -14,8 +14,10 @@ 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.owncloud.android.AbstractIT +import com.owncloud.android.R import com.owncloud.android.lib.resources.users.ClearAt import com.owncloud.android.lib.resources.users.PredefinedStatus import com.owncloud.android.lib.resources.users.Status @@ -41,7 +43,6 @@ class SetStatusMessageBottomSheetIT : AbstractIT() { user, Status(StatusType.DND, "Working hard…", "🤖", -1) ) - sut.show(activity.supportFragmentManager, "") val predefinedStatus: ArrayList = arrayListOf( PredefinedStatus("meeting", "📅", "In a meeting", ClearAt("period", "3600")), PredefinedStatus("commuting", "🚌", "Commuting", ClearAt("period", "1800")), @@ -51,7 +52,11 @@ class SetStatusMessageBottomSheetIT : AbstractIT() { PredefinedStatus("vacationing", "🌴", "Vacationing", null) ) sut.setPredefinedStatus(predefinedStatus) + sut.show(activity.supportFragmentManager, "") } + + onView(withId(R.id.predefinedStatusList)) + .check(matches(isDisplayed())) } } } diff --git a/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt b/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt index 68e99b9b40df..e5d390d94212 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/AutoRenameTests.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Philipp Hasper * SPDX-FileCopyrightText: 2024 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -241,13 +242,52 @@ class AutoRenameTests : AbstractOnServerIT() { assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } } + /** + * For documentation see [com.nextcloud.utils.extensions.checkWCFRestrictions] + */ @Test - fun skipAutoRenameWhenWCFDisabled() { - capability = capability.apply { + fun testWCFDisabledOnNextcloud32ShouldSkipRestrictions() { + val filename = " readme.txt " + val nc32Capability = capability.apply { + versionMayor = NextcloudVersion.nextcloud_32.majorVersionNumber isWCFEnabled = CapabilityBooleanType.FALSE } + val result = AutoRename.rename(filename, nc32Capability, isFolderPath = true) + assert(result == filename) { "Expected $filename but got $result" } + } + + @Test + fun testWCFEnabledOnNextcloud32ShouldApplyRestrictions() { val filename = " readme.txt " - val result = AutoRename.rename(filename, capability, isFolderPath = true) + val nc32Capability = capability.apply { + versionMayor = NextcloudVersion.nextcloud_32.majorVersionNumber + isWCFEnabled = CapabilityBooleanType.TRUE + } + val result = AutoRename.rename(filename, nc32Capability, isFolderPath = true) + val expectedFilename = "readme.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testWCFDisabledOnNextcloud30to31ShouldStillApplyRestrictions() { + val filename = " readme.txt " + val nc30Capability = capability.apply { + versionMayor = NextcloudVersion.nextcloud_30.majorVersionNumber + isWCFEnabled = CapabilityBooleanType.FALSE + } + val result = AutoRename.rename(filename, nc30Capability, isFolderPath = true) + val expectedFilename = "readme.txt" + assert(result == expectedFilename) { "Expected $expectedFilename but got $result" } + } + + @Test + fun testWCFOnNextcloudBelow30ShouldSkipRestrictions() { + val filename = " readme.txt " + val nc29Capability = capability.apply { + versionMayor = NextcloudVersion.nextcloud_29.majorVersionNumber + isWCFEnabled = CapabilityBooleanType.TRUE + } + val result = AutoRename.rename(filename, nc29Capability, isFolderPath = true) assert(result == filename) { "Expected $filename but got $result" } } } diff --git a/app/src/androidTest/java/com/nextcloud/utils/UploadDateTests.kt b/app/src/androidTest/java/com/nextcloud/utils/UploadDateTests.kt index 00177f0f872c..be7cf735a580 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/UploadDateTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/UploadDateTests.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Philipp Hasper * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -13,14 +14,11 @@ import androidx.test.platform.app.InstrumentationRegistry import com.nextcloud.client.database.entity.UploadEntity import com.nextcloud.client.database.entity.toOCUpload import com.nextcloud.client.database.entity.toUploadEntity +import com.nextcloud.utils.date.DateFormatPattern import com.owncloud.android.R import com.owncloud.android.utils.DisplayUtils -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.mockkStatic import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import java.text.SimpleDateFormat @@ -30,25 +28,22 @@ import java.util.Locale class UploadDateTests { companion object { - private const val JANUARY_27_2026 = 1769505718000 - private const val ONE_YEAR = 365L * 24 * 60 * 60 * 1000 - private const val ONE_MONTH = 30L * 24 * 60 * 60 * 1000 - private const val ONE_WEEK = 7 * 24 * 60 * 60 * 1000 - private const val TWO_HOURS = 2 * 60 * 60 * 1000 - private const val ONE_MINUTE = 60_000 - private const val THIRTY_SECONDS = 30_000 - - private const val DATE_FORMATTER_PATTERN = "MMM dd, yyyy, hh:mm:ss a" + private const val THIRTY_SECONDS = 30_000L + private const val ONE_MINUTE = 60_000L + private const val ONE_HOUR = 60 * ONE_MINUTE + private const val ONE_DAY = 24 * ONE_HOUR + + private const val ONE_YEAR = 365L * ONE_DAY + private const val ONE_MONTH = 30L * ONE_DAY + private const val ONE_WEEK = 7L * ONE_DAY + private const val TWO_HOURS = 2L * ONE_HOUR } private lateinit var context: Context @Before fun setup() { - context = InstrumentationRegistry.getInstrumentation().context - MockKAnnotations.init(this, relaxed = true) - mockkStatic(System::class) - every { System.currentTimeMillis() } returns JANUARY_27_2026 + context = InstrumentationRegistry.getInstrumentation().targetContext } @Test @@ -92,62 +87,140 @@ class UploadDateTests { fun getRelativeDateTimeStringReturnsSecondsAgoForRecentPast() { val result = DisplayUtils.getRelativeDateTimeString( context, - JANUARY_27_2026 - THIRTY_SECONDS, - DateUtils.SECOND_IN_MILLIS, + System.currentTimeMillis() - THIRTY_SECONDS, + DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0 ) - assertTrue(result.toString() == context.getString(R.string.file_list_seconds_ago)) + assertEquals(context.getString(R.string.file_list_seconds_ago), result.toString()) } @Test - fun getRelativeDateTimeStringReturnsFutureAsHumanReadableWhenShowFutureIsFalse() { - val formatter = SimpleDateFormat(DATE_FORMATTER_PATTERN, Locale.US) - val time = JANUARY_27_2026 + ONE_MINUTE - val expected = formatter.format(Date(time)) + fun getRelativeDateTimeStringReturnsFutureAsAbsoluteWhenShowFutureIsFalse() { + val formatter = SimpleDateFormat("MMM d, yyyy h:mm:ss a", Locale.US) + val expected = formatter.format(Date(System.currentTimeMillis() + ONE_MINUTE)) - assertRelativeDateTimeString(time, expected, DateUtils.SECOND_IN_MILLIS) + val result = DisplayUtils.getRelativeDateTimeString( + context, + System.currentTimeMillis() + ONE_MINUTE, + DateUtils.SECOND_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0, + false + ) + assertEquals(expected, result.toString()) + } + + @Test + fun getRelativeDateTimeStringReturnsFutureAsRelativeWhenShowFutureIsTrue() { + val expected = "In 1 minute" + val time = System.currentTimeMillis() + ONE_MINUTE + + assertRelativeDateTimeString(time, expected, DateUtils.MINUTE_IN_MILLIS, showFuture = true) } @Test - fun getRelativeDateTimeStringReturnsProperRelativeStringForHoursAgo() { + fun getRelativeDateTimeStringReturnsRelativeStringForHoursAgo() { val expected = "2 hours ago" - val time = JANUARY_27_2026 - TWO_HOURS + val time = System.currentTimeMillis() - TWO_HOURS - assertRelativeDateTimeString(time, expected, DateUtils.MINUTE_IN_MILLIS) + assertRelativeDateTimeString(time, expected, DateUtils.SECOND_IN_MILLIS) } @Test - fun getRelativeDateTimeStringReturnsRelativeStringForOneWeekAgo() { - val expected = "Jan 20" - val time = JANUARY_27_2026 - ONE_WEEK + fun getRelativeDateTimeStringReturnsAbbreviatedStringForOneWeekAgo() { + val time = System.currentTimeMillis() - ONE_WEEK + val formatter = SimpleDateFormat(DateFormatPattern.MonthWithDate.pattern, Locale.US) + val expected = formatter.format(Date(time)) assertRelativeDateTimeString(time, expected) } @Test - fun getRelativeDateTimeStringReturnsRelativeStringForOneMonthAgo() { - val expected = "12/28/2025" - val time = JANUARY_27_2026 - ONE_MONTH + fun getRelativeDateTimeStringReturnsAbbreviatedStringForOneMonthAgo() { + val time = System.currentTimeMillis() - ONE_MONTH + val formatter = SimpleDateFormat(DateFormatPattern.MonthWithDate.pattern, Locale.US) + val expected = formatter.format(Date(time)) - assertRelativeDateTimeString(time, expected, DateUtils.DAY_IN_MILLIS) + assertRelativeDateTimeString(time, expected, DateUtils.SECOND_IN_MILLIS) } @Test - fun getRelativeDateTimeStringReturnsRelativeStringForOneYearAgo() { - val expected = "1/27/2025" - val time = JANUARY_27_2026 - ONE_YEAR + fun getRelativeDateTimeStringReturnsAbsoluteStringForOneYearAgo() { + val time = System.currentTimeMillis() - ONE_YEAR + val formatter = SimpleDateFormat("M/d/YYYY", Locale.US) + val expected = formatter.format(Date(time)) - assertRelativeDateTimeString(time, expected, DateUtils.DAY_IN_MILLIS) + assertRelativeDateTimeString(time, expected, DateUtils.SECOND_IN_MILLIS) + } + + @Suppress("MagicNumber") + @Test + fun getRelativeDateTimeStringReturnsDaysForDayInMillis() { + var testTimestamp = System.currentTimeMillis() + var expected = "Today" + var result = DisplayUtils.getRelativeDateTimeString( + context, + testTimestamp, + DateUtils.DAY_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0, + false + ) + assertEquals(expected, result) + + testTimestamp = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS + expected = "Yesterday" + result = DisplayUtils.getRelativeDateTimeString( + context, + testTimestamp, + DateUtils.DAY_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0, + false + ) + assertEquals(expected, result) + + testTimestamp = System.currentTimeMillis() - 2 * DateUtils.DAY_IN_MILLIS + expected = "2 days ago" + result = DisplayUtils.getRelativeDateTimeString( + context, + testTimestamp, + DateUtils.DAY_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0, + false + ) + assertEquals(expected, result) + + testTimestamp = System.currentTimeMillis() - 7 * DateUtils.DAY_IN_MILLIS + expected = SimpleDateFormat(DateFormatPattern.MonthWithDate.pattern, Locale.US).format(testTimestamp) + result = DisplayUtils.getRelativeDateTimeString( + context, + testTimestamp, + DateUtils.DAY_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0, + false + ) + assertEquals(expected, result) } private fun assertRelativeDateTimeString( time: Long, expected: String, minResolution: Long = DateUtils.MINUTE_IN_MILLIS, - transitionResolution: Long = DateUtils.WEEK_IN_MILLIS + transitionResolution: Long = DateUtils.WEEK_IN_MILLIS, + showFuture: Boolean = false ) { - val result = DisplayUtils.getRelativeDateTimeString(context, time, minResolution, transitionResolution, 0) + val result = DisplayUtils.getRelativeDateTimeString( + context, + time, + minResolution, + transitionResolution, + 0, + showFuture + ) assertEquals(expected.normalizeResult(), result.toString().normalizeResult()) } diff --git a/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java b/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java index 9c255ef1d880..ca4c416f26a9 100644 --- a/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java +++ b/app/src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java @@ -113,10 +113,12 @@ public void largeTest() { } OCUpload[] storedUploads = uploadsStorageManager.getAllStoredUploads(); - assertEquals(size, uploadsStorageManager.getAllStoredUploads().length); + assertEquals(size, storedUploads.length); + assertEquals(uploads.size(), storedUploads.length); for (int i = 0; i < size; i++) { - assertTrue(contains(uploads, storedUploads[i])); + assertTrue("Upload " + (i+1) + "/" + size + " not found in stored uploads: " + storedUploads[i].getLocalPath(), + contains(uploads, storedUploads[i])); } } @@ -138,7 +140,7 @@ public void testIsSame() { private boolean contains(ArrayList uploads, OCUpload storedUpload) { for (int i = 0; i < uploads.size(); i++) { - if (storedUpload.isSame(uploads.get(i))) { + if (storedUpload.isSame(uploads.get(i), true)) { return true; } } diff --git a/app/src/main/java/com/nextcloud/ui/SetStatusMessageBottomSheet.kt b/app/src/main/java/com/nextcloud/ui/SetStatusMessageBottomSheet.kt index 8712727746c4..86efd789df05 100644 --- a/app/src/main/java/com/nextcloud/ui/SetStatusMessageBottomSheet.kt +++ b/app/src/main/java/com/nextcloud/ui/SetStatusMessageBottomSheet.kt @@ -354,7 +354,10 @@ class SetStatusMessageBottomSheet(val user: User, val currentStatus: Status?) : @SuppressLint("NotifyDataSetChanged") @VisibleForTesting fun setPredefinedStatus(predefinedStatus: ArrayList) { - adapter.list = predefinedStatus - binding.predefinedStatusList.adapter?.notifyDataSetChanged() + this.predefinedStatus = predefinedStatus + if (this::adapter.isInitialized) { + adapter.list = predefinedStatus + binding.predefinedStatusList.adapter?.notifyDataSetChanged() + } } } diff --git a/app/src/main/java/com/owncloud/android/db/OCUpload.java b/app/src/main/java/com/owncloud/android/db/OCUpload.java index 8679e3ebd726..6632fcf12537 100644 --- a/app/src/main/java/com/owncloud/android/db/OCUpload.java +++ b/app/src/main/java/com/owncloud/android/db/OCUpload.java @@ -31,6 +31,7 @@ import java.io.File; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** @@ -292,12 +293,18 @@ public int describeContents() { return this.hashCode(); } - @SuppressFBWarnings("SEO_SUBOPTIMAL_EXPRESSION_ORDER") + public boolean isSame(@Nullable Object obj) { + return isSame(obj, false); + } + + @SuppressFBWarnings("SEO_SUBOPTIMAL_EXPRESSION_ORDER") + @VisibleForTesting + public boolean isSame(@Nullable Object obj, boolean ignoreUploadId) { if (!(obj instanceof OCUpload other)) { return false; } - return this.uploadId == other.uploadId && + return (ignoreUploadId || this.uploadId == other.uploadId) && localPath.equals(other.localPath) && remotePath.equals(other.remotePath) && accountName.equals(other.accountName) && diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java index 4d8e4164cbdf..e9eaccb2aba6 100755 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java @@ -383,7 +383,7 @@ public void onBindViewHolder(SectionedViewHolder holder, int section, int relati if (showUploadDate) { CharSequence dateString = DisplayUtils.getRelativeDateTimeString(parentActivity, updateTime, - DateUtils.SECOND_IN_MILLIS, + DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0); itemViewHolder.binding.uploadDate.setText(dateString); diff --git a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java index 7ed24707fb7a..a711c974492f 100644 --- a/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java +++ b/app/src/main/java/com/owncloud/android/utils/DisplayUtils.java @@ -300,7 +300,7 @@ public static CharSequence getRelativeDateTimeString(Context c, } // < 60 seconds -> seconds ago long diff = System.currentTimeMillis() - time; - if (diff > 0 && diff < 60 * 1000 && minResolution == DateUtils.SECOND_IN_MILLIS) { + if (diff > 0 && diff < 60 * 1000 && minResolution == DateUtils.MINUTE_IN_MILLIS) { return c.getString(R.string.file_list_seconds_ago); } else { CharSequence dateString = DateUtils.getRelativeDateTimeString(c, time, minResolution, transitionResolution, flags); diff --git a/scripts/uploadReport.sh b/scripts/uploadReport.sh index 2e1af039cca3..e3bb5eea5459 100755 --- a/scripts/uploadReport.sh +++ b/scripts/uploadReport.sh @@ -51,7 +51,7 @@ if [ -z $USER ] || [ -z $PASS ]; then fi if [ $TYPE = "IT" ]; then - FOLDER=app/build/reports/androidTests/connected/flavors/gplay + FOLDER=app/build/reports/androidTests/connected/debug/flavors/gplay elif [ $TYPE = "Unit" ]; then FOLDER=app/build/reports/tests/testGplayDebugUnitTest else @@ -68,10 +68,10 @@ else -X POST https://api.github.com/repos/nextcloud/android/issues/$PR/comments \ -d "{ \"body\" : \"$BRANCH_TYPE test failed, but no output was generated. Maybe a preliminary stage failed. \" }" - if [ -e app/build/reports/androidTests/connected/flavors/gplay ] ; then + if [ -e app/build/reports/androidTests/connected/debug/flavors/gplay ] ; then TYPE="IT" BRANCH_TYPE=$BRANCH-$TYPE - upload "app/build/reports/androidTests/connected/flavors/gplay" + upload "app/build/reports/androidTests/connected/debug/flavors/gplay" fi if [ -e app/build/reports/tests/testGplayDebugUnitTest ] ; then