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

DROID-789 Widgets | Enhancement | Icon logic for tree widget elements + tests (#2882)

This commit is contained in:
Evgenii Kozlov 2023-01-31 15:05:38 +01:00 committed by GitHub
parent 49730a39bd
commit 9c55d1a894
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 397 additions and 27 deletions

View file

@ -18,6 +18,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.presentation.widgets.TreePath
import com.anytypeio.anytype.presentation.widgets.WidgetView
@ -66,16 +67,27 @@ fun HomeScreen(
.width(20.dp)
.height(20.dp)
) {
if (element.hasChildren) {
Text(
text = ">",
modifier = Modifier.align(Alignment.Center)
)
} else {
Text(
text = "*",
modifier = Modifier.align(Alignment.Center)
)
when(val icon = element.icon) {
is WidgetView.Tree.Icon.Branch -> {
Text(
text = ">",
modifier = Modifier
.align(Alignment.Center)
.rotate(if (icon.isExpanded) 90f else 0f)
)
}
is WidgetView.Tree.Icon.Leaf -> {
Text(
text = "",
modifier = Modifier.align(Alignment.Center)
)
}
is WidgetView.Tree.Icon.Set -> {
Text(
text = "#",
modifier = Modifier.align(Alignment.Center)
)
}
}
}
Text(

View file

@ -113,7 +113,7 @@ class HomeScreenViewModel(
createWidget(
CreateWidget.Params(
ctx = config.widgets,
source = "bafybbsj5xhyf7yvaakfd5bdjqjowp7cjzi4cqxcunfy7ejf4apmowk6u"
source = "bafybaeju6nieodoldknjnadcsjc4ii4vdayn3wkuxm74g2nwtfjiravm"
)
).collect { s ->
Timber.d("Status while creating widget: $s")

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.presentation.widgets
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectTypeIds
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.domain.search.ObjectSearchSubscriptionContainer
@ -37,7 +38,7 @@ class TreeWidgetContainer(
links = widget.source.links,
level = ROOT_INDENT,
expanded = it,
path = widget.id + SEPARATOR + widget.source + SEPARATOR
path = widget.id + SEPARATOR + widget.source.id + SEPARATOR
)
)
}
@ -61,13 +62,15 @@ class TreeWidgetContainer(
val currentLinkPath = path + link
val isExpandable = level < MAX_INDENT
add(
/**
* // TODO Setup [WidgetView.Tree.Icon] here
*/
WidgetView.Tree.Element(
icon = resolveObjectIcon(
obj = obj,
isExpandable = isExpandable,
expanded = expanded,
currentLinkPath = currentLinkPath
),
indent = level,
obj = obj,
hasChildren = obj.links.isNotEmpty() && isExpandable,
path = path + link
)
)
@ -85,12 +88,25 @@ class TreeWidgetContainer(
}
}
private fun resolveObjectIcon(
obj: ObjectWrapper.Basic,
isExpandable: Boolean,
expanded: List<TreePath>,
currentLinkPath: String
) = when {
obj.type.contains(ObjectTypeIds.SET) -> WidgetView.Tree.Icon.Set
!isExpandable -> WidgetView.Tree.Icon.Leaf
obj.links.isEmpty() -> WidgetView.Tree.Icon.Leaf
else -> WidgetView.Tree.Icon.Branch(
isExpanded = expanded.contains(currentLinkPath)
)
}
companion object {
const val ROOT_INDENT = 0
const val MAX_INDENT = 3
const val SEPARATOR = "/"
private val keys = buildList {
val keys = buildList {
addAll(ObjectSearchConstants.defaultKeys)
add(Relations.LINKS)
}

View file

@ -11,16 +11,16 @@ sealed class WidgetView {
val elements: List<Element>
) : WidgetView() {
data class Element(
// TODO val icon: WidgetView.Tree.Icon
val icon: Icon,
val indent: Indent,
val obj: ObjectWrapper.Basic,
val hasChildren: Boolean,
val path: String
)
sealed class Icon {
data class Branch(val isExpanded: Boolean) : Icon()
object Leaf : Icon()
object Set : Icon()
}
}
}

View file

@ -1,5 +0,0 @@
package com.anytypeio.anytype.presentation.home
class HomeTreeManagerTest {
// TODO
}

View file

@ -0,0 +1,345 @@
package com.anytypeio.anytype.presentation.home
import app.cash.turbine.test
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.SearchResult
import com.anytypeio.anytype.core_models.StubObject
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.objects.ObjectStore
import com.anytypeio.anytype.domain.search.ObjectSearchSubscriptionContainer
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
import com.anytypeio.anytype.presentation.widgets.TreePath
import com.anytypeio.anytype.presentation.widgets.TreeWidgetContainer
import com.anytypeio.anytype.presentation.widgets.Widget
import com.anytypeio.anytype.presentation.widgets.WidgetView
import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlin.test.assertEquals
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
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.verifyBlocking
class TreeWidgetContainerTest {
@Mock
lateinit var repo: BlockRepository
@Mock
lateinit var subscriptionEventChannel: SubscriptionEventChannel
@Mock
lateinit var store: ObjectStore
lateinit var objectSearchSubscriptionContainer: ObjectSearchSubscriptionContainer
@Before
fun setup() {
MockitoAnnotations.openMocks(this)
objectSearchSubscriptionContainer = ObjectSearchSubscriptionContainer(
repo = repo,
channel = subscriptionEventChannel,
store = store,
dispatchers = AppCoroutineDispatchers(
io = StandardTestDispatcher(),
main = StandardTestDispatcher(),
computation = StandardTestDispatcher()
)
)
}
@Test
fun `should search for data for source object's links when no data is available about expanded branches`() =
runTest {
// SETUP
val link1 = StubObject()
val link2 = StubObject()
val link3 = StubObject()
val links = listOf(link1, link2, link3)
val source = StubObject(
links = links.map { it.id }
)
val widget = Widget.Tree(
id = MockDataFactory.randomUuid(),
source = source
)
val expanded = flowOf(emptyList<TreePath>())
val container = TreeWidgetContainer(
container = objectSearchSubscriptionContainer,
widget = widget,
expandedBranches = expanded
)
stubObjectSearch(
widget = widget,
targets = links.map { it.id },
results = links
)
// TESTING
container.view.test {
awaitItem()
awaitComplete()
verifyBlocking(
repo, times(1)
) {
searchObjectsByIdWithSubscription(
subscription = widget.id,
ids = links.map { it.id },
keys = TreeWidgetContainer.keys
)
}
}
}
@Test
fun `should search for data for source object links when it is expanded`() = runTest {
// SETUP
val link1 = StubObject(
id = "A",
links = listOf(
"A1",
"A2",
"A3"
)
)
val link2 = StubObject(id = "B")
val link3 = StubObject(id = "C")
val links = listOf(link1, link2, link3)
val source = StubObject(
id = "root",
links = links.map { it.id }
)
val widget = Widget.Tree(
id = "widget",
source = source
)
val expanded = flowOf(
emptyList(),
listOf(
widget.id + "/" + widget.source.id + "/" + link1.id
)
)
val container = TreeWidgetContainer(
container = objectSearchSubscriptionContainer,
widget = widget,
expandedBranches = expanded
)
stubObjectSearch(
widget = widget,
targets = links.map { it.id },
results = links
)
stubObjectSearch(
widget = widget,
targets = links.map { it.id } + link1.links,
results = emptyList()
)
// TESTING
container.view.test {
awaitItem()
verifyBlocking(
repo, times(1)
) {
searchObjectsByIdWithSubscription(
subscription = widget.id,
ids = links.map { it.id },
keys = TreeWidgetContainer.keys
)
}
awaitItem()
verifyBlocking(
repo, times(1)
) {
searchObjectsByIdWithSubscription(
subscription = widget.id,
ids = links.map { it.id },
keys = TreeWidgetContainer.keys
)
}
awaitComplete()
}
}
@Test
fun `should define correct indent level for children of children of source objects`() = runTest {
// SETUP
val linkA = StubObject(
id = "A",
links = listOf(
"A1",
"A2",
"A3"
)
)
val linkA1 = StubObject(id = "A1")
val linkA2 = StubObject(id = "A2")
val linkA3 = StubObject(id = "A3")
val linkB = StubObject(id = "B")
val linkC = StubObject(id = "C")
val sourceLinks = listOf(linkA, linkB, linkC)
val source = StubObject(
id = "root",
links = sourceLinks.map { it.id }
)
val widget = Widget.Tree(
id = "widget",
source = source
)
val expanded = flowOf(
emptyList(),
listOf(
widget.id + "/" + widget.source.id + "/" + linkA.id
)
)
val container = TreeWidgetContainer(
container = objectSearchSubscriptionContainer,
widget = widget,
expandedBranches = expanded
)
stubObjectSearch(
widget = widget,
targets = sourceLinks.map { it.id },
results = sourceLinks
)
stubObjectSearch(
widget = widget,
targets = sourceLinks.map { it.id } + linkA.links,
results = sourceLinks + listOf(linkA1, linkA2, linkA3)
)
// TESTING
container.view.test {
val firstTimeState = awaitItem()
val secondTimeState = awaitItem()
assertEquals(
expected = WidgetView.Tree(
id = widget.id,
obj = widget.source,
elements = listOf(
WidgetView.Tree.Element(
indent = 0,
obj = sourceLinks[0],
path = widget.id + "/" + widget.source.id + "/" + sourceLinks[0].id,
icon = WidgetView.Tree.Icon.Branch(isExpanded = false)
),
WidgetView.Tree.Element(
indent = 0,
obj = sourceLinks[1],
path = widget.id + "/" + widget.source.id + "/" + sourceLinks[1].id,
icon = WidgetView.Tree.Icon.Leaf
),
WidgetView.Tree.Element(
indent = 0,
obj = sourceLinks[2],
path = widget.id + "/" + widget.source.id + "/" + sourceLinks[2].id,
icon = WidgetView.Tree.Icon.Leaf
)
)
),
actual = firstTimeState
)
assertEquals(
expected = WidgetView.Tree(
id = widget.id,
obj = widget.source,
elements = listOf(
WidgetView.Tree.Element(
indent = 0,
obj = sourceLinks[0],
path = widget.id + "/" + widget.source.id + "/" + sourceLinks[0].id,
icon = WidgetView.Tree.Icon.Branch(isExpanded = true)
),
WidgetView.Tree.Element(
indent = 1,
obj = linkA1,
path = widget.id + "/" + widget.source.id + "/" + sourceLinks[0].id + "/" + linkA1.id,
icon = WidgetView.Tree.Icon.Leaf
),
WidgetView.Tree.Element(
indent = 1,
obj = linkA2,
path = widget.id + "/" + widget.source.id + "/" + sourceLinks[0].id + "/" + linkA2.id,
icon = WidgetView.Tree.Icon.Leaf
),
WidgetView.Tree.Element(
indent = 1,
obj = linkA3,
path = widget.id + "/" + widget.source.id + "/" + sourceLinks[0].id + "/" + linkA3.id,
icon = WidgetView.Tree.Icon.Leaf
),
WidgetView.Tree.Element(
indent = 0,
obj = sourceLinks[1],
path = widget.id + "/" + widget.source.id + "/" + sourceLinks[1].id,
icon = WidgetView.Tree.Icon.Leaf
),
WidgetView.Tree.Element(
indent = 0,
obj = sourceLinks[2],
path = widget.id + "/" + widget.source.id + "/" + sourceLinks[2].id,
icon = WidgetView.Tree.Icon.Leaf
)
)
),
actual = secondTimeState
)
awaitComplete()
}
}
private fun stubObjectSearch(
widget: Widget.Tree,
targets: List<Id>,
results: List<ObjectWrapper.Basic>,
) {
repo.stub {
onBlocking {
searchObjectsByIdWithSubscription(
subscription = widget.id,
ids = targets,
keys = TreeWidgetContainer.keys
)
} doReturn SearchResult(
results = results,
dependencies = emptyList()
)
}
}
}

View file

@ -13,7 +13,8 @@ fun StubObject(
description: String? = null,
iconEmoji: String? = null,
isReadOnly: Boolean? = null,
isHidden: Boolean? = null
isHidden: Boolean? = null,
links: List<Id> = emptyList()
): ObjectWrapper.Basic = ObjectWrapper.Basic(
map = mapOf(
Relations.ID to id,
@ -26,7 +27,8 @@ fun StubObject(
Relations.DESCRIPTION to description,
Relations.ICON_EMOJI to iconEmoji,
Relations.IS_READ_ONLY to isReadOnly,
Relations.IS_HIDDEN to isHidden
Relations.IS_HIDDEN to isHidden,
Relations.LINKS to links
)
)