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

Refact | Navigation in sets (#1817)

This commit is contained in:
Evgenii Kozlov 2021-09-20 18:59:11 +03:00 committed by GitHub
parent 07e83b6ae8
commit 79a5a2f600
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 617 additions and 355 deletions

View file

@ -292,8 +292,6 @@ open class EditorTestSetup {
interceptThreadStatus = interceptThreadStatus,
analytics = analytics,
dispatcher = Dispatcher.Default(),
setDocCoverImage = setDocCoverImage,
removeDocCover = removeDocCover,
detailModificationManager = InternalDetailModificationManager(stores.details),
updateDetail = updateDetail,
getCompatibleObjectTypes = getCompatibleObjectTypes,

View file

@ -7,6 +7,7 @@ import android.widget.LinearLayout
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_utils.ext.gone
@ -18,7 +19,7 @@ import timber.log.Timber
class ViewerGridAdapter(
private val onCellClicked: (CellView) -> Unit,
private val onObjectHeaderClicked: (String, String?) -> Unit
private val onObjectHeaderClicked: (Id) -> Unit
) : ListAdapter<Viewer.GridView.Row, ViewerGridAdapter.RecordHolder>(GridDiffUtil) {
var recordNamePositionX = 0f
@ -40,7 +41,7 @@ class ViewerGridAdapter(
val pos = bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) {
val item = getItem(pos)
onObjectHeaderClicked(item.id, item.type)
onObjectHeaderClicked(item.id)
}
}
}

View file

@ -49,6 +49,7 @@ ext {
junit_version = '4.12'
kluent_version = '1.14'
timber_junit = '1.0.1'
turbine_version = '0.6.1'
coroutine_testing_version = '1.4.3'
live_data_testing_version = '1.2.0'
mockito_kotlin_version = '3.2.0'
@ -147,7 +148,8 @@ ext {
coroutineTesting: "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_testing_version",
assertj: "com.squareup.assertj:assertj-android:1.0.0",
androidXTestCore: "androidx.test:core:$androidx_test_core_version",
timberJUnit: "net.lachlanmckee:timber-junit-rule:$timber_junit"
timberJUnit: "net.lachlanmckee:timber-junit-rule:$timber_junit",
turbine: "app.cash.turbine:turbine:$turbine_version"
]
acceptanceTesting = [

View file

@ -18,5 +18,5 @@ class InterceptThreadStatus(
return channel.observe(params.ctx).flowOn(context)
}
class Params(val ctx: Id)
data class Params(val ctx: Id)
}

View file

@ -88,4 +88,5 @@ dependencies {
testImplementation unitTestDependencies.androidXTestCore
testImplementation unitTestDependencies.robolectricLatest
testImplementation unitTestDependencies.timberJUnit
testImplementation unitTestDependencies.turbine
}

View file

@ -30,7 +30,10 @@ import com.anytypeio.anytype.presentation.editor.model.TextUpdate
import com.anytypeio.anytype.presentation.mapper.toDomain
import com.anytypeio.anytype.presentation.navigation.AppNavigation
import com.anytypeio.anytype.presentation.navigation.SupportNavigation
import com.anytypeio.anytype.presentation.relations.*
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig
import com.anytypeio.anytype.presentation.relations.render
import com.anytypeio.anytype.presentation.relations.tabs
import com.anytypeio.anytype.presentation.relations.title
import com.anytypeio.anytype.presentation.sets.model.*
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.Job
@ -368,8 +371,8 @@ class ObjectSetViewModel(
val block = state.dataview
val dv = block.content as DV
val viewer = dv.viewers.find { it.id == session.currentViewerId }?.id
?: dv.viewers.first().id
val viewer =
dv.viewers.find { it.id == session.currentViewerId }?.id ?: dv.viewers.first().id
if (dv.isRelationReadOnly(relationKey = cell.key)) {
val relation = dv.relations.first { it.key == cell.key }
@ -377,10 +380,7 @@ class ObjectSetViewModel(
// TODO terrible workaround, which must be removed in the future!
if (cell is CellView.Object && cell.objects.isNotEmpty()) {
val obj = cell.objects.first()
onObjectClicked(
id = obj.id,
types = obj.types
)
onRelationObjectClicked(target = obj.id)
return
} else {
toast(NOT_ALLOWED_CELL)
@ -451,58 +451,40 @@ class ObjectSetViewModel(
}
}
fun onObjectClicked(id: Id, types: List<String>?) {
Timber.d("onObjectClicked, id:[$id], type:[$types]")
if (types.isNullOrEmpty()) {
Timber.e("onObjectClicked, types is null or empty, layout type unknown")
toast(OBJECT_TYPE_UNKNOWN)
return
}
val targetType = reducer.state.value.objectTypes.getObjectTypeById(types)
if (targetType != null) {
when (targetType.layout) {
ObjectType.Layout.BASIC, ObjectType.Layout.PROFILE -> {
navigate(EventWrapper(AppNavigation.Command.OpenObject(id)))
}
ObjectType.Layout.SET -> {
viewModelScope.launch {
navigate(EventWrapper(AppNavigation.Command.OpenObjectSet(id)))
}
}
else -> Timber.d("Unexpected layout: ${targetType.layout}")
}
} else {
Timber.e("onObjectClicked, types is null or empty, layout type unknown")
toast(OBJECT_TYPE_UNKNOWN)
/**
* @param [target] Object is a dependent object, therefore we look for data in details.
*/
private fun onRelationObjectClicked(target: Id) {
Timber.d("onCellObjectClicked, id:[$target]")
val set = reducer.state.value
if (set.isInitialized) {
val obj = ObjectWrapper.Basic(set.details[target]?.map ?: emptyMap())
proceedWithNavigation(
target = target,
layout = obj.layout
)
}
}
fun onObjectHeaderClicked(id: Id, type: String?) {
Timber.d("onObjectHeaderClicked, id:[$id], type:[$type]")
/**
* @param [target] object is a record contained in this set.
*/
fun onObjectHeaderClicked(target: Id) {
Timber.d("onObjectHeaderClicked, id:[$target]")
val set = reducer.state.value
val objectType = set.objectTypes.find { it.url == type }
if (objectType == null) {
toast("Object type not found: $type")
return
}
when (objectType.layout) {
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.TODO,
ObjectType.Layout.IMAGE,
ObjectType.Layout.FILE -> {
navigate(
EventWrapper(
AppNavigation.Command.OpenObject(
id = id
)
)
if (set.isInitialized) {
val viewer = session.currentViewerId ?: set.viewers.first().id
val records = reducer.state.value.viewerDb[viewer] ?: return
val record = records.records.find { rec -> rec[Relations.ID] == target }
if (record != null) {
val obj = ObjectWrapper.Basic(record)
proceedWithNavigation(
target = target,
layout = obj.layout
)
} else {
toast("Record not found. Please, try again later.")
}
else -> toast("Routing not implemented for this object type: $objectType")
}
}
@ -909,6 +891,49 @@ class ObjectSetViewModel(
//endregion
//region NAVIGATION
private fun proceedWithNavigation(target: Id, layout: ObjectType.Layout?) {
when (layout) {
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.TODO,
ObjectType.Layout.IMAGE,
ObjectType.Layout.FILE -> {
viewModelScope.launch {
closeBlock(CloseBlock.Params(context)).process(
success = {
navigate(EventWrapper(AppNavigation.Command.OpenObject(id = target)))
},
failure = {
Timber.e(it, "Error while closing object set: $context")
navigate(EventWrapper(AppNavigation.Command.OpenObject(id = target)))
}
)
}
}
ObjectType.Layout.SET -> {
viewModelScope.launch {
closeBlock(CloseBlock.Params(context)).process(
success = {
navigate(EventWrapper(AppNavigation.Command.OpenObjectSet(target)))
},
failure = {
Timber.e(it, "Error while closing object set: $context")
navigate(EventWrapper(AppNavigation.Command.OpenObjectSet(target)))
}
)
}
}
else -> {
toast("Unexpected layout: $layout")
Timber.e("Unexpected layout: $layout")
}
}
}
//endregion NAVIGATION
override fun onCleared() {
super.onCleared()
titleUpdateChannel.cancel()

View file

@ -6,7 +6,6 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.core_models.*
import com.anytypeio.anytype.core_models.restrictions.DataViewRestriction
import com.anytypeio.anytype.core_models.restrictions.DataViewRestrictions
import com.anytypeio.anytype.presentation.navigation.AppNavigation
import com.anytypeio.anytype.presentation.relations.ObjectSetConfig
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
import com.anytypeio.anytype.presentation.sets.model.CellView
@ -44,7 +43,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
@After
fun after() {
coroutineTestRule.advanceTime(100)
coroutineTestRule.advanceTime(ObjectSetViewModel.TITLE_CHANNEL_DISPATCH_DELAY)
}
val title = Block(
@ -124,7 +123,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
val initialRecords = listOf(firstRecord, secondRecord)
val viewer1 = DVViewer(
private val viewer1 = DVViewer(
id = MockDataFactory.randomUuid(),
filters = emptyList(),
sorts = emptyList(),
@ -133,7 +132,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
viewerRelations = vrelations
)
val viewer2 = DVViewer(
private val viewer2 = DVViewer(
id = MockDataFactory.randomUuid(),
filters = emptyList(),
sorts = emptyList(),
@ -161,7 +160,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
@Test
fun `should show error toast when clicked on read only cell`() = runBlocking {
val dvRestrictions = listOf<DataViewRestrictions>(
val dvRestrictions = listOf(
DataViewRestrictions(
block = dv.id,
restrictions = listOf(DataViewRestriction.VIEWS)
@ -169,6 +168,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
)
stubInterceptEvents()
stubInterceptThreadStatus()
stubOpenObjectSet(
doc = listOf(
header,
@ -190,7 +190,7 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
val cell = CellView.Description(
id = firstRecordId,
key = relations[1].key,
text = firstRecord.get(relations[1].key).toString()
text = firstRecord[relations[1].key].toString()
)
vm.onGridCellClicked(cell = cell)
@ -199,294 +199,4 @@ class ObjectSetCellTest : ObjectSetViewModelTestSetup() {
assertEquals(ObjectSetViewModel.NOT_ALLOWED_CELL, result)
}
@Test
fun `should show error toast on open readonly object as page 1`() = runBlocking {
val objectTypePage = "_otpage"
val objectTypeImage = "_otimage"
val objectTypeSet = "_otset"
val objectTypes = listOf(
ObjectType(
url = objectTypePage,
name = "Page",
relations = listOf(),
layout = ObjectType.Layout.BASIC,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.PAGE),
isHidden = false,
description = "page",
isArchived = false,
isReadOnly = false
),
ObjectType(
url = objectTypeImage,
name = "Image",
relations = listOf(),
layout = ObjectType.Layout.IMAGE,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.FILE),
isHidden = false,
description = "image",
isArchived = false,
isReadOnly = false
),
ObjectType(
url = objectTypeSet,
name = "Set",
relations = listOf(),
layout = ObjectType.Layout.SET,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.SET),
isHidden = false,
description = "set",
isArchived = false,
isReadOnly = false
)
)
stubInterceptEvents()
stubOpenObjectSet(
doc = listOf(
header,
title,
dv
),
objectTypes = objectTypes
)
stubSetActiveViewer()
stubUpdateDataViewViewer()
val vm = buildViewModel()
// TESTING
vm.onStart(root)
vm.onObjectClicked(id = "dsadas", types = listOf())
val result = vm.toasts.stream().first()
assertEquals(ObjectSetViewModel.OBJECT_TYPE_UNKNOWN, result)
}
@Test
fun `should show error toast on open readonly object as page 2`() = runBlocking {
val objectTypePage = "_otpage"
val objectTypeImage = "_otimage"
val objectTypeSet = "_otset"
val objectTypes = listOf(
ObjectType(
url = objectTypePage,
name = "Page",
relations = listOf(),
layout = ObjectType.Layout.BASIC,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.PAGE),
isHidden = false,
description = "page",
isArchived = false,
isReadOnly = false
),
ObjectType(
url = objectTypeImage,
name = "Image",
relations = listOf(),
layout = ObjectType.Layout.IMAGE,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.FILE),
isHidden = false,
description = "image",
isArchived = false,
isReadOnly = false
),
ObjectType(
url = objectTypeSet,
name = "Set",
relations = listOf(),
layout = ObjectType.Layout.SET,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.SET),
isHidden = false,
description = "set",
isArchived = false,
isReadOnly = false
)
)
stubInterceptEvents()
stubOpenObjectSet(
doc = listOf(
header,
title,
dv
),
objectTypes = objectTypes
)
stubSetActiveViewer()
stubUpdateDataViewViewer()
val vm = buildViewModel()
// TESTING
vm.onStart(root)
vm.onObjectClicked(id = "dsadas", types = listOf("p6Oa"))
val result = vm.toasts.stream().first()
assertEquals(ObjectSetViewModel.OBJECT_TYPE_UNKNOWN, result)
}
@Test
fun `should proceed with open page when open readonly object as page`() = runBlocking {
val objectTypePage = "_otpage"
val objectTypeImage = "_otimage"
val objectTypeSet = "_otset"
val objectId = "UnI9A1"
val objectTypes = listOf(
ObjectType(
url = objectTypePage,
name = "Page",
relations = listOf(),
layout = ObjectType.Layout.BASIC,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.PAGE),
isHidden = false,
description = "page",
isArchived = false,
isReadOnly = false
),
ObjectType(
url = objectTypeImage,
name = "Image",
relations = listOf(),
layout = ObjectType.Layout.IMAGE,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.FILE),
isHidden = false,
description = "image",
isArchived = false,
isReadOnly = false
),
ObjectType(
url = objectTypeSet,
name = "Set",
relations = listOf(),
layout = ObjectType.Layout.SET,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.SET),
isHidden = false,
description = "set",
isArchived = false,
isReadOnly = false
)
)
stubInterceptEvents()
stubOpenObjectSet(
doc = listOf(
header,
title,
dv
),
objectTypes = objectTypes
)
stubSetActiveViewer()
stubUpdateDataViewViewer()
val vm = buildViewModel()
// TESTING
vm.onStart(root)
vm.onObjectClicked(id = objectId, types = listOf(objectTypePage))
val result = vm.navigation.value!!.peekContent()
assertEquals(AppNavigation.Command.OpenObject(id = objectId), result)
}
@Test
fun `should proceed with open set when open readonly object as set`() = runBlocking {
val objectTypePage = "_otpage"
val objectTypeImage = "_otimage"
val objectTypeSet = "_otset"
val objectId = "UnI9A1"
val objectTypes = listOf(
ObjectType(
url = objectTypePage,
name = "Page",
relations = listOf(),
layout = ObjectType.Layout.BASIC,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.PAGE),
isHidden = false,
description = "page",
isArchived = false,
isReadOnly = false
),
ObjectType(
url = objectTypeImage,
name = "Image",
relations = listOf(),
layout = ObjectType.Layout.IMAGE,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.FILE),
isHidden = false,
description = "image",
isArchived = false,
isReadOnly = false
),
ObjectType(
url = objectTypeSet,
name = "Set",
relations = listOf(),
layout = ObjectType.Layout.SET,
emoji = "",
smartBlockTypes = listOf(SmartBlockType.SET),
isHidden = false,
description = "set",
isArchived = false,
isReadOnly = false
)
)
stubInterceptEvents()
stubOpenObjectSet(
doc = listOf(
header,
title,
dv
),
objectTypes = objectTypes
)
stubSetActiveViewer()
stubUpdateDataViewViewer()
val vm = buildViewModel()
// TESTING
vm.onStart(root)
vm.onObjectClicked(id = objectId, types = listOf(objectTypeSet, objectTypeImage))
val result = vm.navigation.value!!.peekContent()
assertEquals(AppNavigation.Command.OpenObjectSet(target = objectId), result)
}
}

View file

@ -0,0 +1,510 @@
package com.anytypeio.anytype.presentation.sets.main
import MockDataFactory
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import app.cash.turbine.test
import com.anytypeio.anytype.core_models.*
import com.anytypeio.anytype.core_models.ext.content
import com.anytypeio.anytype.domain.page.CloseBlock
import com.anytypeio.anytype.presentation.navigation.AppNavigation
import com.anytypeio.anytype.presentation.objects.SupportedLayouts
import com.anytypeio.anytype.presentation.sets.ObjectSetCommand
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
import com.anytypeio.anytype.presentation.sets.model.Viewer
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.jraska.livedata.test
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.times
import org.mockito.kotlin.verifyBlocking
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.time.ExperimentalTime
class ObjectSetNavigationTest : ObjectSetViewModelTestSetup() {
@get:Rule
val rule = InstantTaskExecutorRule()
@get:Rule
val coroutineTestRule = CoroutinesTestRule()
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
}
@After
fun after() {
coroutineTestRule.advanceTime(ObjectSetViewModel.TITLE_CHANNEL_DISPATCH_DELAY)
}
private val title = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Text(
style = Block.Content.Text.Style.TITLE,
text = MockDataFactory.randomString(),
marks = emptyList()
),
children = emptyList(),
fields = Block.Fields.empty()
)
private val header = Block(
id = MockDataFactory.randomUuid(),
content = Block.Content.Layout(
type = Block.Content.Layout.Type.HEADER
),
fields = Block.Fields.empty(),
children = listOf(title.id)
)
private val linkedProjectRelation = Relation(
key = MockDataFactory.randomString(),
name = "Linked objects",
source = Relation.Source.values().random(),
defaultValue = null,
format = Relation.Format.OBJECT,
isHidden = false,
isMulti = true,
isReadOnly = false,
selections = emptyList()
)
private val objectRelations = listOf(linkedProjectRelation)
val viewerRelations = objectRelations.map { relation ->
DVViewerRelation(
key = relation.key,
isVisible = true
)
}
private val viewer = DVViewer(
id = MockDataFactory.randomUuid(),
filters = emptyList(),
sorts = emptyList(),
type = Block.Content.DataView.Viewer.Type.GRID,
name = MockDataFactory.randomString(),
viewerRelations = viewerRelations
)
private val dv = Block(
id = MockDataFactory.randomUuid(),
content = DV(
source = MockDataFactory.randomString(),
relations = objectRelations,
viewers = listOf(viewer)
),
children = emptyList(),
fields = Block.Fields.empty()
)
@ExperimentalTime
@Test
fun `should emit navigation command for editing relation-object cell`() {
// SETUP
val linkedProjectTargetId = "Linked project ID"
val firstRecordId = "First record ID"
val record = mapOf(
Relations.ID to firstRecordId,
linkedProjectRelation.key to linkedProjectTargetId
)
stubInterceptEvents()
stubInterceptThreadStatus()
stubSetActiveViewer()
stubOpenObjectSet(
doc = listOf(
header,
title,
dv
),
dataViewRestrictions = emptyList(),
additionalEvents = listOf(
Event.Command.DataView.SetRecords(
records = listOf(record),
view = viewer.id,
id = dv.id,
total = 1,
context = root
)
)
)
val vm = buildViewModel()
vm.onStart(root)
// TESTING
val state = vm.viewerGrid.value
assertIs<Viewer.GridView>(state)
assertEquals(
expected = 1,
actual = state.rows.size
)
// Clicking on cell with linked projects.
runBlocking {
vm.commands.test {
vm.onGridCellClicked(state.rows.first().cells.last())
assertEquals(
awaitItem(),
ObjectSetCommand.Modal.EditRelationCell(
ctx = root,
dataview = dv.id,
target = firstRecordId,
viewer = viewer.id,
relation = linkedProjectRelation.key
)
)
cancelAndConsumeRemainingEvents()
}
}
}
@Test
fun `should emit navigation command for opening an object contained in given relation if this relation is read-only and object's layout is supported`() {
// SETUP
val supportedObjectLayouts = listOf(
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.TODO,
ObjectType.Layout.IMAGE,
ObjectType.Layout.FILE
)
val linkedProjectTargetId = "Linked project ID"
val firstRecordId = "First record ID"
val record = mapOf(
Relations.ID to firstRecordId,
linkedProjectRelation.key to linkedProjectTargetId
)
val details = Block.Details(
details = mapOf(
linkedProjectTargetId to Block.Fields(
mapOf(
Relations.ID to linkedProjectTargetId,
Relations.LAYOUT to supportedObjectLayouts.random().code.toDouble()
)
)
)
)
stubInterceptEvents()
stubInterceptThreadStatus()
stubSetActiveViewer()
stubCloseBlock()
stubOpenObjectSet(
doc = listOf(
header,
title,
dv.copy(
content = dv.content<DV>().copy(
relations = listOf(
linkedProjectRelation.copy(
isReadOnly = true
)
)
)
)
),
dataViewRestrictions = emptyList(),
additionalEvents = listOf(
Event.Command.DataView.SetRecords(
records = listOf(record),
view = viewer.id,
id = dv.id,
total = 1,
context = root
)
),
details = details
)
val vm = buildViewModel()
vm.onStart(root)
// TESTING
val state = vm.viewerGrid.value
assertIs<Viewer.GridView>(state)
assertEquals(
expected = 1,
actual = state.rows.size
)
// Clicking on cell with linked projects.
val testObserver = vm.navigation.test()
vm.onGridCellClicked(state.rows.first().cells.last())
testObserver.assertValue { value ->
val content = value.peekContent()
content == AppNavigation.Command.OpenObject(linkedProjectTargetId)
}
}
@Test
fun `should close current object before navitating to some other object`() {
// SETUP
val linkedProjectTargetId = "Linked project ID"
val firstRecordId = "First record ID"
val record = mapOf(
Relations.ID to firstRecordId,
linkedProjectRelation.key to linkedProjectTargetId
)
val details = Block.Details(
details = mapOf(
linkedProjectTargetId to Block.Fields(
mapOf(
Relations.ID to linkedProjectTargetId,
Relations.LAYOUT to SupportedLayouts.layouts.random().code.toDouble()
)
)
)
)
stubInterceptEvents()
stubInterceptThreadStatus()
stubSetActiveViewer()
stubCloseBlock()
stubOpenObjectSet(
doc = listOf(
header,
title,
dv.copy(
content = dv.content<DV>().copy(
relations = listOf(
linkedProjectRelation.copy(
isReadOnly = true
)
)
)
)
),
dataViewRestrictions = emptyList(),
additionalEvents = listOf(
Event.Command.DataView.SetRecords(
records = listOf(record),
view = viewer.id,
id = dv.id,
total = 1,
context = root
)
),
details = details
)
val vm = buildViewModel()
vm.onStart(root)
// TESTING
val state = vm.viewerGrid.value
assertIs<Viewer.GridView>(state)
assertEquals(
expected = 1,
actual = state.rows.size
)
// Clicking on cell with linked projects.
vm.onGridCellClicked(state.rows.first().cells.last())
verifyBlocking(closeBlock, times(1)) {
invoke(
CloseBlock.Params(root)
)
}
}
@Test
fun `should not emit any navigation command for opening an object contained in given relation if object's layout is not supported`() {
// SETUP
val unsupportedLayouis = ObjectType.Layout.values().toList() - SupportedLayouts.layouts
val linkedProjectTargetId = "Linked project ID"
val firstRecordId = "First record ID"
val record = mapOf(
Relations.ID to firstRecordId,
linkedProjectRelation.key to linkedProjectTargetId
)
val details = Block.Details(
details = mapOf(
linkedProjectTargetId to Block.Fields(
mapOf(
Relations.ID to linkedProjectTargetId,
Relations.LAYOUT to unsupportedLayouis.random().code.toDouble()
)
)
)
)
stubInterceptEvents()
stubInterceptThreadStatus()
stubSetActiveViewer()
stubCloseBlock()
stubOpenObjectSet(
doc = listOf(
header,
title,
dv.copy(
content = dv.content<DV>().copy(
relations = listOf(
linkedProjectRelation.copy(
isReadOnly = true
)
)
)
)
),
dataViewRestrictions = emptyList(),
additionalEvents = listOf(
Event.Command.DataView.SetRecords(
records = listOf(record),
view = viewer.id,
id = dv.id,
total = 1,
context = root
)
),
details = details
)
val vm = buildViewModel()
vm.onStart(root)
// TESTING
val state = vm.viewerGrid.value
assertIs<Viewer.GridView>(state)
assertEquals(
expected = 1,
actual = state.rows.size
)
// Clicking on cell with linked projects.
val testObserver = vm.navigation.test()
vm.onGridCellClicked(state.rows.first().cells.last())
testObserver.assertNoValue()
}
@Test
fun `should emit navigation command opening an object set contained in given relation if this relation is read-only`() {
// SETUP
val linkedProjectTargetId = "Linked project ID"
val firstRecordId = "First record ID"
val record = mapOf(
Relations.ID to firstRecordId,
linkedProjectRelation.key to linkedProjectTargetId
)
val details = Block.Details(
details = mapOf(
linkedProjectTargetId to Block.Fields(
mapOf(
Relations.ID to linkedProjectTargetId,
Relations.LAYOUT to ObjectType.Layout.SET.code.toDouble()
)
)
)
)
stubInterceptEvents()
stubInterceptThreadStatus()
stubSetActiveViewer()
stubCloseBlock()
stubOpenObjectSet(
doc = listOf(
header,
title,
dv.copy(
content = dv.content<DV>().copy(
relations = listOf(
linkedProjectRelation.copy(
isReadOnly = true
)
)
)
)
),
dataViewRestrictions = emptyList(),
additionalEvents = listOf(
Event.Command.DataView.SetRecords(
records = listOf(record),
view = viewer.id,
id = dv.id,
total = 1,
context = root
)
),
details = details
)
val vm = buildViewModel()
vm.onStart(root)
// TESTING
val state = vm.viewerGrid.value
assertIs<Viewer.GridView>(state)
assertEquals(
expected = 1,
actual = state.rows.size
)
// Clicking on cell with linked projects.
val testObserver = vm.navigation.test()
vm.onGridCellClicked(state.rows.first().cells.last())
testObserver.assertValue { value ->
val content = value.peekContent()
content == AppNavigation.Command.OpenObjectSet(linkedProjectTargetId)
}
}
}

View file

@ -19,6 +19,7 @@ import com.anytypeio.anytype.presentation.sets.ObjectSetSession
import com.anytypeio.anytype.presentation.sets.ObjectSetViewModel
import com.anytypeio.anytype.presentation.util.Dispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import org.mockito.Mock
import org.mockito.kotlin.any
@ -101,6 +102,14 @@ open class ObjectSetViewModelTestSetup {
}
}
fun stubInterceptThreadStatus(
params: InterceptThreadStatus.Params = InterceptThreadStatus.Params(ctx = root)
) {
interceptThreadStatus.stub {
onBlocking { build(params) } doReturn emptyFlow()
}
}
fun stubOpenObjectSet(
doc: List<Block> = emptyList(),
details: Block.Details = Block.Details(),
@ -156,4 +165,10 @@ open class ObjectSetViewModelTestSetup {
)
}
}
fun stubCloseBlock() {
closeBlock.stub {
onBlocking { invoke(any()) } doReturn Either.Right(Unit)
}
}
}