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

Editor | Feature | Slash widget, color and background (#1455)

* update package

* add color adapter + holder

* adapters update

* show items

* color actions

* ci off

* tests

* fix

* ci

* test fixes

* update robolectric
This commit is contained in:
Konstantin Ivanov 2021-05-11 15:39:34 +03:00 committed by GitHub
parent 4b10676603
commit 85092957e8
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1113 additions and 26 deletions

View file

@ -1,7 +1,7 @@
on:
pull_request:
# add "synchronize" in "types", in order to trigger workflow for pull request commit(s) pushes.
types: [open]
types: [open, synchronize]
branches: [develop]
name: Run debug unit tests
jobs:

View file

@ -1,9 +1,11 @@
package com.anytypeio.anytype.core_ui.features.page.slash.holders
package com.anytypeio.anytype.core_ui.features.page.slash
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.features.page.slash.holders.AlignMenuHolder
import com.anytypeio.anytype.core_ui.features.page.slash.holders.SubheaderMenuHolder
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
import kotlinx.android.synthetic.main.item_slash_widget_subheader.view.*

View file

@ -0,0 +1,76 @@
package com.anytypeio.anytype.core_ui.features.page.slash
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.features.page.slash.holders.ColorMenuHolder
import com.anytypeio.anytype.core_ui.features.page.slash.holders.SubheaderMenuHolder
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
import kotlinx.android.synthetic.main.item_slash_widget_subheader.view.*
class SlashColorAdapter(
private var items: List<SlashItem>,
private val clicks: (SlashItem) -> Unit,
private val clickBack: () -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
fun update(items: List<SlashItem>) {
this.items = items
notifyDataSetChanged()
}
fun clear() {
val size = items.size
if (size > 0) {
items = listOf()
notifyItemRangeRemoved(0, size)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
R.layout.item_slash_widget_color -> {
ColorMenuHolder(
view = inflater.inflate(viewType, parent, false)
).apply {
itemView.setOnClickListener {
clicks(items[bindingAdapterPosition])
}
}
}
R.layout.item_slash_widget_subheader -> {
SubheaderMenuHolder(
view = inflater.inflate(viewType, parent, false)
).apply {
itemView.flBack.setOnClickListener {
clickBack.invoke()
}
}
}
else -> throw IllegalArgumentException("Wrong viewtype:$viewType")
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ColorMenuHolder -> {
val item = items[position] as SlashItem.Color
holder.bind(item)
}
is SubheaderMenuHolder -> {
val item = items[position] as SlashItem.Subheader
holder.bind(item)
}
}
}
override fun getItemViewType(position: Int): Int = when (val item = items[position]) {
is SlashItem.Color -> R.layout.item_slash_widget_color
is SlashItem.Subheader -> R.layout.item_slash_widget_subheader
else -> throw IllegalArgumentException("Wrong item type:$item")
}
override fun getItemCount(): Int = items.size
}

View file

@ -7,7 +7,6 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.features.page.slash.holders.SlashAlignmentAdapter
import com.anytypeio.anytype.core_ui.tools.SlashHelper
import com.anytypeio.anytype.presentation.page.editor.slash.SlashCommand
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
@ -90,6 +89,22 @@ class SlashWidget @JvmOverloads constructor(
)
}
private val colorAdapter by lazy {
SlashColorAdapter(
items = listOf(),
clicks = { _clickEvents.offer(it) },
clickBack = { _backEvent.offer(true) }
)
}
private val backgroundAdapter by lazy {
SlashColorAdapter(
items = listOf(),
clicks = { _clickEvents.offer(it) },
clickBack = { _backEvent.offer(true) }
)
}
private val concatAdapter = ConcatAdapter(
mainAdapter,
styleAdapter,
@ -98,7 +113,9 @@ class SlashWidget @JvmOverloads constructor(
relationsAdapter,
otherAdapter,
actionsAdapter,
alignAdapter
alignAdapter,
colorAdapter,
backgroundAdapter
)
init {
@ -127,6 +144,8 @@ class SlashWidget @JvmOverloads constructor(
otherAdapter.clear()
actionsAdapter.clear()
alignAdapter.clear()
colorAdapter.clear()
backgroundAdapter.clear()
}
is SlashCommand.ShowStyleItems -> {
styleAdapter.update(command.items)
@ -139,6 +158,8 @@ class SlashWidget @JvmOverloads constructor(
otherAdapter.clear()
actionsAdapter.clear()
alignAdapter.clear()
colorAdapter.clear()
backgroundAdapter.clear()
}
is SlashCommand.ShowMediaItems -> {
mediaAdapter.update(command.items)
@ -151,6 +172,8 @@ class SlashWidget @JvmOverloads constructor(
otherAdapter.clear()
actionsAdapter.clear()
alignAdapter.clear()
colorAdapter.clear()
backgroundAdapter.clear()
}
is SlashCommand.ShowRelations -> {
relationsAdapter.update(command.relations)
@ -163,6 +186,8 @@ class SlashWidget @JvmOverloads constructor(
otherAdapter.clear()
actionsAdapter.clear()
alignAdapter.clear()
colorAdapter.clear()
backgroundAdapter.clear()
}
is SlashCommand.ShowObjectTypes -> {
objectTypesAdapter.update(command.items)
@ -175,6 +200,8 @@ class SlashWidget @JvmOverloads constructor(
otherAdapter.clear()
actionsAdapter.clear()
alignAdapter.clear()
colorAdapter.clear()
backgroundAdapter.clear()
}
is SlashCommand.ShowOtherItems -> {
otherAdapter.update(command.items)
@ -187,6 +214,8 @@ class SlashWidget @JvmOverloads constructor(
relationsAdapter.clear()
actionsAdapter.clear()
alignAdapter.clear()
colorAdapter.clear()
backgroundAdapter.clear()
}
is SlashCommand.ShowActionItems -> {
actionsAdapter.update(command.items)
@ -199,6 +228,8 @@ class SlashWidget @JvmOverloads constructor(
relationsAdapter.clear()
otherAdapter.clear()
alignAdapter.clear()
colorAdapter.clear()
backgroundAdapter.clear()
}
is SlashCommand.ShowAlignmentItems -> {
alignAdapter.update(command.items)
@ -211,6 +242,36 @@ class SlashWidget @JvmOverloads constructor(
relationsAdapter.clear()
otherAdapter.clear()
actionsAdapter.clear()
colorAdapter.clear()
backgroundAdapter.clear()
}
is SlashCommand.ShowColorItems -> {
colorAdapter.update(command.items)
rvSlash.smoothScrollToPosition(0)
mainAdapter.clear()
styleAdapter.clear()
mediaAdapter.clear()
objectTypesAdapter.clear()
relationsAdapter.clear()
otherAdapter.clear()
actionsAdapter.clear()
alignAdapter.clear()
backgroundAdapter.clear()
}
is SlashCommand.ShowBackgroundItems -> {
backgroundAdapter.update(command.items)
rvSlash.smoothScrollToPosition(0)
mainAdapter.clear()
styleAdapter.clear()
mediaAdapter.clear()
objectTypesAdapter.clear()
relationsAdapter.clear()
otherAdapter.clear()
actionsAdapter.clear()
alignAdapter.clear()
colorAdapter.clear()
}
is SlashCommand.FilterItems -> {
val filter = command.filter.removePrefix(SLASH_PREFIX)

View file

@ -0,0 +1,32 @@
package com.anytypeio.anytype.core_ui.features.page.slash.holders
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.presentation.page.editor.ThemeColor
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
import kotlinx.android.synthetic.main.item_slash_widget_color.view.*
import java.util.*
class ColorMenuHolder(view: View) : RecyclerView.ViewHolder(view) {
private val locale: Locale = Locale.getDefault()
fun bind(item: SlashItem.Color) = with(itemView) {
when (item) {
is SlashItem.Color.Text -> {
circle.isSelected = item.isSelected
val color = ThemeColor.values().first { it.title == item.code }
circle.innerColor = color.text
title.text = item.code.capitalize(locale)
}
is SlashItem.Color.Background -> {
circle.isSelected = item.isSelected
val color = ThemeColor.values().first { it.title == item.code }
circle.innerColor = color.background
val background = item.code.capitalize(Locale.getDefault())
title.text = resources.getString(R.string.slash_widget_background_item, background)
}
}
}
}

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,6 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="55dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
</androidx.constraintlayout.widget.ConstraintLayout>
<com.anytypeio.anytype.core_ui.widgets.ColorCircleWidget
android:id="@+id/circle"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_marginStart="9dp"
android:layout_gravity="start|center_vertical"
app:outerStrokeColor="#DFDDD0"
app:innerRadius="11dp" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="52dp"
style="@style/SlashWidgetStyleItemTitle"
tools:text="Ultramarine" />
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginStart="52dp"
android:background="@drawable/divider_relations"
android:layout_gravity="bottom"/>
</FrameLayout>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -374,6 +374,8 @@
<string name="slash_widget_align_center">Center</string>
<string name="slash_widget_align_left">Left</string>
<string name="slash_widget_background_item">%1$s background</string>
<string name="dv_filter_checkbox_checked">checked</string>
<string name="dv_filter_checkbox_not_checked">not checked</string>
<string name="paste_or_type_a_url">Paste or type a URL</string>

View file

@ -81,5 +81,6 @@ dependencies {
testImplementation unitTestDependencies.liveDataTesting
testImplementation unitTestDependencies.archCoreTesting
testImplementation unitTestDependencies.androidXTestCore
testImplementation unitTestDependencies.robolectricLatest
testImplementation unitTestDependencies.timberJUnit
}

View file

@ -37,7 +37,6 @@ import com.anytypeio.anytype.domain.block.interactor.RemoveLinkMark
import com.anytypeio.anytype.domain.block.interactor.UpdateLinkMarks
import com.anytypeio.anytype.domain.block.interactor.UpdateText
import com.anytypeio.anytype.domain.block.interactor.sets.GetObjectTypes
import com.anytypeio.anytype.domain.clipboard.Copy
import com.anytypeio.anytype.domain.clipboard.Paste
import com.anytypeio.anytype.domain.cover.RemoveDocCover
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
@ -3683,6 +3682,21 @@ class PageViewModel(
val items = listOf(SlashItem.Subheader.AlignmentWithBack) + SlashExtensions.getAlignmentItems()
onSlashCommand(SlashCommand.ShowAlignmentItems(items))
}
is SlashItem.Main.Color -> {
val block = blocks.first { it.id == targetId }
val items = listOf(SlashItem.Subheader.ColorWithBack) + SlashExtensions.getColorItems(
code = block.content.asText().color
)
onSlashCommand(SlashCommand.ShowColorItems(items))
}
is SlashItem.Main.Background -> {
val block = blocks.first { it.id == targetId }
val items = listOf(SlashItem.Subheader.BackgroundWithBack) +
SlashExtensions.getBackgroundItems(
code = block.content.asText().backgroundColor
)
onSlashCommand(SlashCommand.ShowBackgroundItems(items))
}
is SlashItem.Style.Type -> {
onSlashStyleTypeItemClicked(item, targetId)
}
@ -3707,12 +3721,46 @@ class PageViewModel(
is SlashItem.Alignment -> {
onSlashAlignmentItemClicked(item, targetId)
}
is SlashItem.Color -> {
controlPanelInteractor.onEvent(ControlPanelMachine.Event.Slash.OnStop)
onSlashItemColorClicked(item, targetId)
}
else -> {
Timber.d("PRESSED ON SLASH ITEM : $item")
}
}
}
private fun onSlashItemColorClicked(item: SlashItem.Color, targetId: Id) {
viewModelScope.launch {
orchestrator.stores.focus.update(
Editor.Focus(
id = targetId,
cursor = Editor.Cursor.End
)
)
}
val intent = when (item) {
is SlashItem.Color.Background -> {
Intent.Text.UpdateBackgroundColor(
context = context,
targets = listOf(targetId),
color = item.code
)
}
is SlashItem.Color.Text -> {
Intent.Text.UpdateColor(
context = context,
target = targetId,
color = item.code
)
}
}
viewModelScope.launch {
orchestrator.proxies.intents.send(intent)
}
}
private fun onSlashMediaItemClicked(item: SlashItem.Media) {
when (item) {
SlashItem.Media.Bookmark -> {

View file

@ -1,7 +1,7 @@
package com.anytypeio.anytype.presentation.page.editor.slash
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.presentation.page.editor.model.BlockView
import com.anytypeio.anytype.presentation.page.editor.ThemeColor
import com.anytypeio.anytype.presentation.page.editor.model.UiBlock
fun List<ObjectType>.toView(): List<SlashItem.ObjectType> = map { oType ->
@ -85,4 +85,30 @@ object SlashExtensions {
SlashItem.Alignment.Center,
SlashItem.Alignment.Right
)
fun getColorItems(code: String?): List<SlashItem.Color> =
ThemeColor.values().map { themeColor ->
val isSelected = if (themeColor.title == ThemeColor.DEFAULT.title && code == null) {
true
} else {
themeColor.title == code
}
SlashItem.Color.Text(
code = themeColor.title,
isSelected = isSelected
)
}
fun getBackgroundItems(code: String?): List<SlashItem.Color> =
ThemeColor.values().map { themeColor ->
val isSelected = if (themeColor.title == ThemeColor.DEFAULT.title && code == null) {
true
} else {
themeColor.title == code
}
SlashItem.Color.Background(
code = themeColor.title,
isSelected = isSelected
)
}
}

View file

@ -125,10 +125,17 @@ sealed class SlashItem {
//endregion
//region ALIGNMENT
sealed class Alignment : SlashItem(){
object Left: Alignment()
object Center: Alignment()
object Right: Alignment()
sealed class Alignment : SlashItem() {
object Left : Alignment()
object Center : Alignment()
object Right : Alignment()
}
//endregion
//region TEXT COLOR & BACKGROUND
sealed class Color: SlashItem() {
data class Text(val code: String, val isSelected: Boolean) : Color()
data class Background(val code: String, val isSelected: Boolean) : Color()
}
//endregion
}

View file

@ -416,4 +416,24 @@ open class EditorPresentationTestSetup {
onBlocking { invoke(any()) } doReturn Either.Right(Unit)
}
}
fun stubUpdateBackground() {
updateBackgroundColor.stub {
onBlocking {
invoke(any())
} doReturn Either.Right(
Payload(context = root, events = emptyList())
)
}
}
fun stubUpdateTextColor() {
updateTextColor.stub {
onBlocking {
invoke(any())
} doReturn Either.Right(
Payload(context = root, events = emptyList())
)
}
}
}

View file

@ -0,0 +1,797 @@
package com.anytypeio.anytype.presentation.page.editor
import MockDataFactory
import android.os.Build
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.domain.block.interactor.UpdateBackgroundColor
import com.anytypeio.anytype.domain.block.interactor.UpdateTextColor
import com.anytypeio.anytype.domain.editor.Editor
import com.anytypeio.anytype.presentation.MockTypicalDocumentFactory
import com.anytypeio.anytype.presentation.page.PageViewModel
import com.anytypeio.anytype.presentation.page.editor.slash.SlashCommand
import com.anytypeio.anytype.presentation.page.editor.slash.SlashEvent
import com.anytypeio.anytype.presentation.page.editor.slash.SlashItem
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.times
import org.mockito.kotlin.verifyBlocking
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
@Config(sdk = [Build.VERSION_CODES.P])
@RunWith(RobolectricTestRunner::class)
class EditorSlashWidgetColorTest : EditorPresentationTestSetup() {
@get:Rule
val rule = InstantTaskExecutorRule()
@get:Rule
val coroutineTestRule = CoroutinesTestRule()
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
}
//region {TEXT COLOR}
@Test
fun `should selected red color when block text color is red`() {
val code = ThemeColor.RED.title
val header = MockTypicalDocumentFactory.header
val title = MockTypicalDocumentFactory.title
val block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
color = code,
marks = listOf(),
style = Block.Content.Text.Style.NUMBERED
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, block.id)
)
val doc = listOf(
page,
header,
title,
block
)
stubInterceptEvents()
stubUpdateTextColor()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
vm.onSlashItemClicked(SlashItem.Main.Color)
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.command
assertNotNull(command)
val expected = listOf(
SlashItem.Subheader.ColorWithBack,
SlashItem.Color.Text(ThemeColor.DEFAULT.title, false),
SlashItem.Color.Text(ThemeColor.GREY.title, false),
SlashItem.Color.Text(ThemeColor.YELLOW.title, false),
SlashItem.Color.Text(ThemeColor.ORANGE.title, false),
SlashItem.Color.Text(ThemeColor.RED.title, true),
SlashItem.Color.Text(ThemeColor.PINK.title, false),
SlashItem.Color.Text(ThemeColor.PURPLE.title, false),
SlashItem.Color.Text(ThemeColor.BLUE.title, false),
SlashItem.Color.Text(ThemeColor.ICE.title, false),
SlashItem.Color.Text(ThemeColor.TEAL.title, false),
SlashItem.Color.Text(ThemeColor.GREEN.title, false)
)
assertEquals(
expected = expected,
actual = (command as SlashCommand.ShowColorItems).items
)
}
@Test
fun `should selected default color when block text color is null`() {
val code: String? = null
val header = MockTypicalDocumentFactory.header
val title = MockTypicalDocumentFactory.title
val block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
color = code,
marks = listOf(),
style = Block.Content.Text.Style.NUMBERED
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, block.id)
)
val doc = listOf(
page,
header,
title,
block
)
stubInterceptEvents()
stubUpdateTextColor()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
vm.onSlashItemClicked(SlashItem.Main.Color)
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.command as SlashCommand.ShowColorItems
assertNotNull(command)
val expected = listOf(
SlashItem.Subheader.ColorWithBack,
SlashItem.Color.Text(ThemeColor.DEFAULT.title, true),
SlashItem.Color.Text(ThemeColor.GREY.title, false),
SlashItem.Color.Text(ThemeColor.YELLOW.title, false),
SlashItem.Color.Text(ThemeColor.ORANGE.title, false),
SlashItem.Color.Text(ThemeColor.RED.title, false),
SlashItem.Color.Text(ThemeColor.PINK.title, false),
SlashItem.Color.Text(ThemeColor.PURPLE.title, false),
SlashItem.Color.Text(ThemeColor.BLUE.title, false),
SlashItem.Color.Text(ThemeColor.ICE.title, false),
SlashItem.Color.Text(ThemeColor.TEAL.title, false),
SlashItem.Color.Text(ThemeColor.GREEN.title, false)
)
assertEquals(
expected = expected,
actual = command.items
)
}
@Test
fun `should selected default color when block text color is default`() {
val code: String = ThemeColor.DEFAULT.title
val header = MockTypicalDocumentFactory.header
val title = MockTypicalDocumentFactory.title
val block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
color = code,
marks = listOf(),
style = Block.Content.Text.Style.NUMBERED
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, block.id)
)
val doc = listOf(
page,
header,
title,
block
)
stubInterceptEvents()
stubUpdateTextColor()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
vm.onSlashItemClicked(SlashItem.Main.Color)
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.command as SlashCommand.ShowColorItems
assertNotNull(command)
val expected = listOf(
SlashItem.Subheader.ColorWithBack,
SlashItem.Color.Text(ThemeColor.DEFAULT.title, true),
SlashItem.Color.Text(ThemeColor.GREY.title, false),
SlashItem.Color.Text(ThemeColor.YELLOW.title, false),
SlashItem.Color.Text(ThemeColor.ORANGE.title, false),
SlashItem.Color.Text(ThemeColor.RED.title, false),
SlashItem.Color.Text(ThemeColor.PINK.title, false),
SlashItem.Color.Text(ThemeColor.PURPLE.title, false),
SlashItem.Color.Text(ThemeColor.BLUE.title, false),
SlashItem.Color.Text(ThemeColor.ICE.title, false),
SlashItem.Color.Text(ThemeColor.TEAL.title, false),
SlashItem.Color.Text(ThemeColor.GREEN.title, false)
)
assertEquals(
expected = expected,
actual = command.items
)
}
@Test
fun `should store focus as target block end when text color picked`() {
val doc = MockTypicalDocumentFactory.page(root)
val block = MockTypicalDocumentFactory.a
stubInterceptEvents()
stubUpdateTextColor()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
vm.onSlashItemClicked(SlashItem.Main.Color)
vm.onSlashItemClicked(SlashItem.Color.Text(code = "red", isSelected = false))
val focusAfter = orchestrator.stores.focus.current()
assertEquals(block.id, focusAfter.id)
assertEquals(Editor.Cursor.End, focusAfter.cursor)
}
@Test
fun `should hide slash widget when text color picked`() {
val doc = MockTypicalDocumentFactory.page(root)
val block = MockTypicalDocumentFactory.a
stubInterceptEvents()
stubUpdateTextColor()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
vm.onSlashItemClicked(SlashItem.Main.Color)
vm.onSlashItemClicked(SlashItem.Color.Text(code = "red", isSelected = false))
val state = vm.controlPanelViewState.value
assertNotNull(state)
assertFalse(state.slashWidget.isVisible)
}
@Test
fun `should send updateTextColor UseCase when text color picked`() {
val doc = MockTypicalDocumentFactory.page(root)
val block = MockTypicalDocumentFactory.a
stubInterceptEvents()
stubUpdateTextColor()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
val code = ThemeColor.ICE.title
vm.onSlashItemClicked(SlashItem.Main.Color)
vm.onSlashItemClicked(SlashItem.Color.Text(code = code, isSelected = false))
val params = UpdateTextColor.Params(
context = root,
target = block.id,
color = code
)
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
verifyBlocking(updateTextColor, times(1)) { invoke(params) }
}
//endregion
//region {BACKGROUND COLOR}
@Test
fun `should selected green color when block background color is green`() {
val code = ThemeColor.GREEN.title
val header = MockTypicalDocumentFactory.header
val title = MockTypicalDocumentFactory.title
val block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
backgroundColor = code,
marks = listOf(),
style = Block.Content.Text.Style.NUMBERED
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, block.id)
)
val doc = listOf(
page,
header,
title,
block
)
stubInterceptEvents()
stubUpdateBackground()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
vm.onSlashItemClicked(SlashItem.Main.Background)
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.command as SlashCommand.ShowBackgroundItems
assertNotNull(command)
val expected = listOf(
SlashItem.Subheader.BackgroundWithBack,
SlashItem.Color.Background(ThemeColor.DEFAULT.title, false),
SlashItem.Color.Background(ThemeColor.GREY.title, false),
SlashItem.Color.Background(ThemeColor.YELLOW.title, false),
SlashItem.Color.Background(ThemeColor.ORANGE.title, false),
SlashItem.Color.Background(ThemeColor.RED.title, false),
SlashItem.Color.Background(ThemeColor.PINK.title, false),
SlashItem.Color.Background(ThemeColor.PURPLE.title, false),
SlashItem.Color.Background(ThemeColor.BLUE.title, false),
SlashItem.Color.Background(ThemeColor.ICE.title, false),
SlashItem.Color.Background(ThemeColor.TEAL.title, false),
SlashItem.Color.Background(ThemeColor.GREEN.title, true)
)
assertEquals(
expected = expected,
actual = command.items
)
}
@Test
fun `should selected default color when block background color is null`() {
val code: String? = null
val header = MockTypicalDocumentFactory.header
val title = MockTypicalDocumentFactory.title
val block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
backgroundColor = code,
marks = listOf(),
style = Block.Content.Text.Style.NUMBERED
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, block.id)
)
val doc = listOf(
page,
header,
title,
block
)
stubInterceptEvents()
stubUpdateBackground()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
vm.onSlashItemClicked(SlashItem.Main.Background)
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.command as SlashCommand.ShowBackgroundItems
assertNotNull(command)
val expected = listOf(
SlashItem.Subheader.BackgroundWithBack,
SlashItem.Color.Background(ThemeColor.DEFAULT.title, true),
SlashItem.Color.Background(ThemeColor.GREY.title, false),
SlashItem.Color.Background(ThemeColor.YELLOW.title, false),
SlashItem.Color.Background(ThemeColor.ORANGE.title, false),
SlashItem.Color.Background(ThemeColor.RED.title, false),
SlashItem.Color.Background(ThemeColor.PINK.title, false),
SlashItem.Color.Background(ThemeColor.PURPLE.title, false),
SlashItem.Color.Background(ThemeColor.BLUE.title, false),
SlashItem.Color.Background(ThemeColor.ICE.title, false),
SlashItem.Color.Background(ThemeColor.TEAL.title, false),
SlashItem.Color.Background(ThemeColor.GREEN.title, false)
)
assertEquals(
expected = expected,
actual = command.items
)
}
@Test
fun `should selected default color when block background color is default`() {
val code: String = ThemeColor.DEFAULT.title
val header = MockTypicalDocumentFactory.header
val title = MockTypicalDocumentFactory.title
val block = Block(
id = MockDataFactory.randomUuid(),
fields = Block.Fields.empty(),
children = emptyList(),
content = Block.Content.Text(
text = MockDataFactory.randomString(),
color = code,
marks = listOf(),
style = Block.Content.Text.Style.NUMBERED
)
)
val page = Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(
type = Block.Content.Smart.Type.PAGE
),
children = listOf(header.id, block.id)
)
val doc = listOf(
page,
header,
title,
block
)
stubInterceptEvents()
stubUpdateBackground()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
vm.onSlashItemClicked(SlashItem.Main.Background)
val state = vm.controlPanelViewState.value
val command = state?.slashWidget?.command as SlashCommand.ShowBackgroundItems
assertNotNull(command)
val expected = listOf(
SlashItem.Subheader.BackgroundWithBack,
SlashItem.Color.Background(ThemeColor.DEFAULT.title, true),
SlashItem.Color.Background(ThemeColor.GREY.title, false),
SlashItem.Color.Background(ThemeColor.YELLOW.title, false),
SlashItem.Color.Background(ThemeColor.ORANGE.title, false),
SlashItem.Color.Background(ThemeColor.RED.title, false),
SlashItem.Color.Background(ThemeColor.PINK.title, false),
SlashItem.Color.Background(ThemeColor.PURPLE.title, false),
SlashItem.Color.Background(ThemeColor.BLUE.title, false),
SlashItem.Color.Background(ThemeColor.ICE.title, false),
SlashItem.Color.Background(ThemeColor.TEAL.title, false),
SlashItem.Color.Background(ThemeColor.GREEN.title, false)
)
assertEquals(
expected = expected,
actual = command.items
)
}
@Test
fun `should store focus as target block end when background color picked`() {
val doc = MockTypicalDocumentFactory.page(root)
val block = MockTypicalDocumentFactory.a
stubInterceptEvents()
stubUpdateBackground()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
vm.onSlashItemClicked(SlashItem.Main.Background)
vm.onSlashItemClicked(SlashItem.Color.Background(code = "red", isSelected = false))
val focusAfter = orchestrator.stores.focus.current()
assertEquals(block.id, focusAfter.id)
assertEquals(Editor.Cursor.End, focusAfter.cursor)
}
@Test
fun `should hide slash widget when background color picked`() {
val doc = MockTypicalDocumentFactory.page(root)
val block = MockTypicalDocumentFactory.a
stubInterceptEvents()
stubUpdateBackground()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
vm.onSlashItemClicked(SlashItem.Main.Background)
vm.onSlashItemClicked(SlashItem.Color.Background(code = "red", isSelected = false))
val state = vm.controlPanelViewState.value
assertNotNull(state)
assertFalse(state.slashWidget.isVisible)
}
@Test
fun `should send updateBackgroundColor UseCase when background color picked`() {
val doc = MockTypicalDocumentFactory.page(root)
val block = MockTypicalDocumentFactory.a
stubInterceptEvents()
stubUpdateBackground()
stubOpenDocument(document = doc)
val vm = buildViewModel()
vm.onStart(root)
vm.apply {
onBlockFocusChanged(
id = block.id,
hasFocus = true
)
onSlashEvent(
SlashEvent.Start(
cursorCoordinate = 100,
slashStart = 0
)
)
}
// TESTING
val code = ThemeColor.PURPLE.title
vm.onSlashItemClicked(SlashItem.Main.Background)
vm.onSlashItemClicked(SlashItem.Color.Background(code = code, isSelected = false))
val params = UpdateBackgroundColor.Params(
context = root,
targets = listOf(block.id),
color = code
)
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
verifyBlocking(updateBackgroundColor, times(1)) { invoke(params) }
}
//endregion
}