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

com.github.mvysny.kaributesting.v10.Locator.kt Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
@file:Suppress("FunctionName")

package com.github.mvysny.kaributesting.v10

import com.vaadin.flow.component.Component
import com.vaadin.flow.component.HasStyle
import com.vaadin.flow.component.HasValue
import com.vaadin.flow.component.UI
import com.vaadin.flow.component.grid.Grid
import com.vaadin.flow.router.InternalServerError
import java.util.*
import java.util.function.Predicate
import kotlin.streams.toList

/**
 * A criterion for matching components. The component must match all of non-null fields.
 *
 * You can add more properties, simply by creating a write-only property which will register a new [predicate] on write. See
 * [Adding support for custom search criteria](https://github.com/mvysny/karibu-testing/tree/master/karibu-testing-v10#adding-support-for-custom-search-criteria)
 * for more details.
 * @property clazz the class of the component we are searching for.
 * @property id the required [Component.getId]; if null, no particular id is matched.
 * @property caption the required [Component.caption]; if null, no particular caption is matched.
 * @property placeholder the required [Component.placeholder]; if null, no particular placeholder is matched.
 * @property text the [com.vaadin.flow.dom.Element.getText]
 * @property count expected count of matching components, defaults to `0..Int.MAX_VALUE`
 * @property value expected [com.vaadin.flow.component.HasValue.getValue]; if `null`, no particular value is matched.
 * @property classes if not null, the component must match all of these class names. Space-separated.
 * @property withoutClasses if not null, the component must NOT match any of these class names. Space-separated.
 * @property predicates the predicates the component needs to match, not null. May be empty - in such case it is ignored. By default empty.
 */
class SearchSpec(
    val clazz: Class,
    var id: String? = null,
    var caption: String? = null,
    var placeholder: String? = null,
    var text: String? = null,
    var count: IntRange = 0..Int.MAX_VALUE,
    var value: Any? = null,
    var classes: String? = null,
    var withoutClasses: String? = null,
    var predicates: MutableList> = mutableListOf()
) {

    override fun toString(): String {
        val list = mutableListOf(if (clazz.simpleName.isBlank()) clazz.name else clazz.simpleName)
        if (id != null) list.add("id='$id'")
        if (caption != null) list.add("caption='$caption'")
        if (placeholder != null) list.add("placeholder='$placeholder'")
        if (text != null) list.add("text='$text'")
        if (!classes.isNullOrBlank()) list.add("classes='$classes'")
        if (!withoutClasses.isNullOrBlank()) list.add("withoutClasses='$withoutClasses'")
        if (value != null) list.add("value=$value")
        if (count != (0..Int.MAX_VALUE) && count != 1..1) list.add("count=$count")
        list.addAll(predicates.map { it.toString() })
        return list.joinToString(" and ")
    }

    /**
     * Returns a predicate which matches components based on this spec. All rules are matched except the [count] rule. The
     * rules are matched against given component only (not against its children).
     */
    @Suppress("UNCHECKED_CAST")
    fun toPredicate(): (Component) -> Boolean {
        val p = mutableListOf<(Component)->Boolean>()
        p.add { component -> clazz.isInstance(component)}
        if (id != null) p.add { component -> component.id_ == id }
        if (caption != null) p.add { component -> component.caption == caption }
        if (placeholder != null) p.add { component -> component.placeholder == placeholder }
        if (!classes.isNullOrBlank()) p.add { component -> component.hasAllClasses(classes!!) }
        if (!withoutClasses.isNullOrBlank()) p.add { component -> component.doesntHaveAnyClasses(withoutClasses!!) }
        if (text != null) p.add { component -> component.element.text == text }
        if (value != null) p.add { component -> (component as? HasValue<*, *>)?.getValue() == value }
        p.addAll(predicates.map { predicate -> { component: Component -> clazz.isInstance(component) && predicate.test(component as T) } })
        return p.and()
    }
}

fun Iterable.filterNotBlank(): List = filterNotNull().filter { it.isNotBlank() }

private fun Component.hasAllClasses(classes: String): Boolean {
    if (classes.contains(' ')) return classes.split(' ').filterNotBlank().all { hasAllClasses(it) }
    if (this !is HasStyle) return false
    return classNames.contains(classes)
}
private fun Component.doesntHaveAnyClasses(classes: String): Boolean {
    if (classes.contains(' ')) return classes.split(' ').filterNotBlank().all { !hasAllClasses(it) }
    if (this !is HasStyle) return true
    return !classNames.contains(classes)
}

/**
 * Finds a VISIBLE component of given type which matches given [block]. This component and all of its descendants are searched.
 * @param block the search specification
 * @return the only matching component, never null.
 * @throws IllegalArgumentException if no component matched, or if more than one component matches.
 */
inline fun  Component._get(noinline block: SearchSpec.()->Unit = {}): T = this._get(T::class.java, block)

/**
 * Finds a VISIBLE component of given [clazz] which matches given [block]. This component and all of its descendants are searched.
 * @param clazz the component must be of this class.
 * @param block the search specification
 * @return the only matching component, never null.
 * @throws IllegalArgumentException if no component matched, or if more than one component matches.
 */
fun  Component._get(clazz: Class, block: SearchSpec.()->Unit = {}): T {
    val result = _find(clazz) {
        count = 1..1
        block()
        check(count == 1..1) { "You're calling _get which is supposed to return exactly 1 component, yet you tried to specify the count of $count" }
    }
    return clazz.cast(result.single())
}

/**
 * Finds a VISIBLE component in the current UI of given type which matches given [block]. The [UI.getCurrent] and all of its descendants are searched.
 * @return the only matching component, never null.
 * @throws IllegalArgumentException if no component matched, or if more than one component matches.
 */
inline fun  _get(noinline block: SearchSpec.()->Unit = {}): T =
    _get(T::class.java, block)

/**
 * Finds a VISIBLE component in the current UI of given [clazz] which matches given [block]. The [UI.getCurrent] and all of its descendants are searched.
 * @param clazz the component must be of this class.
 * @param block the search specification
 * @return the only matching component, never null.
 * @throws IllegalArgumentException if no component matched, or if more than one component matches.
 */
fun  _get(clazz: Class, block: SearchSpec.()->Unit = {}): T = UI.getCurrent()._get(clazz, block)

/**
 * Finds a list of VISIBLE components of given [clazz] which matches [block]. This component and all of its descendants are searched.
 * @return the list of matching components, may be empty.
 */
fun  Component._find(clazz: Class, block: SearchSpec.()->Unit = {}): List {
    val spec = SearchSpec(clazz)
    spec.block()
    val result = find(spec.toPredicate())
    if (result.size !in spec.count) {
        val loc: String = UI.getCurrent()?.internals?.activeViewLocation?.pathWithQueryParameters ?: "?"
        val message = when {
            result.isEmpty() -> "/$loc: No visible ${clazz.simpleName}"
            result.size < spec.count.first -> "/$loc: Too few (${result.size}) visible ${clazz.simpleName}s"
            else -> "/$loc: Too many visible ${clazz.simpleName}s (${result.size})"
        }

        // find() used to fail with IllegalArgumentException which makes sense for a general-purpose utility method. However,
        // since find() is used in tests overwhelmingly, not finding the correct set of components is generally treated as an assertion error.
        throw AssertionError("$message in ${toPrettyString()} matching $spec: [${result.joinToString { it.toPrettyString() }}]. Component tree:\n${toPrettyTree()}")
    }
    return result.filterIsInstance(clazz)
}

/**
 * Finds a list of VISIBLE components of given type which matches [block]. This component and all of its descendants are searched.
 * @return the list of matching components, may be empty.
 */
inline fun  Component._find(noinline block: SearchSpec.()->Unit = {}): List = this._find(T::class.java, block)

/**
 * Finds a list of VISIBLE components in the current UI of given type which matches given [block]. The [UI.getCurrent] and all of its descendants are searched.
 * @param block the search specification
 * @return the list of matching components, may be empty.
 */
inline fun  _find(noinline block: SearchSpec.()->Unit = {}): List =
    _find(T::class.java, block)

/**
 * Finds a list of VISIBLE components of given [clazz] which matches [block]. The [UI.getCurrent] and all of its descendants are searched.
 * @return the list of matching components, may be empty.
 */
fun  _find(clazz: Class, block: SearchSpec.()->Unit = {}): List =
        UI.getCurrent()._find(clazz, block)

private fun Component.find(predicate: (Component)->Boolean): List {
    testingLifecycleHook.awaitBeforeLookup()
    val descendants = walk().toList()
    testingLifecycleHook.awaitAfterLookup()
    val error: InternalServerError? = descendants.filterIsInstance().firstOrNull()
    if (error != null) throw AssertionError("An internal server error occurred; check log for the actual stack-trace. Error text: ${error.element.text}\n${UI.getCurrent().toPrettyTree()}")
    return descendants.filter { it.isEffectivelyVisible() && predicate(it) }
}

private fun  Iterable<(T)->Boolean>.and(): (T)->Boolean = { component -> all { it(component) } }

private class TreeIterator(root: T, private val children: (T) -> Iterator) : Iterator {
    private val queue: Queue = LinkedList(listOf(root))
    override fun hasNext() = !queue.isEmpty()
    override fun next(): T {
        if (!hasNext()) throw NoSuchElementException()
        val result = queue.remove()
        children(result).forEach { queue.add(it) }
        return result
    }
}
private fun Component.walk(): Iterable = Iterable {
    TreeIterator(this) { component -> component.getAllChildren() }
}

private fun Component.getAllChildren(): Iterator = when(this) {
    is Grid<*> -> {
        val columns = children.toList()
        val headerComponents = this.headerRows.flatMap { it.cells.mapNotNull { cell -> cell.component } }
        val footerComponents = this.footerRows.flatMap { it.cells.mapNotNull { cell -> cell.component } }
        (columns + headerComponents + footerComponents).iterator()
    }
    else -> children.iterator()
}

/**
 * Expects that there are no VISIBLE components of given type which matches [block]. This component and all of its descendants are searched.
 * @throws IllegalArgumentException if one or more components matched.
 */
inline fun  Component._expectNone(noinline block: SearchSpec.()->Unit = {}): Unit = this._expectNone(T::class.java, block)

/**
 * Expects that there are no VISIBLE components of given [clazz] which matches [block]. This component and all of its descendants are searched.
 * @throws IllegalArgumentException if one or more components matched.
 */
fun  Component._expectNone(clazz: Class, block: SearchSpec.()->Unit = {}) {
    val result: List = _find(clazz) {
        count = 0..0
        block()
        check(count == 0..0) { "You're calling _expectNone which expects 0 component, yet you tried to specify the count of $count" }
    }
    check(result.isEmpty()) // safety check that _find works as expected
}

/**
 * Expects that there are no VISIBLE components in the current UI of given type which matches [block]. The [UI.getCurrent] and all of its descendants are searched.
 * @throws IllegalArgumentException if one or more components matched.
 */
inline fun  _expectNone(noinline block: SearchSpec.()->Unit = {}): Unit =
    _expectNone(T::class.java, block)

/**
 * Expects that there are no VISIBLE components in the current UI of given [clazz] which matches [block]. The [UI.getCurrent] and all of its descendants are searched.
 * @throws IllegalArgumentException if one or more components matched.
 */
fun  _expectNone(clazz: Class, block: SearchSpec.()->Unit = {}): Unit = UI.getCurrent()._expectNone(clazz, block)

/**
 * Expects that there is exactly one VISIBLE components of given type which matches [block]. This component and all of its descendants are searched.
 * @throws AssertionError if none, or more than one components matched.
 */
inline fun  Component._expectOne(noinline block: SearchSpec.() -> Unit = {}): Unit = this._expectOne(T::class.java, block)

/**
 * Expects that there is exactly one VISIBLE components of given [clazz] which matches [block]. This component and all of its descendants are searched.
 * @throws AssertionError if none, or more than one components matched.
 */
fun  Component._expectOne(clazz: Class, block: SearchSpec.() -> Unit = {}) {
    // technically _expectOne is the same as _get, but the semantics differ - with _get() we're "just" doing a lookup (and asserting on
    // the component later). _expectOne() explicitly declares in the test sources that we want to check that there is exactly one such component.
    _get(clazz, block)
}

/**
 * Expects that there is exactly one VISIBLE components in the current UI of given type which matches [block]. The [UI.getCurrent] and all of its descendants are searched.
 * @throws AssertionError if none, or more than one components matched.
 */
inline fun  _expectOne(noinline block: SearchSpec.() -> Unit = {}): Unit = _expectOne(T::class.java, block)

/**
 * Expects that there is exactly one VISIBLE components in the current UI of given [clazz] which matches [block]. The [UI.getCurrent] and all of its descendants are searched.
 * @throws AssertionError if none, or more than one components matched.
 */
fun  _expectOne(clazz: Class, block: SearchSpec.() -> Unit = {}): Unit = UI.getCurrent()._expectOne(clazz, block)

/**
 * Expects that there are exactly [count] VISIBLE components matching [block]. This component and all of its descendants are searched. Examples:
 * ```
 * // check that there are 5 buttons in a button bar
 * buttonBar._expect




© 2015 - 2025 Weber Informatics LLC | Privacy Policy