com.github.mvysny.kaributesting.v10.Grid.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.*
import com.vaadin.flow.component.ClickNotifier
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.checkbox.CheckboxGroup
import com.vaadin.flow.component.combobox.ComboBox
import com.vaadin.flow.component.combobox.ComboBoxBase
import com.vaadin.flow.component.grid.*
import com.vaadin.flow.component.grid.editor.Editor
import com.vaadin.flow.component.listbox.ListBoxBase
import com.vaadin.flow.component.radiobutton.RadioButtonGroup
import com.vaadin.flow.component.select.Select
import com.vaadin.flow.component.treegrid.TreeGrid
import com.vaadin.flow.data.binder.HasDataProvider
import com.vaadin.flow.data.binder.HasItems
import com.vaadin.flow.data.provider.*
import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataProvider
import com.vaadin.flow.data.provider.hierarchy.HierarchicalQuery
import com.vaadin.flow.data.renderer.*
import com.vaadin.flow.function.SerializablePredicate
import com.vaadin.flow.function.ValueProvider
import org.intellij.lang.annotations.RegExp
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.util.stream.Stream
import kotlin.reflect.KProperty1
import kotlin.streams.toList
import kotlin.test.expect
import kotlin.test.fail
/**
* Returns the item on given row. Fails if the row index is invalid. The data provider is
* sorted according to given [sortOrders] (empty by default) and filtered according
* to given [filter] (null by default) first.
* @param rowIndex the row, 0..size - 1
* @return the item at given row.
* @throws AssertionError if the row index is out of bounds.
*/
public fun DataProvider._get(
rowIndex: Int,
sortOrders: List = listOf(),
inMemorySorting: Comparator? = null,
filter: F? = null
): T {
require(rowIndex >= 0) { "rowIndex must be 0 or greater: $rowIndex" }
val fetched: Stream =
fetch(Query(rowIndex, 1, sortOrders, inMemorySorting, filter))
return fetched.toList().firstOrNull()
?: throw AssertionError(
"Requested to get row $rowIndex but the data provider only has ${
_size(
filter
)
} rows matching filter $filter"
)
}
/**
* Returns all items in given data provider, sorted according to given [sortOrders] (empty by default) and filtered according
* to given [filter] (null by default).
* @return the list of items.
*/
@JvmOverloads
public fun DataProvider._findAll(
sortOrders: List = listOf(),
inMemorySorting: Comparator? = null,
filter: F? = null
): List {
val fetched: Stream =
fetch(Query(0, Int.MAX_VALUE, sortOrders, inMemorySorting, filter))
return fetched.toList()
}
/**
* Returns the item on given row. Fails if the row index is invalid. Uses current Grid sorting.
*
* For [TreeGrid] this function returns the x-th displayed row; skips children of collapsed nodes.
* Uses [_rowSequence].
*
* WARNING: Very slow operation for [TreeGrid].
* @param rowIndex the row, 0..size - 1
* @return the item at given row, not null.
*/
public fun Grid._get(rowIndex: Int): T {
require(rowIndex >= 0) { "rowIndex must be 0 or greater: $rowIndex" }
if (this !is TreeGrid && _dataProviderSupportsSizeOp) {
// only perform this check for regular Grid. TreeGrid._fetch()'s Sequence consults size() internally.
val size: Int = _size()
if (rowIndex >= size) {
throw AssertionError("Requested to get row $rowIndex but the data provider only has ${_size()} rows\n${_dump()}")
}
}
return _getOrNull(rowIndex)
?: throw AssertionError("Requested to get row $rowIndex but the data provider returned 0 rows\n${_dump()}")
}
/**
* Returns the item on given row, or null if the [rowIndex] is larger than the number
* of items the data provider can provide. Uses current Grid sorting.
*
* For [TreeGrid] this returns the x-th displayed row; skips children of collapsed nodes.
* Uses [_rowSequence].
*
* WARNING: Very slow operation for [TreeGrid].
* @param rowIndex the row, 0 or larger.
* @return the item at given row or null if the data provider provides less rows.
*/
public fun Grid._getOrNull(rowIndex: Int): T? {
require(rowIndex >= 0) { "rowIndex must be 0 or greater: $rowIndex" }
if (this !is TreeGrid && _dataProviderSupportsSizeOp) {
// only perform this check for regular Grid. TreeGrid._fetch()'s Sequence consults size() internally.
val size: Int = _size()
if (rowIndex >= size) {
return null
}
}
val fetchedItems: List = _fetch(rowIndex, 1)
val fetchedItem = fetchedItems.firstOrNull() ?: return null
// cache the item, to simulate the actual grid communication with its client-side
// counterpart. See https://github.com/mvysny/karibu-testing/issues/124 for more details.
return _getCached(fetchedItem)
}
private fun Grid._getCached(bean: T): T {
val keyMapper = dataCommunicator.keyMapper
if (keyMapper.has(bean)) {
// return the cached version
return keyMapper.get(keyMapper.key(bean))!!
}
// there is no cached version. cache & return.
// first, run all column ValueProviders: if they're not pure, they could modify the bean
// or do something similarly crazy. See https://github.com/mvysny/karibu-testing/issues/124 for more details.
_getFormattedRow(bean)
keyMapper.key(bean) // stores the bean into the cache
return bean
}
/**
* Vaadin 19+ Grids support setting data providers which do not support retrieving
* the number of available rows. See `FetchCallback` for more details.
* @return true if the current data provider supports [_size] retrieval, false
* if not. Returns true for Vaadin 14.
*/
public val Grid<*>._dataProviderSupportsSizeOp: Boolean
get() = dataCommunicator.isDefinedSize
/**
* Returns items in given range from Grid's data provider. Uses current Grid sorting.
*
* For [TreeGrid] this walks the [_rowSequence].
*
* The Grid never sets any filters into the data provider, however any
* ConfigurableFilterDataProvider will automatically apply its filters.
*
* WARNING: Very slow operation for [TreeGrid].
*/
public fun Grid._fetch(offset: Int, limit: Int): List = when (this) {
is TreeGrid -> this._rowSequence().drop(offset).take(limit).toList()
else -> dataCommunicator.fetch(offset, limit)
}
public val DataCommunicator<*>._saneFetchLimit: Int
get() = Int.MAX_VALUE / 1000
public val Grid<*>._saneFetchLimit: Int get() = dataCommunicator._saneFetchLimit
private val _DataCommunicator_fetchFromProvider: Method =
DataCommunicator::class.java.getDeclaredMethod(
"fetchFromProvider",
Int::class.java,
Int::class.java
).apply {
isAccessible = true
}
private val _DataCommunicator_setPagingEnabled: Method? =
DataCommunicator::class.java.declaredMethods.firstOrNull { it.name == "setPagingEnabled" }
private val _DataCommunicator_isPagingEnabled: Method? =
DataCommunicator::class.java.declaredMethods.firstOrNull { it.name == "isPagingEnabled" }
/**
* Returns items in given range from this data communicator. Uses current Grid sorting.
* Any ConfigurableFilterDataProvider will automatically apply its filters.
*
* This is an internal stuff, most probably you wish to call [_fetch].
*/
public fun DataCommunicator.fetch(offset: Int, limit: Int): List {
require(limit <= _saneFetchLimit) { "Vaadin doesn't handle fetching of many items very well unfortunately. The sane limit is $_saneFetchLimit but you asked for $limit" }
if (_DataCommunicator_setPagingEnabled != null && _DataCommunicator_isPagingEnabled != null) {
if (_DataCommunicator_isPagingEnabled.invoke(this) as Boolean) {
// make sure the DataCommunicator is not in paged mode: https://github.com/mvysny/karibu-testing/issues/99
_DataCommunicator_setPagingEnabled.invoke(this, false)
}
}
@Suppress("UNCHECKED_CAST")
val fetched: Stream = _DataCommunicator_fetchFromProvider.invoke(
this,
offset,
limit
) as Stream
return fetched.toList()
}
/**
* Returns all items from this data communicator. Uses current Grid sorting.
* Any ConfigurableFilterDataProvider will automatically apply its filters.
*
* This is an internal stuff, most probably you wish to call [_fetch].
*/
public fun DataCommunicator.fetchAll(): List =
fetch(0, _saneFetchLimit)
/**
* Returns all items in given data provider. Uses current Grid sorting.
*
* For [TreeGrid] this returns all displayed rows; skips children of collapsed nodes.
*
* The Grid never sets any filters into the data provider, however any
* ConfigurableFilterDataProvider will automatically apply its filters.
*
* @return the list of items.
*/
public fun Grid._findAll(): List = _fetch(0, _saneFetchLimit)
/**
* Returns the number of items in this data provider.
*
* In case of [HierarchicalDataProvider]
* this returns the number of ALL items including all leafs.
*/
public fun DataProvider._size(filter: F? = null): Int {
if (this is HierarchicalDataProvider) {
return this._size(null, filter)
}
return size(Query(filter))
}
/**
* Returns the number of items in this data provider, including child items.
* The function traverses recursively until all children are found; then a total size
* is returned. The function uses [HierarchicalDataProvider.size] mostly, but
* also uses [HierarchicalDataProvider.fetchChildren] to discover children.
* Only children matching [filter] are considered for recursive computation of
* the size.
*
* Note that this can differ to `Grid._size()` since `Grid._size()` ignores children
* of collapsed tree nodes.
* @param root start with this item; defaults to null to iterate all items
* @param filter filter to pass to [HierarchicalQuery]
*/
@JvmOverloads
public fun HierarchicalDataProvider._size(
root: T? = null,
filter: F? = null
): Int =
_rowSequence(root, filter = filter).count()
public fun DataCommunicator<*>._size(): Int = dataProviderSize
/**
* Returns the number of items in this Grid.
*
* For [TreeGrid] this computes the number of items the [TreeGrid] is actually showing on-screen,
* ignoring children of collapsed nodes.
*
* A very slow operation for [TreeGrid] since it walks through all items returned by [_rowSequence].
*
* If [_dataProviderSupportsSizeOp] is false, this function will fetch all the data
* and count the result returned, which is also very slow.
*/
public fun Grid<*>._size(): Int {
if (this is TreeGrid<*>) {
return this._size()
}
if (!_dataProviderSupportsSizeOp) {
return _findAll().size
}
return dataCommunicator._size()
}
/**
* Gets a [Grid.Column] of this grid by its [columnKey].
* @throws AssertionError if no such column exists.
*/
public fun Grid._getColumnByKey(columnKey: String): Grid.Column =
getColumnByKey(columnKey)
?: throw AssertionError("${toPrettyString()}: No such column with key '$columnKey'; available columns: ${columns.mapNotNull { it.key }}")
/**
* Gets a [Grid.Column] of this grid by its [columnKey].
* @throws AssertionError if no such column exists.
*/
public fun Grid._getColumnByHeader(header: String): Grid.Column =
columns.firstOrNull { it.header2 == header }
?: throw AssertionError("${toPrettyString()}: No such column with header '$header'; available columns: ${columns.mapNotNull { it.header2 }}")
/**
* Performs a click on a [ClickableRenderer] in given [Grid] cell. Only supports the following scenarios:
* * [ClickableRenderer]
* * [ComponentRenderer] which renders a [Button] or a [ClickNotifier].
*
* The `click` closure is no longer supported - please see https://github.com/mvysny/karibu-testing/issues/67 for more details.
* @param rowIndex the row index, 0 or higher.
* @param columnKey the column key [Grid.Column.getKey]
* @throws AssertionError if the renderer is not [ClickableRenderer] nor [ComponentRenderer].
*/
public fun Grid._clickRenderer(rowIndex: Int, columnKey: String) {
_expectEditableByUser()
val column: Grid.Column = _getColumnByKey(columnKey)
val renderer: Renderer? = column.renderer
val item: T = _get(rowIndex)
if (renderer is ClickableRenderer<*>) {
@Suppress("UNCHECKED_CAST")
(renderer as ClickableRenderer).onClick(item)
} else if (renderer is ComponentRenderer<*, *>) {
val component: Component =
(renderer as ComponentRenderer<*, T>).createComponent(item)
if (component is Button) {
component._click()
} else if (component is ClickNotifier<*>) {
component._click()
} else {
// don't try to do anything smart here since things will break silently for the customer as they upgrade Vaadin version
// https://github.com/mvysny/karibu-testing/issues/67
fail("${this.toPrettyString()} column key='$columnKey': ComponentRenderer produced ${component.toPrettyString()} which is not a button nor a ClickNotifier - please use _getCellComponent() instead")
}
} else {
fail("${this.toPrettyString()} column key='$columnKey' has renderer $renderer which is not supported by this method")
}
}
/**
* Retrieves a component produced by [ComponentRenderer] in given [Grid] cell. Fails if the
* renderer is not a [ComponentRenderer].
* @param rowIndex the row index, 0 or higher.
* @param columnKey the column key [Grid.Column.getKey]
* @throws IllegalStateException if the renderer is not [ComponentRenderer].
*/
public fun Grid._getCellComponent(
rowIndex: Int,
columnKey: String
): Component {
val column: Grid.Column = _getColumnByKey(columnKey)
val renderer: Renderer? = column.renderer
if (renderer !is ComponentRenderer<*, *>) {
fail("${this.toPrettyString()} column key='$columnKey' uses renderer $renderer but we expect a ComponentRenderer here")
}
if (renderer is NativeButtonRenderer<*>) {
fail("${this.toPrettyString()} column key='$columnKey' uses NativeButtonRenderer which is not supported by this function")
}
val item: T = _get(rowIndex)
val component: Component =
(renderer as ComponentRenderer<*, T>).createComponent(item)
return component
}
/**
* Returns the formatted value of given column as a String. Uses [getPresentationValue]
* and converts the result to string (even if the result is a [Component]).
* @param rowIndex the row index, 0 or higher.
* @param columnKey the column ID.
*/
@Suppress("UNCHECKED_CAST")
public fun Grid._getFormatted(
rowIndex: Int,
columnKey: String
): String {
val rowObject: T = _get(rowIndex)
val column: Grid.Column = _getColumnByKey(columnKey)
return column._getFormatted(rowObject)
}
/**
* Returns the formatted value as a String. Uses [getPresentationValue]
* and converts the result to string (even if the result is a [Component]).
* @param rowObject the bean
*/
@Suppress("UNCHECKED_CAST")
public fun Grid.Column._getFormatted(rowObject: T): String =
getPresentationValue(rowObject).toString()
/**
* Returns the formatted row as a list of Strings, one for every visible column.
* Uses [_getFormatted].
* @param rowObject the bean
*/
public fun Grid._getFormattedRow(rowObject: T): List =
columns.filter { it.isVisible }.map { it._getFormatted(rowObject) }
/**
* Returns the formatted row as a list of Strings, one for every visible column.
* Uses [_getFormatted]. Fails if the [rowIndex] is not within the limits.
* @param rowIndex the index of the row, 0..size-1.
*/
public fun Grid._getFormattedRow(rowIndex: Int): List {
val rowObject: T = _get(rowIndex)
return _getFormattedRow(rowObject)
}
/**
* Returns the formatted row as a list of Strings, one for every visible column.
* Uses [_getFormatted]. Returns null if the [rowIndex] is not within the limits.
* @param rowIndex the index of the row, 0-based.
*/
public fun Grid._getFormattedRowOrNull(rowIndex: Int): List? {
val rowObject: T = _getOrNull(rowIndex) ?: return null
return _getFormattedRow(rowObject)
}
private val _ColumnPathRenderer_provider: Field by lazy(LazyThreadSafetyMode.PUBLICATION) {
val f = ColumnPathRenderer::class.java.getDeclaredField("provider")
f.isAccessible = true
f
}
@Suppress("UNCHECKED_CAST")
public val ColumnPathRenderer.valueProvider: ValueProvider
get() = _ColumnPathRenderer_provider.get(this) as ValueProvider
/**
* Returns the output of renderer set for this column for given [rowObject] formatted as close as possible
* to the client-side output, using [Grid.Column.renderer].
*/
@Suppress("UNCHECKED_CAST")
public fun Grid.Column.getPresentationValue(rowObject: T): Any? {
val renderer: Renderer = this.renderer
if (renderer is ColumnPathRenderer) {
val value: Any? = renderer.valueProvider.apply(rowObject)
return value.toString()
}
return renderer._getPresentationValue(rowObject)
}
private fun Grid.getSortIndicator(column: Grid.Column): String {
val so: GridSortOrder? = sortOrder.firstOrNull { it.sorted == column }
return when {
so == null -> ""
so.direction == SortDirection.ASCENDING -> "v"
else -> "^"
}
}
/**
* Dumps given range of [rows] of the Grid, formatting the values using the [_getFormatted] function. The output example:
* ```
* --[Name]--[Age]--[Occupation]--
* 0: John, 25, Service Worker
* 1: Fred, 40, Supervisor
* --and 198 more
* ```
*/
@JvmOverloads
public fun Grid._dump(rows: IntRange = 0..9): String =
buildString {
append(toPrettyString()).append('\n')
val visibleColumns: List> =
columns.filter { it.isVisible }
visibleColumns.map { "[${it.header2}]${getSortIndicator(it)}" }
.joinTo(this, prefix = "--", separator = "-", postfix = "--\n")
val dsIndices: IntRange
val displayIndices: Set
if (this@_dump is TreeGrid) {
val tree: PrettyPrintTree = this@_dump._dataSourceToPrettyTree()
val lines = tree.print().split('\n').filterNotBlank().drop(1)
dsIndices = lines.indices
displayIndices = rows.intersect(dsIndices)
for (i in displayIndices) {
append("$i: ${lines[i]}\n")
}
val andMore = dsIndices.size - displayIndices.size
if (andMore > 0) {
append("--and $andMore more\n")
}
} else if (_dataProviderSupportsSizeOp) {
dsIndices = 0 until _size()
displayIndices = rows.intersect(dsIndices)
for (i in displayIndices) {
_getFormattedRow(i).joinTo(
this,
prefix = "$i: ",
postfix = "\n"
)
}
val andMore = dsIndices.size - displayIndices.size
if (andMore > 0) {
append("--and $andMore more\n")
}
} else {
var rowsOutputted = 0
for (i in rows) {
val row = _getFormattedRowOrNull(i) ?: break
row.joinTo(this, prefix = "$i: ", postfix = "\n")
rowsOutputted++
}
if (rowsOutputted == rows.size) {
append("--and possibly more\n")
} else {
append("--\n")
}
}
}
/**
* Asserts that this grid's provider returns given [count] of items. If not,
* an [AssertionError] is thrown with the Grid [_dump].
*/
public fun Grid<*>.expectRows(count: Int) {
val actual = _size()
if (actual != count) {
throw AssertionError("${this.toPrettyString()}: expected $count rows but got $actual\n${_dump()}")
}
}
/**
* Asserts that this grid's [rowIndex] row is formatted as expected.
* @param row the expected row formatting, one value per every visible column. Must match the value returned by [_getFormattedRow].
*/
public fun Grid<*>.expectRow(rowIndex: Int, vararg row: String) {
val expected: List = row.toList()
val actual: List = _getFormattedRow(rowIndex)
if (expected != actual) {
throw AssertionError("${this.toPrettyString()} at $rowIndex: expected $expected but got $actual\n${_dump()}")
}
}
/**
* Asserts that this grid's [rowIndex] row is formatted as expected.
* @param row the expected row formatting, one regex for every visible column. Must match the value returned by [_getFormattedRow].
*/
public fun Grid<*>.expectRowRegex(rowIndex: Int, @RegExp vararg row: String) {
val expected: List = row.map { it.toRegex() }
val actual: List = _getFormattedRow(rowIndex)
var matches = expected.size == actual.size
if (matches) {
matches =
expected.filterIndexed { index, regex -> !regex.matches(actual[index]) }
.isEmpty()
}
if (!matches) {
throw AssertionError("${this.toPrettyString()} at $rowIndex: expected $expected but got $actual\n${_dump()}")
}
}
/**
* Returns `com.vaadin.flow.component.grid.AbstractColumn`
*/
@Suppress("ConflictingExtensionProperty") // conflicting property is "protected"
internal val HeaderRow.HeaderCell.column: Any
get() = _AbstractCell_getColumn.invoke(this)
private val abstractCellClass: Class<*> =
Class.forName("com.vaadin.flow.component.grid.AbstractRow\$AbstractCell")
private val abstractColumnClass: Class<*> =
Class.forName("com.vaadin.flow.component.grid.AbstractColumn")
private val _AbstractCell_getColumn: Method by lazy(LazyThreadSafetyMode.PUBLICATION) {
val m: Method = abstractCellClass.getDeclaredMethod("getColumn")
m.isAccessible = true
m
}
/**
* Returns `com.vaadin.flow.component.grid.AbstractColumn`
*/
@Suppress("ConflictingExtensionProperty") // conflicting property is "protected"
private val FooterRow.FooterCell.column: Any
get() = _AbstractCell_getColumn.invoke(this)
/**
* Retrieves the cell for given [property]; it matches [Grid.Column.getKey] to [KProperty1.name].
* @return the corresponding cell
* @throws IllegalArgumentException if no such column exists.
*/
public fun HeaderRow.getCell(property: KProperty1<*, *>): HeaderRow.HeaderCell =
getCell(property.name)
/**
* Retrieves the cell for given [Grid.Column.getKey].
* @return the corresponding cell
* @throws IllegalArgumentException if no such column exists.
*/
public fun HeaderRow.getCell(key: String): HeaderRow.HeaderCell {
val cell: HeaderRow.HeaderCell? =
cells.firstOrNull { (it.column as Grid.Column<*>).key == key }
require(cell != null) { "This grid has no property named ${key}: $cells" }
return cell
}
private val _AbstractColumn_getBottomLevelColumn: Method by lazy(
LazyThreadSafetyMode.PUBLICATION
) {
val method: Method =
abstractColumnClass.getDeclaredMethod("getBottomLevelColumn")
method.isAccessible = true
method
}
/**
* Retrieves column key from the `AbstractColumn` receiver. The problem here is that receiver can be `ColumnGroup` which doesn't have
* a key.
*/
private val Any.columnKey: String?
get() {
abstractColumnClass.cast(this)
val gridColumn: Grid.Column<*> =
_AbstractColumn_getBottomLevelColumn.invoke(this) as Grid.Column<*>
return gridColumn.key
}
/**
* Retrieves the cell for given [property]; it matches [Grid.Column.getKey] to [KProperty1.name].
* @return the corresponding cell
* @throws IllegalArgumentException if no such column exists.
*/
public fun FooterRow.getCell(property: KProperty1<*, *>): FooterRow.FooterCell =
getCell(property.name)
/**
* Retrieves the cell for given [Grid.Column.getKey].
* @return the corresponding cell
* @throws IllegalArgumentException if no such column exists.
*/
public fun FooterRow.getCell(key: String): FooterRow.FooterCell {
val cell: FooterRow.FooterCell? =
cells.firstOrNull { it.column.columnKey == key }
require(cell != null) { "This grid has no property named ${key}: $cells" }
return cell
}
/**
* Sorts given grid. Affects [_findAll], [_get] and other data-fetching functions.
*/
@Deprecated("Use _sort()")
public fun Grid.sort(vararg sortOrder: QuerySortOrder) {
_expectEditableByUser()
sort(sortOrder.map {
GridSortOrder(
_getColumnByKey(it.sorted),
it.direction
)
})
}
/**
* Sorts given grid. Affects [_findAll], [_get] and other data-fetching functions.
*/
public fun Grid._sort(vararg sortOrder: QuerySortOrder) {
_expectEditableByUser()
sort(sortOrder.map {
GridSortOrder(
_getColumnByKey(it.sorted),
it.direction
)
})
}
/**
* Sorts given grid. Affects [_findAll], [_get] and other data-fetching functions.
*/
public fun Grid._sortByKey(columnKey: String, direction: SortDirection) {
_sort(QuerySortOrder(columnKey, direction))
}
/**
* Sorts given grid. Affects [_findAll], [_get] and other data-fetching functions.
*/
public fun Grid._sortByHeader(header: String, direction: SortDirection) {
_expectEditableByUser()
sort(listOf(GridSortOrder(_getColumnByHeader(header), direction)))
}
/**
* Emulates a click on a Grid's row:
* * Fires the [ItemClickEvent] event for given [rowIndex] which invokes all item click listeners registered via
* [Grid.addItemClickListener].
* * May also fire [com.vaadin.flow.data.selection.SelectionEvent].
* @param button the id of the pressed mouse button
* @param ctrlKey `true` if the control key was down when the event was fired, `false` otherwise
* @param shiftKey `true` if the shift key was down when the event was fired, `false` otherwise
* @param altKey `true` if the alt key was down when the event was fired, `false` otherwise
* @param metaKey `true` if the meta key was down when the event was fired, `false` otherwise
*/
@JvmOverloads
public fun Grid._clickItem(
rowIndex: Int, button: Int = 1, ctrlKey: Boolean = false,
shiftKey: Boolean = false, altKey: Boolean = false, metaKey: Boolean = false
) {
_clickItem(rowIndex, null, button, ctrlKey, shiftKey, altKey, metaKey)
}
/**
* Emulates a click on a Grid's row:
* * Fires the [ItemClickEvent] event for given [rowIndex] and a [column] which invokes all item click listeners
* registered via [Grid.addItemClickListener].
* * May also fire [com.vaadin.flow.data.selection.SelectionEvent].
* @param button the id of the pressed mouse button
* @param column optional column to be clicked
* @param ctrlKey `true` if the control key was down when the event was fired, `false` otherwise
* @param shiftKey `true` if the shift key was down when the event was fired, `false` otherwise
* @param altKey `true` if the alt key was down when the event was fired, `false` otherwise
* @param metaKey `true` if the meta key was down when the event was fired, `false` otherwise
*/
@JvmOverloads
public fun Grid._clickItem(
rowIndex: Int,
column: Grid.Column<*>?,
button: Int = 1,
ctrlKey: Boolean = false,
shiftKey: Boolean = false,
altKey: Boolean = false,
metaKey: Boolean = false
) {
_expectEditableByUser()
// fire SelectionEvent if need be: https://github.com/mvysny/karibu-testing/issues/96
val item: T = _get(rowIndex)
if (selectionMode == Grid.SelectionMode.SINGLE) {
val selectedItem: T? = selectedItems.firstOrNull()
if (selectedItem != item) {
select(item)
} else {
// clicking on a selected item will de-select it.
deselectAll()
}
}
// fire ItemClickEvent
val itemKey: String = dataCommunicator.keyMapper.key(item)
val internalColumnId = column?._internalId
val event = ItemClickEvent(
this,
true,
itemKey,
internalColumnId,
-1,
-1,
-1,
-1,
1,
button,
ctrlKey,
shiftKey,
altKey,
metaKey
)
_fireEvent(event)
}
/**
* Fires the [ItemClickEvent] event for given [rowIndex] and a [columnKey] which invokes all item click listeners
* registered via [Grid.addItemClickListener].
* @param button the id of the pressed mouse button
* @param columnKey the key of the column to be clicked
* @param ctrlKey `true` if the control key was down when the event was fired, `false` otherwise
* @param shiftKey `true` if the shift key was down when the event was fired, `false` otherwise
* @param altKey `true` if the alt key was down when the event was fired, `false` otherwise
* @param metaKey `true` if the meta key was down when the event was fired, `false` otherwise
*/
@JvmOverloads
public fun Grid._clickItem(
rowIndex: Int, columnKey: String, button: Int = 1, ctrlKey: Boolean = false,
shiftKey: Boolean = false, altKey: Boolean = false, metaKey: Boolean = false
) {
_clickItem(
rowIndex,
_getColumnByKey(columnKey),
button,
ctrlKey,
shiftKey,
altKey,
metaKey
)
}
/**
* Fires the [ItemDoubleClickEvent] event for given [rowIndex] which invokes all item click listeners registered via
* [Grid.addItemDoubleClickListener].
* @param button the id of the pressed mouse button
* @param ctrlKey `true` if the control key was down when the event was fired, `false` otherwise
* @param shiftKey `true` if the shift key was down when the event was fired, `false` otherwise
* @param altKey `true` if the alt key was down when the event was fired, `false` otherwise
* @param metaKey `true` if the meta key was down when the event was fired, `false` otherwise
*/
@JvmOverloads
public fun Grid._doubleClickItem(
rowIndex: Int, button: Int = 1, ctrlKey: Boolean = false,
shiftKey: Boolean = false, altKey: Boolean = false, metaKey: Boolean = false
) {
_expectEditableByUser()
val itemKey: String = dataCommunicator.keyMapper.key(_get(rowIndex))
val event = ItemDoubleClickEvent(
this,
true,
itemKey,
null,
-1,
-1,
-1,
-1,
1,
button,
ctrlKey,
shiftKey,
altKey,
metaKey
)
_fireEvent(event)
}
/**
* Returns a sequence which walks over all rows the [TreeGrid] is actually showing.
* The sequence will *skip* children of collapsed nodes.
*
* Iterating the entire sequence is a very slow operation since it will repeatedly
* poll [HierarchicalDataProvider] for list of children.
*
* Honors current grid ordering.
*/
public fun TreeGrid._rowSequence(filter: SerializablePredicate? = null): Sequence {
val isExpanded: (T) -> Boolean = { item: T -> isExpanded(item) }
return dataProvider._rowSequence(null, isExpanded, filter)
}
/**
* Returns a sequence which walks over all rows the [TreeGrid] is actually showing.
* The sequence will *skip* children of collapsed nodes.
*
* Iterating the entire sequence is a very slow operation since it will repeatedly
* poll [HierarchicalDataProvider] for list of children.
*
* Honors current grid ordering.
* @param root start with this item; defaults to null to iterate all items
* @param isExpanded if returns null for an item, children of that item are skipped
* @param filter filter to pass to [HierarchicalQuery]
*/
@JvmOverloads
public fun HierarchicalDataProvider._rowSequence(
root: T? = null,
isExpanded: (T) -> Boolean = { true },
filter: F? = null
): Sequence {
fun getChildrenOf(item: T?): List =
if (item == null || isExpanded(item)) {
checkedFetch(HierarchicalQuery(filter, item))
} else {
listOf()
}
fun itemSubtreeSequence(item: T): Sequence =
DepthFirstTreeIterator(item) { getChildrenOf(it) }.asSequence()
val roots: List = getChildrenOf(root)
return roots.map { itemSubtreeSequence(it) }.asSequence().flatten()
}
/**
* Returns the number of items the [TreeGrid] is actually showing. For example
* it doesn't count in children of collapsed nodes.
*
* A very slow operation since it walks through all items returned by [_rowSequence].
*/
public fun TreeGrid<*>._size(): Int = _rowSequence().count()
private fun HierarchicalDataProvider.checkedSize(query: HierarchicalQuery): Int {
if (query.parent != null && !hasChildren(query.parent)) return 0
val result: Int = size(query)
check(result >= 0) { "size($query) returned negative count: $result" }
return result
}
private fun HierarchicalDataProvider.checkedFetch(query: HierarchicalQuery): List =
when {
checkedSize(query) == 0 -> listOf()
else -> fetchChildren(query).toList()
}
public fun TreeGrid._dataSourceToPrettyTree(): PrettyPrintTree {
fun getChildrenOf(item: T?): List =
if (item == null || isExpanded(item)) {
dataProvider.checkedFetch(
HierarchicalQuery?>(
null,
item
)
)
} else {
listOf()
}
fun toPrettyTree(item: T): PrettyPrintTree {
val self: String = _getFormattedRow(item).joinToString(postfix = "\n")
val children: List = getChildrenOf(item)
return PrettyPrintTree(
self,
children.map { toPrettyTree(it) }.toMutableList()
)
}
val roots: List = getChildrenOf(null)
return PrettyPrintTree(
"TreeGrid",
roots.map { toPrettyTree(it) }.toMutableList()
)
}
@Suppress("UNCHECKED_CAST")
public fun TreeGrid._getRootItems(): List =
dataProvider.fetch(HierarchicalQuery(null, null)).toList()
/**
* Expands all nodes. May invoke massive data loading.
*/
@JvmOverloads
public fun TreeGrid._expandAll(depth: Int = 100) {
expandRecursively(_getRootItems(), depth)
}
/**
* Returns the data provider currently set to this Component.
*
* Works both with Vaadin 16 and Vaadin 17: Vaadin 17 components no longer implement HasItems.
*/
public val Component.dataProvider: DataProvider<*, *>?
get() = when {
// until https://github.com/vaadin/flow/issues/6296 is resolved
this is Grid<*> -> this.dataProvider
this is Select<*> -> this.dataProvider
this is ListBoxBase<*, *, *> -> this.dataProvider
this is RadioButtonGroup<*> -> this.dataProvider
this is CheckboxGroup<*> -> this.dataProvider
this is ComboBoxBase<*, *, *> -> this.dataProvider
else -> this.getDataProviderViaReflection()
}
private fun Component.getDataProviderViaReflection(): DataProvider<*, *>? {
if (this is HasDataProvider<*>) {
val mGetDataProvider: Method? =
this.javaClass.methods.firstOrNull { it.name == "getDataProvider" && it.parameterCount == 0 && it.returnType == DataProvider::class.java }
if (mGetDataProvider != null) {
return mGetDataProvider.invoke(this) as DataProvider<*, *>?
}
}
return null
}
/**
* Call this instead of [Editor.editItem] - this function makes sure that the editor opening is
* mocked properly, calls the editor bindings, and fires the editor-open-event.
*/
public fun Editor._editItem(item: T) {
grid._expectEditableByUser()
expect(
true,
"${grid.toPrettyString()} is not attached, editItem() would do nothing. Make sure the Grid is attached to an UI"
) {
grid.isAttached
}
editItem(item)
MockVaadin.clientRoundtrip()
}
/**
* Clears the selection and selects only given [item]. Fails if selection
* is disabled in the Grid ([Grid.SelectionMode.NONE] is set).
*/
public fun Grid._select(item: T) {
_expectEditableByUser()
deselectAll()
// fails properly if the Grid doesn't support selection.
selectionModel.selectFromClient(item)
}
/**
* Clears the selection and select a single item at given [index]. Fails if the Grid
* doesn't support selection ([Grid.SelectionMode.NONE] is set).
*/
public fun Grid._selectRow(index: Int) {
_expectEditableByUser()
_select(_get(index))
}
/**
* Selects all items in the Grid; runs the same code as when the "select all" checkbox is checked.
* Fails if the grid is not multi-select or the "select all" checkbox is hidden.
*/
public fun Grid._selectAll() {
_expectEditableByUser()
expect(
Grid.SelectionMode.MULTI,
"Expected multi-select but got $selectionMode"
) { selectionMode }
expect(true, "SelectAll checkbox not visible") {
(selectionModel as AbstractGridMultiSelectionModel).isSelectAllCheckboxVisible
}
// call AbstractGridMultiSelectionModel.clientSelectAll()
val clientSelectAllMethod =
AbstractGridMultiSelectionModel::class.java.getDeclaredMethod("clientSelectAll")
clientSelectAllMethod.isAccessible = true
clientSelectAllMethod.invoke(selectionModel)
}
/**
* Simulates user resizing column to [newWidth] pixels. Fires [ColumnResizeEvent] and updates [Grid.Column.setWidth].
* You can use [_getColumnByKey] to lookup column by key.
*/
public fun Grid._fireColumnResizedEvent(column: Grid.Column, newWidth: Int) {
require(column.grid == this) { "Column ${column.toPrettyString()} is from grid ${column.grid.toPrettyString()} but this grid is ${toPrettyString()}" }
require(newWidth >= 0) { "The width must be 0 or higher but was $newWidth" }
require(column.isResizable) { "The column ${column.toPrettyString()} is not resizable" }
val id = column._internalId
column.setWidth("${newWidth}px")
_fireEvent(ColumnResizeEvent(this, true, id))
}