1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-2793 Date as an Object | Tech | Observe relations store (#1924)

This commit is contained in:
Konstantin Ivanov 2024-12-16 13:42:21 +01:00 committed by Evgenii Kozlov
parent 998cd1cf2d
commit 9ed299a3c2
10 changed files with 885 additions and 108 deletions

View file

@ -24,7 +24,7 @@ suspend fun List<RelationListWithValueItem>.toUiFieldsItem(
.mapNotNull { item ->
val relation = storeOfRelations.getByKey(item.key.key)
if (relation == null) {
Timber.e("Relation ${item.key.key} not found in the relation store")
Timber.w("Relation ${item.key.key} not found in the relation store")
return@mapNotNull null
}
if (relation.key == Relations.LINKS || relation.key == Relations.BACKLINKS) {

View file

@ -9,6 +9,7 @@ import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.`object`.GetObject
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
import com.anytypeio.anytype.domain.objects.SetObjectListIsArchived
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
@ -33,7 +34,8 @@ class DateObjectVMFactory @Inject constructor(
private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider,
private val createObject: CreateObject,
private val fieldParser: FieldParser,
private val setObjectListIsArchived: SetObjectListIsArchived
private val setObjectListIsArchived: SetObjectListIsArchived,
private val getDateObjectByTimestamp: GetDateObjectByTimestamp
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -53,7 +55,7 @@ class DateObjectVMFactory @Inject constructor(
spaceSyncAndP2PStatusProvider = spaceSyncAndP2PStatusProvider,
createObject = createObject,
fieldParser = fieldParser,
setObjectListIsArchived = setObjectListIsArchived
setObjectListIsArchived = setObjectListIsArchived,
getDateObjectByTimestamp = getDateObjectByTimestamp
) as T
}

View file

@ -8,6 +8,7 @@ import com.anytypeio.anytype.core_models.DATE_PICKER_YEAR_RANGE
import com.anytypeio.anytype.core_models.DVSortType
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.RelationListWithValueItem
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.TimeInSeconds
import com.anytypeio.anytype.core_models.getSingleValue
@ -21,6 +22,7 @@ import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.`object`.GetObject
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
import com.anytypeio.anytype.domain.objects.SetObjectListIsArchived
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
@ -44,7 +46,6 @@ import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
import com.anytypeio.anytype.presentation.home.navigation
import com.anytypeio.anytype.presentation.objects.getCreateObjectParams
import com.anytypeio.anytype.presentation.search.GlobalSearchViewModel.Companion.DEFAULT_DEBOUNCE_DURATION
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeys
import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState
import com.anytypeio.anytype.presentation.sync.toSyncStatusWidgetState
import kotlin.collections.map
@ -61,6 +62,7 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
@ -91,7 +93,8 @@ class DateObjectViewModel(
private val spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider,
private val createObject: CreateObject,
private val fieldParser: FieldParser,
private val setObjectListIsArchived: SetObjectListIsArchived
private val setObjectListIsArchived: SetObjectListIsArchived,
private val getDateObjectByTimestamp: GetDateObjectByTimestamp
) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate {
val uiCalendarIconState = MutableStateFlow<UiCalendarIconState>(UiCalendarIconState.Hidden)
@ -114,6 +117,7 @@ class DateObjectViewModel(
private val _dateId = MutableStateFlow<Id?>(null)
private val _dateTimestamp = MutableStateFlow<TimeInSeconds?>(null)
private val _activeField = MutableStateFlow<ActiveField?>(null)
private val _dateObjectFieldIds: MutableStateFlow<List<RelationListWithValueItem>> = MutableStateFlow(emptyList())
/**
* Paging and subscription limit. If true, we can paginate after reaching bottom items.
@ -144,9 +148,9 @@ class DateObjectViewModel(
uiHeaderState.value = UiHeaderState.Loading
uiFieldsState.value = UiFieldsState.LoadingState
uiObjectsListState.value = UiObjectsListState.LoadingState
proceedWithObservingRelationListWithValue()
proceedWithObservingPermissions()
proceedWithGettingDateObject()
proceedWithGettingDateObjectRelationList()
proceedWithObservingDateId()
proceedWithObservingSyncStatus()
setupSearchStateFlow()
_dateId.value = vmParams.objectId
@ -185,14 +189,27 @@ class DateObjectViewModel(
private fun proceedWithReopenDateObjectByTimestamp(timeInSeconds: TimeInSeconds) {
viewModelScope.launch {
fieldParser.getDateObjectByTimeInSeconds(
timeInSeconds = timeInSeconds,
spaceId = vmParams.spaceId,
actionSuccess = { obj ->
reopenDateObject(obj.id)
val params = GetDateObjectByTimestamp.Params(
space = vmParams.spaceId,
timestampInSeconds = timeInSeconds
)
getDateObjectByTimestamp.async(params).fold(
onSuccess = { dateObject ->
val obj = ObjectWrapper.Basic(dateObject.orEmpty())
if (obj.isValid) {
reopenDateObject(obj.id)
} else {
Timber.w("Date object is invalid")
errorState.value = UiErrorState.Show(
Reason.Other(msg = "Couldn't open date object, object is invalid")
)
}
},
actionFailure = {
Timber.e("GettingDateByTimestamp error, object has no id")
onFailure = { e ->
Timber.e(e, "Failed to get date object by timestamp :$timeInSeconds")
errorState.value = UiErrorState.Show(
Reason.Other(msg = "Couldn't open date object:\n${e.message?.take(30)}")
)
}
)
}
@ -264,79 +281,102 @@ class DateObjectViewModel(
}
}
private fun proceedWithGettingDateObjectRelationList() {
@OptIn(ExperimentalCoroutinesApi::class)
private fun proceedWithObservingRelationListWithValue() {
viewModelScope.launch {
combine(
_dateObjectFieldIds,
storeOfRelations.observe().filter { it.isNotEmpty() }
) { relationIds, _ ->
relationIds
}.collect { relationIds ->
Timber.d("RelationListWithValue: $relationIds")
initFieldsState(
items = relationIds.toUiFieldsItem(storeOfRelations = storeOfRelations)
)
}
}
}
private fun proceedWithObservingDateId() {
viewModelScope.launch {
_dateId
.filterNotNull()
.collect { id ->
val params = GetObjectRelationListById.Params(
space = vmParams.spaceId,
value = id
)
Timber.d("Start RelationListWithValue with params: $params")
getObjectRelationListById.async(params).fold(
onSuccess = { result ->
Timber.d("RelationListWithValue Success: $result")
val items =
result.toUiFieldsItem(storeOfRelations = storeOfRelations)
initFieldsState(items)
},
onFailure = { e ->
Timber.e(e, "RelationListWithValue Error")
errorState.value = UiErrorState.Show(
Reason.ErrorGettingFields(
msg = e.message ?: "Error getting fields"
)
)
}
)
Timber.d("Getting date object with id: $id")
proceedWithGettingDateObject(id)
proceedWithGettingDateObjectRelationList(id)
}
}
}
private fun proceedWithGettingDateObject() {
viewModelScope.launch {
_dateId
.filterNotNull()
.collect { id ->
val params = GetObject.Params(
target = id,
space = vmParams.spaceId,
saveAsLastOpened = true
private suspend fun proceedWithGettingDateObjectRelationList(id: Id) {
val params = GetObjectRelationListById.Params(
space = vmParams.spaceId,
value = id
)
getObjectRelationListById.async(params).fold(
onSuccess = { result ->
_dateObjectFieldIds.value = result
},
onFailure = { e ->
Timber.e(e, "RelationListWithValue Error")
errorState.value = UiErrorState.Show(
Reason.ErrorGettingFields(
msg = e.message ?: "Error getting fields"
)
Timber.d("Start GetObject with params: $params")
getObject.async(params).fold(
onSuccess = { obj ->
Timber.d("GetObject Success, obj:[$obj]")
val timestampInSeconds =
obj.details[id]?.getSingleValue<Double>(
Relations.TIMESTAMP
)?.toLong()
if (timestampInSeconds != null) {
_dateTimestamp.value = timestampInSeconds
val (formattedDate, _) = dateProvider.formatTimestampToDateAndTime(
timestamp = timestampInSeconds * 1000,
)
uiCalendarIconState.value = UiCalendarIconState.Visible(
timestampInSeconds = TimestampInSeconds(timestampInSeconds)
)
uiHeaderState.value = UiHeaderState.Content(
title = formattedDate,
relativeDate = dateProvider.calculateRelativeDates(
dateInSeconds = timestampInSeconds
)
)
}
},
onFailure = { e -> Timber.e(e, "GetObject Error") }
)
}
)
}
private suspend fun proceedWithGettingDateObject(id: Id) {
val params = GetObject.Params(
target = id,
space = vmParams.spaceId,
saveAsLastOpened = true
)
getObject.async(params).fold(
onSuccess = { obj ->
val timestampInSeconds =
obj.details[id]?.getSingleValue<Double>(
Relations.TIMESTAMP
)?.toLong()
if (timestampInSeconds != null) {
_dateTimestamp.value = timestampInSeconds
val (formattedDate, _) = dateProvider.formatTimestampToDateAndTime(
timestamp = timestampInSeconds * 1000,
)
uiCalendarIconState.value = UiCalendarIconState.Visible(
timestampInSeconds = TimestampInSeconds(timestampInSeconds)
)
uiHeaderState.value = UiHeaderState.Content(
title = formattedDate,
relativeDate = dateProvider.calculateRelativeDates(
dateInSeconds = timestampInSeconds
)
)
} else {
Timber.e("Error getting timestamp")
errorState.value = UiErrorState.Show(
Reason.Other(msg = "Error getting timestamp from date object")
)
}
}
},
onFailure = { e ->
Timber.e(e, "GetObject Error")
errorState.value = UiErrorState.Show(
Reason.Other(
msg = "Error opening date object: ${e.message?.take(30) ?: "Unknown error"}"
)
)
}
)
}
//endregion
//region Subscription
private fun subscriptionId() = "date_object_subscription_${vmParams.spaceId}"
fun subscriptionId() = "date_object_subscription_${vmParams.spaceId}"
@OptIn(ExperimentalCoroutinesApi::class)
private fun setupUiStateFlow() {
@ -419,29 +459,6 @@ class DateObjectViewModel(
return items
}
private fun createSearchParams(
dateId: Id,
timestamp: TimeInSeconds,
field: ActiveField,
space: SpaceId,
itemsLimit: Int
): StoreSearchParams {
val (filters, sorts) = filtersAndSortsForSearch(
spaces = listOf(space.id),
field = field,
timestamp = timestamp,
dateId = dateId
)
return StoreSearchParams(
space = space,
filters = filters,
sorts = sorts,
keys = defaultKeys,
limit = itemsLimit,
subscription = subscriptionId()
)
}
/**
* Updates the limit for the number of items fetched and triggers data reload.
*/
@ -900,28 +917,38 @@ class DateObjectViewModel(
//endregion
//region Ui State
private fun initFieldsState(relations: List<UiFieldsItem.Item>) {
val relation = relations.getOrNull(0)
if (relation == null) {
Timber.e("Error getting relation")
private fun initFieldsState(items: List<UiFieldsItem.Item>) {
Timber.d("Init fields state with items: $items")
if (items.isEmpty()) {
handleEmptyFieldsState()
return
}
val firstItem = items.first()
_activeField.value = ActiveField(
key = relation.key,
format = relation.relationFormat
key = firstItem.key,
format = firstItem.relationFormat
)
restartSubscription.value++
uiFieldsState.value = UiFieldsState(
items = buildList {
add(UiFieldsItem.Settings())
addAll(relations)
addAll(items)
},
selectedRelationKey = _activeField.value?.key
)
if (relations.isEmpty()) {
uiContentState.value = UiContentState.Empty
uiObjectsListState.value = UiObjectsListState.Empty
}
}
private fun handleEmptyFieldsState() {
uiFieldsState.value = UiFieldsState.Empty
uiContentState.value = UiContentState.Empty
uiObjectsListState.value = UiObjectsListState.Empty
_activeField.value = null
canPaginate.value = false
resetLimit()
shouldScrollToTopItems = true
uiFieldsSheetState.value = UiFieldsSheetState.Hidden
Timber.w("Error getting fields for date object:${_dateId.value}, fields are empty")
}
private fun updateHorizontalListState(selectedItem: UiFieldsItem.Item, needToScroll: Boolean = false) {

View file

@ -13,6 +13,32 @@ import com.anytypeio.anytype.core_models.Relations.LAST_MODIFIED_DATE
import com.anytypeio.anytype.core_models.Relations.RELATION_KEY
import com.anytypeio.anytype.core_models.Relations.TYPE
import com.anytypeio.anytype.core_models.TimeInSeconds
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.library.StoreSearchParams
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.defaultKeys
fun DateObjectViewModel.createSearchParams(
dateId: Id,
timestamp: TimeInSeconds,
field: ActiveField,
space: SpaceId,
itemsLimit: Int
): StoreSearchParams {
val (filters, sorts) = filtersAndSortsForSearch(
spaces = listOf(space.id),
field = field,
timestamp = timestamp,
dateId = dateId
)
return StoreSearchParams(
space = space,
filters = filters,
sorts = sorts,
keys = defaultKeys,
limit = itemsLimit,
subscription = subscriptionId()
)
}
fun filtersAndSortsForSearch(
dateId: Id,

View file

@ -0,0 +1,649 @@
package com.anytypeio.anytype.feature_date
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.*
import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions
import com.anytypeio.anytype.core_models.primitives.RelationKey
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.base.Resultat
import com.anytypeio.anytype.domain.event.interactor.SpaceSyncAndP2PStatusProvider
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.misc.DateProvider
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.`object`.GetObject
import com.anytypeio.anytype.domain.objects.DefaultStoreOfRelations
import com.anytypeio.anytype.domain.objects.GetDateObjectByTimestamp
import com.anytypeio.anytype.domain.objects.SetObjectListIsArchived
import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes
import com.anytypeio.anytype.domain.objects.StoreOfRelations
import com.anytypeio.anytype.domain.page.CreateObject
import com.anytypeio.anytype.domain.primitives.FieldParser
import com.anytypeio.anytype.domain.relations.GetObjectRelationListById
import com.anytypeio.anytype.feature_date.ui.models.DateEvent
import com.anytypeio.anytype.feature_date.viewmodel.ActiveField
import com.anytypeio.anytype.feature_date.viewmodel.DateObjectViewModel
import com.anytypeio.anytype.feature_date.viewmodel.DateObjectViewModel.Companion.DEFAULT_SEARCH_LIMIT
import com.anytypeio.anytype.feature_date.viewmodel.DateObjectVmParams
import com.anytypeio.anytype.feature_date.viewmodel.createSearchParams
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import net.bytebuddy.utility.RandomString
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.mockito.Mock
import org.mockito.Mockito.*
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.mock
import org.mockito.kotlin.verifyBlocking
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever
@ExperimentalCoroutinesApi
class DateObjectViewModelTest {
@get:Rule
var rule: TestRule = DefaultCoroutineTestRule()
@Mock private lateinit var getObject: GetObject
@Mock private lateinit var analytics: Analytics
@Mock private lateinit var urlBuilder: UrlBuilder
@Mock lateinit var dateProvider: DateProvider
@Mock private lateinit var analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate
@Mock private lateinit var userPermissionProvider: UserPermissionProvider
@Mock private lateinit var relationListWithValue: GetObjectRelationListById
@Mock private lateinit var storeOfObjectTypes: StoreOfObjectTypes
@Mock private lateinit var storelessSubscriptionContainer: StorelessSubscriptionContainer
@Mock private lateinit var spaceSyncAndP2PStatusProvider: SpaceSyncAndP2PStatusProvider
@Mock lateinit var createObject: CreateObject
@Mock lateinit var setObjectListIsArchived: SetObjectListIsArchived
@Mock lateinit var fieldParser: FieldParser
@Mock lateinit var getDateObjectByTimestamp: GetDateObjectByTimestamp
private lateinit var storeOfRelations: StoreOfRelations
private val spaceId = SpaceId("testSpaceId")
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
storelessSubscriptionContainer = mock(verboseLogging = true)
setupDefaultMocks()
storeOfRelations = DefaultStoreOfRelations()
}
private fun setupDefaultMocks() {
`when`(userPermissionProvider.get(space = spaceId)).thenReturn(SpaceMemberPermissions.OWNER)
`when`(userPermissionProvider.observe(space = spaceId)).thenReturn(flowOf(SpaceMemberPermissions.OWNER))
}
private fun createGetObjectParams(objectId: String): GetObject.Params {
return GetObject.Params(
target = objectId,
space = spaceId,
saveAsLastOpened = true
)
}
private fun createRelationListWithValueParams(objectId: String): GetObjectRelationListById.Params {
return GetObjectRelationListById.Params(
space = spaceId,
value = objectId
)
}
private suspend fun mockGetObjectSuccess(objectId: String, stubObjectView: ObjectView) {
val params = createGetObjectParams(objectId)
whenever(getObject.async(params)).thenReturn(Resultat.success(stubObjectView))
}
private suspend fun mockRelationListWithValueSuccess(objectId: String, list: List<RelationListWithValueItem>) {
val params = createRelationListWithValueParams(objectId)
whenever(relationListWithValue.async(params)).thenReturn(Resultat.success(list))
}
@Test
fun `should call getObjects and getRelationListWithValue on init`() = runTest {
// Arrange
val objectId = "testObjectId-${RandomString.make()}"
val stubObjectView = StubObjectView(root = objectId)
mockGetObjectSuccess(objectId, stubObjectView)
mockRelationListWithValueSuccess(objectId, emptyList())
// Act
getViewModel(objectId = objectId, spaceId = spaceId)
advanceUntilIdle()
// Assert
verifyBlocking(getObject, times(1)) { async(createGetObjectParams(objectId)) }
verifyBlocking(relationListWithValue, times(1)) { async(createRelationListWithValueParams(objectId)) }
}
@Test
fun `should call getObjects and getRelationListWithValue again after the dateObjectId was updated `() =
runTest {
// Arrange
val objectId = "testObjectId1-${RandomString.make()}"
val nextObjectId = "nextObjectId-${RandomString.make()}"
val stubObjectView = StubObjectView(root = objectId)
mockGetObjectSuccess(objectId, stubObjectView)
mockRelationListWithValueSuccess(objectId, emptyList())
val vm = getViewModel(objectId = objectId, spaceId = spaceId)
// Act
advanceUntilIdle()
// Assert
verifyBlocking(getObject, times(1)) { async(createGetObjectParams(objectId)) }
verifyBlocking(relationListWithValue, times(1)) { async(createRelationListWithValueParams(objectId)) }
// Arrange for next call
val tomorrowTimestamp = 211L
whenever(dateProvider.getTimestampForTomorrowAtStartOfDay())
.thenReturn(tomorrowTimestamp)
val params = GetDateObjectByTimestamp.Params(
timestampInSeconds = tomorrowTimestamp,
space = spaceId
)
whenever(getDateObjectByTimestamp.async(params)).thenReturn(
Resultat.success(
mapOf(
Relations.ID to nextObjectId
)
)
)
mockGetObjectSuccess(nextObjectId, stubObjectView)
mockRelationListWithValueSuccess(nextObjectId, emptyList())
// Act
vm.onDateEvent(DateEvent.Calendar.OnTomorrowClick)
advanceUntilIdle()
// Assert
verifyBlocking(getObject, times(1)) { async(createGetObjectParams(nextObjectId)) }
verifyBlocking(relationListWithValue, times(1)) { async(createRelationListWithValueParams(nextObjectId)) }
}
@Test
fun `should properly filter date object relation links`() =
runTest {
// Arrange
val fieldKey = RelationKey("key1-${RandomString.make()}")
val fieldKey2 = RelationKey("key2-${RandomString.make()}")
val fieldKey3 = RelationKey("key3-${RandomString.make()}")
val relationsListWithValues = listOf(
RelationListWithValueItem(
key = RelationKey(Relations.LINKS),
counter = 2L
),
RelationListWithValueItem(
key = fieldKey3,
counter = 13L
),
RelationListWithValueItem(
key = RelationKey(Relations.MENTIONS),
counter = 5L
),
RelationListWithValueItem(
key = RelationKey(Relations.BACKLINKS),
counter = 5L
),
RelationListWithValueItem(
key = fieldKey, //hidden
counter = 9L
),
RelationListWithValueItem(
key = fieldKey2,
counter = 1L
),
RelationListWithValueItem(
key = RelationKey(RandomString.make()), // not present in the store
counter = 1L
),
)
whenever(dateProvider.formatTimestampToDateAndTime(123 * 1000))
.thenReturn("01-01-2024" to "12:00")
whenever(dateProvider.calculateRelativeDates(123))
.thenReturn(
RelativeDate.Other(
initialTimeInMillis = 123 * 1000,
dayOfWeek = DayOfWeekCustom.MONDAY,
formattedDate = "01-01-2024",
formattedTime = "12:00"
)
)
val objectId = "testObjectId1-${RandomString.make()}"
val stubObjectView = StubObjectView(
root = objectId,
details = mapOf(
objectId to mapOf(
Relations.TIMESTAMP to 123.0
)
)
)
mockGetObjectSuccess(objectId, stubObjectView)
mockRelationListWithValueSuccess(objectId, relationsListWithValues)
val vm = getViewModel(objectId = objectId, spaceId = spaceId)
val subscribeParams = vm.createSearchParams(
dateId = objectId,
timestamp = 123,
space = spaceId,
itemsLimit = DEFAULT_SEARCH_LIMIT,
field = ActiveField(
key = RelationKey(Relations.MENTIONS),
format = RelationFormat.OBJECT
)
)
whenever(storelessSubscriptionContainer.subscribe(subscribeParams))
.thenReturn(emptyFlow<List<ObjectWrapper.Basic>>())
// Act
advanceUntilIdle()
// Assert
verifyNoInteractions(storelessSubscriptionContainer)
// Arrange, list of relations in store
val relationObjects = listOf(
StubRelationObject(
key = fieldKey.key,
name = "Hidden field",
format = RelationFormat.OBJECT,
isHidden = true
),
StubRelationObject(
key = fieldKey2.key,
name = "Some date relations 1",
format = RelationFormat.DATE
),
StubRelationObject(
key = Relations.LINKS,
name = "Links",
format = RelationFormat.OBJECT
),
StubRelationObject(
key = Relations.BACKLINKS,
name = "Backlinks",
format = RelationFormat.OBJECT
),
StubRelationObject(
key = Relations.MENTIONS,
name = "Mentions",
format = RelationFormat.OBJECT
),
StubRelationObject(
key = fieldKey3.key,
name = "Some date relations 2",
format = RelationFormat.DATE
),
)
// Act, store of relations get updated
storeOfRelations.merge(relationObjects)
advanceUntilIdle()
// Assert
verifyNoInteractions(storelessSubscriptionContainer)
// Act
vm.onStart()
advanceUntilIdle()
// Assert
verifyBlocking(storelessSubscriptionContainer, times(1)) {
subscribe(subscribeParams)
}
}
@Test
fun `should not start the subscription if the store was not updated`() =
runTest {
// Arrange
val objectId = "testObjectId1-${RandomString.make()}"
val stubObjectView = StubObjectView(
root = objectId,
details = mapOf(
objectId to mapOf(
Relations.TIMESTAMP to 123.0
)
)
)
val relationsListWithValues = listOf(
RelationListWithValueItem(
key = RelationKey(Relations.MENTIONS),
counter = 5L
)
)
mockGetObjectSuccess(objectId, stubObjectView)
mockRelationListWithValueSuccess(objectId, relationsListWithValues)
whenever(dateProvider.formatTimestampToDateAndTime(123 * 1000))
.thenReturn("01-01-2024" to "12:00")
whenever(dateProvider.calculateRelativeDates(123))
.thenReturn(
RelativeDate.Other(
initialTimeInMillis = 123 * 1000,
dayOfWeek = DayOfWeekCustom.MONDAY,
formattedDate = "01-01-2024",
formattedTime = "12:00"
)
)
val vm = getViewModel(objectId = objectId, spaceId = spaceId)
val subscribeParams = vm.createSearchParams(
dateId = objectId,
timestamp = 123,
space = spaceId,
itemsLimit = DEFAULT_SEARCH_LIMIT,
field = ActiveField(
key = RelationKey(Relations.MENTIONS),
format = RelationFormat.OBJECT
)
)
whenever(storelessSubscriptionContainer.subscribe(subscribeParams))
.thenReturn(emptyFlow<List<ObjectWrapper.Basic>>())
// Act waiting for the init to finish
advanceUntilIdle()
// Act 2 waiting for the start to finish
vm.onStart()
advanceUntilIdle()
// Assert
verifyNoInteractions(storelessSubscriptionContainer)
}
@Test
fun `should start the subscription if the store was updated before init`() =
runTest {
// Arrange
val objectId = "testObjectId1-${RandomString.make()}"
val stubObjectView = StubObjectView(
root = objectId,
details = mapOf(
objectId to mapOf(
Relations.TIMESTAMP to 123.0
)
)
)
val relationsListWithValues = listOf(
RelationListWithValueItem(
key = RelationKey(Relations.MENTIONS),
counter = 5L
)
)
mockGetObjectSuccess(objectId, stubObjectView)
mockRelationListWithValueSuccess(objectId, relationsListWithValues)
whenever(dateProvider.formatTimestampToDateAndTime(123 * 1000))
.thenReturn("01-01-2024" to "12:00")
whenever(dateProvider.calculateRelativeDates(123))
.thenReturn(
RelativeDate.Other(
initialTimeInMillis = 123 * 1000,
dayOfWeek = DayOfWeekCustom.MONDAY,
formattedDate = "01-01-2024",
formattedTime = "12:00"
)
)
// Arrange, list of relations in store
val relationObjects = listOf(
StubRelationObject(
key = Relations.MENTIONS,
name = "Mentions",
format = RelationFormat.OBJECT
)
)
// Act, store of relations get updated
storeOfRelations.merge(relationObjects)
advanceUntilIdle()
val vm = getViewModel(objectId = objectId, spaceId = spaceId)
val subscribeParams = vm.createSearchParams(
dateId = objectId,
timestamp = 123,
space = spaceId,
itemsLimit = DEFAULT_SEARCH_LIMIT,
field = ActiveField(
key = RelationKey(Relations.MENTIONS),
format = RelationFormat.OBJECT
)
)
whenever(storelessSubscriptionContainer.subscribe(subscribeParams))
.thenReturn(emptyFlow<List<ObjectWrapper.Basic>>())
// Act waiting for the init to finish
advanceUntilIdle()
// Act 2 waiting for the start to finish
vm.onStart()
advanceUntilIdle()
// Assert
verifyBlocking(storelessSubscriptionContainer, times(1)) {
subscribe(subscribeParams)
}
}
@Test
fun `should start the subscription if the store was updated before onStart`() =
runTest {
// Arrange
val objectId = "testObjectId1-${RandomString.make()}"
val stubObjectView = StubObjectView(
root = objectId,
details = mapOf(
objectId to mapOf(
Relations.TIMESTAMP to 123.0
)
)
)
val relationsListWithValues = listOf(
RelationListWithValueItem(
key = RelationKey(Relations.MENTIONS),
counter = 5L
)
)
mockGetObjectSuccess(objectId, stubObjectView)
mockRelationListWithValueSuccess(objectId, relationsListWithValues)
whenever(dateProvider.formatTimestampToDateAndTime(123 * 1000))
.thenReturn("01-01-2024" to "12:00")
whenever(dateProvider.calculateRelativeDates(123))
.thenReturn(
RelativeDate.Other(
initialTimeInMillis = 123 * 1000,
dayOfWeek = DayOfWeekCustom.MONDAY,
formattedDate = "01-01-2024",
formattedTime = "12:00"
)
)
val vm = getViewModel(objectId = objectId, spaceId = spaceId)
val subscribeParams = vm.createSearchParams(
dateId = objectId,
timestamp = 123,
space = spaceId,
itemsLimit = DEFAULT_SEARCH_LIMIT,
field = ActiveField(
key = RelationKey(Relations.MENTIONS),
format = RelationFormat.OBJECT
)
)
whenever(storelessSubscriptionContainer.subscribe(subscribeParams))
.thenReturn(emptyFlow<List<ObjectWrapper.Basic>>())
// Act waiting for the init to finish
advanceUntilIdle()
// Assert
verifyNoInteractions(storelessSubscriptionContainer)
// Arrange, list of relations in store
val relationObjects = listOf(
StubRelationObject(
key = Relations.MENTIONS,
name = "Mentions",
format = RelationFormat.OBJECT
)
)
// Act, store of relations get updated
storeOfRelations.merge(relationObjects)
advanceUntilIdle()
// Act 2 waiting for the start to finish
vm.onStart()
advanceUntilIdle()
// Assert
verifyBlocking(storelessSubscriptionContainer, times(1)) {
subscribe(subscribeParams)
}
}
@Test
fun `should start the subscription if the store was updated after onStart`() =
runTest {
// Arrange
val objectId = "testObjectId1-${RandomString.make()}"
val stubObjectView = StubObjectView(
root = objectId,
details = mapOf(
objectId to mapOf(
Relations.TIMESTAMP to 123.0
)
)
)
val relationsListWithValues = listOf(
RelationListWithValueItem(
key = RelationKey(Relations.MENTIONS),
counter = 5L
)
)
mockGetObjectSuccess(objectId, stubObjectView)
mockRelationListWithValueSuccess(objectId, relationsListWithValues)
whenever(dateProvider.formatTimestampToDateAndTime(123 * 1000))
.thenReturn("01-01-2024" to "12:00")
whenever(dateProvider.calculateRelativeDates(123))
.thenReturn(
RelativeDate.Other(
initialTimeInMillis = 123 * 1000,
dayOfWeek = DayOfWeekCustom.MONDAY,
formattedDate = "01-01-2024",
formattedTime = "12:00"
)
)
val vm = getViewModel(objectId = objectId, spaceId = spaceId)
val subscribeParams = vm.createSearchParams(
dateId = objectId,
timestamp = 123,
space = spaceId,
itemsLimit = DEFAULT_SEARCH_LIMIT,
field = ActiveField(
key = RelationKey(Relations.MENTIONS),
format = RelationFormat.OBJECT
)
)
whenever(storelessSubscriptionContainer.subscribe(subscribeParams))
.thenReturn(emptyFlow<List<ObjectWrapper.Basic>>())
// Act waiting for the init to finish
advanceUntilIdle()
// Act 2 waiting for the start to finish
vm.onStart()
advanceUntilIdle()
// Assert
verifyNoInteractions(storelessSubscriptionContainer)
// Arrange, list of relations in store
val relationObjects = listOf(
StubRelationObject(
key = Relations.MENTIONS,
name = "Mentions",
format = RelationFormat.OBJECT
)
)
// Act, store of relations get updated
storeOfRelations.merge(relationObjects)
advanceUntilIdle()
// Assert
verifyBlocking(storelessSubscriptionContainer, times(1)) {
subscribe(subscribeParams)
}
}
private fun getViewModel(objectId: Id, spaceId: SpaceId): DateObjectViewModel {
val vmParams = DateObjectVmParams(
objectId = objectId,
spaceId = spaceId
)
return DateObjectViewModel(
getObject = getObject,
analytics = analytics,
urlBuilder = urlBuilder,
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate,
userPermissionProvider = userPermissionProvider,
getObjectRelationListById = relationListWithValue,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
storelessSubscriptionContainer = storelessSubscriptionContainer,
spaceSyncAndP2PStatusProvider = spaceSyncAndP2PStatusProvider,
fieldParser = fieldParser,
vmParams = vmParams,
dateProvider = dateProvider,
createObject = createObject,
setObjectListIsArchived = setObjectListIsArchived,
getDateObjectByTimestamp = getDateObjectByTimestamp
)
}
}

View file

@ -0,0 +1,30 @@
package com.anytypeio.anytype.feature_date
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description
@ExperimentalCoroutinesApi
class DefaultCoroutineTestRule(
val dispatcher: TestDispatcher = StandardTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
fun advanceTime(millis: Long = 100L) {
dispatcher.scheduler.advanceTimeBy(millis)
}
fun advanceUntilIdle() = dispatcher.scheduler.advanceUntilIdle()
}