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.vaadin.flow.component.Component
import com.vaadin.flow.component.button.Button
import com.vaadin.flow.component.grid.*
import com.vaadin.flow.component.treegrid.TreeGrid
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.ClickableRenderer
import com.vaadin.flow.data.renderer.ComponentRenderer
import com.vaadin.flow.data.renderer.Renderer
import java.util.stream.Stream
import kotlin.reflect.KProperty1
import kotlin.streams.toList
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.
*/
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 = 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.
*/
fun DataProvider._findAll(sortOrders: List = listOf(), inMemorySorting: Comparator? = null, filter: F? = null): List {
val fetched = 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 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.
*/
fun Grid._get(rowIndex: Int): T {
require(rowIndex >= 0) { "rowIndex must be 0 or greater: $rowIndex" }
val fetched = _fetch(rowIndex, 1)
return fetched.firstOrNull()
?: throw AssertionError("Requested to get row $rowIndex but the data provider only has ${_size()}")
}
/**
* For [TreeGrid] this walks the [_rowSequence].
*
* WARNING: Very slow operation for [TreeGrid].
*/
fun Grid._fetch(offset: Int, limit: Int): List = when(this) {
is TreeGrid -> this._rowSequence().drop(offset).take(limit).toList()
else -> dataCommunicator.fetch(offset, limit)
}
fun DataCommunicator.fetch(offset: Int, limit: Int): List {
val m = DataCommunicator::class.java.getDeclaredMethod("fetchFromProvider", Int::class.java, Int::class.java).apply { isAccessible = true }
@Suppress("UNCHECKED_CAST") val fetched: Stream = m.invoke(this, offset, limit) as Stream
return fetched.toList()
}
/**
* Returns all items in given data provider. Uses current Grid sorting.
*
* For [TreeGrid] this returns all displayed rows; skips children of collapsed nodes.
*
* @return the list of items.
*/
fun Grid._findAll(): List = _fetch(0, Int.MAX_VALUE)
/**
* Returns the number of items in this data provider.
*
* In case of [HierarchicalDataProvider]
* this returns the number of ALL items including all leafs.
*/
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.
*/
fun HierarchicalDataProvider._size(parent: T? = null, filter: F? = null): Int {
val query = HierarchicalQuery(filter, parent)
val countOfDirectChildren: Int = size(query)
val children: List = fetchChildren(query).toList()
val recursiveChildrenSizes: Int = children.sumBy { _size(it, filter) }
return countOfDirectChildren + recursiveChildrenSizes
}
/**
* 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].
*/
fun Grid<*>._size(): Int {
if (this is TreeGrid<*>) {
return this._size()
}
val m = DataCommunicator::class.java.getDeclaredMethod("getDataProviderSize").apply { isAccessible = true }
return m.invoke(dataCommunicator) as Int
}
/**
* Gets a [Grid.Column] of this grid by its [columnKey].
* @throws AssertionError if no such column exists.
*/
fun Grid._getColumnByKey(columnKey: String): Grid.Column = getColumnByKey(columnKey)
?: throw AssertionError("${toPrettyString()}: No such column with key '$columnKey'; available columns: ${columns.mapNotNull { it.key }}")
/**
* Performs a click on a [ClickableRenderer] in given [Grid] cell. Only supports the following scenarios:
* * [ClickableRenderer]
* * [ComponentRenderer] which renders a [Button]
* * [ComponentRenderer] which renders something else than a [Button]; then you need to provide the [click] closure which can click on such a component.
* @param rowIndex the row index, 0 or higher.
* @param columnKey the column key [Grid.Column.getKey]
* @param click if [ComponentRenderer] doesn't produce a button, this is called, to click the component returned by the [ComponentRenderer]
* @throws IllegalStateException if the renderer is not [ClickableRenderer] nor [ComponentRenderer].
*/
@JvmOverloads
fun Grid._clickRenderer(rowIndex: Int, columnKey: String,
click: (Component) -> Unit = { component ->
fail("${this.toPrettyString()} column $columnKey: ClickableRenderer produced ${component.toPrettyString()} which is not a button - you need to provide your own custom 'click' closure which knows how to click this component")
}) {
checkEditableByUser()
val column = _getColumnByKey(columnKey)
val 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 = (renderer as ComponentRenderer<*, T>).createComponent(item)
if (component is Button) {
component._click()
} else {
click(component)
}
} else {
fail("${this.toPrettyString()} column $columnKey has renderer $renderer which is not supported by this method")
}
}
/**
* Returns the formatted value as a String. Does not use renderer to render the value - simply calls value provider and presentation provider
* and converts the result to string (even if the result is a [Component]).
* @param rowIndex the row index, 0 or higher.
* @param columnId the column ID.
*/
@Suppress("UNCHECKED_CAST")
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. Does not use renderer to render the value - simply calls value provider and presentation provider
* and converts the result to string (even if the result is a [Component]).
* @param rowIndex the row index, 0 or higher.
*/
@Suppress("UNCHECKED_CAST")
fun Grid.Column._getFormatted(rowObject: T): String = "${getPresentationValue(rowObject)}"
fun Grid._getFormattedRow(rowObject: T): List =
columns.filter { it.isVisible }.map { it._getFormatted(rowObject) }
fun Grid._getFormattedRow(rowIndex: Int): List {
val rowObject: T = _get(rowIndex)
return _getFormattedRow(rowObject)
}
@Suppress("UNCHECKED_CAST")
fun Grid.Column.getPresentationValue(rowObject: T): Any? {
val valueProviders = renderer.valueProviders
val valueProvider = valueProviders[internalId2] ?: return null
// there is no value provider for NativeButtonRenderer, just return null
val value = valueProvider.apply(rowObject)
return "" + value
}
@Suppress("UNCHECKED_CAST")
private val Grid.Column.internalId2: String
get() = Grid.Column::class.java.getDeclaredMethod("getInternalId").run {
isAccessible = true
invoke(this@internalId2) as String
}
val Renderer<*>.template: String
get() {
val template = Renderer::class.java.getDeclaredField("template").run {
isAccessible = true
get(this@template) as String?
}
return template ?: ""
}
/**
* Sets and retrieves the column header as set by [Grid.Column.setHeader] (String). The result value is undefined if a component has been set as the header.
*/
var Grid.Column.header2: String
get() {
// nasty reflection. Added a feature request to have this: https://github.com/vaadin/vaadin-grid-flow/issues/567
val e: Renderer<*>? = Class.forName("com.vaadin.flow.component.grid.AbstractColumn").getDeclaredField("headerRenderer").run {
isAccessible = true
get(this@header2) as Renderer<*>?
}
return e?.template ?: ""
}
set(value) {
setHeader(value)
}
private fun Grid.getSortIndicator(column: Grid.Column): String {
val so = 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
* ```
*/
fun Grid._dump(rows: IntRange = 0..10): String = buildString {
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")
}
} else {
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")
}
}
fun Grid<*>.expectRows(count: Int) {
if (_size() != count) {
throw AssertionError("${this.toPrettyString()}: expected $count rows\n${_dump()}")
}
}
fun Grid<*>.expectRow(rowIndex: Int, vararg row: String) {
val expected = row.toList()
val actual = _getFormattedRow(rowIndex)
if (expected != actual) {
throw AssertionError("${this.toPrettyString()} at $rowIndex: expected $expected but got $actual\n${_dump()}")
}
}
/**
* Returns `com.vaadin.flow.component.grid.AbstractColumn`
*/
internal val HeaderRow.HeaderCell.column: Any
get() {
val getColumn = abstractCellClass.getDeclaredMethod("getColumn")
getColumn.isAccessible = true
return getColumn.invoke(this)
}
private val abstractCellClass = Class.forName("com.vaadin.flow.component.grid.AbstractRow\$AbstractCell")
private val abstractColumnClass = Class.forName("com.vaadin.flow.component.grid.AbstractColumn")
/**
* Returns `com.vaadin.flow.component.grid.AbstractColumn`
*/
private val FooterRow.FooterCell.column: Any
get() {
val getColumn = abstractCellClass.getDeclaredMethod("getColumn")
getColumn.isAccessible = true
return 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.
*/
fun HeaderRow.getCell(property: KProperty1<*, *>): HeaderRow.HeaderCell {
val cell = cells.firstOrNull { (it.column as Grid.Column<*>).key == property.name }
require(cell != null) { "This grid has no property named ${property.name}: $cells" }
return cell
}
/**
* 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 method = abstractColumnClass.getDeclaredMethod("getBottomLevelColumn")
method.isAccessible = true
val gridColumn = method.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.
*/
fun FooterRow.getCell(property: KProperty1<*, *>): FooterRow.FooterCell {
val cell = cells.firstOrNull { it.column.columnKey == property.name }
require(cell != null) { "This grid has no property named ${property.name}: $cells" }
return cell
}
val HeaderRow.HeaderCell.renderer: Renderer<*>?
get() {
val method = abstractColumnClass.getDeclaredMethod("getHeaderRenderer")
method.isAccessible = true
val renderer = method.invoke(column)
return renderer as Renderer<*>?
}
val FooterRow.FooterCell.renderer: Renderer<*>?
get() {
val method = abstractColumnClass.getDeclaredMethod("getFooterRenderer")
method.isAccessible = true
val renderer = method.invoke(column)
return renderer as Renderer<*>?
}
/**
* Returns or sets the component in grid's footer cell. Returns `null` if the cell contains String, something else than a component or nothing at all.
*/
var FooterRow.FooterCell.component: Component?
get() {
val cr = (renderer as? ComponentRenderer<*, *>) ?: return null
return cr.createComponent(null)
}
set(value) {
setComponent(value)
}
private val gridSorterComponentRendererClass = Class.forName("com.vaadin.flow.component.grid.GridSorterComponentRenderer")
/**
* Returns or sets the component in grid's header cell. Returns `null` if the cell contains String, something else than a component or nothing at all.
*/
var HeaderRow.HeaderCell.component: Component?
get() {
val r = renderer
if (!gridSorterComponentRendererClass.isInstance(r)) return null
val componentField = gridSorterComponentRendererClass.getDeclaredField("component")
componentField.isAccessible = true
return componentField.get(r) as Component?
}
set(value) {
setComponent(value)
}
val KProperty1<*, *>.asc get() = QuerySortOrder(name, SortDirection.ASCENDING)
val KProperty1<*, *>.desc get() = QuerySortOrder(name, SortDirection.DESCENDING)
/**
* Sorts given grid. Affects [_findAll], [_get] and other data-fetching functions.
*/
fun Grid.sort(vararg sortOrder: QuerySortOrder) {
sort(sortOrder.map { GridSortOrder(getColumnByKey(it.sorted), it.direction) })
}
/**
* Fires the [ItemClickEvent] event for given [rowIndex] which invokes all item click listeners registered via
* [Grid.addItemClickListener].
* @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
fun Grid._clickItem(rowIndex: Int, button: Int = 1, ctrlKey: Boolean = false,
shiftKey: Boolean = false, altKey: Boolean = false, metaKey: Boolean = false) {
checkEditableByUser()
val itemKey = dataCommunicator.keyMapper.key(_get(rowIndex))
_fireEvent(ItemClickEvent(this, true, itemKey, -1, -1, -1, -1, 1, button, ctrlKey, shiftKey, altKey, metaKey))
}
/**
* 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.
*/
fun TreeGrid._rowSequence(): Sequence {
fun getChildrenOf(item: T): Iterator {
return if (isExpanded(item)) {
dataProvider.fetchChildren(HierarchicalQuery(null, item)).iterator()
} else {
listOf().iterator()
}
}
fun itemSubtreeSequence(item: T): Sequence =
TreeIterator(item) { getChildrenOf(it) } .asSequence()
val roots: List = _getRootItems()
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].
*/
fun TreeGrid<*>._size(): Int = _rowSequence().count()
fun TreeGrid._dataSourceToPrettyTree(): PrettyPrintTree {
fun getChildrenOf(item: T): List {
return if (isExpanded(item)) {
dataProvider.fetchChildren(HierarchicalQuery(null, item)).toList()
} else {
listOf()
}
}
fun toPrettyTree(item: T): PrettyPrintTree {
val self = _getFormattedRow(item).joinToString(postfix = "\n")
val children = getChildrenOf(item)
return PrettyPrintTree(self, children.map { toPrettyTree(it) } .toMutableList())
}
val roots: List = _getRootItems()
return PrettyPrintTree("TreeGrid", roots.map { toPrettyTree(it) } .toMutableList())
}
@Suppress("UNCHECKED_CAST")
fun TreeGrid._getRootItems(): List =
dataProvider.fetch(HierarchicalQuery(null, null)).toList()
/**
* Expands all nodes. May invoke massive data loading.
*/
fun TreeGrid._expandAll(depth: Int = 100) {
expandRecursively(_getRootItems(), depth)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy