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

com.vaadin.testbench.ElementQuery Maven / Gradle / Ivy

/*
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */
package com.vaadin.testbench;

import com.vaadin.testbench.annotations.Attribute;
import com.vaadin.testbench.elementsbase.Element;
import com.vaadin.testbench.internal.SharedUtil;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static com.vaadin.testbench.ElementQuery.AttributeMatch.Comparison.CONTAINS;
import static com.vaadin.testbench.ElementQuery.AttributeMatch.Comparison.CONTAINS_WORD;
import static com.vaadin.testbench.ElementQuery.AttributeMatch.Comparison.EXISTS;
import static com.vaadin.testbench.ElementQuery.AttributeMatch.Comparison.MATCHES_EXACTLY;
import static com.vaadin.testbench.ElementQuery.AttributeMatch.Comparison.NOT_CONTAINS;
import static com.vaadin.testbench.ElementQuery.AttributeMatch.Comparison.NOT_CONTAINS_WORD;
import static com.vaadin.testbench.ElementQuery.AttributeMatch.Comparison.NOT_EXISTS;
import static com.vaadin.testbench.ElementQuery.AttributeMatch.Comparison.NOT_MATCHES_EXACTLY;

/**
 * Query class used for finding a given element inside a given search context.
 * 

* The search context is either a {@link WebDriver} instance which searches * starting from the root of the current document, or a {@link WebElement} * instance, which searches both in the light DOM and inside the shadow root of * the given element. *

* When the search context is a {@link WebElement}, the shadow root is searched * first. E.g. when searching by ID and the same ID is used by a light DOM child * of the element and also inside its shadow root, the element from the shadow * root is returned. *

* The element class specified in the constructor defines the tag name which is * searched for and also the type of element returned. */ public class ElementQuery { private static final int DEFAULT_WAIT_TIME_OUT_IN_SECONDS = 10; private static final String NULL_COMPARISON_MSG = "comparison must not be null"; private static final String NULL_CONDITION_MSG = "condition must not be null"; private static final String NULL_GETTER_MSG = "getter function must not be null"; private static final String NULL_TEXT_MSG = "text must not be null"; /** * Class for holding name, comparison, and value for matching attributes. */ public static class AttributeMatch { /** * Attribute matching comparisons. * This is a combination of a CSS selection operator and a negation flag. */ public enum Comparison { /** * Attribute exists (with or without a value). */ EXISTS(""), /** * Attribute value and given value match exactly. */ MATCHES_EXACTLY("="), /** * Attribute value contains the given value. */ CONTAINS("*="), /** * Attribute value contains a space-separated word * that matches the given value. */ CONTAINS_WORD("~="), /** * Attribute value begins with a space/hyphen-separated prefix * that matches the given value. * The prefix must be either the entire attribute value or * the leading hyphen-separated segments of the attribute value. */ CONTAINS_PREFIX("|="), /** * Attribute value begins with the given value. */ BEGINS_WITH("^="), /** * Attribute value ends with the given value. */ ENDS_WITH("$="), /** * Attribute does not exist (with or without a value). */ NOT_EXISTS(EXISTS.operator, true), /** * Attribute value and given value do not match exactly. */ NOT_MATCHES_EXACTLY(MATCHES_EXACTLY.operator, true), /** * Attribute value does not contain the given value. */ NOT_CONTAINS(CONTAINS.operator, true), /** * Attribute value must not contain a space-separated word * that is prefixed with the given value. * @see #CONTAINS_WORD */ NOT_CONTAINS_WORD(CONTAINS_WORD.operator, true), /** * Attribute value does not begin with a space/hyphen-separated prefix * that matches the given value. */ NOT_CONTAINS_PREFIX(CONTAINS_PREFIX.operator, true), /** * Attribute value does not begin with the given value. */ NOT_BEGINS_WITH(BEGINS_WITH.operator, true), /** * Attribute value does not end with the given value. */ NOT_ENDS_WITH(ENDS_WITH.operator, true); private final String operator; private final boolean negated; Comparison(String operator, boolean negated) { this.operator = operator; this.negated = negated; } Comparison(String operator) { this(operator, false); } /** * Return the CSS selector operator for this comparison * * @return CSS selector operator */ public String getOperator() { return operator; } /** * Return the if the operator is to be negated. * * @return true if this comparison's operator is to be negated, false otherwise */ public boolean isNegated() { return negated; } /** * Builds the correct CSS matching expression * for the given attribute name and value * for this comparison. * * @param name the name of the attribute * @param value the value to match against the named attribute * @return the CSS matching expression */ public String expressionFor(String name, String value) { var expression = name; if (value != null) { expression += getOperator() + "'" + escapeAttributeValue(value) + "'"; } expression = "[" + expression + "]"; if (isNegated()) { expression = ":not(" + expression + ")"; } return expression; } private static String escapeAttributeValue(String value) { return value.replace("'", "\\'"); } } private final String name; private final Comparison comparison; private final String value; /** * Instantiates an attribute matching expression * having the supplied attribute name, comparison, and * value to compare with the attribute's value. * * @param name the name of the attribute * @param comparison the comparison to use for matching * @param value the value to compare with the attribute's value */ public AttributeMatch(String name, Comparison comparison, String value) { this.name = name; this.comparison = comparison; this.value = value; } /** * Instantiates an attribute matching expression * having the supplied attribute name, comparison, and * value to compare with the attribute's value. * * @param name the name of the attribute * @param operator the operator to use for matching * @param value the value to compare with the attribute's value * * @deprecated use {@link #AttributeMatch(String, Comparison, String)} */ @Deprecated(forRemoval = true, since = "9.3") public AttributeMatch(String name, String operator, String value) { this(name, Arrays.stream(Comparison.values()) .filter(comp -> comp.getOperator().equals(operator)) .findFirst() .orElseThrow(() -> new java.util.NoSuchElementException( "Invalid operator \"" + operator + "\" supplied. As this constructor is unsafe and deprecated, please use AttributeMatch(String, Comparison, String) instead." )), value); } /** * Instantiates an attribute exact matching expression * having the supplied attribute name and * value to compare with the attribute's value. * * @param name the name of the attribute * @param value the value to exactly match against with the attribute's value */ public AttributeMatch(String name, String value) { this(name, MATCHES_EXACTLY, value); } /** * Instantiates an attribute exists expression * having the supplied attribute name. * * @param name the name of the attribute */ public AttributeMatch(String name) { this(name, EXISTS, null); } @Override public String toString() { return getExpression(); } public String getExpression() { return comparison.expressionFor(name, value); } @Override public boolean equals(Object obj) { if (!(obj instanceof AttributeMatch)) { return false; } return getExpression() .equals(((AttributeMatch) obj).getExpression()); } @Override public int hashCode() { return getExpression().hashCode(); } } private final Class elementClass; private final String tagName; private final Set attributes; private final List> conditions; private SearchContext searchContext; /** * Instantiate a new ElementQuery to look for the given type of element. * * @param elementClass * the type of element to look for and return */ public ElementQuery(Class elementClass) { this(elementClass, getTagName(elementClass)); } /** * Instantiate a new ElementQuery to look for the given type of element. * * @param elementClass * the type of element to return * @param tagName * the tag name of the element to find */ public ElementQuery(Class elementClass, String tagName) { this.elementClass = elementClass; this.tagName = tagName; // Linked to ensure that elements are always returned in the same order. this.attributes = new LinkedHashSet<>(getAttributes(elementClass)); this.conditions = new ArrayList<>(); } /** * Selects on elements having the given attribute. *

* Note, this attribute need not have a value. * * @param name * the attribute name * @return this element query instance for chaining * * @deprecated use {@link #withAttribute(String)} */ @Deprecated(since = "9.3") public ElementQuery hasAttribute(String name) { return withAttribute(name); } /** * Selects on elements with the given attribute having the given value. *

* For matching a substring of the attribute value, see * {@link #withAttributeContaining(String, String)}. * For matching a word within the attribute, see * {@link #withAttributeContainingWord(String, String)}. * * @param name * the attribute name * @param value * the attribute value * @return this element query instance for chaining * * @see #withAttribute(String, String) * @see #withAttributeContaining(String, String) * @see #withAttributeContainingWord(String, String) * * @deprecated use {@link #withAttribute(String, String)} */ @Deprecated(since = "9.3") public ElementQuery attribute(String name, String value) { return withAttribute(name, value); } /** * Selects on elements with the given attribute containing the given word. *

* Compares with space separated words so that e.g. * attributeContains("class", "myclass"); matches * class='someclass myclass'. *

* For matching the full attribute value, see * {@link #withAttribute(String, String)}. * For matching a substring of the attribute value, see * {@link #withAttributeContaining(String, String)}. * * @param name * the attribute name * @param word * the word to look for * @return this element query instance for chaining * * @see #withAttribute(String, String) * @see #withAttributeContaining(String, String) * @see #withAttributeContainingWord(String, String) * * @deprecated use {@link #withAttributeContainingWord(String, String)} */ @Deprecated(since = "9.3") public ElementQuery attributeContains(String name, String word) { return withAttributeContainingWord(name, word); } /** * Selects on elements having the given attribute. *

* Note, the attribute need not have a value--it just needs to exist. * * @param attribute * the attribute name * @return this element query instance for chaining */ public ElementQuery withAttribute(String attribute) { attributes.add(new AttributeMatch(attribute)); return this; } /** * Selects on elements with the given attribute using the given comparison and value. *

* The given value must match the attribute value according to the comparison. * * @param attribute * the attribute name * @param value * the attribute value * @param comparison * the comparison to use * @return this element query instance for chaining */ public ElementQuery withAttribute(String attribute, String value, AttributeMatch.Comparison comparison) { attributes.add(new AttributeMatch(attribute, comparison, value)); return this; } /** * Selects on elements with the given attribute having the given value. *

* The given value must match the attribute value exactly. *

* For matching a substring of the attribute, see * {@link #withAttributeContaining(String, String)}. * For matching a word within the attribute, see * {@link #withAttributeContainingWord(String, String)}. * * @param attribute * the attribute name * @param value * the attribute value * @return this element query instance for chaining * * @see #withAttributeContaining(String, String) * @see #withAttributeContainingWord(String, String) */ public ElementQuery withAttribute(String attribute, String value) { return withAttribute(attribute, value, MATCHES_EXACTLY); } /** * Selects on elements with the given attribute containing the given text. *

* The given text must match a substring of the attribute value. *

* For matching the full attribute value, see * {@link #withAttribute(String, String)}. * For matching a word within the attribute, see * {@link #withAttributeContainingWord(String, String)}. * * @param attribute * the attribute name * @param text * the substring to look for * @return this element query instance for chaining * * @see #withAttribute(String, String) * @see #withAttributeContainingWord(String, String) */ public ElementQuery withAttributeContaining(String attribute, String text) { return withAttribute(attribute, text, CONTAINS); } /** * Selects on elements with the given attribute containing the given word. *

* Compares with space separated words so that e.g. * withAttributeContainingWord("class", "myclass"); matches * class='someclass myclass'. *

* For matching the full attribute value, see * {@link #withAttribute(String, String)}. * For matching a substring of the attribute value, see * {@link #withAttributeContaining(String, String)}. * * @param attribute * the attribute name * @param word * the word to look for * @return this element query instance for chaining * * @see #withAttribute(String, String) * @see #withAttributeContaining(String, String) */ public ElementQuery withAttributeContainingWord(String attribute, String word) { return withAttribute(attribute, word, CONTAINS_WORD); } /** * Selects on elements not having the given attribute. *

* Note, attributes both with and without values are skipped. * * @param attribute * the attribute name * @return this element query instance for chaining */ public ElementQuery withoutAttribute(String attribute) { return withAttribute(attribute, null, NOT_EXISTS); } /** * Selects on elements not having the given attribute with the given value. *

* The given value must match the attribute value exactly in order to be skipped. *

* For matching the full attribute value, see * {@link #withoutAttributeContaining(String, String)}. * For skipping elements having a word within the attribute, see * {@link #withoutAttributeContainingWord(String, String)}. * * @param attribute * the attribute name * @param value * the attribute value * @return this element query instance for chaining * * @see #withoutAttributeContaining(String, String) * @see #withoutAttributeContainingWord(String, String) */ public ElementQuery withoutAttribute(String attribute, String value) { return withAttribute(attribute, value, NOT_MATCHES_EXACTLY); } /** * Selects on elements not having the given attribute containing the given text. *

* The given value must match any substring of the attribute value * in order to be skipped. *

* For matching the full attribute value, see * {@link #withoutAttribute(String, String)}. * For skipping elements having a word within the attribute, see * {@link #withoutAttributeContainingWord(String, String)}. * * @param attribute * the attribute name * @param text * the substring to look for * @return this element query instance for chaining * * @see #withoutAttribute(String, String) * @see #withoutAttributeContainingWord(String, String) */ public ElementQuery withoutAttributeContaining(String attribute, String text) { return withAttribute(attribute, text, NOT_CONTAINS); } /** * Selects on elements not having the given attribute containing the given word. *

* Compares with space separated words so that e.g. * withoutAttributeContainingWord("class", "myclass"); skips * class='someclass myclass'. *

* For matching the full attribute value, see * {@link #withoutAttribute(String, String)}. * For skipping elements containing a substring of the attribute, see * {@link #withoutAttributeContaining(String, String)}. * * @param attribute * the attribute name * @param word * the word to look for * @return this element query instance for chaining * * @see #withoutAttribute(String, String) * @see #withoutAttributeContaining(String, String) */ public ElementQuery withoutAttributeContainingWord(String attribute, String word) { return withAttribute(attribute, word, NOT_CONTAINS_WORD); } /** * Selects on elements having the given id. *

* This selector does not require the id to be unique. * To obtain the unique id, chain with {@link #single()} * or use {@link #id(String)} instead of this selector. * If you legitimately have duplicate ids and just want the first one, * chain with {@link #first()}. * * @param id * the id to look up * @return the element with the given id * * @see #id(String) * @see #single() * @see #first() */ public ElementQuery withId(String id) { return withAttribute("id", id); } /** * Selects on elements having the given class names. * * @param classNames * the class names * @return this element query instance for chaining */ public ElementQuery withClassName(String... classNames) { Arrays.stream(classNames) .forEach(className -> withAttributeContainingWord("class", className)); return this; } /** * Selects on elements not having the given class names. * * @param classNames * the class names * @return this element query instance for chaining */ public ElementQuery withoutClassName(String... classNames) { Arrays.stream(classNames) .forEach(className -> withoutAttributeContainingWord("class", className)); return this; } /** * Selects on elements having the given theme. * * @param theme * the theme * @return this element query instance for chaining */ public ElementQuery withTheme(String theme) { return withAttribute("theme", theme); } /** * Selects on elements not having the given theme. * * @param theme * the theme * @return this element query instance for chaining */ public ElementQuery withoutTheme(String theme) { return withoutAttribute("theme", theme); } /** * Requires the element to satisfy the given condition. *

* For example, to select only enabled elements, you could use *
* {@code withCondition(TestBenchElement::isEnabled)} *
* or to select only those having a non-zero height, you could use *
* {@code withCondition(element -> element.getSize().getHeight() != 0)} *

* Note that conditions are evaluated in order * after the element is selected by its attributes. * * @param condition * the condition for the element to satisfy; not null * @return this element query instance for chaining */ public ElementQuery withCondition(Predicate condition) { Objects.requireNonNull(condition, NULL_CONDITION_MSG); conditions.add(condition); return this; } /** * Requires the element's given property getter return value * to satisfy the given comparison with the supplied value. *

* For example, to select {@code TextFieldElement}s having helper text * containing the word "person", * you could use *

     * {@code withPropertyValue(TextFieldElement::getHelperText, "person", String::contains)}
     * 
* * @param getter * the function to get the value of the property of the element; * not null * @param propertyValue * value to be compared with the one obtained from the * getter function of the element * @param comparison * the comparison to use when comparing the getter's property value * against the supplied property value * (i.e., {@code comparison.test(elementPropertyValue, propertyValue)}; * not null * @param * the type of the property values * @return this element query instance for chaining * * @see #withPropertyValue(Function, Object) */ public ElementQuery withPropertyValue(Function getter, V propertyValue, BiPredicate comparison) { Objects.requireNonNull(getter, NULL_GETTER_MSG); Objects.requireNonNull(comparison, NULL_COMPARISON_MSG); return withCondition(element -> comparison.test(getter.apply(element), propertyValue)); } /** * Requires the element's given property getter return value * to equal the supplied value. * * @param getter * the function to get the value of the property of the element, * not null * @param propertyValue * value to be compared with the one obtained from the * getter function of the element * @param * the type of the property values * @return this element query instance for chaining * * @see #withPropertyValue(Function, Object, BiPredicate) */ public ElementQuery withPropertyValue(Function getter, V propertyValue) { return withPropertyValue(getter, propertyValue, Objects::equals); } /** * Requires the element's label to satisfy the given comparison * with the supplied text. *

* For matching a label exactly, see {@link #withLabel(String)}, * and for matching a label partially, see {@link #withLabelContaining(String)}. *

* This method can be used for other label matching needs, * such as performing a case-insensitive match: * {@code withLabel("name", String::equalsIgnoreCase)} *
a label-ending match: * {@code withLabel(" Name", String::endsWith)} *
or a regular expression match: * {@code withLabel("(First|Last) Name", String::matches)} * * @param text * the text to compare with the label; not null * @param comparison * the comparison to use when comparing element's label * against the supplied text * (i.e., {@code comparison.test(elementLabel, text)}; not null * @return this element query instance for chaining * * @see #withLabel(String) * @see #withLabelContaining(String) */ public ElementQuery withLabel(String text, BiPredicate comparison) { Objects.requireNonNull(text, NULL_TEXT_MSG); Objects.requireNonNull(comparison, NULL_COMPARISON_MSG); return withCondition(element -> (element instanceof HasLabel hasLabel) && comparison.test(hasLabel.getLabel(), text)); } /** * Requires the element's label to exactly match the given label value. *

* For partially matching text within the label, * see {@link #withLabelContaining(String)}. * * @param label * the label to match * @return this element query instance for chaining * * @see #withLabelContaining(String) * @see #withLabel(String, BiPredicate) */ public ElementQuery withLabel(String label) { return withLabel(label, String::equals); } /** * Requires the element's label to partially match the given text value. *

* For exactly matching the label, * see {@link #withLabel(String)}. * * @param text * the text to match * @return this element query instance for chaining * * @see #withLabel(String) * @see #withLabel(String, BiPredicate) */ public ElementQuery withLabelContaining(String text) { return withLabel(text, String::contains); } /** * Requires the element's placeholder to satisfy the given comparison * with the supplied text. *

* For matching a placeholder exactly, see {@link #withPlaceholder(String)}, * and for matching a placeholder partially, see {@link #withPlaceholderContaining(String)}. *

* This method can be used for other placeholder matching needs, * such as performing a case-insensitive match: * {@code withPlaceholder("name", String::equalsIgnoreCase)} *
a placeholder-ending match: * {@code withPlaceholder(" Name", String::endsWith)} *
or a regular expression match: * {@code withPlaceholder("(First|Last) Name", String::matches)} * * @param text * the text to compare with the placeholder; not null * @param comparison * the comparison to use when comparing the placeholder * against the supplied text * (i.e., {@code comparison.test(elementPlaceholder, text)}; not null * @return this element query instance for chaining * * @see #withPlaceholder(String) * @see #withPlaceholderContaining(String) */ public ElementQuery withPlaceholder(String text, BiPredicate comparison) { Objects.requireNonNull(text, NULL_TEXT_MSG); Objects.requireNonNull(comparison, NULL_COMPARISON_MSG); return withCondition(element -> (element instanceof HasPlaceholder hasPlaceholder) && comparison.test(hasPlaceholder.getPlaceholder(), text)); } /** * Requires the element's placeholder to exactly match the given placeholder value. *

* For partially matching text within the placeholder, see * {@link #withPlaceholderContaining(String)}. * * @param placeholder * the placeholder to match * @return this element query instance for chaining * * @see #withPlaceholderContaining(String) * @see #withPlaceholder(String, BiPredicate) */ public ElementQuery withPlaceholder(String placeholder) { return withPlaceholder(placeholder, String::equals); } /** * Requires the element's placeholder to partially match the given text value. *

* For exactly matching the placeholder, see * {@link #withPlaceholder(String)}. * * @param text * the text to match * @return this element query instance for chaining * * @see #withPlaceholder(String) * @see #withPlaceholder(String, BiPredicate) */ public ElementQuery withPlaceholderContaining(String text) { return withPlaceholder(text, String::contains); } /** * Requires the element's caption (i.e., label, placeholder, or text label) * to satisfy the given comparison with the supplied text. *

* This is a convenience selector method to select an element * by its label, placeholder, or text label, as supported by the element. * These values are generically considered "captions" * as they are used to identify the element to the user. *

* The comparison against the values of the element follows this priority: *

    *
  1. * Label - If the element supports a label and its label is not empty, * the element's label value is used in the comparison. * If the comparison with the given text is not satisfied * by the element's label value, * the comparison does not fall through * to compare against the placeholder. *
  2. *
  3. * Placeholder - Even if an element supports a label, * its label value may be empty. In that situation, * the element may be using a placeholder in lieu of a label. * So in that situation, the comparison falls through * to compare against the placeholder * if the element supports a placeholder and its placeholder is not empty, * the element's placeholder value is used in the comparison. *
  4. *
  5. * Text - If the element supports neither labels nor placeholders * but does support a caption via its text (such as a button does), * the comparison is made against the element's text. *
  6. *
* * Note that if the given text is empty, * then if the element supports both a label and a placeholder, * they must both be empty to be selected. * *

* For matching a caption exactly, see {@link #withCaption(String)}, * and for matching a caption partially, see {@link #withCaptionContaining(String)}. *

* This method can be used for other caption matching needs, * such as performing a case-insensitive match: * {@code withCaption("name", String::equalsIgnoreCase)} *
a caption-ending match: * {@code withCaption(" Name", String::endsWith)} *
or a regular expression match: * {@code withCaption("(First|Last) Name", String::matches)} * * @param text * the text to compare with the caption; not null * @param comparison * the comparison to use when comparing the caption * against the supplied text * (i.e., {@code comparison.test(elementCaption, text)}; not null * @return this element query instance for chaining * * @see #withCaption(String) * @see #withCaptionContaining(String) */ @SuppressWarnings("java:S3776") // cognitive complexity > 15 public ElementQuery withCaption(String text, BiPredicate comparison) { Objects.requireNonNull(text, NULL_TEXT_MSG); Objects.requireNonNull(comparison, NULL_COMPARISON_MSG); return withCondition(element -> { // special case when text is empty, // so if they both exist, both label and placeholder must be empty if (text.isEmpty() && element instanceof HasLabel hasLabel && element instanceof HasPlaceholder hasPlaceholder) { var label = hasLabel.getLabel(); var placeholder = hasPlaceholder.getPlaceholder(); return label.isEmpty() && placeholder.isEmpty(); } // compare with label if (element instanceof HasLabel hasLabel) { var label = hasLabel.getLabel(); if (!label.isEmpty()) { return comparison.test(label, text); } } // compare with placeholder if (element instanceof HasPlaceholder hasPlaceholder) { var placeholder = hasPlaceholder.getPlaceholder(); if (!placeholder.isEmpty()) { return comparison.test(placeholder, text); } } // compare with text if (element instanceof HasLabelAsText labelAsText) { var label = Objects.requireNonNullElse(labelAsText.getText(), ""); if (!label.isEmpty()) { return comparison.test(label, text); } } return false; }); } /** * Requires the element's caption (i.e., label, placeholder, or text label) * to exactly match the given caption value. *

* For partially matching text within the caption, see * {@link #withCaptionContaining(String)}. * * @param caption * the caption to match * @return this element query instance for chaining * * @see #withCaptionContaining(String) * @see #withCaption(String, BiPredicate) */ public ElementQuery withCaption(String caption) { return withCaption(caption, String::equals); } /** * Requires the element's caption (i.e., label, placeholder, or text label) * to partially match the given text value. *

* For exactly matching the caption, see * {@link #withCaption(String)}. * * @param text * the text to match * @return this element query instance for chaining * * @see #withCaption(String) * @see #withCaption(String, BiPredicate) */ public ElementQuery withCaptionContaining(String text) { return withCaption(text, String::contains); } /** * Requires the element's text * to satisfy the given comparison with the supplied text. *

* For matching the element's text exactly, see {@link #withText(String)}, * and for matching the element's text partially, see {@link #withTextContaining(String)}. *

* This method can be used for other text matching needs, * such as performing a case-insensitive match: * {@code withText("name", String::equalsIgnoreCase)} *
a text-ending match: * {@code withText(" Name", String::endsWith)} *
or a regular expression match: * {@code withText("(First|Last) Name", String::matches)} * * @param text * the text to compare with the element's text; not null * @param comparison * the comparison to use when comparing the element's text * against the supplied text * (i.e., {@code comparison.test(elementText, text)}; not null * @return this element query instance for chaining * * @see #withText(String) * @see #withTextContaining(String) */ public ElementQuery withText(String text, BiPredicate comparison) { Objects.requireNonNull(text, NULL_TEXT_MSG); Objects.requireNonNull(comparison, NULL_COMPARISON_MSG); return withCondition(element -> comparison.test(Objects.requireNonNullElse(element.getText(), ""), text)); } /** * Requires the element's text * to exactly match the given text value. *

* For partially matching text within the element's text, see * {@link #withTextContaining(String)}. * * @param text * the text to match * @return this element query instance for chaining * * @see #withTextContaining(String) * @see #withText(String, BiPredicate) */ public ElementQuery withText(String text) { return withText(text, String::equals); } /** * Requires the element's text * to partially match the given text value. *

* For exactly matching the text, see * {@link #withText(String)}. * * @param text * the text to match * @return this element query instance for chaining * * @see #withText(String) * @see #withText(String, BiPredicate) */ public ElementQuery withTextContaining(String text) { return withText(text, String::contains); } /** * Sets the context to search inside. * * @param searchContext * a {@link SearchContext}; either a {@link TestBenchElement} or * {@link WebDriver} (to search from the root) instance * @return this element query instance for chaining */ public ElementQuery context(SearchContext searchContext) { this.searchContext = searchContext; return this; } /** * Defines that the query should start the search from the root of the page, * in practice from the {@code } tag. * * @return this element query instance for chaining */ public ElementQuery onPage() { return context(getDriver()); } /** * Returns the context (element or driver) to search inside. * * @return a {@link SearchContext} instance */ protected SearchContext getContext() { return searchContext; } /** * Executes the search and returns an element having the given unique id. *

* This selector expects the id to be unique. * If there are duplicate ids, this selector will * throw an exception. If you legitimately have duplicate ids, * use {@link #withId(String)}.{@link #first()} instead. * (Note, this alternate usage is the former behavior of this selector.) * * @param id * the id to look up * @return the element with the given id * * @throws NoSuchElementException * if no unique id element is found * * @see #withId(String) * @see #first() */ public T id(String id) { return withId(id).single(); } /** * Executes the search and returns the sole result. * * @return The element of the type specified in the constructor * @throws NoSuchElementException * if no unique element is found */ public T single() { List all = all(); if (all.size() != 1) { throw new NoSuchElementException(getNoSuchElementMessage(null, all.size())); } return all.get(0); } /** * Executes the search and returns the first result. * * @return The element of the type specified in the constructor * @throws NoSuchElementException * if no element is found */ public T first() { return get(0); } /** * Executes the search and returns the first result once at least once * result is available. *

* This method is identical to {@link #first()} if at least one element is * present. If no element is found, this method will keep searching until an * element is found or if 10 seconds has elapsed. * * @return The element of the type specified in the constructor * @throws NoSuchElementException * if no element is found * * @see #first() */ public T waitForFirst() { return waitForFirst(DEFAULT_WAIT_TIME_OUT_IN_SECONDS); } /** * Executes the search and returns the first result once at least once * result is available. *

* This method is identical to {@link #first()} if at least one element is * present. If no element is found, this method will keep searching until an * element is found or {@code timeOutInSeconds} seconds has elapsed. * * @param timeOutInSeconds * timeout in seconds before this method throws a * {@link NoSuchElementException} exception * @return The element of the type specified in the constructor * @throws NoSuchElementException * if no element is found * * @see #first() */ public T waitForFirst(long timeOutInSeconds) { T result = new WebDriverWait(getDriver(), Duration.ofSeconds(timeOutInSeconds)).until(driver -> { try { return first(); } catch (NoSuchElementException e) { return null; } }); if (result == null) { throw new NoSuchElementException(getNoSuchElementMessage(null)); } else { return result; } } /** * Executes the search and returns the last result. * * @return The element of the type specified in the constructor * * @throws NoSuchElementException * if no element is found */ public T last() { List all = all(); return all.get(all.size() - 1); } /** * Executes the search and returns the requested element. * * @param index * the index of the element to return * @return The element of the type specified in the constructor * @throws NoSuchElementException * if no element is found */ public T get(int index) { Objects.checkIndex(index, Integer.MAX_VALUE); List elements = executeSearch(index); if (elements.isEmpty()) { throw new NoSuchElementException(getNoSuchElementMessage(index)); } return elements.get(0); } private String getNoSuchElementMessage(Integer index, int foundCount) { String msg = (foundCount == 0 ? "No element" : "Multiple elements (" + foundCount + ")") + " with tag <" + tagName + "> found"; String attrPairs = getAttributePairs(); if (!attrPairs.isEmpty()) { msg += " with the attributes " + attrPairs; } if (index != null) { msg += " using index " + index; } return msg + "."; } private String getNoSuchElementMessage(Integer index) { return getNoSuchElementMessage(index, 0); } /** * Checks if this ElementQuery describes existing elements. Same as * .all().isEmpty(). * * @return true if elements exists. false if not */ public boolean exists() { return !all().isEmpty(); } /** * Search the open Vaadin application for a list of matching components * relative to given context. * * @return Components as a list of corresponding elements */ public List all() { return executeSearch(null); } private WebDriver getDriver() { var context = getContext(); if (context instanceof WebDriver webDriver) { return webDriver; } else { return ((TestBenchElement) context).getDriver(); } } /** * Executes the search operation with the given conditions and returns a * list of matching elements. * * @param index * the index of the element to return or null to * return all matching elements * @return a list of matching elements or an empty list if no matches were * found */ private List executeSearch(Integer index) { StringBuilder script = new StringBuilder(); TestBenchElement elementContext; JavascriptExecutor executor; var context = getContext(); if (context instanceof TestBenchElement testBenchElement) { script.append("var result = [];" // + "if (arguments[0].shadowRoot) {" // + " var shadow = arguments[0].shadowRoot.querySelectorAll(arguments[1]+arguments[2]);" // + " result = result.concat(Array.prototype.slice.call(shadow));" // + "}" // + "var light = arguments[0].querySelectorAll(arguments[1]+arguments[2]);" // + "result = result.concat(Array.prototype.slice.call(light));" // + "return result" // ); elementContext = testBenchElement; executor = elementContext.getCommandExecutor().getDriver(); } else if (context instanceof WebDriver webDriver) { // Search the whole document script.append("var result = [];" // + "const queryResult = document.querySelectorAll(arguments[1]+arguments[2]);" + "result = result.concat(Array.prototype.slice.call(queryResult));" + "return result"); elementContext = null; executor = (JavascriptExecutor) webDriver; } else { if (context == null) { throw new IllegalStateException("Context cannot be null"); } else { throw new IllegalStateException("Unknown context type: " + context.getClass().getName()); } } if (index != null) { script.append("[").append(index).append("]"); } return executeSearchScript(script.toString(), elementContext, tagName, getAttributePairs(), executor).stream() .filter(this::satisfiesAllConditions) .toList(); } private boolean satisfiesAllConditions(T element) { return conditions.stream().allMatch(condition -> condition.test(element)); } private static String getTagName(Class elementClass) { Element annotation = elementClass.getAnnotation(Element.class); if (annotation == null) { throw new IllegalStateException("The given element class " + elementClass.getName() + " must be annotated using @" + Element.class.getName()); } return annotation.value(); } static Set getAttributes( Class elementClass) { Attribute[] attrs = elementClass.getAnnotationsByType(Attribute.class); if (attrs.length == 0) { return Collections.emptySet(); } Set classAttributes = new HashSet<>(); for (Attribute attr : attrs) { if (!Attribute.DEFAULT_VALUE.equals(attr.value())) { if (!Attribute.DEFAULT_VALUE.equals(attr.contains())) { throw new RuntimeException( "You can only define either 'contains' or 'value' for an @" + Attribute.class.getSimpleName()); } String value = attr.value().equals(Attribute.SIMPLE_CLASS_NAME) ? getClassConventionValue(elementClass) : attr.value(); // [label='my-text'] classAttributes .add(new AttributeMatch(attr.name(), MATCHES_EXACTLY, value)); } else if (!Attribute.DEFAULT_VALUE.equals(attr.contains())) { // [class~='js-card-name'] String value = attr.contains().equals(Attribute.SIMPLE_CLASS_NAME) ? getClassConventionValue(elementClass) : attr.contains(); classAttributes .add(new AttributeMatch(attr.name(), CONTAINS_WORD, value)); } else { // [disabled] classAttributes.add(new AttributeMatch(attr.name())); } } return classAttributes; } private static String getClassConventionValue(Class elementClass) { String value = elementClass.getSimpleName(); value = value.replaceAll("(Element|PageObject)$", ""); value = SharedUtil.camelCaseToDashSeparated(value).replaceAll("^-*", ""); return value; } private String getAttributePairs() { // [id='username'][label='Email'][special='foo\\'bar'] return attributes.stream() .map(AttributeMatch::getExpression) .collect(Collectors.joining()); } /** * Executes the given search script. *

* Package private to enable testing * * @param script * the script to execute * @param context * the element to start the search from or null for * a whole document search * @param tagName * the tag name to look for * @param attributePairs * the attribute pairs to match * @return a list of matching elements of the type defined in the * constructor */ @SuppressWarnings("unchecked") List executeSearchScript(String script, Object context, String tagName, String attributePairs, JavascriptExecutor executor) { Object result = executor.executeScript(script, context, tagName, attributePairs); if (result == null) { return Collections.emptyList(); } else if (result instanceof TestBenchElement testBenchElement) { return Collections.singletonList(TestBench.wrap(testBenchElement, elementClass)); } else { List elements = (List) result; // Wrap as the correct type elements.replaceAll(element -> TestBench.wrap(element, elementClass)); return (List) elements; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy