Skip to content

Commit

Permalink
inject noteId to NoteViewModel constructor
Browse files Browse the repository at this point in the history
  • Loading branch information
softartdev committed Oct 28, 2024
1 parent 64a9645 commit c63ab37
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 101 deletions.
2 changes: 1 addition & 1 deletion iosApp/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 0dc93a6f6109335ea8cd3f91d2c87cc8c99f04a3

COCOAPODS: 1.15.2
COCOAPODS: 1.12.1
2 changes: 1 addition & 1 deletion iosApp/Pods/Manifest.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions iosApp/Pods/Pods.xcodeproj/project.pbxproj

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(KoinExperimentalAPI::class)

package com.softartdev.notedelight

import androidx.compose.runtime.Composable
Expand Down Expand Up @@ -26,7 +28,10 @@ import com.softartdev.notedelight.ui.dialog.security.EnterPasswordDialog
import com.softartdev.theme.material3.PreferableMaterialTheme
import com.softartdev.theme.material3.ThemeDialog
import com.softartdev.theme.pref.PreferenceHelper
import org.koin.compose.viewmodel.koinNavViewModel
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.core.parameter.parametersOf

@Composable
fun App(
Expand All @@ -51,10 +56,9 @@ fun App(
MainScreen(mainViewModel = koinViewModel())
}
composable<AppNavGraph.Details> { backStackEntry: NavBackStackEntry ->
NoteDetail(
noteViewModel = koinViewModel(),
noteId = backStackEntry.toRoute<AppNavGraph.Details>().noteId,
)
NoteDetail(noteViewModel = koinNavViewModel {
parametersOf(backStackEntry.toRoute<AppNavGraph.Details>().noteId)
})
}
composable<AppNavGraph.Settings> {
SettingsScreen(settingsViewModel = koinViewModel())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,14 @@ import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource

@Composable
fun NoteDetail(noteViewModel: NoteViewModel, noteId: Long) {
LaunchedEffect(key1 = noteId, key2 = noteViewModel) {
when (noteId) {
0L -> noteViewModel.createNote()
else -> noteViewModel.loadNote(noteId)
}
}
fun NoteDetail(noteViewModel: NoteViewModel) {
val noteResultState: State<NoteResult> = noteViewModel.stateFlow.collectAsState()
val titleState: MutableState<String> = remember { mutableStateOf("") }
val textState: MutableState<String> = remember { mutableStateOf("") }
val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(
key1 = noteId,
key2 = noteViewModel,
key3 = noteResultState.value
key1 = noteViewModel,
key2 = noteResultState.value
) {
when (val noteResult: NoteResult = noteResultState.value) {
is NoteResult.Loading,
Expand Down
3 changes: 3 additions & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ kotlin {
jvmTest.dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-junit"))
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.test)
implementation(libs.coroutines.swing)
}
all {
languageSettings.optIn("kotlin.RequiresOptIn")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions

class NoteViewModelTest {

Expand All @@ -41,7 +43,6 @@ class NoteViewModelTest {
private val mockDeleteNoteUseCase = Mockito.mock(DeleteNoteUseCase::class.java)
private val mockRouter = Mockito.mock(Router::class.java)
private val coroutineDispatchers = CoroutineDispatchersStub(testDispatcher = mainDispatcherRule.testDispatcher)
private val noteViewModel = NoteViewModel(mockNoteDAO, mockCreateNoteUseCase, saveNoteUseCase, mockDeleteNoteUseCase, mockRouter, coroutineDispatchers)

private val id = 1L
private val title: String = "title"
Expand All @@ -58,16 +59,38 @@ class NoteViewModelTest {

@After
fun tearDown() = runTest {
noteViewModel.onCleared()
Napier.takeLogarithm()
Mockito.reset(mockNoteDAO, mockCreateNoteUseCase, mockDeleteNoteUseCase, mockRouter)
}

@Test
fun createNote() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())
fun `init with noteId 0 creates new note`() = runTest {
val viewModel = createViewModel(noteId = 0)
viewModel.stateFlow.test {
assertEquals(NoteResult.Created(id), awaitItem())
verify(mockCreateNoteUseCase).invoke()
verifyNoMoreInteractions(mockNoteDAO)

noteViewModel.createNote()
cancelAndIgnoreRemainingEvents()
}
}

@Test
fun `init with existing noteId loads note`() = runTest {
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
assertEquals(NoteResult.Loaded(note), awaitItem())
verify(mockNoteDAO).load(id)
verifyNoMoreInteractions(mockCreateNoteUseCase)

cancelAndIgnoreRemainingEvents()
}
}

@Test
fun createNote() = runTest {
val viewModel = createViewModel(noteId = 0)
viewModel.stateFlow.test {
assertEquals(NoteResult.Created(id), awaitItem())

cancelAndIgnoreRemainingEvents()
Expand All @@ -76,10 +99,8 @@ class NoteViewModelTest {

@Test
fun loadNote() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())

noteViewModel.loadNote(id)
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
assertEquals(NoteResult.Loaded(note), awaitItem())

cancelAndIgnoreRemainingEvents()
Expand All @@ -88,10 +109,11 @@ class NoteViewModelTest {

@Test
fun saveNoteEmpty() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
assertEquals(NoteResult.Loaded(note), awaitItem())

noteViewModel.saveNote("", "")
viewModel.saveNote("", "")
assertEquals(NoteResult.Empty, awaitItem())

cancelAndIgnoreRemainingEvents()
Expand All @@ -100,11 +122,11 @@ class NoteViewModelTest {

@Test
fun saveNote() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
assertEquals(NoteResult.Loaded(note), awaitItem())

noteViewModel.setIdForTest(id)
noteViewModel.saveNote(title, text)
viewModel.saveNote(title, text)
assertEquals(NoteResult.Saved(title), awaitItem())

cancelAndIgnoreRemainingEvents()
Expand All @@ -113,14 +135,15 @@ class NoteViewModelTest {

@Test
fun editTitle() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
assertEquals(NoteResult.Loaded(note), awaitItem())

noteViewModel.setIdForTest(id)
noteViewModel.editTitle()
Mockito.verify(mockRouter).navigate(route = AppNavGraph.EditTitleDialog(noteId = id))
viewModel.editTitle()
verify(mockRouter).navigate(route = AppNavGraph.EditTitleDialog(noteId = id))

UpdateTitleUseCase.titleChannel.send(title)
assertEquals(NoteResult.Loading, awaitItem())
assertEquals(NoteResult.TitleUpdated(title), awaitItem())

cancelAndIgnoreRemainingEvents()
Expand All @@ -129,11 +152,11 @@ class NoteViewModelTest {

@Test
fun deleteNote() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
assertEquals(NoteResult.Loaded(note), awaitItem())

noteViewModel.setIdForTest(id)
noteViewModel.deleteNote()
viewModel.deleteNote()
assertEquals(NoteResult.Deleted, awaitItem())

cancelAndIgnoreRemainingEvents()
Expand All @@ -142,43 +165,40 @@ class NoteViewModelTest {

@Test
fun checkSaveChange() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())

noteViewModel.setIdForTest(id)
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
Mockito.`when`(mockNoteDAO.load(id)).thenReturn(note.copy(text = "new text"))
noteViewModel.checkSaveChange(title, text)
Mockito.verify(mockRouter).navigate(route = AppNavGraph.SaveChangesDialog)

viewModel.checkSaveChange(title, text)
verify(mockRouter).navigate(route = AppNavGraph.SaveChangesDialog)

SaveNoteUseCase.dialogChannel.send(true)
Mockito.verify(mockRouter).popBackStack()
verify(mockRouter).popBackStack()

Mockito.verifyNoMoreInteractions(mockRouter)
verifyNoMoreInteractions(mockRouter)
cancelAndIgnoreRemainingEvents()
}
}

@Test
fun checkSaveChangeNavBack() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())

noteViewModel.setIdForTest(id)
noteViewModel.checkSaveChange(title, text)
Mockito.verify(mockRouter).popBackStack()
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
viewModel.checkSaveChange(title, text)
verify(mockRouter).popBackStack()

cancelAndIgnoreRemainingEvents()
}
}

@Test
fun checkSaveChangeDeleted() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
assertEquals(NoteResult.Loaded(note), awaitItem())

noteViewModel.setIdForTest(id)
Mockito.`when`(mockNoteDAO.load(id)).thenReturn(note.copy(text = "", title = ""))
noteViewModel.checkSaveChange("", "")
viewModel.checkSaveChange("", "")
assertEquals(NoteResult.Deleted, awaitItem())

cancelAndIgnoreRemainingEvents()
Expand All @@ -187,41 +207,47 @@ class NoteViewModelTest {

@Test
fun saveNoteAndNavBack() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())

noteViewModel.setIdForTest(id)
noteViewModel.saveNoteAndNavBack(title, text)
Mockito.verify(mockRouter).popBackStack()
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
viewModel.saveNoteAndNavBack(title, text)
verify(mockRouter).popBackStack()

cancelAndIgnoreRemainingEvents()
}
}

@Test
fun doNotSaveAndNavBack() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())

noteViewModel.setIdForTest(id)
noteViewModel.doNotSaveAndNavBack()
Mockito.verify(mockRouter).popBackStack()
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
viewModel.doNotSaveAndNavBack()
verify(mockRouter).popBackStack()

cancelAndIgnoreRemainingEvents()
}
}

@Test
fun doNotSaveAndNavBackDeleted() = runTest {
noteViewModel.stateFlow.test {
assertEquals(NoteResult.Loading, awaitItem())
val viewModel = createViewModel(noteId = id)
viewModel.stateFlow.test {
assertEquals(NoteResult.Loaded(note), awaitItem())

noteViewModel.setIdForTest(id)
Mockito.`when`(mockNoteDAO.load(id)).thenReturn(note.copy(text = "", title = ""))
noteViewModel.doNotSaveAndNavBack()
viewModel.doNotSaveAndNavBack()
assertEquals(NoteResult.Deleted, awaitItem())

cancelAndIgnoreRemainingEvents()
}
}

private fun createViewModel(noteId: Long): NoteViewModel = NoteViewModel(
noteId = noteId,
noteDAO = mockNoteDAO,
createNoteUseCase = mockCreateNoteUseCase,
saveNoteUseCase = saveNoteUseCase,
deleteNoteUseCase = mockDeleteNoteUseCase,
router = mockRouter,
coroutineDispatchers = coroutineDispatchers
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.softartdev.notedelight.shared.di

import com.softartdev.notedelight.shared.db.NoteDAO
import com.softartdev.notedelight.shared.db.SafeRepo
import com.softartdev.notedelight.shared.presentation.main.MainViewModel
import com.softartdev.notedelight.shared.presentation.note.DeleteViewModel
Expand Down Expand Up @@ -33,7 +34,7 @@ Provide the [SafeRepo]
expect val repoModule: Module

val daoModule: Module = module {
factory { get<SafeRepo>().noteDAO }
factory<NoteDAO> { get<SafeRepo>().noteDAO }
}

val useCaseModule: Module = module {
Expand Down
Loading

0 comments on commit c63ab37

Please sign in to comment.