com.github.mvysny.kaributesting.v10.Locator.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.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 = currentPath ?: "?"
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) } }
internal 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