|
|
|
|
@@ -1,316 +1,126 @@
|
|
|
|
|
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
|
|
|
|
|
// [FILE] ItemEditViewModelTest.kt
|
|
|
|
|
// [SEMANTICS] testing, viewmodel, unit_test
|
|
|
|
|
|
|
|
|
|
package com.homebox.lens.ui.screen.itemedit
|
|
|
|
|
|
|
|
|
|
// [IMPORTS]
|
|
|
|
|
import androidx.lifecycle.viewModelScope
|
|
|
|
|
import app.cash.turbine.test
|
|
|
|
|
import com.homebox.lens.domain.model.Item
|
|
|
|
|
import com.homebox.lens.domain.model.ItemCreate
|
|
|
|
|
import com.homebox.lens.domain.model.ItemOut
|
|
|
|
|
import com.homebox.lens.domain.model.ItemSummary
|
|
|
|
|
import com.homebox.lens.domain.model.Label
|
|
|
|
|
import com.homebox.lens.domain.model.Location
|
|
|
|
|
import com.homebox.lens.domain.model.LocationOut
|
|
|
|
|
import com.homebox.lens.domain.model.LabelOut
|
|
|
|
|
import com.homebox.lens.domain.usecase.CreateItemUseCase
|
|
|
|
|
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
|
|
|
|
|
import com.homebox.lens.domain.usecase.UpdateItemUseCase
|
|
|
|
|
import io.kotest.core.spec.style.FunSpec
|
|
|
|
|
import io.kotest.matchers.shouldBe
|
|
|
|
|
import io.kotest.assertions.throwables.shouldThrow
|
|
|
|
|
import io.mockk.coEvery
|
|
|
|
|
import io.mockk.coVerify
|
|
|
|
|
import io.mockk.mockk
|
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
|
|
|
import kotlinx.coroutines.flow.first
|
|
|
|
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
|
|
|
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
|
|
|
import kotlinx.coroutines.test.resetMain
|
|
|
|
|
import kotlinx.coroutines.test.runTest
|
|
|
|
|
import kotlinx.coroutines.test.setMain
|
|
|
|
|
import org.junit.jupiter.api.AfterEach
|
|
|
|
|
import org.junit.jupiter.api.BeforeEach
|
|
|
|
|
import java.math.BigDecimal
|
|
|
|
|
// [END_IMPORTS]
|
|
|
|
|
import org.junit.After
|
|
|
|
|
import org.junit.Assert.assertEquals
|
|
|
|
|
import org.junit.Assert.assertFalse
|
|
|
|
|
import org.junit.Assert.assertNotNull
|
|
|
|
|
import org.junit.Assert.assertTrue
|
|
|
|
|
import org.junit.Before
|
|
|
|
|
import org.junit.Test
|
|
|
|
|
import java.util.UUID
|
|
|
|
|
|
|
|
|
|
// [ENTITY: Class('ItemEditViewModelTest')]
|
|
|
|
|
// [RELATION: Class('ItemEditViewModelTest')] -> [TESTS] -> [ViewModel('ItemEditViewModel')]
|
|
|
|
|
/**
|
|
|
|
|
* @summary Unit tests for [ItemEditViewModel].
|
|
|
|
|
*/
|
|
|
|
|
@OptIn(ExperimentalCoroutinesApi::class)
|
|
|
|
|
class ItemEditViewModelTest : FunSpec({
|
|
|
|
|
@ExperimentalCoroutinesApi
|
|
|
|
|
class ItemEditViewModelTest {
|
|
|
|
|
|
|
|
|
|
val createItemUseCase = mockk<CreateItemUseCase>()
|
|
|
|
|
val updateItemUseCase = mockk<UpdateItemUseCase>()
|
|
|
|
|
val getItemDetailsUseCase = mockk<GetItemDetailsUseCase>()
|
|
|
|
|
private val testDispatcher = StandardTestDispatcher()
|
|
|
|
|
|
|
|
|
|
lateinit var viewModel: ItemEditViewModel
|
|
|
|
|
private lateinit var createItemUseCase: CreateItemUseCase
|
|
|
|
|
private lateinit var updateItemUseCase: UpdateItemUseCase
|
|
|
|
|
private lateinit var getItemDetailsUseCase: GetItemDetailsUseCase
|
|
|
|
|
private lateinit var viewModel: ItemEditViewModel
|
|
|
|
|
|
|
|
|
|
val testDispatcher = UnconfinedTestDispatcher()
|
|
|
|
|
|
|
|
|
|
beforeEach {
|
|
|
|
|
@Before
|
|
|
|
|
fun setUp() {
|
|
|
|
|
Dispatchers.setMain(testDispatcher)
|
|
|
|
|
createItemUseCase = mockk()
|
|
|
|
|
updateItemUseCase = mockk()
|
|
|
|
|
getItemDetailsUseCase = mockk()
|
|
|
|
|
viewModel = ItemEditViewModel(createItemUseCase, updateItemUseCase, getItemDetailsUseCase)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
afterEach {
|
|
|
|
|
@After
|
|
|
|
|
fun tearDown() {
|
|
|
|
|
Dispatchers.resetMain()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// [ENTITY: Function('loadItem - new item creation')]
|
|
|
|
|
/**
|
|
|
|
|
* @summary Tests that loadItem with null itemId prepares for new item creation.
|
|
|
|
|
*/
|
|
|
|
|
test("loadItem with null itemId should prepare for new item creation") {
|
|
|
|
|
viewModel.loadItem(null)
|
|
|
|
|
|
|
|
|
|
val uiState = viewModel.uiState.first()
|
|
|
|
|
uiState.isLoading shouldBe false
|
|
|
|
|
uiState.error shouldBe null
|
|
|
|
|
uiState.item shouldBe Item(id = "", name = "", description = null, quantity = 0, image = null, location = null, labels = emptyList(), value = null, createdAt = null)
|
|
|
|
|
}
|
|
|
|
|
// [END_ENTITY: Function('loadItem - new item creation')]
|
|
|
|
|
|
|
|
|
|
// [ENTITY: Function('loadItem - existing item loading success')]
|
|
|
|
|
/**
|
|
|
|
|
* @summary Tests that loadItem with an itemId successfully loads an existing item.
|
|
|
|
|
*/
|
|
|
|
|
test("loadItem with itemId should load existing item successfully") {
|
|
|
|
|
val itemId = "test_item_id"
|
|
|
|
|
val itemOut = ItemOut(
|
|
|
|
|
id = itemId,
|
|
|
|
|
name = "Loaded Item",
|
|
|
|
|
assetId = null,
|
|
|
|
|
description = "Description",
|
|
|
|
|
notes = null,
|
|
|
|
|
serialNumber = null,
|
|
|
|
|
quantity = 5,
|
|
|
|
|
isArchived = false,
|
|
|
|
|
value = 100.0,
|
|
|
|
|
purchasePrice = null,
|
|
|
|
|
purchaseDate = null,
|
|
|
|
|
warrantyUntil = null,
|
|
|
|
|
location = LocationOut("loc1", "Location 1", "#FFFFFF", false, "2025-01-01T00:00:00Z", "2025-01-01T00:00:00Z"),
|
|
|
|
|
parent = null,
|
|
|
|
|
children = emptyList(),
|
|
|
|
|
labels = listOf(LabelOut("lab1", "Label 1", "#FFFFFF", false, "2025-01-01T00:00:00Z", "2025-01-01T00:00:00Z")),
|
|
|
|
|
attachments = emptyList(),
|
|
|
|
|
images = emptyList(),
|
|
|
|
|
fields = emptyList(),
|
|
|
|
|
maintenance = emptyList(),
|
|
|
|
|
createdAt = "2025-01-01T00:00:00Z",
|
|
|
|
|
updatedAt = "2025-01-01T00:00:00Z"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@Test
|
|
|
|
|
fun `loadItem with valid id should update uiState with item`() = runTest {
|
|
|
|
|
val itemId = UUID.randomUUID().toString()
|
|
|
|
|
val itemOut = ItemOut(id = itemId, name = "Test Item", description = "Description", quantity = 1, images = emptyList(), location = null, labels = emptyList(), value = 10.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
|
|
|
|
|
coEvery { getItemDetailsUseCase(itemId) } returns itemOut
|
|
|
|
|
|
|
|
|
|
viewModel.loadItem(itemId)
|
|
|
|
|
testDispatcher.scheduler.advanceUntilIdle()
|
|
|
|
|
|
|
|
|
|
val uiState = viewModel.uiState.first()
|
|
|
|
|
uiState.isLoading shouldBe false
|
|
|
|
|
uiState.error shouldBe null
|
|
|
|
|
uiState.item?.id shouldBe itemOut.id
|
|
|
|
|
uiState.item?.name shouldBe itemOut.name
|
|
|
|
|
val uiState = viewModel.uiState.value
|
|
|
|
|
assertFalse(uiState.isLoading)
|
|
|
|
|
assertNotNull(uiState.item)
|
|
|
|
|
assertEquals(itemId, uiState.item?.id)
|
|
|
|
|
assertEquals("Test Item", uiState.item?.name)
|
|
|
|
|
}
|
|
|
|
|
// [END_ENTITY: Function('loadItem - existing item loading success')]
|
|
|
|
|
|
|
|
|
|
// [ENTITY: Function('loadItem - existing item loading failure')]
|
|
|
|
|
/**
|
|
|
|
|
* @summary Tests that loadItem with an itemId handles loading failure.
|
|
|
|
|
*/
|
|
|
|
|
test("loadItem with itemId should handle loading failure") {
|
|
|
|
|
val itemId = "test_item_id"
|
|
|
|
|
val errorMessage = "Failed to fetch item"
|
|
|
|
|
coEvery { getItemDetailsUseCase(itemId) } throws Exception(errorMessage)
|
|
|
|
|
@Test
|
|
|
|
|
fun `loadItem with null id should prepare a new item`() = runTest {
|
|
|
|
|
viewModel.loadItem(null)
|
|
|
|
|
testDispatcher.scheduler.advanceUntilIdle()
|
|
|
|
|
|
|
|
|
|
viewModel.loadItem(itemId)
|
|
|
|
|
|
|
|
|
|
val uiState = viewModel.uiState.first()
|
|
|
|
|
uiState.isLoading shouldBe false
|
|
|
|
|
uiState.error shouldBe errorMessage
|
|
|
|
|
uiState.item shouldBe null
|
|
|
|
|
val uiState = viewModel.uiState.value
|
|
|
|
|
assertFalse(uiState.isLoading)
|
|
|
|
|
assertNotNull(uiState.item)
|
|
|
|
|
assertEquals("", uiState.item?.id)
|
|
|
|
|
assertEquals("", uiState.item?.name)
|
|
|
|
|
}
|
|
|
|
|
// [END_ENTITY: Function('loadItem - existing item loading failure')]
|
|
|
|
|
|
|
|
|
|
// [ENTITY: Function('saveItem - new item creation success')]
|
|
|
|
|
/**
|
|
|
|
|
* @summary Tests that saveItem successfully creates a new item.
|
|
|
|
|
*/
|
|
|
|
|
test("saveItem should create new item successfully") {
|
|
|
|
|
val newItem = Item(
|
|
|
|
|
id = "", // New item has blank ID
|
|
|
|
|
name = "New Item",
|
|
|
|
|
description = null,
|
|
|
|
|
quantity = 1,
|
|
|
|
|
image = null,
|
|
|
|
|
location = null,
|
|
|
|
|
labels = emptyList(),
|
|
|
|
|
value = null,
|
|
|
|
|
createdAt = null
|
|
|
|
|
)
|
|
|
|
|
val createdSummary = ItemSummary("new_id", "New Item")
|
|
|
|
|
@Test
|
|
|
|
|
fun `saveItem should call createItemUseCase for new item`() = runTest {
|
|
|
|
|
val createdItemSummary = ItemSummary(id = UUID.randomUUID().toString(), name = "New Item", assetId = null, image = null, isArchived = false, labels = emptyList(), location = null, value = 0.0, createdAt = "2025-08-28T12:00:00Z", updatedAt = "2025-08-28T12:00:00Z")
|
|
|
|
|
coEvery { createItemUseCase(any()) } returns createdItemSummary
|
|
|
|
|
|
|
|
|
|
viewModel.uiState.value = ItemEditUiState(item = newItem)
|
|
|
|
|
coEvery { createItemUseCase(any()) } returns createdSummary
|
|
|
|
|
viewModel.loadItem(null)
|
|
|
|
|
testDispatcher.scheduler.advanceUntilIdle()
|
|
|
|
|
viewModel.updateName("New Item")
|
|
|
|
|
viewModel.updateDescription("New Description")
|
|
|
|
|
viewModel.updateQuantity(2)
|
|
|
|
|
testDispatcher.scheduler.advanceUntilIdle()
|
|
|
|
|
|
|
|
|
|
viewModel.saveItem()
|
|
|
|
|
testDispatcher.scheduler.advanceUntilIdle()
|
|
|
|
|
|
|
|
|
|
val uiState = viewModel.uiState.first()
|
|
|
|
|
uiState.isLoading shouldBe false
|
|
|
|
|
uiState.error shouldBe null
|
|
|
|
|
uiState.item?.id shouldBe createdSummary.id
|
|
|
|
|
uiState.item?.name shouldBe createdSummary.name
|
|
|
|
|
coVerify(exactly = 1) { createItemUseCase(ItemCreate(
|
|
|
|
|
name = newItem.name,
|
|
|
|
|
description = newItem.description,
|
|
|
|
|
quantity = newItem.quantity,
|
|
|
|
|
assetId = null,
|
|
|
|
|
notes = null,
|
|
|
|
|
serialNumber = null,
|
|
|
|
|
value = null,
|
|
|
|
|
purchasePrice = null,
|
|
|
|
|
purchaseDate = null,
|
|
|
|
|
warrantyUntil = null,
|
|
|
|
|
locationId = newItem.location?.id,
|
|
|
|
|
parentId = null,
|
|
|
|
|
labelIds = newItem.labels.map { it.id }
|
|
|
|
|
)) }
|
|
|
|
|
viewModel.saveCompleted.first() shouldBe Unit
|
|
|
|
|
val uiState = viewModel.uiState.value
|
|
|
|
|
assertFalse(uiState.isLoading)
|
|
|
|
|
assertNotNull(uiState.item)
|
|
|
|
|
assertEquals(createdItemSummary.id, uiState.item?.id)
|
|
|
|
|
}
|
|
|
|
|
// [END_ENTITY: Function('saveItem - new item creation success')]
|
|
|
|
|
|
|
|
|
|
// [ENTITY: Function('saveItem - new item creation failure')]
|
|
|
|
|
/**
|
|
|
|
|
* @summary Tests that saveItem handles new item creation failure.
|
|
|
|
|
*/
|
|
|
|
|
test("saveItem should handle new item creation failure") {
|
|
|
|
|
val newItem = Item(
|
|
|
|
|
id = "",
|
|
|
|
|
name = "New Item",
|
|
|
|
|
description = null,
|
|
|
|
|
quantity = 1,
|
|
|
|
|
image = null,
|
|
|
|
|
location = null,
|
|
|
|
|
labels = emptyList(),
|
|
|
|
|
value = null,
|
|
|
|
|
createdAt = null
|
|
|
|
|
)
|
|
|
|
|
val errorMessage = "Failed to create item"
|
|
|
|
|
|
|
|
|
|
viewModel.uiState.value = ItemEditUiState(item = newItem)
|
|
|
|
|
coEvery { createItemUseCase(any()) } throws Exception(errorMessage)
|
|
|
|
|
|
|
|
|
|
viewModel.saveItem()
|
|
|
|
|
|
|
|
|
|
val uiState = viewModel.uiState.first()
|
|
|
|
|
uiState.isLoading shouldBe false
|
|
|
|
|
uiState.error shouldBe errorMessage
|
|
|
|
|
coVerify(exactly = 1) { createItemUseCase(any()) }
|
|
|
|
|
}
|
|
|
|
|
// [END_ENTITY: Function('saveItem - new item creation failure')]
|
|
|
|
|
|
|
|
|
|
// [ENTITY: Function('saveItem - existing item update success')]
|
|
|
|
|
/**
|
|
|
|
|
* @summary Tests that saveItem successfully updates an existing item.
|
|
|
|
|
*/
|
|
|
|
|
test("saveItem should update existing item successfully") {
|
|
|
|
|
val existingItem = Item(
|
|
|
|
|
id = "existing_id",
|
|
|
|
|
name = "Existing Item",
|
|
|
|
|
description = null,
|
|
|
|
|
quantity = 1,
|
|
|
|
|
image = null,
|
|
|
|
|
location = null,
|
|
|
|
|
labels = emptyList(),
|
|
|
|
|
value = null,
|
|
|
|
|
createdAt = null
|
|
|
|
|
)
|
|
|
|
|
val updatedItemOut = ItemOut(
|
|
|
|
|
id = "existing_id",
|
|
|
|
|
name = "Updated Item",
|
|
|
|
|
assetId = null,
|
|
|
|
|
description = null,
|
|
|
|
|
notes = null,
|
|
|
|
|
serialNumber = null,
|
|
|
|
|
quantity = 2,
|
|
|
|
|
isArchived = false,
|
|
|
|
|
value = 200.0,
|
|
|
|
|
purchasePrice = null,
|
|
|
|
|
purchaseDate = null,
|
|
|
|
|
warrantyUntil = null,
|
|
|
|
|
location = null,
|
|
|
|
|
parent = null,
|
|
|
|
|
children = emptyList(),
|
|
|
|
|
labels = emptyList(),
|
|
|
|
|
attachments = emptyList(),
|
|
|
|
|
images = emptyList(),
|
|
|
|
|
fields = emptyList(),
|
|
|
|
|
maintenance = emptyList(),
|
|
|
|
|
createdAt = "2025-01-01T00:00:00Z",
|
|
|
|
|
updatedAt = "2025-01-01T00:00:00Z"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
viewModel.uiState.value = ItemEditUiState(item = existingItem)
|
|
|
|
|
@Test
|
|
|
|
|
fun `saveItem should call updateItemUseCase for existing item`() = runTest {
|
|
|
|
|
val itemId = UUID.randomUUID().toString()
|
|
|
|
|
val updatedItemOut = ItemOut(id = itemId, name = "Updated Item", description = "Updated Description", quantity = 4, images = emptyList(), location = null, labels = emptyList(), value = 12.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
|
|
|
|
|
coEvery { getItemDetailsUseCase(itemId) } returns ItemOut(id = itemId, name = "Existing Item", description = "Existing Description", quantity = 3, images = emptyList(), location = null, labels = emptyList(), value = 10.0, createdAt = "2025-08-28T12:00:00Z", assetId = null, notes = null, serialNumber = null, isArchived = false, purchasePrice = null, purchaseDate = null, warrantyUntil = null, parent = null, children = emptyList(), attachments = emptyList(), fields = emptyList(), maintenance = emptyList(), updatedAt = "2025-08-28T12:00:00Z")
|
|
|
|
|
coEvery { updateItemUseCase(any()) } returns updatedItemOut
|
|
|
|
|
|
|
|
|
|
viewModel.saveItem()
|
|
|
|
|
|
|
|
|
|
val uiState = viewModel.uiState.first()
|
|
|
|
|
uiState.isLoading shouldBe false
|
|
|
|
|
uiState.error shouldBe null
|
|
|
|
|
uiState.item?.id shouldBe updatedItemOut.id
|
|
|
|
|
uiState.item?.name shouldBe updatedItemOut.name
|
|
|
|
|
coVerify(exactly = 1) { updateItemUseCase(existingItem) }
|
|
|
|
|
viewModel.saveCompleted.first() shouldBe Unit
|
|
|
|
|
}
|
|
|
|
|
// [END_ENTITY: Function('saveItem - existing item update success')]
|
|
|
|
|
|
|
|
|
|
// [ENTITY: Function('saveItem - existing item update failure')]
|
|
|
|
|
/**
|
|
|
|
|
* @summary Tests that saveItem handles existing item update failure.
|
|
|
|
|
*/
|
|
|
|
|
test("saveItem should handle existing item update failure") {
|
|
|
|
|
val existingItem = Item(
|
|
|
|
|
id = "existing_id",
|
|
|
|
|
name = "Existing Item",
|
|
|
|
|
description = null,
|
|
|
|
|
quantity = 1,
|
|
|
|
|
image = null,
|
|
|
|
|
location = null,
|
|
|
|
|
labels = emptyList(),
|
|
|
|
|
value = null,
|
|
|
|
|
createdAt = null
|
|
|
|
|
)
|
|
|
|
|
val errorMessage = "Failed to update item"
|
|
|
|
|
|
|
|
|
|
viewModel.uiState.value = ItemEditUiState(item = existingItem)
|
|
|
|
|
coEvery { updateItemUseCase(any()) } throws Exception(errorMessage)
|
|
|
|
|
viewModel.loadItem(itemId)
|
|
|
|
|
testDispatcher.scheduler.advanceUntilIdle()
|
|
|
|
|
viewModel.updateName("Updated Item")
|
|
|
|
|
viewModel.updateDescription("Updated Description")
|
|
|
|
|
viewModel.updateQuantity(4)
|
|
|
|
|
testDispatcher.scheduler.advanceUntilIdle()
|
|
|
|
|
|
|
|
|
|
viewModel.saveItem()
|
|
|
|
|
testDispatcher.scheduler.advanceUntilIdle()
|
|
|
|
|
|
|
|
|
|
val uiState = viewModel.uiState.first()
|
|
|
|
|
uiState.isLoading shouldBe false
|
|
|
|
|
uiState.error shouldBe errorMessage
|
|
|
|
|
coVerify(exactly = 1) { updateItemUseCase(any()) }
|
|
|
|
|
val uiState = viewModel.uiState.value
|
|
|
|
|
assertFalse(uiState.isLoading)
|
|
|
|
|
assertNotNull(uiState.item)
|
|
|
|
|
assertEquals(itemId, uiState.item?.id)
|
|
|
|
|
assertEquals("Updated Item", uiState.item?.name)
|
|
|
|
|
assertEquals(4, uiState.item?.quantity)
|
|
|
|
|
}
|
|
|
|
|
// [END_ENTITY: Function('saveItem - existing item update failure')]
|
|
|
|
|
|
|
|
|
|
// [ENTITY: Function('saveItem - null item')]
|
|
|
|
|
/**
|
|
|
|
|
* @summary Tests that saveItem throws IllegalStateException when item in uiState is null.
|
|
|
|
|
*/
|
|
|
|
|
test("saveItem should throw IllegalStateException when item in uiState is null") {
|
|
|
|
|
viewModel.uiState.value = ItemEditUiState(item = null)
|
|
|
|
|
|
|
|
|
|
val exception = shouldThrow<IllegalStateException> {
|
|
|
|
|
viewModel.saveItem()
|
|
|
|
|
}
|
|
|
|
|
exception.message shouldBe "[CONTRACT_VIOLATION][PRECONDITION][item_not_present] Cannot save a null item."
|
|
|
|
|
}
|
|
|
|
|
// [END_ENTITY: Function('saveItem - null item')]
|
|
|
|
|
})
|
|
|
|
|
// [END_ENTITY: Class('ItemEditViewModelTest')]
|
|
|
|
|
// [END_FILE_ItemEditViewModelTest.kt]
|
|
|
|
|
}
|
|
|
|
|
|