DROID-1546 Editor | Fix | Link-to-object block should not crash when applying search-on-page highlights (#213)

This commit is contained in:
Konstantin Ivanov 2023-07-17 14:59:21 +02:00 committed by GitHub
parent 30eccc5bce
commit bf1fe10865
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 209 additions and 16 deletions

View File

@ -6,13 +6,10 @@ import android.text.style.LeadingMarginSpan
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.BuildConfig
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.common.SearchHighlightSpan
import com.anytypeio.anytype.core_ui.common.SearchTargetHighlightSpan
import com.anytypeio.anytype.core_ui.databinding.ItemBlockObjectLinkBinding
import com.anytypeio.anytype.core_ui.extensions.setBlockBackgroundColor
import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil
import com.anytypeio.anytype.core_ui.features.editor.BlockViewHolder
import com.anytypeio.anytype.core_ui.features.editor.EditorTouchProcessor
@ -22,7 +19,6 @@ import com.anytypeio.anytype.core_utils.ext.dimen
import com.anytypeio.anytype.core_utils.ext.gone
import com.anytypeio.anytype.core_utils.ext.removeSpans
import com.anytypeio.anytype.core_utils.ext.visible
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.presentation.editor.editor.listener.ListenerType
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView.Searchable.Field.Companion.DEFAULT_SEARCH_FIELD_KEY
@ -41,7 +37,7 @@ class LinkToObject(
private val untitled = itemView.resources.getString(R.string.untitled)
val objectIcon = binding.objectIconWidget
private val objectIconContainer = binding.iconObjectContainer
private val title = binding.text
val title = binding.text
private val description = binding.tvDescription
private val objectType = binding.tvObjectType
@ -63,8 +59,8 @@ class LinkToObject(
applyText(item)
applyDescription(item)
applyObjectType(item)
applySearchHighlight(item)
applyImageOrEmoji(item)
applySearchHighlight(item)
itemView.setOnClickListener { clicked(ListenerType.LinkToObject(item.id)) }
}
@ -89,7 +85,7 @@ class LinkToObject(
)
}
title.visible()
title.text = sb
title.setText(sb, TextView.BufferType.EDITABLE)
}
else -> {
val sb = SpannableString(name)
@ -101,7 +97,7 @@ class LinkToObject(
)
}
title.visible()
title.text = sb
title.setText(sb, TextView.BufferType.EDITABLE)
}
}
}
@ -181,9 +177,6 @@ class LinkToObject(
if (payload.isSelectionChanged) {
applySelectedState(item)
}
if (payload.isSearchHighlightChanged) {
applySearchHighlight(item)
}
if (payload.isObjectTitleChanged) {
applyText(item)
}
@ -196,6 +189,9 @@ class LinkToObject(
if (payload.isObjectTypeChanged) {
applyObjectType(item)
}
if (payload.isSearchHighlightChanged) {
applySearchHighlight(item)
}
}
}

View File

@ -35,7 +35,7 @@ class LinkToObjectArchive(
private val root = binding.root
private val untitled = itemView.resources.getString(R.string.untitled)
private val title = binding.pageTitle
val title = binding.pageTitle
override val editorTouchProcessor = EditorTouchProcessor(
fallback = { e -> itemView.onTouchEvent(e) }

View File

@ -77,12 +77,12 @@ abstract class LinkToObjectCard(
applyBackground(item.background)
applySearchHighlight(item)
applyImageOrEmoji(item)
applyObjectType(item)
applySearchHighlight(item)
itemView.setOnClickListener { clicked(ListenerType.LinkToObject(item.id)) }
}
@ -97,12 +97,12 @@ abstract class LinkToObjectCard(
name.isBlank() -> {
titleView.visible()
val sb = SpannableString(untitled)
titleView.setText(sb, TextView.BufferType.SPANNABLE)
titleView.setText(sb, TextView.BufferType.EDITABLE)
}
else -> {
titleView.visible()
val sb = SpannableString(name)
titleView.setText(sb, TextView.BufferType.SPANNABLE)
titleView.setText(sb, TextView.BufferType.EDITABLE)
}
}
}

View File

@ -0,0 +1,197 @@
package com.anytypeio.anytype.core_ui.features.editor.holders.other
import android.content.Context
import android.os.Build
import android.view.LayoutInflater
import android.widget.TextView
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.core.app.ApplicationProvider
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.common.SearchHighlightSpan
import com.anytypeio.anytype.core_ui.common.SearchTargetHighlightSpan
import com.anytypeio.anytype.core_ui.databinding.ItemBlockObjectLinkArchiveBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockObjectLinkBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockObjectLinkCardMediumIconBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockObjectLinkCardMediumIconCoverBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockObjectLinkCardSmallIconBinding
import com.anytypeio.anytype.core_ui.databinding.ItemBlockObjectLinkCardSmallIconCoverBinding
import com.anytypeio.anytype.presentation.editor.editor.model.BlockView
import com.anytypeio.anytype.presentation.objects.ObjectIcon
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.TestFragment
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.core.IsEqual.equalTo
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(
manifest = Config.NONE,
sdk = [Build.VERSION_CODES.P],
instrumentedPackages = [
"androidx.loader.content"
]
)
class LinkToObjectTest {
private val context: Context = ApplicationProvider.getApplicationContext()
lateinit var scenario: FragmentScenario<TestFragment>
private lateinit var linkToObject: LinkToObject
private lateinit var linkToObjectArchived: LinkToObjectArchive
private lateinit var linkToObjectCardSmallIcon: LinkToObjectCardSmallIcon
private lateinit var linkToObjectCardSmallIconCover: LinkToObjectCardSmallIconCover
private lateinit var linkToObjectCardMediumIcon: LinkToObjectCardMediumIcon
private lateinit var linkToObjectCardMediumIconCover: LinkToObjectCardMediumIconCover
private lateinit var item: BlockView.LinkToObject.Default.Text
private lateinit var itemArchived: BlockView.LinkToObject.Archived
private lateinit var itemCardSmallIcon: BlockView.LinkToObject.Default.Card.SmallIcon
private lateinit var itemCardSmallIconCover: BlockView.LinkToObject.Default.Card.SmallIconCover
private lateinit var itemCardMediumIcon: BlockView.LinkToObject.Default.Card.MediumIcon
private lateinit var itemCardMediumIconCover: BlockView.LinkToObject.Default.Card.MediumIconCover
@Before
fun setup() {
context.setTheme(R.style.Theme_MaterialComponents)
scenario = launchFragmentInContainer()
val layoutInflater = LayoutInflater.from(context)
linkToObject = LinkToObject(ItemBlockObjectLinkBinding.inflate(layoutInflater))
linkToObjectArchived = LinkToObjectArchive(ItemBlockObjectLinkArchiveBinding.inflate(layoutInflater))
linkToObjectCardSmallIcon = LinkToObjectCardSmallIcon(ItemBlockObjectLinkCardSmallIconBinding.inflate(layoutInflater))
linkToObjectCardSmallIconCover = LinkToObjectCardSmallIconCover(
ItemBlockObjectLinkCardSmallIconCoverBinding.inflate(layoutInflater))
linkToObjectCardMediumIcon = LinkToObjectCardMediumIcon(
ItemBlockObjectLinkCardMediumIconBinding.inflate(layoutInflater))
linkToObjectCardMediumIconCover = LinkToObjectCardMediumIconCover(
ItemBlockObjectLinkCardMediumIconCoverBinding.inflate(layoutInflater))
val id = MockDataFactory.randomUuid()
val searchField = BlockView.Searchable.Field(
highlights = listOf(IntRange(0, 4), IntRange(6, 10)),
target = IntRange(0, 4)
)
item = BlockView.LinkToObject.Default.Text(
id = id,
indent = MockDataFactory.randomInt(),
isSelected = false,
icon = ObjectIcon.None,
searchFields = listOf(searchField),
text = "Text1 Text2"
)
itemArchived = BlockView.LinkToObject.Archived(
id = id,
indent = MockDataFactory.randomInt(),
isSelected = false,
searchFields = listOf(searchField),
text = "Text1 Text2"
)
itemCardSmallIcon = BlockView.LinkToObject.Default.Card.SmallIcon(
id = id,
indent = MockDataFactory.randomInt(),
isSelected = false,
icon = ObjectIcon.None,
searchFields = listOf(searchField),
text = "Text1 Text2",
background = ThemeColor.BLUE,
isPreviousBlockMedia = false
)
itemCardSmallIconCover = BlockView.LinkToObject.Default.Card.SmallIconCover(
id = id,
indent = MockDataFactory.randomInt(),
isSelected = false,
icon = ObjectIcon.None,
searchFields = listOf(searchField),
text = "Text1 Text2",
background = ThemeColor.BLUE,
isPreviousBlockMedia = false,
cover = null
)
itemCardMediumIcon = BlockView.LinkToObject.Default.Card.MediumIcon(
id = id,
indent = MockDataFactory.randomInt(),
isSelected = false,
icon = ObjectIcon.None,
searchFields = listOf(searchField),
text = "Text1 Text2",
background = ThemeColor.BLUE,
isPreviousBlockMedia = false
)
itemCardMediumIconCover = BlockView.LinkToObject.Default.Card.MediumIconCover(
id = id,
indent = MockDataFactory.randomInt(),
isSelected = false,
icon = ObjectIcon.None,
searchFields = listOf(searchField),
text = "Text1 Text2",
background = ThemeColor.BLUE,
isPreviousBlockMedia = false,
cover = null
)
}
@Test
fun `test linkToObject block`() {
linkToObject.bind(item, {})
assertSearchSpans(linkToObject.title)
}
@Test
fun `test linkToObjectArchive block`() {
linkToObjectArchived.bind(itemArchived, {})
assertSearchSpans(linkToObjectArchived.title)
}
@Test
fun `test linkToObjectCard block`() {
linkToObjectCardSmallIcon.bind(itemCardSmallIcon, {})
assertSearchSpans(linkToObjectCardSmallIcon.titleView)
}
@Test
fun `test linkToObjectCardCover block`() {
linkToObjectCardSmallIconCover.bind(itemCardSmallIconCover, {})
assertSearchSpans(linkToObjectCardSmallIconCover.titleView)
}
@Test
fun `test linkToObjectCardMediumIcon block`() {
linkToObjectCardMediumIcon.bind(itemCardMediumIcon, {})
assertSearchSpans(linkToObjectCardMediumIcon.titleView)
}
@Test
fun `test linkToObjectCardMediumIconCover block`() {
linkToObjectCardMediumIconCover.bind(itemCardMediumIconCover, {})
assertSearchSpans(linkToObjectCardMediumIconCover.titleView)
}
private fun assertSearchSpans(textView: TextView) {
// verify
val spans = textView.editableText.getSpans(0, item.text!!.length, SearchHighlightSpan::class.java)
assertThat(spans.size, equalTo(2))
assertThat(textView.editableText.getSpanStart(spans[0]), equalTo(0))
assertThat(textView.editableText.getSpanEnd(spans[0]), equalTo(4))
assertThat(textView.editableText.getSpanStart(spans[1]), equalTo(6))
assertThat(textView.editableText.getSpanEnd(spans[1]), equalTo(10))
val targetSpans = textView.editableText.getSpans(0, textView.text.length, SearchTargetHighlightSpan::class.java)
assertThat(targetSpans.size, equalTo(1))
assertThat(textView.editableText.getSpanStart(targetSpans[0]), equalTo(0))
assertThat(textView.editableText.getSpanEnd(targetSpans[0]), equalTo(4))
}
}