mirror of
https://github.com/wismna/DoNext.git
synced 2026-07-25 20:23:06 -04:00
Compare commits
5 Commits
150058437f
...
7704d9280b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7704d9280b | |||
| bbf9949e21 | |||
| 4f53d28f5c | |||
| 1db7d4f37f | |||
| 210b509132 |
@@ -33,9 +33,6 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-gradle-
|
||||||
|
|
||||||
- name: Run unit tests
|
|
||||||
run: ./gradlew :donextv2:testDebugUnitTest
|
|
||||||
|
|
||||||
- name: Decode signing key
|
- name: Decode signing key
|
||||||
run: echo "${{ secrets.SIGNING_KEY_BASE64 }}" | base64 --decode > ${{ env.MODULE }}/upload.jks
|
run: echo "${{ secrets.SIGNING_KEY_BASE64 }}" | base64 --decode > ${{ env.MODULE }}/upload.jks
|
||||||
|
|
||||||
|
|||||||
Generated
-1
@@ -9,7 +9,6 @@
|
|||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/donext" />
|
|
||||||
<option value="$PROJECT_DIR$/donextv2" />
|
<option value="$PROJECT_DIR$/donextv2" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.google.devtools.ksp") version '2.3.2' apply false
|
id("com.google.devtools.ksp") version '2.3.10' apply false
|
||||||
id("com.google.dagger.hilt.android") version "2.60.1" apply false
|
id("com.google.dagger.hilt.android") version "2.60.1" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
|
||||||
id("org.jetbrains.kotlin.plugin.compose")
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
id("com.google.dagger.hilt.android")
|
id("com.google.dagger.hilt.android")
|
||||||
@@ -36,7 +35,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("debug").assets.srcDirs(files("$projectDir/schemas"))
|
getByName("debug").assets.directories.add("$projectDir/schemas")
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -56,11 +55,6 @@ android {
|
|||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
kotlin {
|
|
||||||
compilerOptions {
|
|
||||||
jvmTarget = JvmTarget.JVM_11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
@@ -78,6 +72,12 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = JvmTarget.JVM_11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.11.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.11.0")
|
||||||
implementation("androidx.activity:activity-compose:1.13.0")
|
implementation("androidx.activity:activity-compose:1.13.0")
|
||||||
@@ -91,6 +91,7 @@ dependencies {
|
|||||||
implementation("androidx.navigation:navigation-compose:2.9.8")
|
implementation("androidx.navigation:navigation-compose:2.9.8")
|
||||||
implementation("androidx.hilt:hilt-navigation-compose:1.4.0")
|
implementation("androidx.hilt:hilt-navigation-compose:1.4.0")
|
||||||
implementation("sh.calvin.reorderable:reorderable:3.1.0")
|
implementation("sh.calvin.reorderable:reorderable:3.1.0")
|
||||||
|
implementation("androidx.datastore:datastore-preferences:1.2.1")
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("io.mockk:mockk:1.14.11")
|
testImplementation("io.mockk:mockk:1.14.11")
|
||||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0")
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.wismna.geoffroy.donext.data.injection
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
|
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object PreferencesModule {
|
||||||
|
private const val SETTINGS_PREFERENCES_NAME = "settings"
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideSettingsDataStore(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
): DataStore<Preferences> =
|
||||||
|
PreferenceDataStoreFactory.create(
|
||||||
|
produceFile = { context.preferencesDataStoreFile(SETTINGS_PREFERENCES_NAME) }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.wismna.geoffroy.donext.data.injection
|
package com.wismna.geoffroy.donext.data.injection
|
||||||
|
|
||||||
|
import com.wismna.geoffroy.donext.data.local.preferences.SettingsRepositoryImpl
|
||||||
import com.wismna.geoffroy.donext.data.local.repository.TaskListRepositoryImpl
|
import com.wismna.geoffroy.donext.data.local.repository.TaskListRepositoryImpl
|
||||||
import com.wismna.geoffroy.donext.data.local.repository.TaskRepositoryImpl
|
import com.wismna.geoffroy.donext.data.local.repository.TaskRepositoryImpl
|
||||||
|
import com.wismna.geoffroy.donext.domain.repository.SettingsRepository
|
||||||
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
import com.wismna.geoffroy.donext.domain.repository.TaskListRepository
|
||||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
@@ -22,4 +24,9 @@ abstract class RepositoryModule {
|
|||||||
abstract fun bindTaskListRepository(
|
abstract fun bindTaskListRepository(
|
||||||
impl: TaskListRepositoryImpl
|
impl: TaskListRepositoryImpl
|
||||||
): TaskListRepository
|
): TaskListRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindSettingsRepository(
|
||||||
|
impl: SettingsRepositoryImpl
|
||||||
|
): SettingsRepository
|
||||||
}
|
}
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package com.wismna.geoffroy.donext.data.local.preferences
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
|
import androidx.datastore.preferences.core.longPreferencesKey
|
||||||
|
import com.wismna.geoffroy.donext.domain.repository.SettingsRepository
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import java.io.IOException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SettingsRepositoryImpl @Inject constructor(
|
||||||
|
private val dataStore: DataStore<Preferences>
|
||||||
|
) : SettingsRepository {
|
||||||
|
override suspend fun getLastOpenedTaskListId(): Long? =
|
||||||
|
dataStore.data
|
||||||
|
.catch { e -> if (e is IOException) emit(emptyPreferences()) else throw e }
|
||||||
|
.first()[LAST_OPENED_TASK_LIST_ID_KEY]
|
||||||
|
|
||||||
|
override suspend fun setLastOpenedTaskListId(taskListId: Long) {
|
||||||
|
dataStore.edit { prefs -> prefs[LAST_OPENED_TASK_LIST_ID_KEY] = taskListId }
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val LAST_OPENED_TASK_LIST_ID_KEY = longPreferencesKey("last_opened_task_list_id")
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
package com.wismna.geoffroy.donext.domain.repository
|
||||||
|
|
||||||
|
interface SettingsRepository {
|
||||||
|
suspend fun getLastOpenedTaskListId(): Long?
|
||||||
|
suspend fun setLastOpenedTaskListId(taskListId: Long)
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package com.wismna.geoffroy.donext.domain.usecase
|
||||||
|
|
||||||
|
import com.wismna.geoffroy.donext.domain.repository.SettingsRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetLastOpenedTaskListIdUseCase @Inject constructor(
|
||||||
|
private val repository: SettingsRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(): Long? = repository.getLastOpenedTaskListId()
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package com.wismna.geoffroy.donext.domain.usecase
|
||||||
|
|
||||||
|
import com.wismna.geoffroy.donext.domain.repository.SettingsRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SaveLastOpenedTaskListUseCase @Inject constructor(
|
||||||
|
private val repository: SettingsRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(taskListId: Long) = repository.setLastOpenedTaskListId(taskListId)
|
||||||
|
}
|
||||||
+22
-7
@@ -7,18 +7,20 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import com.wismna.geoffroy.donext.domain.model.AppDestination
|
import com.wismna.geoffroy.donext.domain.model.AppDestination
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.GetLastOpenedTaskListIdUseCase
|
||||||
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.SaveLastOpenedTaskListUseCase
|
||||||
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
||||||
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainViewModel @Inject constructor(
|
class MainViewModel @Inject constructor(
|
||||||
getTaskListsUseCase: GetTaskListsUseCase,
|
getTaskListsUseCase: GetTaskListsUseCase,
|
||||||
|
private val getLastOpenedTaskListIdUseCase: GetLastOpenedTaskListIdUseCase,
|
||||||
|
private val saveLastOpenedTaskListUseCase: SaveLastOpenedTaskListUseCase,
|
||||||
val uiEventBus: UiEventBus
|
val uiEventBus: UiEventBus
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@@ -38,8 +40,9 @@ class MainViewModel @Inject constructor(
|
|||||||
var showAddListSheet by mutableStateOf(false)
|
var showAddListSheet by mutableStateOf(false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getTaskListsUseCase()
|
viewModelScope.launch {
|
||||||
.onEach { lists ->
|
val lastOpenedTaskListId = getLastOpenedTaskListIdUseCase()
|
||||||
|
getTaskListsUseCase().collect { lists ->
|
||||||
destinations = lists.map { taskList ->
|
destinations = lists.map { taskList ->
|
||||||
AppDestination.TaskList(taskList.id!!, taskList.name)
|
AppDestination.TaskList(taskList.id!!, taskList.name)
|
||||||
} +
|
} +
|
||||||
@@ -47,11 +50,14 @@ class MainViewModel @Inject constructor(
|
|||||||
AppDestination.RecycleBin +
|
AppDestination.RecycleBin +
|
||||||
AppDestination.DueTodayList
|
AppDestination.DueTodayList
|
||||||
if (startDestination == AppDestination.ManageLists && destinations.isNotEmpty()) {
|
if (startDestination == AppDestination.ManageLists && destinations.isNotEmpty()) {
|
||||||
startDestination = destinations.first()
|
startDestination = destinations
|
||||||
|
.filterIsInstance<AppDestination.TaskList>()
|
||||||
|
.firstOrNull { it.taskListId == lastOpenedTaskListId }
|
||||||
|
?: destinations.first()
|
||||||
}
|
}
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun navigateBack() {
|
fun navigateBack() {
|
||||||
@@ -79,12 +85,21 @@ class MainViewModel @Inject constructor(
|
|||||||
val route = navBackStackEntry?.destination?.route
|
val route = navBackStackEntry?.destination?.route
|
||||||
val taskListId = navBackStackEntry?.arguments?.getLong("taskListId")
|
val taskListId = navBackStackEntry?.arguments?.getLong("taskListId")
|
||||||
|
|
||||||
currentDestination = destinations.firstOrNull { dest ->
|
val resolved = destinations.firstOrNull { dest ->
|
||||||
when (dest) {
|
when (dest) {
|
||||||
is AppDestination.TaskList -> taskListId != null && dest.taskListId == taskListId
|
is AppDestination.TaskList -> taskListId != null && dest.taskListId == taskListId
|
||||||
else -> dest.route == route
|
else -> dest.route == route
|
||||||
}
|
}
|
||||||
} ?: startDestination
|
} ?: startDestination
|
||||||
|
|
||||||
|
if (resolved != currentDestination) {
|
||||||
|
currentDestination = resolved
|
||||||
|
(resolved as? AppDestination.TaskList)?.let { taskList ->
|
||||||
|
viewModelScope.launch {
|
||||||
|
saveLastOpenedTaskListUseCase(taskList.taskListId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doesListExist(taskListId: Long): Boolean {
|
fun doesListExist(taskListId: Long): Boolean {
|
||||||
|
|||||||
+43
@@ -0,0 +1,43 @@
|
|||||||
|
package com.wismna.geoffroy.donext.data.local.preferences
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class SettingsRepositoryImplTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val tempFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
private fun createRepository(): SettingsRepositoryImpl {
|
||||||
|
// DataStore's internal actor coroutine runs on this scope. A plain TestScope() uses
|
||||||
|
// StandardTestDispatcher, which queues work that only runs when its own scheduler is
|
||||||
|
// advanced — but nothing here ever advances it, so reads/writes deadlock forever.
|
||||||
|
// UnconfinedTestDispatcher runs dispatched coroutines eagerly instead, so the actor
|
||||||
|
// makes progress without needing to be pumped.
|
||||||
|
val testDataStore = PreferenceDataStoreFactory.create(
|
||||||
|
scope = TestScope(UnconfinedTestDispatcher()),
|
||||||
|
produceFile = { tempFolder.newFile("test.preferences_pb") }
|
||||||
|
)
|
||||||
|
return SettingsRepositoryImpl(testDataStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getLastOpenedTaskListId returns null when nothing saved`() = runTest {
|
||||||
|
assertThat(createRepository().getLastOpenedTaskListId()).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setLastOpenedTaskListId then get returns saved value`() = runTest {
|
||||||
|
val repo = createRepository()
|
||||||
|
repo.setLastOpenedTaskListId(42L)
|
||||||
|
assertThat(repo.getLastOpenedTaskListId()).isEqualTo(42L)
|
||||||
|
}
|
||||||
|
}
|
||||||
+115
-7
@@ -4,7 +4,9 @@ import androidx.navigation.NavBackStackEntry
|
|||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.wismna.geoffroy.donext.domain.model.AppDestination
|
import com.wismna.geoffroy.donext.domain.model.AppDestination
|
||||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.GetLastOpenedTaskListIdUseCase
|
||||||
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
||||||
|
import com.wismna.geoffroy.donext.domain.usecase.SaveLastOpenedTaskListUseCase
|
||||||
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEvent
|
||||||
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
import com.wismna.geoffroy.donext.presentation.ui.events.UiEventBus
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
@@ -23,8 +25,17 @@ class MainViewModelTest {
|
|||||||
private val uiEventBus: UiEventBus = mockk(relaxUnitFun = true)
|
private val uiEventBus: UiEventBus = mockk(relaxUnitFun = true)
|
||||||
private lateinit var getTaskListsFlow: MutableSharedFlow<List<TaskList>>
|
private lateinit var getTaskListsFlow: MutableSharedFlow<List<TaskList>>
|
||||||
private lateinit var getTaskListsUseCase: GetTaskListsUseCase
|
private lateinit var getTaskListsUseCase: GetTaskListsUseCase
|
||||||
|
private lateinit var getLastOpenedTaskListIdUseCase: GetLastOpenedTaskListIdUseCase
|
||||||
|
private val saveLastOpenedTaskListUseCase: SaveLastOpenedTaskListUseCase = mockk(relaxUnitFun = true)
|
||||||
private lateinit var viewModel: MainViewModel
|
private lateinit var viewModel: MainViewModel
|
||||||
|
|
||||||
|
private fun createViewModel() = MainViewModel(
|
||||||
|
getTaskListsUseCase,
|
||||||
|
getLastOpenedTaskListIdUseCase,
|
||||||
|
saveLastOpenedTaskListUseCase,
|
||||||
|
uiEventBus
|
||||||
|
)
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
Dispatchers.setMain(dispatcher)
|
Dispatchers.setMain(dispatcher)
|
||||||
@@ -32,6 +43,9 @@ class MainViewModelTest {
|
|||||||
getTaskListsUseCase = mockk {
|
getTaskListsUseCase = mockk {
|
||||||
every { this@mockk.invoke() } returns getTaskListsFlow
|
every { this@mockk.invoke() } returns getTaskListsFlow
|
||||||
}
|
}
|
||||||
|
getLastOpenedTaskListIdUseCase = mockk {
|
||||||
|
coEvery { this@mockk.invoke() } returns null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
@@ -41,14 +55,14 @@ class MainViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initially isLoading is true and destinations are empty`() = runTest {
|
fun `initially isLoading is true and destinations are empty`() = runTest {
|
||||||
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
viewModel = createViewModel()
|
||||||
assertThat(viewModel.isLoading).isTrue()
|
assertThat(viewModel.isLoading).isTrue()
|
||||||
assertThat(viewModel.destinations).isEmpty()
|
assertThat(viewModel.destinations).isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when task lists are emitted they populate destinations and isLoading becomes false`() = runTest {
|
fun `when task lists are emitted they populate destinations and isLoading becomes false`() = runTest {
|
||||||
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
viewModel = createViewModel()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
val lists = listOf(
|
val lists = listOf(
|
||||||
@@ -72,9 +86,45 @@ class MainViewModelTest {
|
|||||||
assertThat(viewModel.isLoading).isFalse()
|
assertThat(viewModel.isLoading).isFalse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `starts on the last opened list when it still exists`() = runTest {
|
||||||
|
getLastOpenedTaskListIdUseCase = mockk {
|
||||||
|
coEvery { this@mockk.invoke() } returns 2L
|
||||||
|
}
|
||||||
|
viewModel = createViewModel()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val lists = listOf(
|
||||||
|
TaskList(id = 1L, name = "Work", isDeleted = false, order = 0),
|
||||||
|
TaskList(id = 2L, name = "Personal", isDeleted = false, order = 1)
|
||||||
|
)
|
||||||
|
getTaskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.startDestination).isEqualTo(AppDestination.TaskList(2L, "Personal"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `falls back to the first list when the last opened list no longer exists`() = runTest {
|
||||||
|
getLastOpenedTaskListIdUseCase = mockk {
|
||||||
|
coEvery { this@mockk.invoke() } returns 99L
|
||||||
|
}
|
||||||
|
viewModel = createViewModel()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val lists = listOf(
|
||||||
|
TaskList(id = 1L, name = "Work", isDeleted = false, order = 0),
|
||||||
|
TaskList(id = 2L, name = "Personal", isDeleted = false, order = 1)
|
||||||
|
)
|
||||||
|
getTaskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertThat(viewModel.startDestination).isEqualTo(AppDestination.TaskList(1L, "Work"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `navigateBack sends UiEvent_NavigateBack`() = runTest {
|
fun `navigateBack sends UiEvent_NavigateBack`() = runTest {
|
||||||
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.navigateBack()
|
viewModel.navigateBack()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
@@ -84,7 +134,7 @@ class MainViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `onNewTaskButtonClicked sets showTaskSheet true and sends CreateNewTask`() = runTest {
|
fun `onNewTaskButtonClicked sets showTaskSheet true and sends CreateNewTask`() = runTest {
|
||||||
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
viewModel = createViewModel()
|
||||||
val taskListId = 42L
|
val taskListId = 42L
|
||||||
|
|
||||||
viewModel.onNewTaskButtonClicked(taskListId)
|
viewModel.onNewTaskButtonClicked(taskListId)
|
||||||
@@ -96,7 +146,7 @@ class MainViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `onDismissTaskSheet sets showTaskSheet false and clears sticky`() = runTest {
|
fun `onDismissTaskSheet sets showTaskSheet false and clears sticky`() = runTest {
|
||||||
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
viewModel = createViewModel()
|
||||||
|
|
||||||
viewModel.showTaskSheet = true
|
viewModel.showTaskSheet = true
|
||||||
viewModel.onDismissTaskSheet()
|
viewModel.onDismissTaskSheet()
|
||||||
@@ -110,7 +160,7 @@ class MainViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `doesListExist returns true when taskListId is present`() = runTest {
|
fun `doesListExist returns true when taskListId is present`() = runTest {
|
||||||
val lists = listOf(TaskList(id = 1L, name = "Work", isDeleted = false, order = 0))
|
val lists = listOf(TaskList(id = 1L, name = "Work", isDeleted = false, order = 0))
|
||||||
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
viewModel = createViewModel()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
getTaskListsFlow.emit(lists)
|
getTaskListsFlow.emit(lists)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
@@ -122,7 +172,7 @@ class MainViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `setCurrentDestination sets currentDestination based on navBackStackEntry`() = runTest {
|
fun `setCurrentDestination sets currentDestination based on navBackStackEntry`() = runTest {
|
||||||
val lists = listOf(TaskList(id = 1L, name = "Work", isDeleted = false, order = 0))
|
val lists = listOf(TaskList(id = 1L, name = "Work", isDeleted = false, order = 0))
|
||||||
viewModel = MainViewModel(getTaskListsUseCase, uiEventBus)
|
viewModel = createViewModel()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
getTaskListsFlow.emit(lists)
|
getTaskListsFlow.emit(lists)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
@@ -135,4 +185,62 @@ class MainViewModelTest {
|
|||||||
viewModel.setCurrentDestination(entry)
|
viewModel.setCurrentDestination(entry)
|
||||||
assertThat(viewModel.currentDestination).isEqualTo(AppDestination.TaskList(1L, "Work"))
|
assertThat(viewModel.currentDestination).isEqualTo(AppDestination.TaskList(1L, "Work"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setCurrentDestination saves the new list when navigating to a different TaskList`() = runTest {
|
||||||
|
val lists = listOf(
|
||||||
|
TaskList(id = 1L, name = "Work", isDeleted = false, order = 0),
|
||||||
|
TaskList(id = 2L, name = "Personal", isDeleted = false, order = 1)
|
||||||
|
)
|
||||||
|
viewModel = createViewModel()
|
||||||
|
advanceUntilIdle()
|
||||||
|
getTaskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val entry = mockk<NavBackStackEntry> {
|
||||||
|
every { destination.route } returns AppDestination.TaskList(2L, "Personal").route
|
||||||
|
every { arguments?.getLong("taskListId") } returns 2L
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.setCurrentDestination(entry)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify { saveLastOpenedTaskListUseCase(2L) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setCurrentDestination does not save when navigating to a non-list destination`() = runTest {
|
||||||
|
viewModel = createViewModel()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val entry = mockk<NavBackStackEntry> {
|
||||||
|
every { destination.route } returns AppDestination.RecycleBin.route
|
||||||
|
every { arguments } returns null
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.setCurrentDestination(entry)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify(exactly = 0) { saveLastOpenedTaskListUseCase(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setCurrentDestination only saves once for repeated identical navigation`() = runTest {
|
||||||
|
val lists = listOf(TaskList(id = 1L, name = "Work", isDeleted = false, order = 0))
|
||||||
|
viewModel = createViewModel()
|
||||||
|
advanceUntilIdle()
|
||||||
|
getTaskListsFlow.emit(lists)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
val entry = mockk<NavBackStackEntry> {
|
||||||
|
every { destination.route } returns AppDestination.TaskList(1L, "Work").route
|
||||||
|
every { arguments?.getLong("taskListId") } returns 1L
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.setCurrentDestination(entry)
|
||||||
|
viewModel.setCurrentDestination(entry)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { saveLastOpenedTaskListUseCase(any()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ platform :android do
|
|||||||
UI.user_error!("VersionCode #{gradle_version} should be higher than Play Store version (#{play_version}). Aborting upload.")
|
UI.user_error!("VersionCode #{gradle_version} should be higher than Play Store version (#{play_version}). Aborting upload.")
|
||||||
end
|
end
|
||||||
|
|
||||||
gradle(task: "testDebugUnitTest")
|
gradle(task: ":#{module_name}:testDebugUnitTest")
|
||||||
gradle(task: "clean :#{module_name}:bundleRelease")
|
gradle(task: "clean :#{module_name}:bundleRelease")
|
||||||
upload_to_play_store(
|
upload_to_play_store(
|
||||||
track: "internal",
|
track: "internal",
|
||||||
|
|||||||
+1
-2
@@ -16,12 +16,11 @@
|
|||||||
# This option should only be used with decoupled projects. More details, visit
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
android.builtInKotlin=false
|
|
||||||
android.defaults.buildfeatures.resvalues=true
|
android.defaults.buildfeatures.resvalues=true
|
||||||
|
android.dependency.excludeLibraryComponentsFromConstraints=true
|
||||||
android.dependency.useConstraints=true
|
android.dependency.useConstraints=true
|
||||||
android.enableAppCompileTimeRClass=false
|
android.enableAppCompileTimeRClass=false
|
||||||
android.enableJetifier=false
|
android.enableJetifier=false
|
||||||
android.newDsl=false
|
|
||||||
android.r8.optimizedResourceShrinking=false
|
android.r8.optimizedResourceShrinking=false
|
||||||
android.r8.strictFullModeForKeepRules=false
|
android.r8.strictFullModeForKeepRules=false
|
||||||
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0'
|
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||||
}
|
}
|
||||||
include ':donext'
|
//include ':donext'
|
||||||
include ':donextv2'
|
include ':donextv2'
|
||||||
|
|||||||
Reference in New Issue
Block a user