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.1.8
Show newest version
@file:Suppress("FunctionName")

package com.github.mvysny.kaributesting.v10

import com.github.mvysny.kaributools.*
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.HasStyle
import com.vaadin.flow.component.HasText
import com.vaadin.flow.component.HasValue
import com.vaadin.flow.component.UI
import com.vaadin.flow.component.dialog.Dialog
import com.vaadin.flow.component.icon.VaadinIcon
import com.vaadin.flow.router.InternalServerError
import java.io.PrintStream
import java.util.function.Predicate

/**
 * 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 [predicates] 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.
 * @param T the class of the component we are searching for.
 * @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 label the required [Component.label]; if null, no particular label is matched.
 * @property caption the required [Component.caption]; if null, no particular caption is matched. Deprecated: use [text] for Buttons, [label] for everything else.
 * @property placeholder the required [Component.placeholder]; if null, no particular placeholder is matched.
 * @property text the [HasText.getText]; use for button's "caption"
 * @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 themes if not null, the component must have all theme names defined. Space-separated
 * @property withoutThemes if not null, the component must NOT have any of the theme names defined. Space-separated
 * @property icon if not null, the component must have given [_iconName].
 * @property enabled if not null, the component's [Component.isEnabled] must match this value.
 * @property predicates the predicates the component needs to match, not null. May be empty - in such case it is ignored. By default, empty.
 */
public class SearchSpec(
        public val clazz: Class,
        public var id: String? = null,
        public var label: String? = null,
        @Deprecated("Use 'text' for Buttons, 'label' for everything else")
        public var caption: String? = null,
        public var placeholder: String? = null,
        public var text: String? = null,
        public var count: IntRange = 0..Int.MAX_VALUE,
        public var value: Any? = null,
        public var classes: String? = null,
        public var withoutClasses: String? = null,
        public var themes: String? = null,
        public var withoutThemes: String? = null,
        public var icon: IconName? = null,
        public var enabled: Boolean? = null,
        public var predicates: MutableList> = mutableListOf()
) {

    /**
     * Provides a nice summary of all rules set to this spec.
     */
    override fun toString(): String {
        val list = mutableListOf(clazz.simpleName.ifBlank { clazz.name })
        if (id != null) list.add("id='$id'")
        if (label != null) list.add("label='$label'")
        @Suppress("DEPRECATION")
        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 (!themes.isNullOrBlank()) list.add("themes='$themes'")
        if (!withoutThemes.isNullOrBlank()) list.add("withoutThemes='$withoutThemes'")
        if (icon != null) list.add("icon='$icon'")
        if (value != null) list.add("value=$value")
        if (enabled != null) list.add(if (enabled!!) "enabled" else "disabled")
        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).
     */
    public 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 (label != null) p.add { component -> testingLifecycleHook.getLabel(component) == label }
        @Suppress("DEPRECATION")
        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 (!themes.isNullOrBlank()) p.add { component -> component.hasAllThemes(themes!!) }
        if (!withoutThemes.isNullOrBlank()) p.add { component -> component.notContainsThemes(withoutThemes!!) }
        if (text != null) p.add { component -> component._text == text }
        if (value != null) p.add { component -> (component as? HasValue<*, *>)?.value == value }
        if (icon != null) p.add { component -> component._iconName == icon }
        if (enabled != null) p.add { component -> component.isEnabled == enabled }
        @Suppress("UNCHECKED_CAST")
        p.addAll(predicates.map { predicate -> { component: Component -> clazz.isInstance(component) && predicate.test(component as T) } })
        return p.and()
    }

    /**
     * Makes sure that [_iconName] is of given [collection] and matches the [iconName].
     */
    public fun iconIs(collection: String, iconName: String) {
        this.icon = IconName(collection, iconName)
    }

    /**
     * Makes sure that [_iconName] is given [vaadinIcon].
     */
    public fun iconIs(vaadinIcon: VaadinIcon) {
        this.icon = IconName.of(vaadinIcon)
    }
}

/**
 * Return a list containing only strings that are not null nor [String.isNotBlank].
 */
public fun Iterable.filterNotBlank(): List = filterNotNull().filter { it.isNotBlank() }

private fun Component.hasAllClasses(classes: String): Boolean {
    @Suppress("USELESS_IS_CHECK") // still needed for earlier Vaadin
    if (this !is HasStyle) return false
    return classes.split(' ').filterNotBlank().all { classNames.contains(it) }
}
private fun Component.doesntHaveAnyClasses(classes: String): Boolean {
    @Suppress("USELESS_IS_CHECK") // still needed for earlier Vaadin
    if (this !is HasStyle) return true
    return classes.split(' ').filterNotBlank().all { !classNames.contains(it) }
}

private fun Component.hasAllThemes(themes: String): Boolean {
    return themes.split(' ').filterNotBlank().all { element.themeList.contains(it) }
}

private fun Component.notContainsThemes(themes: String): Boolean {
    return themes.split(' ').filterNotBlank().all { !element.themeList.contains(it) }
}

/**
 * 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.
 */
public 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.
 */
public fun  Component._get(clazz: Class, block: SearchSpec.()->Unit = {}): T {
    val result: List = _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 [currentUI] 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.
 */
public 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 [currentUI] 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.
 */
public fun  _get(clazz: Class, block: SearchSpec.()->Unit = {}): T = currentUI._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.
 */
public fun  Component._find(clazz: Class, block: SearchSpec.()->Unit = {}): List {
    val spec: SearchSpec = SearchSpec(clazz)
    spec.block()
    val result: List = find(spec.toPredicate())
    if (result.size !in spec.count) {
        val loc: String = currentPath ?: "?"
        var message: String = 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})"
        }
        message = "$message in ${toPrettyString()} matching $spec: [${result.joinToString { it.toPrettyString() }}]. Component tree:\n${toPrettyTree()}"

        // if there's a PolymerTemplate, warn that Karibu-Testing can't really locate components in there:
        // https://github.com/mvysny/karibu-testing/tree/master/karibu-testing-v10#polymer-templates
        // fixes https://github.com/mvysny/karibu-testing/issues/35
        val hasPolymerTemplates: Boolean = _walkAll().any { it.isTemplate }
        if (hasPolymerTemplates) {
            message = "$message\nWarning: Karibu-Testing is not able to look up components from inside of PolymerTemplate/LitTemplate. Please see https://github.com/mvysny/karibu-testing/tree/master/karibu-testing-v10#polymer-templates--lit-templates for more details."
        }

        // 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)
    }
    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.
 */
public 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.
 */
public 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.
 */
public fun  _find(clazz: Class, block: SearchSpec.()->Unit = {}): List =
        currentUI._find(clazz, block)

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

/**
 * `AND`s all predicates.
 */
private fun  Iterable<(T) -> Boolean>.and(): (T) -> Boolean =
    { component -> all { it(component) } }

/**
 * Walks the component child/descendant tree, depth-first: first the component, then its descendants,
 * then its next sibling. Uses [TestingLifecycleHook.getAllChildren] to get the children.
 *
 * Returns [this] as the first item.
 *
 * This is a low-level API which doesn't run the [TestingLifecycleHook] lifecycle methods.
 * Please consider using [find] instead: `component.find()`
 */
public fun Component._walkAll(): Iterable = Iterable {
    DepthFirstTreeIterator(this) { component: Component -> testingLifecycleHook.getAllChildren(component) }
}

/**
 * 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.
 */
public inline fun  Component._expectNone(noinline block: SearchSpec.()->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.
 */
public 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 dialogs shown.
 */
public fun _expectNoDialogs() {
    _expectNone()
}

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

/**
 * Expects that there are no VISIBLE components in the current UI of given [clazz] which matches [block]. The [currentUI] and all of its descendants are searched.
 * @throws IllegalArgumentException if one or more components matched.
 */
public fun  _expectNone(clazz: Class, block: SearchSpec.()->Unit = {}) {
    currentUI._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.
 */
public inline fun  Component._expectOne(noinline block: SearchSpec.() -> 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.
 */
public 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 [currentUI] and all of its descendants are searched.
 * @throws AssertionError if none, or more than one components matched.
 */
public inline fun  _expectOne(noinline block: SearchSpec.() -> 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 [currentUI] and all of its descendants are searched.
 * @throws AssertionError if none, or more than one components matched.
 */
public fun  _expectOne(clazz: Class, block: SearchSpec.() -> Unit = {}) {
    currentUI._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 - 2024 Weber Informatics LLC | Privacy Policy