diff --git a/Habitica/build.gradle b/Habitica/build.gradle index 723a78bd0..8e2325631 100644 --- a/Habitica/build.gradle +++ b/Habitica/build.gradle @@ -58,12 +58,12 @@ dependencies { implementation "com.amplitude:analytics-android:$amplitude_version" //Tests - testImplementation 'io.kotest:kotest-runner-junit5:5.5.5' testImplementation 'androidx.test:core:1.5.0' - testImplementation 'io.mockk:mockk:1.13.4' - testImplementation 'io.mockk:mockk-android:1.13.4' - testImplementation 'io.kotest:kotest-assertions-core:5.5.5' - testImplementation 'io.kotest:kotest-framework-datatest:5.5.5' + testImplementation "io.mockk:mockk:$mockk_version" + testImplementation "io.mockk:mockk-android:$mockk_version" + testImplementation "io.kotest:kotest-runner-junit5:$kotest_version" + testImplementation "io.kotest:kotest-assertions-core:$kotest_version" + testImplementation "io.kotest:kotest-framework-datatest:$kotest_version" androidTestImplementation ('com.kaspersky.android-components:kaspresso:1.5.1') { exclude module: "protobuf-lite" } @@ -74,8 +74,8 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test:core-ktx:1.5.0' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' - androidTestImplementation 'io.mockk:mockk-android:1.13.4' - androidTestImplementation 'io.kotest:kotest-assertions-core:5.5.5' + androidTestImplementation "io.mockk:mockk-android:$mockk_version" + androidTestImplementation "io.kotest:kotest-assertions-core:$kotest_version" androidTestUtil("androidx.test:orchestrator:1.4.2") implementation 'com.facebook.shimmer:shimmer:0.5.0' diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/SubscriptionPlan.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/SubscriptionPlan.kt index b96f63df1..883e67e80 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/SubscriptionPlan.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/user/SubscriptionPlan.kt @@ -71,7 +71,7 @@ open class SubscriptionPlan : RealmObject(), BaseObject { val monthsUntilNextHourglass: Int get() { return if (subMonthCount > 0) { - (consecutive?.offset ?: 0) + 1 + (consecutive?.offset ?: 0) } else { (3 - (((consecutive?.count ?: 0)) % 3)) } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/AdventureGuideActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/AdventureGuideActivity.kt index 55564e5c6..5190f1f1f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/AdventureGuideActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/AdventureGuideActivity.kt @@ -13,7 +13,7 @@ import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.databinding.ActivityAdventureGuideBinding import com.habitrpg.android.habitica.databinding.AdventureGuideItemBinding -import com.habitrpg.android.habitica.extensions.fromHtml +import com.habitrpg.common.habitica.extensions.fromHtml import com.habitrpg.android.habitica.helpers.AmplitudeManager import com.habitrpg.android.habitica.models.user.User import com.habitrpg.android.habitica.ui.viewmodels.MainUserViewModel diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/DeathActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/DeathActivity.kt index ace307285..e9d46c356 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/DeathActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/DeathActivity.kt @@ -10,7 +10,7 @@ import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.data.InventoryRepository import com.habitrpg.android.habitica.databinding.ActivityDeathBinding -import com.habitrpg.android.habitica.extensions.fromHtml +import com.habitrpg.common.habitica.extensions.fromHtml import com.habitrpg.android.habitica.extensions.observeOnce import com.habitrpg.android.habitica.helpers.AdHandler import com.habitrpg.android.habitica.helpers.AdType diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt index a71941d55..9b5e4291e 100755 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/MainActivity.kt @@ -20,7 +20,6 @@ import androidx.appcompat.app.ActionBarDrawerToggle import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.core.os.bundleOf import androidx.core.view.children import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.lifecycleScope @@ -264,16 +263,10 @@ open class MainActivity : BaseActivity(), SnackbarActivity { showAsBottomSheet { onClose -> val group by viewModel.userViewModel.currentTeamPlanGroup.collectAsState(null) val members by viewModel.userViewModel.currentTeamPlanMembers.observeAsState() - GroupPlanMemberList(members, group, { + GroupPlanMemberList(members, group) { onClose() FullProfileActivity.open(it) - }, { member -> - onClose() - MainNavigationController.navigate( - R.id.inboxMessageListFragment, - bundleOf(Pair("username", member.username), Pair("userID", member.id)) - ) - }) + } } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/NotificationsActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/NotificationsActivity.kt index 10403547b..76e33b747 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/NotificationsActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/NotificationsActivity.kt @@ -19,7 +19,7 @@ import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.data.InventoryRepository import com.habitrpg.android.habitica.data.SocialRepository import com.habitrpg.android.habitica.databinding.ActivityNotificationsBinding -import com.habitrpg.android.habitica.extensions.fromHtml +import com.habitrpg.common.habitica.extensions.fromHtml import com.habitrpg.android.habitica.helpers.ExceptionHandler import com.habitrpg.android.habitica.helpers.launchCatching import com.habitrpg.android.habitica.models.inventory.QuestContent diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/inventory/ShopRecyclerAdapter.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/inventory/ShopRecyclerAdapter.kt index 72591627b..6405d49b3 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/inventory/ShopRecyclerAdapter.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/adapter/inventory/ShopRecyclerAdapter.kt @@ -8,7 +8,7 @@ import android.widget.Button import android.widget.TextView import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.databinding.ShopHeaderBinding -import com.habitrpg.android.habitica.extensions.fromHtml +import com.habitrpg.common.habitica.extensions.fromHtml import com.habitrpg.android.habitica.extensions.inflate import com.habitrpg.android.habitica.helpers.MainNavigationController import com.habitrpg.android.habitica.models.shops.Shop diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/QuestDetailFragment.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/QuestDetailFragment.kt index 403c3fee5..c1a0b6a7b 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/QuestDetailFragment.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/fragments/social/QuestDetailFragment.kt @@ -16,7 +16,7 @@ import com.habitrpg.android.habitica.components.UserComponent import com.habitrpg.android.habitica.data.InventoryRepository import com.habitrpg.android.habitica.data.SocialRepository import com.habitrpg.android.habitica.databinding.FragmentQuestDetailBinding -import com.habitrpg.android.habitica.extensions.fromHtml +import com.habitrpg.common.habitica.extensions.fromHtml import com.habitrpg.android.habitica.helpers.ExceptionHandler import com.habitrpg.android.habitica.helpers.HapticFeedbackManager import com.habitrpg.android.habitica.helpers.launchCatching diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/GroupViewModel.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/GroupViewModel.kt index b6c555310..ebe36681f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/GroupViewModel.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/viewmodels/GroupViewModel.kt @@ -163,6 +163,7 @@ open class GroupViewModel(initializeComponent: Boolean) : BaseViewModel(initiali fun leaveGroup( groupChallenges: List, keepChallenges: Boolean = true, + function: (() -> Unit)? = null ) { if (!keepChallenges) { viewModelScope.launchCatching { @@ -174,6 +175,7 @@ open class GroupViewModel(initializeComponent: Boolean) : BaseViewModel(initiali viewModelScope.launch(ExceptionHandler.coroutine()) { socialRepository.leaveGroup(groupID ?: "", keepChallenges) userRepository.retrieveUser(withTasks = false, forced = true) + function?.invoke() } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/GroupPlanMemberList.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/GroupPlanMemberList.kt index 46eba020e..95c38e75a 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/GroupPlanMemberList.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/GroupPlanMemberList.kt @@ -40,7 +40,8 @@ import kotlin.random.Random fun GroupPlanMemberList( members: List?, group: Group?, - onMemberClicked: (String) -> Unit) { + onMemberClicked: (String) -> Unit +) { LazyColumn { item { Text( diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/AchievementDetailDialog.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/AchievementDetailDialog.kt index ac374a498..b444c6eb1 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/AchievementDetailDialog.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/AchievementDetailDialog.kt @@ -6,7 +6,7 @@ import android.view.View import android.widget.TextView import com.habitrpg.android.habitica.databinding.DialogAchievementDetailBinding import com.habitrpg.android.habitica.extensions.addCloseButton -import com.habitrpg.android.habitica.extensions.fromHtml +import com.habitrpg.common.habitica.extensions.fromHtml import com.habitrpg.android.habitica.models.Achievement import com.habitrpg.common.habitica.extensions.loadImage import com.habitrpg.common.habitica.views.PixelArtView diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/AchievementDialog.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/AchievementDialog.kt index 518934844..57fab078f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/AchievementDialog.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/AchievementDialog.kt @@ -7,7 +7,7 @@ import android.view.View import android.widget.TextView import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.databinding.DialogAchievementDetailBinding -import com.habitrpg.android.habitica.extensions.fromHtml +import com.habitrpg.common.habitica.extensions.fromHtml import com.habitrpg.android.habitica.helpers.MainNavigationController import com.habitrpg.android.habitica.models.user.User import com.habitrpg.common.habitica.extensions.layoutInflater diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/QuestCompletedDialogContent.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/QuestCompletedDialogContent.kt index 2037487ca..7e4652e9f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/QuestCompletedDialogContent.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/QuestCompletedDialogContent.kt @@ -11,7 +11,7 @@ import android.widget.LinearLayout import android.widget.TextView import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.databinding.DialogCompletedQuestContentBinding -import com.habitrpg.android.habitica.extensions.fromHtml +import com.habitrpg.common.habitica.extensions.fromHtml import com.habitrpg.android.habitica.models.inventory.QuestContent import com.habitrpg.android.habitica.models.inventory.QuestDropItem import com.habitrpg.android.habitica.ui.views.HabiticaIconsHelper diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/WonChallengeDialog.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/WonChallengeDialog.kt index 2aa02ab60..3cacdf2d8 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/WonChallengeDialog.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/dialogs/WonChallengeDialog.kt @@ -6,7 +6,7 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import com.habitrpg.android.habitica.R -import com.habitrpg.android.habitica.extensions.fromHtml +import com.habitrpg.common.habitica.extensions.fromHtml import com.habitrpg.common.habitica.extensions.loadImage import com.habitrpg.common.habitica.models.notifications.ChallengeWonData import com.habitrpg.common.habitica.views.PixelArtView diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialogContent.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialogContent.kt index cedab5f2b..b92814aa3 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialogContent.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/views/shops/PurchaseDialogContent.kt @@ -5,7 +5,7 @@ import android.util.AttributeSet import android.view.Gravity import android.widget.LinearLayout import android.widget.TextView -import com.habitrpg.android.habitica.extensions.fromHtml +import com.habitrpg.common.habitica.extensions.fromHtml import com.habitrpg.android.habitica.models.inventory.QuestContent import com.habitrpg.android.habitica.models.shops.ShopItem import com.habitrpg.common.habitica.extensions.dpToPx diff --git a/Habitica/src/test/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImplTest.kt b/Habitica/src/test/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImplTest.kt index 8c03c44d4..7849e9f5b 100644 --- a/Habitica/src/test/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImplTest.kt +++ b/Habitica/src/test/java/com/habitrpg/android/habitica/data/implementation/TaskRepositoryImplTest.kt @@ -81,13 +81,6 @@ class TaskRepositoryImplTest : WordSpec({ localRepository.getUser("") } } - "does not update user for team tasks" { - val data = TaskDirectionData() - data.lvl = 0 - coEvery { apiClient.postTaskDirection(any(), "up") } returns data - repository.taskChecked(user, task, true, false, null) - verify(exactly = 0) { user.stats } - } "builds task result correctly" { val data = TaskDirectionData() data.lvl = 10 diff --git a/Habitica/src/test/java/com/habitrpg/android/habitica/models/members/MemberTest.kt b/Habitica/src/test/java/com/habitrpg/android/habitica/models/members/MemberTest.kt index 1908d6cc1..c08d98225 100644 --- a/Habitica/src/test/java/com/habitrpg/android/habitica/models/members/MemberTest.kt +++ b/Habitica/src/test/java/com/habitrpg/android/habitica/models/members/MemberTest.kt @@ -31,9 +31,17 @@ class MemberTest : WordSpec({ member.hasClass shouldBe false } + "false if user is below level 10" { + member.flags?.classSelected = true + member.stats?.habitClass = Stats.ROGUE + member.stats?.lvl = 9 + member.hasClass shouldBe false + } + "true if class was selected and not disabled" { member.flags?.classSelected = true member.stats?.habitClass = Stats.ROGUE + member.stats?.lvl = 10 member.hasClass shouldBe true } } diff --git a/Habitica/src/test/java/com/habitrpg/android/habitica/models/user/UserTest.kt b/Habitica/src/test/java/com/habitrpg/android/habitica/models/user/UserTest.kt index a5812ccbc..434b3ec95 100644 --- a/Habitica/src/test/java/com/habitrpg/android/habitica/models/user/UserTest.kt +++ b/Habitica/src/test/java/com/habitrpg/android/habitica/models/user/UserTest.kt @@ -37,10 +37,17 @@ class UserTest : WordSpec({ user.hasClass shouldBe false } + "false if user is below level 10" { + user.flags?.classSelected = true + user.stats?.habitClass = Stats.ROGUE + user.stats?.lvl = 9 + user.hasClass shouldBe false + } + "true if class was selected and not disabled" { user.flags?.classSelected = true - user.preferences?.disableClasses = false user.stats?.habitClass = Stats.ROGUE + user.stats?.lvl = 10 user.hasClass shouldBe true } } diff --git a/build.gradle b/build.gradle index 6d75a6e22..5c9ff0dac 100644 --- a/build.gradle +++ b/build.gradle @@ -16,10 +16,12 @@ buildscript { coroutines_version = '1.6.4' daggerhilt_version = '2.44.2' firebase_bom = '31.2.0' + kotest_version = '5.5.5' kotlin_version = '1.8.10' ktlint_version = '0.48.2' lifecycle_version = '2.5.1' markwon_version = '4.6.2' + mockk_version = '1.13.4' moshi_version = '1.14.0' navigation_version = '2.5.3' okhttp_version = '4.10.0' diff --git a/common/build.gradle b/common/build.gradle index 8c170797c..6ec62d75e 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -38,6 +38,37 @@ android { jvmTarget = JavaVersion.VERSION_1_8.toString() } namespace 'com.habitrpg.common.habitica' + + flavorDimensions "buildType" + + productFlavors { + dev { + dimension "buildType" + } + + staff { + dimension "buildType" + buildConfigField "String", "TESTING_LEVEL", "\"staff\"" + } + + partners { + dimension "buildType" + buildConfigField "String", "TESTING_LEVEL", "\"partners\"" + } + + alpha { + dimension "buildType" + buildConfigField "String", "TESTING_LEVEL", "\"alpha\"" + } + + beta { + buildConfigField "String", "TESTING_LEVEL", "\"beta\"" + } + + prod { + buildConfigField "String", "TESTING_LEVEL", "\"production\"" + } + } } dependencies { @@ -57,13 +88,23 @@ dependencies { implementation("io.coil-kt:coil:$coil_version") implementation("io.coil-kt:coil-gif:$coil_version") - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + testImplementation "io.mockk:mockk:$mockk_version" + testImplementation "io.mockk:mockk-android:$mockk_version" + testImplementation "io.kotest:kotest-runner-junit5:$kotest_version" + testImplementation "io.kotest:kotest-assertions-core:$kotest_version" + testImplementation "io.kotest:kotest-framework-datatest:$kotest_version" + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation project(':shared') } +android.testOptions { + unitTests.all { + it.useJUnitPlatform() + } +} + // Add Habitica Properties to buildConfigField final File HRPG_PROPS_FILE = new File(projectDir.absolutePath + '/../habitica.properties') if (HRPG_PROPS_FILE.canRead()) { diff --git a/common/src/main/java/com/habitrpg/common/habitica/extensions/String-Extensions.kt b/common/src/main/java/com/habitrpg/common/habitica/extensions/String-Extensions.kt index 0c2c698d4..5bff2abae 100644 --- a/common/src/main/java/com/habitrpg/common/habitica/extensions/String-Extensions.kt +++ b/common/src/main/java/com/habitrpg/common/habitica/extensions/String-Extensions.kt @@ -1,4 +1,4 @@ -package com.habitrpg.android.habitica.extensions +package com.habitrpg.common.habitica.extensions import android.text.Html import android.text.Spannable diff --git a/common/src/test/java/com/habitrpg/common/habitica/api/ServerTest.kt b/common/src/test/java/com/habitrpg/common/habitica/api/ServerTest.kt new file mode 100644 index 000000000..5f64ced8d --- /dev/null +++ b/common/src/test/java/com/habitrpg/common/habitica/api/ServerTest.kt @@ -0,0 +1,21 @@ +package com.habitrpg.common.habitica.api + +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.string.shouldEndWith + +class ServerTest : WordSpec({ + "constructor" should { + "add api version if missing" { + val server = Server("https://habitica.com") + server.toString() shouldEndWith "/api/v4/" + } + "add api version if missing but has trailing slash" { + val server = Server("https://habitica.com") + server.toString() shouldEndWith ".com/api/v4/" + } + "not add api version multiple times" { + val server = Server("https://habitica.com") + server.toString() shouldEndWith ".com/api/v4/" + } + } +}) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index a6b41766a..6c1801e5e 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -3,20 +3,19 @@ plugins { id("com.android.library") id("kotlin-parcelize") id("kotlin-kapt") + id("io.kotest.multiplatform") version "5.5.5" +} + +allprojects { + repositories { + mavenCentral() + mavenLocal() + } } kotlin { android() - - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "shared" - } - } + ios() sourceSets { val commonMain by getting { @@ -26,29 +25,13 @@ kotlin { } val commonTest by getting { dependencies { - implementation(kotlin("test")) + implementation(kotlin("test")) // This brings all the platform dependencies automatically } } val androidMain by getting val androidTest by getting - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosX64Main.dependsOn(this) - iosArm64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - } - val iosX64Test by getting - val iosArm64Test by getting - val iosSimulatorArm64Test by getting - val iosTest by creating { - dependsOn(commonTest) - iosX64Test.dependsOn(this) - iosArm64Test.dependsOn(this) - iosSimulatorArm64Test.dependsOn(this) - } + val iosMain by getting + val iosTest by getting } } @@ -66,4 +49,4 @@ android { } namespace = "com.habitrpg.shared.habitica" -} +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/habitrpg/shared/habitica/models/tasks/BaseTask.kt b/shared/src/commonMain/kotlin/com/habitrpg/shared/habitica/models/tasks/BaseTask.kt index ed63c2205..049b9428a 100644 --- a/shared/src/commonMain/kotlin/com/habitrpg/shared/habitica/models/tasks/BaseTask.kt +++ b/shared/src/commonMain/kotlin/com/habitrpg/shared/habitica/models/tasks/BaseTask.kt @@ -1,7 +1,6 @@ package com.habitrpg.shared.habitica.models.tasks interface BaseTask { - val completed: Boolean var type: TaskType? var isDue: Boolean? diff --git a/shared/src/commonTest/kotlin/com/habitrpg/shared/habitica/models/tasks/AttributeTest.kt b/shared/src/commonTest/kotlin/com/habitrpg/shared/habitica/models/tasks/AttributeTest.kt new file mode 100644 index 000000000..d7d186a54 --- /dev/null +++ b/shared/src/commonTest/kotlin/com/habitrpg/shared/habitica/models/tasks/AttributeTest.kt @@ -0,0 +1,14 @@ +package com.habitrpg.shared.habitica.models.tasks + +import kotlin.test.Test +import kotlin.test.assertEquals + +class AttributeTest { + @Test + fun testFrom() { + assertEquals(Attribute.STRENGTH, Attribute.from("str")) + assertEquals(Attribute.CONSTITUTION, Attribute.from("con")) + assertEquals(Attribute.PERCEPTION, Attribute.from("per")) + assertEquals(Attribute.INTELLIGENCE, Attribute.from("int")) + } +} \ No newline at end of file diff --git a/wearos/build.gradle b/wearos/build.gradle index 19cdc29d2..d5ce34038 100644 --- a/wearos/build.gradle +++ b/wearos/build.gradle @@ -11,6 +11,13 @@ apply plugin: 'kotlin-android' android { compileSdk target_sdk + testOptions { + unitTests { + includeAndroidResources = true + } + animationsDisabled = true + } + defaultConfig { applicationId "com.habitrpg.android.habitica" minSdk 26 @@ -137,11 +144,25 @@ dependencies { kapt "com.google.dagger:hilt-compiler:$daggerhilt_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + + testImplementation "io.mockk:mockk:$mockk_version" + testImplementation "io.mockk:mockk-android:$mockk_version" + testImplementation "io.kotest:kotest-runner-junit5:$kotest_version" + testImplementation "io.kotest:kotest-assertions-core:$kotest_version" + testImplementation "io.kotest:kotest-framework-datatest:$kotest_version" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" + testImplementation 'app.cash.turbine:turbine:0.12.1' } repositories { mavenCentral() } +android.testOptions { + unitTests.all { + it.useJUnitPlatform() + } +} + final File HRPG_PROPS_FILE = new File(projectDir.absolutePath + '/../habitica.properties') if (HRPG_PROPS_FILE.canRead()) { Properties HRPG_PROPS = new Properties() diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/MainApplication.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/MainApplication.kt index 6ef2090ac..7a4b84479 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/MainApplication.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/MainApplication.kt @@ -20,6 +20,7 @@ import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.MainScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -40,6 +41,7 @@ class MainApplication : Application() { MainScope().launch { userRepository.getUser() + .filterNotNull() .onEach { if (it.isDead && BaseActivity.currentActivityClassName == MainActivity::class.java.name) { val intent = Intent(this@MainApplication, FaintActivity::class.java) diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/UserLocalRepository.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/UserLocalRepository.kt index ef51b168e..4506a9d80 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/UserLocalRepository.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/data/repositories/UserLocalRepository.kt @@ -1,16 +1,14 @@ package com.habitrpg.wearos.habitica.data.repositories -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.asFlow import com.habitrpg.wearos.habitica.models.user.User -import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject import javax.inject.Singleton @Singleton class UserLocalRepository @Inject constructor() { - private val user = MutableLiveData() - fun getUser() = user.asFlow().filterNotNull() + private val user = MutableStateFlow(null) + fun getUser() = user fun saveUser(user: User) { this.user.value = user diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/models/NetworkResult.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/models/NetworkResult.kt index 008d2ba74..1f0d11f8d 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/models/NetworkResult.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/models/NetworkResult.kt @@ -23,6 +23,6 @@ sealed class NetworkResult { val isError: Boolean get() = this is Error - data class Success(val data: T, val isFresh: Boolean) : NetworkResult() - data class Error(val exception: Exception, val isFresh: Boolean) : NetworkResult() + data class Success(val data: T, internal val isFresh: Boolean) : NetworkResult() + data class Error(val exception: Exception, internal val isFresh: Boolean) : NetworkResult() } diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/AvatarActivity.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/AvatarActivity.kt index 931ee019a..d46a0924d 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/AvatarActivity.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/AvatarActivity.kt @@ -6,10 +6,13 @@ import android.view.Gravity import android.view.ViewOutlineProvider import android.widget.FrameLayout import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope import com.habitrpg.android.habitica.databinding.ActivityAvatarBinding import com.habitrpg.common.habitica.extensions.dpToPx import com.habitrpg.wearos.habitica.ui.viewmodels.AvatarViewModel import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch import java.lang.Integer.max import kotlin.math.roundToInt @@ -21,8 +24,10 @@ class AvatarActivity : BaseActivity() { binding = ActivityAvatarBinding.inflate(layoutInflater) super.onCreate(savedInstanceState) - viewModel.user.observe(this) { - binding.avatarView.setAvatar(it) + lifecycleScope.launch { + viewModel.user.filterNotNull().collect { + binding.avatarView.setAvatar(it) + } } } diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/MainActivity.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/MainActivity.kt index b4f9b0aaa..8f09ac074 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/MainActivity.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/MainActivity.kt @@ -17,6 +17,7 @@ import com.habitrpg.wearos.habitica.ui.adapters.HubAdapter import com.habitrpg.wearos.habitica.ui.viewmodels.MainViewModel import com.habitrpg.wearos.habitica.util.HabiticaScrollingLayoutCallback import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch @AndroidEntryPoint @@ -111,12 +112,17 @@ class MainActivity : BaseActivity() { openSettingsActivity() } ) - viewModel.user.observe(this) { user -> - adapter.title = user.profile?.name ?: "" - val index = adapter.data.indexOfFirst { it.identifier == "stats" } - adapter.data[index].detailText = getString(R.string.user_level, user.stats?.lvl ?: 0) - adapter.notifyItemChanged(index + 1) + lifecycleScope.launch { + viewModel.user + .filterNotNull() + .collect { user -> + adapter.title = user.profile?.name ?: "" + val index = adapter.data.indexOfFirst { it.identifier == "stats" } + adapter.data[index].detailText = getString(R.string.user_level, user.stats?.lvl ?: 0) + adapter.notifyItemChanged(index + 1) + } } + viewModel.taskCounts.observe(this) { adapter.data.forEach { menuItem -> if (it.containsKey(menuItem.identifier)) { diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/TaskResultActivity.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/TaskResultActivity.kt index 47a179d58..efbf7268c 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/TaskResultActivity.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/activities/TaskResultActivity.kt @@ -12,7 +12,7 @@ import androidx.preference.PreferenceManager import com.habitrpg.android.habitica.R import com.habitrpg.android.habitica.databinding.ActivityTaskResultBinding import com.habitrpg.android.habitica.databinding.TaskRewardDropBinding -import com.habitrpg.android.habitica.extensions.localizedCapitalize +import com.habitrpg.common.habitica.extensions.localizedCapitalize import com.habitrpg.common.habitica.extensions.dpToPx import com.habitrpg.common.habitica.extensions.loadImage import com.habitrpg.shared.habitica.models.responses.TaskScoringResult diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/AvatarViewModel.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/AvatarViewModel.kt index 762fa61ee..706b61512 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/AvatarViewModel.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/AvatarViewModel.kt @@ -1,6 +1,5 @@ package com.habitrpg.wearos.habitica.ui.viewmodels -import androidx.lifecycle.asLiveData import com.habitrpg.wearos.habitica.data.repositories.TaskRepository import com.habitrpg.wearos.habitica.data.repositories.UserRepository import com.habitrpg.wearos.habitica.managers.AppStateManager @@ -19,5 +18,5 @@ class AvatarViewModel @Inject constructor( taskRepository, exceptionBuilder, appStateManager ) { - var user = userRepository.getUser().asLiveData() + var user = userRepository.getUser() } diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/LevelupViewModel.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/LevelupViewModel.kt index ee4178e56..c0e1a0bd6 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/LevelupViewModel.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/LevelupViewModel.kt @@ -17,6 +17,6 @@ class LevelupViewModel @Inject constructor( appStateManager: AppStateManager ) : BaseViewModel(userRepository, taskRepository, exceptionBuilder, appStateManager) { val level = userRepository.getUser() - .map { it.stats?.lvl } + .map { it?.stats?.lvl } .asLiveData() } diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/MainViewModel.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/MainViewModel.kt index 179d3a207..274a8c96e 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/MainViewModel.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/MainViewModel.kt @@ -28,5 +28,5 @@ class MainViewModel @Inject constructor( } val taskCounts = taskRepository.getActiveTaskCounts().asLiveData() - val user = userRepository.getUser().asLiveData() + val user = userRepository.getUser() } diff --git a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/StatsViewModel.kt b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/StatsViewModel.kt index 9092a4342..d6bcae3ac 100644 --- a/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/StatsViewModel.kt +++ b/wearos/src/main/java/com/habitrpg/wearos/habitica/ui/viewmodels/StatsViewModel.kt @@ -10,6 +10,7 @@ import com.habitrpg.wearos.habitica.models.user.User import com.habitrpg.wearos.habitica.util.ExceptionHandlerBuilder import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import javax.inject.Inject @@ -27,6 +28,7 @@ class StatsViewModel @Inject constructor( } var user: LiveData = userRepository.getUser() + .filterNotNull() .distinctUntilChanged { old, new -> val oldStats = old.stats ?: return@distinctUntilChanged false val newStats = new.stats ?: return@distinctUntilChanged false diff --git a/wearos/src/test/java/com/habitrpg/wearos/habitica/ProjectConfig.kt b/wearos/src/test/java/com/habitrpg/wearos/habitica/ProjectConfig.kt new file mode 100644 index 000000000..e241e1b65 --- /dev/null +++ b/wearos/src/test/java/com/habitrpg/wearos/habitica/ProjectConfig.kt @@ -0,0 +1,40 @@ +package com.habitrpg.wearos.habitica + +import androidx.arch.core.executor.ArchTaskExecutor +import androidx.arch.core.executor.TaskExecutor +import io.kotest.core.config.AbstractProjectConfig +import kotlinx.coroutines.test.TestCoroutineDispatcher + +object ProjectConfig : AbstractProjectConfig() { + private val testDispatcher = TestCoroutineDispatcher() + + override suspend fun beforeProject() { + super.beforeProject() + setupLiveData() + } + + override suspend fun afterProject() { + super.afterProject() + resetLiveData() + } + + private fun setupLiveData() { + ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() { + override fun executeOnDiskIO(runnable: Runnable) { + runnable.run() + } + + override fun postToMainThread(runnable: Runnable) { + runnable.run() + } + + override fun isMainThread(): Boolean { + return true + } + }) + } + + private fun resetLiveData() { + ArchTaskExecutor.getInstance().setDelegate(null) + } +} \ No newline at end of file diff --git a/wearos/src/test/java/com/habitrpg/wearos/habitica/data/repositories/TaskLocalRepositoryTest.kt b/wearos/src/test/java/com/habitrpg/wearos/habitica/data/repositories/TaskLocalRepositoryTest.kt new file mode 100644 index 000000000..7c802c754 --- /dev/null +++ b/wearos/src/test/java/com/habitrpg/wearos/habitica/data/repositories/TaskLocalRepositoryTest.kt @@ -0,0 +1,36 @@ +package com.habitrpg.wearos.habitica.data.repositories + +import app.cash.turbine.test +import com.habitrpg.shared.habitica.models.tasks.TaskType +import com.habitrpg.wearos.habitica.models.tasks.Task +import com.habitrpg.wearos.habitica.models.tasks.TaskList +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.shouldBe + +class TaskLocalRepositoryTest : WordSpec({ + val repository = TaskLocalRepository() + val list = TaskList() + list.tasks["1"] = Task().apply { + id = "1" + type = TaskType.HABIT + } + list.tasks["2"] = Task().apply { + id = "2" + type = TaskType.DAILY + } + list.tasks["3"] = Task().apply { + id = "3" + type = TaskType.DAILY + } + list.tasks["4"] = Task().apply { + id = "4" + type = TaskType.REWARD + } + "getTask" should { + "return right task" { + repository.getTask("3").test { + awaitItem()?.id shouldBe "3" + } + } + } +}) diff --git a/wearos/src/test/java/com/habitrpg/wearos/habitica/data/repositories/UserLocalRepositoryTest.kt b/wearos/src/test/java/com/habitrpg/wearos/habitica/data/repositories/UserLocalRepositoryTest.kt new file mode 100644 index 000000000..a939d4e38 --- /dev/null +++ b/wearos/src/test/java/com/habitrpg/wearos/habitica/data/repositories/UserLocalRepositoryTest.kt @@ -0,0 +1,32 @@ +package com.habitrpg.wearos.habitica.data.repositories + +import app.cash.turbine.test +import com.habitrpg.wearos.habitica.models.user.User +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.shouldBe + +class UserLocalRepositoryTest : WordSpec({ + coroutineTestScope = true + val repository = UserLocalRepository() + + "saveUser" should { + "update user in flow" { + val existing = User() + repository.saveUser(existing) + repository.getUser().test { + awaitItem() shouldBe existing + } + } + } + + "clearData" should { + "clear user from flow" { + val existing = User() + repository.saveUser(existing) + repository.clearData() + repository.getUser().test { + awaitItem() shouldBe null + } + } + } +}) diff --git a/wearos/src/test/java/com/habitrpg/wearos/habitica/models/NetworkResultTest.kt b/wearos/src/test/java/com/habitrpg/wearos/habitica/models/NetworkResultTest.kt new file mode 100644 index 000000000..57272b9bd --- /dev/null +++ b/wearos/src/test/java/com/habitrpg/wearos/habitica/models/NetworkResultTest.kt @@ -0,0 +1,42 @@ +package com.habitrpg.wearos.habitica.models + +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.shouldBe + +class NetworkResultTest : WordSpec({ + "isSuccess" should { + "be true if it's successful" { + val response = NetworkResult.Success("", true) + response.isSuccess shouldBe true + } + + "be false if it errored" { + val response = NetworkResult.Error(Exception(), true) + response.isSuccess shouldBe false + } + } + + "isError" should { + "be true if it's errored" { + val response = NetworkResult.Error(Exception(), true) + response.isError shouldBe true + } + + "be false if it's successful" { + val response = NetworkResult.Success("", true) + response.isError shouldBe false + } + } + + "isResponseFresh" should { + "be true if it's a fresh response" { + val response = NetworkResult.Success("", true) + response.isResponseFresh shouldBe true + } + + "be false if it errored" { + val response = NetworkResult.Success("", false) + response.isResponseFresh shouldBe false + } + } +})