All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.mvysny.kaributesting.v8.Grid.kt Maven / Gradle / Ivy

The newest version!
@file:Suppress("FunctionName")

package com.github.mvysny.kaributesting.v8

import com.vaadin.data.ValueProvider
import com.vaadin.data.provider.*
import com.vaadin.shared.MouseEventDetails
import com.vaadin.shared.data.sort.SortDirection
import com.vaadin.ui.Button
import com.vaadin.ui.Component
import com.vaadin.ui.Grid
import com.vaadin.ui.TreeGrid
import com.vaadin.ui.renderers.ClickableRenderer
import com.vaadin.ui.renderers.ComponentRenderer
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.
 */
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 = 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.
 */
public 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.
 * @param rowIndex the row, 0..size - 1
 * @return the item at given row, not null.
 */
public fun  Grid._get(rowIndex: Int): T {
    if (this !is TreeGrid) {
        // 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()}")
        }
    }
    val fetched: List = _fetch(rowIndex, 1)
    return fetched.firstOrNull()
            ?: throw AssertionError("Requested to get row $rowIndex but the data provider only has ${_size()} rows\n${_dump()}")
}

/**
 * For [TreeGrid] this walks the [_rowSequence].
 *
 * 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.fetchItemsWithRange(offset, limit)
}

/**
 * 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.
 */
public 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.
 */
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()

/**
 * Returns the number of items in this data provider.
 *
 * 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].
 */
public fun Grid<*>._size(): Int = when(this) {
    is TreeGrid<*> -> this._size()
    else -> dataCommunicator.dataProviderSize
}

/**
 * Performs a click on a [ClickableRenderer] in given [Grid] cell. Fails if [Grid.Column.getRenderer] is not a [ClickableRenderer].
 * @receiver the grid, not null.
 * @param rowIndex the row index, 0 or higher.
 * @param columnId the column ID.
 */
@JvmOverloads
public fun  Grid._clickRenderer(rowIndex: Int, columnId: String, mouseEventDetails: MouseEventDetails = MouseEventDetails(),
                                     click: (Component)->Unit = { component ->
                                         fail("${this.toPrettyString()} column $columnId: 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 = getColumnById(columnId)
    val renderer = column.renderer
    val item: T = _get(rowIndex)
    if (renderer is ClickableRenderer<*, *>) {
        @Suppress("UNCHECKED_CAST")
        (renderer as ClickableRenderer)._fireEvent(object : ClickableRenderer.RendererClickEvent(this, item, column, mouseEventDetails) {})
    } else if (renderer is ComponentRenderer) {
        val component = column.valueProvider.apply(item) as Component
        if (component is Button) {
            component._click()
        } else {
            click(component)
        }
    } else {
        fail("${this.toPrettyString()} column $columnId has renderer $renderer which is not supported by this method")
    }
}

/**
 * Returns the formatted value of a cell as a String. Does not use renderer to render the value - simply calls the value provider and presentation provider
 * and converts the result to string (even if the result is a [com.vaadin.ui.Component]).
 * @param rowIndex the row index, 0 or higher.
 * @param columnId the column ID.
 */
@Suppress("UNCHECKED_CAST")
public fun  Grid._getFormatted(rowIndex: Int, columnId: String): String {
    val rowObject: T = dataProvider._get(rowIndex)
    val column: Grid.Column = getColumnById(columnId)
    return column._getFormatted(rowObject)
}

/**
 * Returns the formatted value of a cell as a String. Does not use [renderer][Grid.Column.getRenderer] to render the value
 * - it only calls the [value provider][Grid.Column.getValueProvider] and [presentation provider][Grid.Column.presentationProvider]
 * and converts the result to string (even if the result is a [com.vaadin.ui.Component]).
 *
 * Calls [getPresentationValue] and converts the result to string; `null`s are converted to the string "null".
 * @param rowObject the row object. The object doesn't even have to be present in the Grid itself.
 */
@Suppress("UNCHECKED_CAST")
public fun  Grid.Column._getFormatted(rowObject: T): String = "${getPresentationValue(rowObject)}"

/**
 * Returns the formatted value of a Grid row as a list of strings, one for every visible column. Calls [_getFormatted] to
 * obtain the formatted cell value.
 * @param rowIndex the row index, 0 or higher.
 */
public fun  Grid._getFormattedRow(rowObject: T): List =
        columns.filterNot { it.isHidden }.map { it._getFormatted(rowObject) }

public fun  Grid._getFormattedRow(rowIndex: Int): List {
    val rowObject: T = _get(rowIndex)
    return _getFormattedRow(rowObject)
}

/**
 * Returns the [Grid.Column]'s presentation provider. Never null, may be [ValueProvider.identity].
 */
@Suppress("UNCHECKED_CAST")
public val  Grid.Column<*, V>.presentationProvider: ValueProvider
    get() =
        javaClass.getDeclaredField("presentationProvider").run {
            isAccessible = true
            get(this@presentationProvider) as ValueProvider
        }

/**
 * Returns the formatted value as outputted by the value provider + presentation provider. Does not use [renderer][com.vaadin.ui.Grid.Column.getRenderer] to render the value
 * - it only calls the [value provider][com.vaadin.ui.Grid.Column.getValueProvider] and [presentation provider][com.vaadin.ui.Grid.Column.presentationProvider].
 * @param rowObject the row object. The object doesn't even have to be present in the Grid itself.
 */
public fun  Grid.Column.getPresentationValue(rowObject: T): Any? = presentationProvider.apply(valueProvider.apply(rowObject))

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 the first [maxRows] 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..10): String = buildString {
    val visibleColumns: List> = columns.filterNot { it.isHidden }
    visibleColumns.joinTo(this, prefix = "--", separator = "-", postfix = "--\n") { "[${it.caption}]${getSortIndicator(it)}" }
    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")
    }
}

/**
 * Expects that [Grid.getDataProvider] currently contains exactly expected [count] of items. Fails if not; [_dump]s
 * first 10 rows of the Grid on failure.
 */
public fun Grid<*>.expectRows(count: Int) {
    if (_size() != count) {
        throw AssertionError("${this.toPrettyString()}: expected $count rows\n${_dump()}")
    }
}

/**
 * Expects that the row at [rowIndex] looks exactly as [expected].
 */
@Suppress("NAME_SHADOWING")
public fun Grid<*>.expectRow(rowIndex: Int, vararg expected: String) {
    val expected: List = expected.toList()
    val actual: List = _getFormattedRow(rowIndex)
    if (expected != actual) {
        throw AssertionError("${this.toPrettyString()} at $rowIndex: expected $expected but got $actual\n${_dump()}")
    }
}

/**
 * Fires the [com.vaadin.ui.Grid.ItemClick] event for given [rowIndex] which invokes all [com.vaadin.ui.components.grid.ItemClickListener]s registered via
 * [Grid.addItemClickListener].
 * @param column click this column; defaults to the first visible column in the Grid since it often doesn't really matter
 * which column was clicked - only the row index matters.
 * @param mouseEventDetails optionally mock mouse buttons and/or keyboard modifiers here.
 */
@JvmOverloads
public fun  Grid._clickItem(rowIndex: Int, column: Grid.Column = columns.first { !it.isHidden } ,
                           mouseEventDetails: MouseEventDetails = MouseEventDetails()) {
    checkEditableByUser()
    _fireEvent(Grid.ItemClick(this, column, _get(rowIndex), mouseEventDetails, rowIndex))
}

/**
 * Retrieves the column for given [columnId].
 * @throws IllegalArgumentException if no such column exists.
 */
@Suppress("UNCHECKED_CAST")
public fun  Grid.getColumnById(columnId: String): Grid.Column =
        getColumn(columnId) as Grid.Column?
                ?: throw IllegalArgumentException("${this.toPrettyString()}: No column with ID '$columnId'; available column IDs: ${columns.mapNotNull { it.id }}")

@Deprecated("replaced by getColumnById()", replaceWith = ReplaceWith("getColumnById(columnId)"))
public fun  Grid.getColumnBy(columnId: String): Grid.Column = getColumnById(columnId)

/**
 * Returns the component in given grid cell at [rowIndex]/[columnId]. Fails if there is something else in the cell (e.g. a String or other
 * value).
 *
 * **WARNING**: This function doesn't return the button produced by [com.vaadin.ui.renderers.ButtonRenderer]! There must be an actual component
 * produced by [Grid.Column.getValueProvider], possibly fed to [com.vaadin.ui.renderers.ComponentRenderer].
 */
public fun  Grid._getComponentAt(rowIndex: Int, columnId: String): Component {
    val item = _get(rowIndex)
    val column = getColumnById(columnId)
    val possibleComponent = column.getPresentationValue(item)
    if (possibleComponent !is Component) {
        fail("Expected Component at $rowIndex/$columnId but got $possibleComponent")
    }
    return possibleComponent
}

public val KProperty1<*, *>.asc: QuerySortOrder get() = QuerySortOrder(name, SortDirection.ASCENDING)
public val KProperty1<*, *>.desc: QuerySortOrder get() = QuerySortOrder(name, SortDirection.DESCENDING)

/**
 * Sorts given grid. Affects [_findAll], [_get] and other data-fetching functions.
 */
public fun  Grid.sort(vararg sortOrder: QuerySortOrder) {
    setSortOrder(sortOrder.map { GridSortOrder(getColumnById(it.sorted), it.direction) })
}

/**
 * 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.
 */
@JvmOverloads
@Suppress("UNCHECKED_CAST")
public fun  TreeGrid._rowSequence(filter: Any? = null): Sequence {
    val isExpanded: (T) -> Boolean = { item: T -> isExpanded(item) }
    return (dataProvider as HierarchicalDataProvider)._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 =
            PreorderTreeIterator(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 {
    @Suppress("UNCHECKED_CAST")
    fun getChildrenOf(item: T?): List =
            if (item == null || isExpanded(item)) {
                (dataProvider as HierarchicalDataProvider)
                        .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 as HierarchicalDataProvider)
            .fetch(HierarchicalQuery(null, null))
            .toList()

/**
 * Expands all nodes. May invoke massive data loading.
 */
public fun  TreeGrid._expandAll(depth: Int = 100) {
    expandRecursively(_getRootItems(), depth)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy