mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-933 Widgets | Enhancement | React to changes in order and number of widgets (#2887)
This commit is contained in:
parent
1abdf1a3b0
commit
779b10cc2e
10 changed files with 510 additions and 34 deletions
|
@ -2,6 +2,7 @@ package com.anytypeio.anytype.di.feature.home
|
|||
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.anytypeio.anytype.analytics.base.Analytics
|
||||
import com.anytypeio.anytype.core_models.Payload
|
||||
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
|
||||
import com.anytypeio.anytype.di.common.ComponentDependencies
|
||||
import com.anytypeio.anytype.di.feature.widgets.SelectWidgetSourceSubcomponent
|
||||
|
@ -9,6 +10,8 @@ import com.anytypeio.anytype.domain.auth.repo.AuthRepository
|
|||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.domain.config.ConfigStorage
|
||||
import com.anytypeio.anytype.domain.event.interactor.EventChannel
|
||||
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.`object`.OpenObject
|
||||
import com.anytypeio.anytype.domain.objects.ObjectStore
|
||||
|
@ -102,6 +105,19 @@ object HomeScreenModule {
|
|||
@PerScreen
|
||||
fun widgetEventDispatcher() : Dispatcher<WidgetDispatchEvent> = Dispatcher.Default()
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun objectPayloadDispatcher() : Dispatcher<Payload> = Dispatcher.Default()
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun interceptEvents(channel: EventChannel) : InterceptEvents = InterceptEvents(
|
||||
context = Dispatchers.IO,
|
||||
channel = channel
|
||||
)
|
||||
|
||||
@Module
|
||||
interface Declarations {
|
||||
@PerScreen
|
||||
|
@ -119,4 +135,5 @@ interface HomeScreenDependencies : ComponentDependencies {
|
|||
fun subscriptionEventChannel(): SubscriptionEventChannel
|
||||
fun workspaceManager(): WorkspaceManager
|
||||
fun analytics(): Analytics
|
||||
fun eventChannel() : EventChannel
|
||||
}
|
|
@ -15,6 +15,7 @@ import androidx.transition.ChangeBounds
|
|||
import androidx.transition.TransitionManager
|
||||
import androidx.transition.TransitionSet
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.anytypeio.anytype.BuildConfig
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
import com.anytypeio.anytype.core_ui.reactive.click
|
||||
|
@ -35,9 +36,9 @@ import com.anytypeio.anytype.ui.base.NavigationFragment
|
|||
import com.anytypeio.anytype.ui.editor.EditorFragment
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class DashboardFragment :
|
||||
NavigationFragment<FragmentDashboardBinding>(R.layout.fragment_dashboard) {
|
||||
|
@ -317,6 +318,16 @@ class DashboardFragment :
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
// For debugging and testing widgets
|
||||
binding.widgets.visible()
|
||||
binding.widgets.setOnClickListener {
|
||||
findNavController().navigate(
|
||||
R.id.homeScreen
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateSelectionHiding() {
|
||||
|
|
|
@ -6,6 +6,21 @@
|
|||
android:layout_height="match_parent"
|
||||
app:layoutDescription="@xml/fragment_dashboard_scene">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widgets"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:visibility="visible"
|
||||
android:text="[Go to Widgets]"
|
||||
android:stateListAnimator="@animator/scale_shrink"
|
||||
android:textSize="10sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="monospace"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ivSettings"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivSettings"
|
||||
app:layout_constraintTop_toTopOf="@+id/ivSettings" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvGreeting"
|
||||
android:layout_width="0dp"
|
||||
|
@ -214,12 +229,12 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:background="@drawable/default_ripple"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:gravity="center"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:text="@string/fragment_dashboard_restore"
|
||||
android:background="@drawable/default_ripple"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="17sp" />
|
||||
|
||||
|
|
|
@ -69,6 +69,16 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:visibilityMode="ignore" />
|
||||
<Constraint
|
||||
android:id="@+id/widgets"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="Widgets"
|
||||
app:visibilityMode="ignore"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ivSettings"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivSettings"
|
||||
app:layout_constraintTop_toTopOf="@+id/ivSettings"/>
|
||||
</ConstraintSet>
|
||||
|
||||
<ConstraintSet android:id="@+id/end">
|
||||
|
@ -138,6 +148,16 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:visibilityMode="ignore" />
|
||||
<Constraint
|
||||
android:id="@+id/widgets"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="Widgets"
|
||||
app:visibilityMode="ignore"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/ivSettings"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivSettings"
|
||||
app:layout_constraintTop_toTopOf="@+id/ivSettings"/>
|
||||
</ConstraintSet>
|
||||
|
||||
<Transition
|
||||
|
|
|
@ -197,7 +197,8 @@ data class Block(
|
|||
val iconSize: IconSize,
|
||||
val cardStyle: CardStyle,
|
||||
val description: Description,
|
||||
val relations: Set<Relation>,
|
||||
@Deprecated("To be removed")
|
||||
val relations: Set<Relation> = emptySet(),
|
||||
) : Content() {
|
||||
sealed interface Relation {
|
||||
object COVER : Relation
|
||||
|
|
|
@ -3,11 +3,16 @@ package com.anytypeio.anytype.presentation.home
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ObjectView
|
||||
import com.anytypeio.anytype.core_models.Payload
|
||||
import com.anytypeio.anytype.core_utils.ext.replace
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.Resultat
|
||||
import com.anytypeio.anytype.domain.config.ConfigStorage
|
||||
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
|
||||
import com.anytypeio.anytype.domain.misc.Reducer
|
||||
import com.anytypeio.anytype.domain.`object`.OpenObject
|
||||
import com.anytypeio.anytype.domain.search.ObjectSearchSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.widgets.CreateWidget
|
||||
|
@ -21,12 +26,14 @@ import com.anytypeio.anytype.presentation.widgets.WidgetDispatchEvent
|
|||
import com.anytypeio.anytype.presentation.widgets.WidgetView
|
||||
import com.anytypeio.anytype.presentation.widgets.parseWidgets
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.scan
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
|
@ -36,24 +43,61 @@ class HomeScreenViewModel(
|
|||
private val createWidget: CreateWidget,
|
||||
private val objectSearchSubscriptionContainer: ObjectSearchSubscriptionContainer,
|
||||
private val appCoroutineDispatchers: AppCoroutineDispatchers,
|
||||
private val dispatcher: Dispatcher<WidgetDispatchEvent>
|
||||
) : BaseViewModel() {
|
||||
private val widgetEventDispatcher: Dispatcher<WidgetDispatchEvent>,
|
||||
private val objectPayloadDispatcher: Dispatcher<Payload>,
|
||||
private val interceptEvents: InterceptEvents
|
||||
) : BaseViewModel(), Reducer<ObjectView, Payload> {
|
||||
|
||||
val obj = MutableSharedFlow<ObjectView>()
|
||||
val views = MutableStateFlow<List<WidgetView>>(emptyList())
|
||||
|
||||
private val objectViewState = MutableStateFlow<ObjectViewState>(ObjectViewState.Idle)
|
||||
private val widgets = MutableStateFlow<List<Widget>>(emptyList())
|
||||
private val containers = MutableStateFlow<List<TreeWidgetContainer>>(emptyList())
|
||||
private val expanded = TreeWidgetBranchStateHolder()
|
||||
|
||||
init {
|
||||
|
||||
val config = configStorage.get()
|
||||
|
||||
val externalChannelEvents = interceptEvents.build(InterceptEvents.Params(config.widgets)).map {
|
||||
Payload(
|
||||
context = config.widgets,
|
||||
events = it
|
||||
)
|
||||
}
|
||||
|
||||
val internalChannelEvents = objectPayloadDispatcher.flow()
|
||||
|
||||
val payloads = merge(externalChannelEvents, internalChannelEvents)
|
||||
|
||||
viewModelScope.launch {
|
||||
obj.map { o ->
|
||||
o.blocks.parseWidgets(
|
||||
root = o.root,
|
||||
details = o.details
|
||||
)
|
||||
objectViewState.flatMapLatest { state ->
|
||||
when (state) {
|
||||
is ObjectViewState.Idle -> flowOf(state)
|
||||
is ObjectViewState.Failure -> flowOf(state)
|
||||
is ObjectViewState.Loading -> flowOf(state)
|
||||
is ObjectViewState.Success -> {
|
||||
payloads.scan(state) { s, p -> s.copy(obj = reduce(s.obj, p)) }
|
||||
}
|
||||
}
|
||||
}.map { state ->
|
||||
when (state) {
|
||||
is ObjectViewState.Failure -> {
|
||||
emptyList()
|
||||
}
|
||||
is ObjectViewState.Idle -> {
|
||||
emptyList()
|
||||
}
|
||||
is ObjectViewState.Loading -> {
|
||||
emptyList()
|
||||
}
|
||||
is ObjectViewState.Success -> {
|
||||
state.obj.blocks.parseWidgets(
|
||||
root = state.obj.root,
|
||||
details = state.obj.details
|
||||
)
|
||||
}
|
||||
}
|
||||
}.collect {
|
||||
widgets.value = it
|
||||
}
|
||||
|
@ -87,32 +131,29 @@ class HomeScreenViewModel(
|
|||
}
|
||||
}.flowOn(appCoroutineDispatchers.io).collect {
|
||||
Timber.d("Views update: $it")
|
||||
views.value = it + listOf(
|
||||
WidgetView.Action.EditWidgets,
|
||||
WidgetView.Action.CreateWidget,
|
||||
WidgetView.Action.Refresh
|
||||
)
|
||||
views.value = it + actions
|
||||
}
|
||||
}
|
||||
|
||||
proceedWithOpeningObject()
|
||||
proceedWithOpeningWidgetObject(widgetObject = config.widgets)
|
||||
proceedWithDispatches()
|
||||
}
|
||||
|
||||
private fun proceedWithOpeningObject() {
|
||||
private fun proceedWithOpeningWidgetObject(widgetObject: Id) {
|
||||
viewModelScope.launch {
|
||||
val config = configStorage.get()
|
||||
openObject(config.widgets).flowOn(appCoroutineDispatchers.io).collect { result ->
|
||||
openObject(widgetObject).flowOn(appCoroutineDispatchers.io).collect { result ->
|
||||
when (result) {
|
||||
is Resultat.Failure -> {
|
||||
objectViewState.value = ObjectViewState.Failure(result.exception)
|
||||
Timber.e(result.exception, "Error while opening object.")
|
||||
}
|
||||
is Resultat.Loading -> {
|
||||
// Do nothing.
|
||||
objectViewState.value = ObjectViewState.Loading
|
||||
}
|
||||
is Resultat.Success -> {
|
||||
Timber.d("Object view on start:\n${result.value}")
|
||||
obj.emit(result.value)
|
||||
objectViewState.value = ObjectViewState.Success(
|
||||
obj = result.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +162,7 @@ class HomeScreenViewModel(
|
|||
|
||||
private fun proceedWithDispatches() {
|
||||
viewModelScope.launch {
|
||||
dispatcher.flow().collect { dispatch ->
|
||||
widgetEventDispatcher.flow().collect { dispatch ->
|
||||
when (dispatch) {
|
||||
is WidgetDispatchEvent.SourcePicked -> {
|
||||
proceedWithCreatingWidget(source = dispatch.source)
|
||||
|
@ -139,15 +180,27 @@ class HomeScreenViewModel(
|
|||
ctx = config.widgets,
|
||||
source = source
|
||||
)
|
||||
).collect { s ->
|
||||
Timber.d("Status while creating widget: $s")
|
||||
).collect { status ->
|
||||
Timber.d("Status while creating widget: $status")
|
||||
when (status) {
|
||||
is Resultat.Failure -> {
|
||||
sendToast("Error while creating widget: ${status.exception}")
|
||||
Timber.e(status.exception, "Error while creating widget")
|
||||
}
|
||||
is Resultat.Loading -> {
|
||||
// Do nothing?
|
||||
}
|
||||
is Resultat.Success -> {
|
||||
objectPayloadDispatcher.send(status.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("For debugging only")
|
||||
fun onRefresh() {
|
||||
proceedWithOpeningObject()
|
||||
proceedWithOpeningWidgetObject(widgetObject = configStorage.get().widgets)
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
|
@ -158,13 +211,43 @@ class HomeScreenViewModel(
|
|||
expanded.onExpand(linkPath = path)
|
||||
}
|
||||
|
||||
// TODO move to a separate reducer inject into this VM's constructor
|
||||
override fun reduce(state: ObjectView, event: Payload): ObjectView {
|
||||
var curr = state
|
||||
event.events.forEach { e ->
|
||||
when (e) {
|
||||
is Event.Command.AddBlock -> {
|
||||
curr = curr.copy(
|
||||
blocks = curr.blocks + e.blocks
|
||||
)
|
||||
}
|
||||
is Event.Command.UpdateStructure -> {
|
||||
curr = curr.copy(
|
||||
blocks = curr.blocks.replace(
|
||||
replacement = { target ->
|
||||
target.copy(children = e.children)
|
||||
},
|
||||
target = { block -> block.id == e.id }
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
Timber.d("Skipping event: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
return curr
|
||||
}
|
||||
|
||||
class Factory @Inject constructor(
|
||||
private val configStorage: ConfigStorage,
|
||||
private val openObject: OpenObject,
|
||||
private val createWidget: CreateWidget,
|
||||
private val objectSearchSubscriptionContainer: ObjectSearchSubscriptionContainer,
|
||||
private val appCoroutineDispatchers: AppCoroutineDispatchers,
|
||||
private val dispatcher: Dispatcher<WidgetDispatchEvent>
|
||||
private val widgetEventDispatcher: Dispatcher<WidgetDispatchEvent>,
|
||||
private val objectPayloadDispatcher: Dispatcher<Payload>,
|
||||
private val interceptEvents: InterceptEvents
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = HomeScreenViewModel(
|
||||
|
@ -173,7 +256,27 @@ class HomeScreenViewModel(
|
|||
createWidget = createWidget,
|
||||
objectSearchSubscriptionContainer = objectSearchSubscriptionContainer,
|
||||
appCoroutineDispatchers = appCoroutineDispatchers,
|
||||
dispatcher = dispatcher
|
||||
widgetEventDispatcher = widgetEventDispatcher,
|
||||
objectPayloadDispatcher = objectPayloadDispatcher,
|
||||
interceptEvents = interceptEvents
|
||||
) as T
|
||||
}
|
||||
|
||||
companion object {
|
||||
val actions = listOf(
|
||||
WidgetView.Action.EditWidgets,
|
||||
WidgetView.Action.CreateWidget,
|
||||
WidgetView.Action.Refresh
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State representing session while working with an object.
|
||||
*/
|
||||
sealed class ObjectViewState {
|
||||
object Idle : ObjectViewState()
|
||||
object Loading : ObjectViewState()
|
||||
data class Success(val obj: ObjectView) : ObjectViewState()
|
||||
data class Failure(val e: Throwable) : ObjectViewState()
|
||||
}
|
|
@ -1,5 +1,256 @@
|
|||
package com.anytypeio.anytype.presentation.home
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.anytypeio.anytype.core_models.Block
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.ObjectView
|
||||
import com.anytypeio.anytype.core_models.Payload
|
||||
import com.anytypeio.anytype.core_models.SmartBlockType
|
||||
import com.anytypeio.anytype.core_models.StubConfig
|
||||
import com.anytypeio.anytype.core_models.StubLinkToObjectBlock
|
||||
import com.anytypeio.anytype.core_models.StubObject
|
||||
import com.anytypeio.anytype.core_models.StubObjectView
|
||||
import com.anytypeio.anytype.core_models.StubSmartBlock
|
||||
import com.anytypeio.anytype.core_models.StubWidgetBlock
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.Resultat
|
||||
import com.anytypeio.anytype.domain.config.ConfigStorage
|
||||
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
|
||||
import com.anytypeio.anytype.domain.`object`.OpenObject
|
||||
import com.anytypeio.anytype.domain.search.ObjectSearchSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.widgets.CreateWidget
|
||||
import com.anytypeio.anytype.presentation.util.DefaultCoroutineTestRule
|
||||
import com.anytypeio.anytype.presentation.util.Dispatcher
|
||||
import com.anytypeio.anytype.presentation.widgets.TreeWidgetContainer
|
||||
import com.anytypeio.anytype.presentation.widgets.WidgetDispatchEvent
|
||||
import com.anytypeio.anytype.presentation.widgets.WidgetView
|
||||
import com.anytypeio.anytype.test_utils.MockDataFactory
|
||||
import kotlin.test.assertEquals
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.stub
|
||||
import org.mockito.kotlin.times
|
||||
import org.mockito.kotlin.verify
|
||||
|
||||
class HomeViewModelTest {
|
||||
// TODO
|
||||
|
||||
@get:Rule
|
||||
val coroutineTestRule = DefaultCoroutineTestRule()
|
||||
|
||||
@Mock
|
||||
lateinit var configStorage: ConfigStorage
|
||||
|
||||
@Mock
|
||||
lateinit var createWidget: CreateWidget
|
||||
|
||||
@Mock
|
||||
lateinit var interceptEvents: InterceptEvents
|
||||
|
||||
@Mock
|
||||
lateinit var openObject: OpenObject
|
||||
|
||||
@Mock
|
||||
lateinit var objectSearchSubscriptionContainer: ObjectSearchSubscriptionContainer
|
||||
|
||||
private val objectPayloadDispatcher = Dispatcher.Default<Payload>()
|
||||
private val widgetEventDispatcher = Dispatcher.Default<WidgetDispatchEvent>()
|
||||
|
||||
lateinit var vm: HomeScreenViewModel
|
||||
|
||||
private val config = StubConfig(
|
||||
widgets = WIDGET_OBJECT_ID
|
||||
)
|
||||
|
||||
private val appCoroutineDispatchers = AppCoroutineDispatchers(
|
||||
io = coroutineTestRule.dispatcher,
|
||||
main = coroutineTestRule.dispatcher,
|
||||
computation = coroutineTestRule.dispatcher
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.openMocks(this)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit empty list if there is no block`() = runTest {
|
||||
|
||||
// SETUP
|
||||
|
||||
val smartBlock = StubSmartBlock(
|
||||
id = WIDGET_OBJECT_ID,
|
||||
children = emptyList(),
|
||||
type = SmartBlockType.WIDGET
|
||||
)
|
||||
|
||||
val givenObjectView = StubObjectView(
|
||||
root = WIDGET_OBJECT_ID,
|
||||
type = SmartBlockType.WIDGET,
|
||||
blocks = listOf(smartBlock)
|
||||
)
|
||||
|
||||
val events : Flow<List<Event>> = emptyFlow()
|
||||
|
||||
stubConfig()
|
||||
stubInterceptEvents(events)
|
||||
stubOpenObject(givenObjectView)
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.views.test {
|
||||
val firstTimeState = awaitItem()
|
||||
assertEquals(
|
||||
actual = firstTimeState,
|
||||
expected = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
delay(1)
|
||||
|
||||
verify(openObject, times(1)).invoke(WIDGET_OBJECT_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit tree-widget with empty elements when source has no links`() = runTest {
|
||||
|
||||
// SETUP
|
||||
|
||||
val sourceObject = StubObject(
|
||||
id = "SOURCE OBJECT",
|
||||
links = emptyList()
|
||||
)
|
||||
|
||||
val sourceLink = StubLinkToObjectBlock(
|
||||
id = "SOURCE LINK",
|
||||
target = sourceObject.id
|
||||
)
|
||||
|
||||
val widgetBlock = StubWidgetBlock(
|
||||
id = "WIDGET BLOCK",
|
||||
layout = Block.Content.Widget.Layout.TREE,
|
||||
children = listOf(sourceLink.id)
|
||||
)
|
||||
|
||||
val smartBlock = StubSmartBlock(
|
||||
id = WIDGET_OBJECT_ID,
|
||||
children = listOf(widgetBlock.id),
|
||||
type = SmartBlockType.WIDGET
|
||||
)
|
||||
|
||||
val givenObjectView = StubObjectView(
|
||||
root = WIDGET_OBJECT_ID,
|
||||
type = SmartBlockType.WIDGET,
|
||||
blocks = listOf(
|
||||
smartBlock,
|
||||
widgetBlock,
|
||||
sourceLink
|
||||
),
|
||||
details = mapOf(
|
||||
sourceObject.id to sourceObject.map
|
||||
)
|
||||
)
|
||||
|
||||
stubConfig()
|
||||
stubInterceptEvents(events = emptyFlow())
|
||||
stubOpenObject(givenObjectView)
|
||||
stubObjectSearchContainer(
|
||||
subscription = widgetBlock.id,
|
||||
targets = emptyList()
|
||||
)
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
// TESTING
|
||||
|
||||
vm.views.test {
|
||||
val firstTimeState = awaitItem()
|
||||
assertEquals(
|
||||
actual = firstTimeState,
|
||||
expected = emptyList()
|
||||
)
|
||||
val secondTimeItem = awaitItem()
|
||||
assertEquals(
|
||||
expected = buildList {
|
||||
add(
|
||||
WidgetView.Tree(
|
||||
id = widgetBlock.id,
|
||||
obj = sourceObject,
|
||||
elements = emptyList()
|
||||
)
|
||||
)
|
||||
addAll(HomeScreenViewModel.actions)
|
||||
},
|
||||
actual = secondTimeItem
|
||||
)
|
||||
verify(openObject, times(1)).invoke(WIDGET_OBJECT_ID)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stubInterceptEvents(events: Flow<List<Event>>) {
|
||||
interceptEvents.stub {
|
||||
on { build(InterceptEvents.Params(WIDGET_OBJECT_ID)) } doReturn events
|
||||
}
|
||||
}
|
||||
|
||||
private fun stubConfig() {
|
||||
configStorage.stub {
|
||||
on { get() } doReturn config
|
||||
}
|
||||
}
|
||||
|
||||
private fun stubOpenObject(givenObjectView: ObjectView) {
|
||||
openObject.stub {
|
||||
on {
|
||||
invoke(WIDGET_OBJECT_ID)
|
||||
} doReturn flowOf(
|
||||
Resultat.Success(
|
||||
value = givenObjectView
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stubObjectSearchContainer(
|
||||
subscription: Id,
|
||||
targets: List<Id>,
|
||||
keys: List<Key> = TreeWidgetContainer.keys
|
||||
) {
|
||||
objectSearchSubscriptionContainer.stub {
|
||||
onBlocking {
|
||||
get(
|
||||
subscription = subscription,
|
||||
keys = keys,
|
||||
targets = targets
|
||||
)
|
||||
} doReturn emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildViewModel() = HomeScreenViewModel(
|
||||
configStorage = configStorage,
|
||||
interceptEvents = interceptEvents,
|
||||
createWidget = createWidget,
|
||||
objectPayloadDispatcher = objectPayloadDispatcher,
|
||||
widgetEventDispatcher = widgetEventDispatcher,
|
||||
openObject = openObject,
|
||||
objectSearchSubscriptionContainer = objectSearchSubscriptionContainer,
|
||||
appCoroutineDispatchers = appCoroutineDispatchers
|
||||
)
|
||||
|
||||
companion object {
|
||||
val WIDGET_OBJECT_ID: Id = MockDataFactory.randomUuid()
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ import org.junit.runner.Description
|
|||
@ExperimentalCoroutinesApi
|
||||
class DefaultCoroutineTestRule() : TestWatcher() {
|
||||
|
||||
private val dispatcher = StandardTestDispatcher(name = "Default test dispatcher")
|
||||
val dispatcher = StandardTestDispatcher(name = "Default test dispatcher")
|
||||
|
||||
override fun starting(description: Description) {
|
||||
super.starting(description)
|
||||
|
|
|
@ -234,12 +234,13 @@ fun StubBookmark(
|
|||
|
||||
fun StubSmartBlock(
|
||||
id: Id = MockDataFactory.randomString(),
|
||||
children: List<Id> = emptyList()
|
||||
children: List<Id> = emptyList(),
|
||||
type: SmartBlockType = SmartBlockType.PAGE
|
||||
): Block = Block(
|
||||
id = id,
|
||||
children = children,
|
||||
fields = Block.Fields.empty(),
|
||||
content = Block.Content.Smart()
|
||||
content = Block.Content.Smart(type = type)
|
||||
)
|
||||
|
||||
fun StubTable(
|
||||
|
@ -337,4 +338,41 @@ fun StubDataViewBlock(
|
|||
children = children,
|
||||
fields = fields,
|
||||
backgroundColor = backgroundColor
|
||||
)
|
||||
|
||||
fun StubLinkToObjectBlock(
|
||||
id: Id = MockDataFactory.randomUuid(),
|
||||
target: Id,
|
||||
children: List<Id> = emptyList(),
|
||||
fields: Block.Fields = Block.Fields.empty(),
|
||||
backgroundColor: String? = null,
|
||||
) : Block = Block(
|
||||
id = id,
|
||||
content = Block.Content.Link(
|
||||
target = target,
|
||||
description = Block.Content.Link.Description.values().random(),
|
||||
cardStyle = Block.Content.Link.CardStyle.values().random(),
|
||||
iconSize = Block.Content.Link.IconSize.values().random(),
|
||||
relations = emptySet(),
|
||||
type = Block.Content.Link.Type.PAGE
|
||||
),
|
||||
children = children,
|
||||
fields = fields,
|
||||
backgroundColor = backgroundColor
|
||||
)
|
||||
|
||||
fun StubWidgetBlock(
|
||||
id: Id = MockDataFactory.randomUuid(),
|
||||
layout: Block.Content.Widget.Layout,
|
||||
children: List<Id> = emptyList(),
|
||||
fields: Block.Fields = Block.Fields.empty(),
|
||||
backgroundColor: String? = null,
|
||||
) : Block = Block(
|
||||
id = id,
|
||||
children = children,
|
||||
fields = fields,
|
||||
backgroundColor = backgroundColor,
|
||||
content = Block.Content.Widget(
|
||||
layout = layout
|
||||
)
|
||||
)
|
|
@ -1,5 +1,7 @@
|
|||
package com.anytypeio.anytype.core_models
|
||||
|
||||
import com.anytypeio.anytype.core_models.restrictions.DataViewRestrictions
|
||||
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
|
||||
import com.anytypeio.anytype.test_utils.MockDataFactory
|
||||
|
||||
fun StubObject(
|
||||
|
@ -32,6 +34,24 @@ fun StubObject(
|
|||
)
|
||||
)
|
||||
|
||||
fun StubObjectView(
|
||||
root: Id,
|
||||
type: SmartBlockType,
|
||||
blocks: List<Block> = emptyList(),
|
||||
details: Map<Id, Struct> = emptyMap(),
|
||||
relations: List<RelationLink> = emptyList(),
|
||||
objectRestrictions: List<ObjectRestriction> = emptyList(),
|
||||
dataViewRestrictions: List<DataViewRestrictions> = emptyList()
|
||||
): ObjectView = ObjectView(
|
||||
root = root,
|
||||
blocks = blocks,
|
||||
details = details,
|
||||
type = type,
|
||||
relations = relations,
|
||||
objectRestrictions = objectRestrictions,
|
||||
dataViewRestrictions = dataViewRestrictions
|
||||
)
|
||||
|
||||
fun StubObjectType(
|
||||
id: String = MockDataFactory.randomUuid(),
|
||||
name: String = MockDataFactory.randomString(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue