com.github.mvysny.kaributesting.v10.ComboBox.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of karibu-testing-v10 Show documentation
Show all versions of karibu-testing-v10 Show documentation
Karibu Testing, support for browserless Vaadin testing in Kotlin
@file:Suppress("FunctionName")
package com.github.mvysny.kaributesting.v10
import com.github.mvysny.kaributools.VaadinVersion
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.ComponentEvent
import com.vaadin.flow.component.HasValue
import com.vaadin.flow.component.ItemLabelGenerator
import com.vaadin.flow.component.combobox.ComboBox
import com.vaadin.flow.component.combobox.ComboBoxBase
import com.vaadin.flow.component.select.Select
import com.vaadin.flow.data.provider.DataCommunicator
import com.vaadin.flow.data.provider.DataProvider
import com.vaadin.flow.data.renderer.ComponentRenderer
import com.vaadin.flow.data.renderer.Renderer
import com.vaadin.flow.function.SerializableConsumer
import java.lang.reflect.Field
import java.lang.reflect.Method
import kotlin.test.fail
/**
* Emulates user inputting something into the combo box, filtering items.
*
* In order to verify that the filter on your data provider works properly,
* use [getSuggestionItems] to retrieve filtered items.
*
* Note: this function will not change the value of the combo box.
* @param userInput emulate user typing something into the ComboBox, thus attempting
* to filter out items/search for an item. Pass in `null` to clear.
*/
public fun ComboBox.setUserInput(userInput: String?) {
_expectEditableByUser()
KaribuInternalComboBoxSupport.get().setUserInput(this, userInput)
}
/**
* Select an item in the combo box by [label]. Calls [setUserInput] to filter the items first, then
* calls [getSuggestionItems] to obtain filtered items, then selects the sole item that matches [label].
*
* Fails if the item is not found, or multiple items are found. Fails if the combo box is not editable.
* @param bypassSetUserInput if false (default), the [setUserInput] is called to filter the items first.
* This has much higher performance on a large data set since it will perform the filtering in the
* data provider itself (in the backend rather than in-memory). However, if this does not work
* for some reason, set this to `true` to search in all items.
*/
@JvmOverloads
public fun ComboBox.selectByLabel(
label: String,
bypassSetUserInput: Boolean = false
) {
val suggestionItems: List = if (!bypassSetUserInput) {
setUserInput(label)
getSuggestionItems()
} else {
_expectEditableByUser()
dataProvider._findAll()
}
val items: List =
suggestionItems.filter { itemLabelGenerator.apply(it) == label }
when {
items.isEmpty() -> {
val msg = StringBuilder()
msg.append("${toPrettyString()}: No item found with label '$label'")
if (dataProvider.isInMemory) {
val allItems: List = dataProvider._findAll()
msg.append(". Available items: ${allItems.map { "'${itemLabelGenerator.apply(it)}'=>$it" }}")
}
fail(msg.toString())
}
items.size > 1 -> fail("${(this as Component).toPrettyString()}: Multiple items found with label '$label': $items")
else -> _value = items[0]
}
}
/**
* Simulates the user creating a custom item. Only works if the field is editable by the user
* and allows custom values ([ComboBox.isAllowCustomValue] is true).
*/
public fun ComboBox._fireCustomValueSet(userInput: String) {
_expectEditableByUser()
check(isAllowCustomValue) { "${toPrettyString()} doesn't allow custom values" }
_fireEvent(ComboBoxBase.CustomValueSetEvent>(this, true, userInput))
}
@Suppress("UNCHECKED_CAST")
internal val ComboBox._dataCommunicator: DataCommunicator
get() = KaribuInternalComboBoxSupport.get().getDataCommunicator(this) as DataCommunicator?
?: fail("${toPrettyString()}: items/dataprovider has not been set")
/**
* Fetches items currently displayed in the suggestion box. This list is filtered
* by any user input set via [setUserInput].
*/
public fun ComboBox.getSuggestionItems(): List =
_dataCommunicator.fetchAll()
/**
* Fetches captions of items currently displayed in the suggestion box. This list is filtered
* by any user input set via [setUserInput].
*/
public fun ComboBox.getSuggestions(): List {
val items: List = getSuggestionItems()
return items.map { itemLabelGenerator.apply(it) }
}
/**
* Fetches items currently displayed in the suggestion box.
*/
@Suppress("UNCHECKED_CAST")
public fun Select.getSuggestionItems(): List = dataProvider._findAll()
/**
* Fetches captions of items currently displayed in the suggestion box.
*/
public fun Select.getSuggestions(): List {
val items: List = getSuggestionItems()
val g: ItemLabelGenerator =
itemLabelGenerator ?: ItemLabelGenerator { it.toString() }
return items.map { g.apply(it) }
}
/**
* Select an item in the combo box by [label]. Calls [getSuggestionItems] to obtain filtered items,
* then selects the sole item that matches [label].
*
* Fails if the item is not found, or multiple items are found. Fails if the combo box is not editable.
*/
public fun Select.selectByLabel(label: String) {
// it's OK to use selectByLabel(): Select's dataprovider is expected to hold small amount of data
// since Select doesn't offer any filtering capabilities.
selectByLabel(
label,
dataProvider,
itemLabelGenerator ?: ItemLabelGenerator { it.toString() })
}
/**
* Beware: the function will poll all items from the [dataProvider]. Use cautiously and only for small data providers.
*/
internal fun HasValue<*, T>.selectByLabel(
label: String,
dataProvider: DataProvider,
itemLabelGenerator: ItemLabelGenerator
) {
val items = dataProvider._findAll()
val itemsWithLabel: List =
items.filter { itemLabelGenerator.apply(it) == label }
when {
itemsWithLabel.isEmpty() ->
fail("${(this as Component).toPrettyString()}: No item found with label '$label'. Available items: ${items.map { "'${itemLabelGenerator.apply(it)}'=>$it" }}")
itemsWithLabel.size > 1 ->
fail("${(this as Component).toPrettyString()}: Multiple items found with label '$label': $itemsWithLabel")
else -> _value = itemsWithLabel[0]
}
}
/**
* Internal, don't use.
*/
public interface KaribuInternalComboBoxSupport {
/**
* @param comboBox [ComboBox] or `MultiSelectComboBox`.
*/
public fun getRenderer(comboBox: Any): Renderer<*>
public fun getDataCommunicator(comboBox: Any): DataCommunicator<*>?
/**
* Simulates the user creating a custom item. Only works if the field is editable by the user
* and allows custom values ([ComboBox.isAllowCustomValue] is true).
*/
public fun fireCustomValueSet(comboBox: ComboBox, userInput: String)
/**
* @param comboBox [ComboBox] or `MultiSelectComboBox`.
*/
public fun setUserInput(comboBox: Any, userInput: String?)
public companion object {
public fun get(): KaribuInternalComboBoxSupport = KaribuInternalComboBoxSupportVaadin23_2
}
}
private object KaribuInternalComboBoxSupportVaadin23_2 : KaribuInternalComboBoxSupport {
private val _ComboBoxBase: Class<*> by lazy(LazyThreadSafetyMode.PUBLICATION) {
Class.forName("com.vaadin.flow.component.combobox.ComboBoxBase")
}
private val _ComboBox_23_2_dataCommunicator: Method by lazy(LazyThreadSafetyMode.PUBLICATION) {
val m = _ComboBoxBase.getDeclaredMethod("getDataCommunicator")
m.isAccessible = true
m
}
/**
* Vaadin 23+ uses RendererManager to store renderers.
*/
private val _ComboBoxBase_renderManager: Method by lazy(LazyThreadSafetyMode.PUBLICATION) {
val m = _ComboBoxBase.getDeclaredMethod("getRenderManager")
m.isAccessible = true
m
}
private val _ComboBoxRenderManager: Class<*> by lazy(LazyThreadSafetyMode.PUBLICATION) {
Class.forName("com.vaadin.flow.component.combobox.ComboBoxRenderManager")
}
private val _ComboBoxRenderManager_renderer: Field by lazy(LazyThreadSafetyMode.PUBLICATION) {
val m = checkNotNull(_ComboBoxRenderManager).getDeclaredField("renderer")
m.isAccessible = true
m
}
override fun getRenderer(comboBox: Any): Renderer<*> {
val rendererManager = _ComboBoxBase_renderManager.invoke(comboBox)
val renderer: Renderer<*> = _ComboBoxRenderManager_renderer.get(rendererManager) as Renderer<*>
return renderer
}
override fun getDataCommunicator(comboBox: Any): DataCommunicator<*>? = _ComboBox_23_2_dataCommunicator.invoke(comboBox) as DataCommunicator<*>?
override fun fireCustomValueSet(comboBox: ComboBox, userInput: String) {
val eventClass = Class.forName("com.vaadin.flow.component.combobox.ComboBoxBase${'$'}CustomValueSetEvent")
val event = eventClass.constructors[0].newInstance(comboBox, true, userInput)
comboBox._fireEvent(event as ComponentEvent<*>)
}
private val _ComboBoxBase_getDataController: Method by lazy(LazyThreadSafetyMode.PUBLICATION) {
val m = _ComboBoxBase.getDeclaredMethod("getDataController")
m.isAccessible = true
m
}
private val _ComboBoxDataController_filterSlot: Field by lazy(LazyThreadSafetyMode.PUBLICATION) {
val comboBoxFilterSlot: Field =
Class.forName("com.vaadin.flow.component.combobox.ComboBoxDataController")
.getDeclaredField("filterSlot")
comboBoxFilterSlot.isAccessible = true
comboBoxFilterSlot
}
/**
* Calls `(this as ComboBoxBase).getDataController().filterSlot`.
*/
private fun getComboBoxBaseFilterSlot(comboBoxBase: /*com.vaadin.flow.component.combobox.ComboBoxBase*/Any): SerializableConsumer {
_ComboBoxBase.cast(comboBoxBase)
val dataController: /*ComboBoxDataController*/Any = _ComboBoxBase_getDataController.invoke(comboBoxBase)
@Suppress("UNCHECKED_CAST")
val filterSlot =
_ComboBoxDataController_filterSlot.get(dataController) as SerializableConsumer
return filterSlot
}
override fun setUserInput(comboBox: Any, userInput: String?) {
getComboBoxBaseFilterSlot(comboBox).accept(userInput)
}
}
/**
* Returns the renderer set via [ComboBox.setRenderer].
*/
@Suppress("UNCHECKED_CAST")
public val ComboBox._renderer: Renderer get() = KaribuInternalComboBoxSupport.get().getRenderer(this) as Renderer
/**
* Returns the component rendered in [ComboBox] dropdown overlay for given [item].
*
* Fails if the [ComboBox] renderer is something else than [ComponentRenderer].
*/
public fun ComboBox._getRenderedComponentFor(item: T): Component {
val r = _renderer
val r2 = r as? ComponentRenderer<*, T> ?: fail("${toPrettyString()}: expected ComponentRenderer but got $r")
return r2.createComponent(item)
}
/**
* Returns the component rendered in [Select] dropdown overlay for given [item].
*/
public fun Select._getRenderedComponentFor(item: T): Component = itemRenderer.createComponent(item)