Skip to content

Commit

Permalink
Add support for ComboBox._getRenderedComponentFor()
Browse files Browse the repository at this point in the history
  • Loading branch information
mvysny committed Mar 18, 2024
1 parent 6dcc571 commit 024ab7c
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 57 deletions.
8 changes: 8 additions & 0 deletions karibu-testing-v10/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,10 @@ new item in the ComboBox.
Finally, call `comboBox.selectByLabel()`/`select.selectByLabel()` to select an item by its label,
changing the value of the combobox/select. (Since Karibu-Testing 1.3.15)

When using `ComponentRenderer` to render items in the dropdown overlay, call
`comboBox._getRenderedComponentFor()`/`ComboBoxKt._getRenderedComponentFor()` to call renderer
to create the component for particular item. (Since Karibu-Testing 1.4.0/2.1.5).

### Support for ListBox/MultiSelectListBox

(Since Karibu-Testing 1.3.9)
Expand All @@ -1306,6 +1310,10 @@ to retrieve filtered items, in order to verify that the filter on your data prov
Finally, call `msComboBox.selectByLabel()` to select an item by its label,
changing the value of the combobox.

When using `ComponentRenderer` to render items in the dropdown overlay, call
`msComboBox._getRenderedComponentFor()`/`MultiSelectComboBoxUtilsKt._getRenderedComponentFor()` to call renderer
to create the component for particular item. (Since Karibu-Testing 1.4.0/2.1.5).

Make sure to depend on `karibu-testing-v23` Maven dependency to gain access to these functions
- see the top of this page for more information.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import com.github.mvysny.dynatest.expectList
import com.github.mvysny.dynatest.expectThrows
import com.vaadin.flow.component.ItemLabelGenerator
import com.vaadin.flow.component.combobox.ComboBox
import com.vaadin.flow.component.html.Span
import com.vaadin.flow.component.select.Select
import com.vaadin.flow.data.provider.ListDataProvider
import com.vaadin.flow.data.renderer.ComponentRenderer
import kotlin.test.expect

@DynaTestDsl
Expand Down Expand Up @@ -128,6 +130,20 @@ internal fun DynaNodeGroup.comboBoxTestbatch() {
}
}
}
group("_getRenderedComponentFor") {
test("ComboBox") {
val cb = ComboBox<String>().apply {
setRenderer(ComponentRenderer { it -> Span(it) })
}
expect("foo") { (cb._getRenderedComponentFor("foo") as Span).text }
}
test("Select") {
val cb = Select<String>().apply {
setRenderer(ComponentRenderer { it -> Span(it) })
}
expect("foo") { (cb._getRenderedComponentFor("foo") as Span).text }
}
}
}

fun <T> ComboBox<T>.setItems2(items: Collection<T>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

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
Expand All @@ -27,34 +31,7 @@ import kotlin.test.fail
*/
public fun <T> ComboBox<T>.setUserInput(userInput: String?) {
_expectEditableByUser()
getComboBoxBaseFilterSlot(this).accept(userInput)
}

private val _ComboBoxBase_getDataController: Method by lazy(LazyThreadSafetyMode.PUBLICATION) {
val m = ComboBoxBase::class.java.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
}

/**
* INTERNAL, DON'T USE. Only for Vaadin 23.2+. Calls `(this as ComboBoxBase).getDataController().filterSlot`.
*/
public fun getComboBoxBaseFilterSlot(comboBoxBase: /*com.vaadin.flow.component.combobox.ComboBoxBase*/Any): SerializableConsumer<String?> {
val dataController: /*ComboBoxDataController*/Any = _ComboBoxBase_getDataController.invoke(comboBoxBase)

@Suppress("UNCHECKED_CAST")
val filterSlot =
_ComboBoxDataController_filterSlot.get(dataController) as SerializableConsumer<String?>
return filterSlot
KaribuInternalComboBoxSupport.get().setUserInput(this, userInput)
}

/**
Expand Down Expand Up @@ -108,19 +85,9 @@ public fun <T> ComboBox<T>._fireCustomValueSet(userInput: String) {
_fireEvent(ComboBoxBase.CustomValueSetEvent<ComboBox<T>>(this, true, userInput))
}

private val _ComboBox_23_2_dataCommunicator: Method by lazy(LazyThreadSafetyMode.PUBLICATION) {
val m = ComboBoxBase::class.java.getDeclaredMethod("getDataCommunicator")
m.isAccessible = true
m
}

@Suppress("UNCHECKED_CAST")
private fun <T> getDataCommunicator(cb: ComboBox<T>): DataCommunicator<T>? =
_ComboBox_23_2_dataCommunicator.invoke(cb) as DataCommunicator<T>?

@Suppress("UNCHECKED_CAST")
internal val <T> ComboBox<T>._dataCommunicator: DataCommunicator<T>
get() = getDataCommunicator(this)
get() = KaribuInternalComboBoxSupport.get().getDataCommunicator(this) as DataCommunicator<T>?
?: fail("${toPrettyString()}: items/dataprovider has not been set")

/**
Expand Down Expand Up @@ -191,3 +158,124 @@ internal fun <T> HasValue<*, T>.selectByLabel(
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 <T> fireCustomValueSet(comboBox: ComboBox<T>, 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 <T> fireCustomValueSet(comboBox: ComboBox<T>, 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<String?> {
_ComboBoxBase.cast(comboBoxBase)
val dataController: /*ComboBoxDataController*/Any = _ComboBoxBase_getDataController.invoke(comboBoxBase)

@Suppress("UNCHECKED_CAST")
val filterSlot =
_ComboBoxDataController_filterSlot.get(dataController) as SerializableConsumer<String?>
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 <T> ComboBox<T>._renderer: Renderer<T> get() = KaribuInternalComboBoxSupport.get().getRenderer(this) as Renderer<T>

/**
* Returns the component rendered in [ComboBox] dropdown overlay for given [item].
*
* Fails if the [ComboBox] renderer is something else than [ComponentRenderer].
*/
public fun <T> ComboBox<T>._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 <T> Select<T>._getRenderedComponentFor(item: T): Component = itemRenderer.createComponent(item)
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import com.github.mvysny.dynatest.DynaNodeGroup
import com.github.mvysny.dynatest.DynaTestDsl
import com.github.mvysny.dynatest.expectList
import com.github.mvysny.dynatest.expectThrows
import com.github.mvysny.kaributesting.v23.getSuggestionItems
import com.github.mvysny.kaributesting.v23.getSuggestions
import com.github.mvysny.kaributesting.v23.selectByLabel
import com.github.mvysny.kaributesting.v23.setUserInput
import com.github.mvysny.kaributesting.v23.*
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.combobox.ComboBox
import com.vaadin.flow.component.combobox.MultiSelectComboBox
import com.vaadin.flow.component.html.Span
import com.vaadin.flow.component.select.Select
import com.vaadin.flow.data.renderer.ComponentRenderer
import kotlin.test.expect

@DynaTestDsl
Expand Down Expand Up @@ -83,4 +84,13 @@ internal fun DynaNodeGroup.multiselectComboBoxTests() {
withBypassSetUserInput(true)
}
}

group("_getRenderedComponentFor") {
test("MultiSelectComboBox") {
val cb = MultiSelectComboBox<String>().apply {
setRenderer(ComponentRenderer { it -> Span(it) })
}
expect("foo") { (cb._getRenderedComponentFor("foo") as Span).text }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package com.github.mvysny.kaributesting.v23

import com.github.mvysny.kaributesting.v10.*
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.combobox.ComboBoxBase
import com.vaadin.flow.component.combobox.MultiSelectComboBox
import com.vaadin.flow.data.provider.DataCommunicator
import java.lang.reflect.Method
import com.vaadin.flow.data.renderer.ComponentRenderer
import com.vaadin.flow.data.renderer.Renderer
import kotlin.test.fail

/**
Expand All @@ -20,7 +20,7 @@ import kotlin.test.fail
*/
public fun <T> MultiSelectComboBox<T>.setUserInput(userInput: String?) {
_expectEditableByUser()
getComboBoxBaseFilterSlot(this).accept(userInput)
KaribuInternalComboBoxSupport.get().setUserInput(this, userInput)
}

/**
Expand Down Expand Up @@ -67,19 +67,9 @@ public fun <T> MultiSelectComboBox<T>.selectByLabel(
}
}

private val _ComboBox_dataCommunicator: Method by lazy(LazyThreadSafetyMode.PUBLICATION) {
val m = ComboBoxBase::class.java.getDeclaredMethod("getDataCommunicator")
m.isAccessible = true
m
}

@Suppress("UNCHECKED_CAST")
private fun <T> getDataCommunicator(cb: MultiSelectComboBox<T>): DataCommunicator<T>? =
_ComboBox_dataCommunicator.invoke(cb) as DataCommunicator<T>?

@Suppress("UNCHECKED_CAST")
internal val <T> MultiSelectComboBox<T>._dataCommunicator: DataCommunicator<T>
get() = getDataCommunicator(this)
get() = KaribuInternalComboBoxSupport.get().getDataCommunicator(this) as DataCommunicator<T>?
?: fail("${toPrettyString()}: items/dataprovider has not been set")

/**
Expand All @@ -97,3 +87,20 @@ public fun <T> MultiSelectComboBox<T>.getSuggestions(): List<String> {
val items: List<T> = getSuggestionItems()
return items.map { itemLabelGenerator.apply(it) }
}

/**
* Returns the renderer set via [ComboBox.setRenderer].
*/
@Suppress("UNCHECKED_CAST")
public val <T> MultiSelectComboBox<T>._renderer: Renderer<T> get() = KaribuInternalComboBoxSupport.get().getRenderer(this) as Renderer<T>

/**
* Returns the component rendered in [ComboBox] dropdown overlay for given [item].
*
* Fails if the [ComboBox] renderer is something else than [ComponentRenderer].
*/
public fun <T> MultiSelectComboBox<T>._getRenderedComponentFor(item: T): Component {
val r = _renderer
val r2 = r as? ComponentRenderer<*, T> ?: fail("${toPrettyString()}: expected ComponentRenderer but got $r")
return r2.createComponent(item)
}

0 comments on commit 024ab7c

Please sign in to comment.