Support for ElementQuery.caption()
I need to look up a component by a caption. Currently the only way is to look up all components and then go through each of them and compare their caption with the expected one.
The problem is that there's no such notion as caption in Vaadin 23, not in the same sense as in Vaadin 8. The components generally fall into three categories:
- Don't have a label
- Have a label (
Elements implementing the TestBenchHasLabelinterface) - mostly fields - Has a text (
ButtonElement)
Two solutions would work for me:
ElementQuery.caption()emulating Vaadin 8 caption, ORElementQuery.text()matching component text against the expected one (this one would be used forButtonElements), andElementQuery.label()matching label against the expected one (this one would work for allHasLabels).
The case with fields when there is only a textual label is the simplest. One can use "label" property then. This is trivial.
TextFieldElement nameField = $(TextFieldElement.class).all().stream()
.filter(elem -> elem.getPropertyString("label").equals("Name"))
.findFirst().get();
But there is also technical possibility in Flow to replace the label with a component appended in "label" slot. In that case label property can be null or empty and something different is needed to find the element, as you would need to element which has a child element with possibly a textual content you are looking for.
TextFieldElement nameField = $(TextFieldElement.class).all().stream()
.filter(elem -> elem.getPropertyString("label").equals("Name"))
This creates n+1 requests to the browser; depending on the number of text fields on the page this could be very slow.
Then my recommendation is to add id to components in the Java code and use id selector in TestBench, that will use css selector with attribute and it will be faster.
Regarding finding a button by text content there are two alternatives
Finding it using plain Selenium xpath:
WebElement buttonElement = getDriver().findElement(By.xpath("//vaadin-button[text()='Caption']"));
Or finding buttons and picking one having right text content
ButtonElement vaadinButton = $(ButtonElement.class).all().stream().filter(b -> b.getText().equals("Caption")).findFirst()
.orElseThrow(() -> new NotFoundException("Button not found"));
The difference is the result type
Is this solved by https://github.com/vaadin/testbench/pull/1784 in v24?
@alvarezguille , yes. Implementation logic in Vaadin 24 is similar to my workaround example above.
Yup, #1784 technically solves the issue.
Unfortunately, #1784 filters the components in Java, which will cause n+1 requests to the browser, and therefore this will be inherently slow.
It would be preferable to filter the components in the browser, in JS, thus making just one request to the browser.
Thank you @TatuLund , this solution looks very interesting: WebElement buttonElement = getDriver().findElement(By.xpath("//vaadin-button[text()='Caption']"));. Let me test whether it works for buttons with icons as well.
EDIT: this code works well, from within TestBenchTestCase:
final List<WebElement> buttons = findElements(By.xpath("//vaadin-button[text()='" + caption + "']"));
final TestBenchElement button = (TestBenchElement) buttons.get(0);
final ButtonElement be = button.wrap(clazz);
Workaround:
public class TestBenchUtils {
public static String getCaption(TestBenchElement element) {
if (element instanceof HasLabel) {
String caption = ((HasLabel) element).getText();
if (caption.isEmpty()) {
// check whether the component is placed in FormLayout - if it is, it may take the caption from FormItemElement.
final TestBenchElement parent = getParent(element);
if (parent != null && parent.getTagName().equals("vaadin-form-item")) {
final FormItemElement formItemElement = parent.wrap(FormItemElement.class);
caption = formItemElement.getLabel().getText();
}
}
return caption;
} else if (element instanceof ButtonElement) {
return ((ButtonElement) element).getText();
} else if (element instanceof DialogElement) {
return ((DialogElement) element).getPropertyString("headerTitle");
}
return "";
}
private static String getTagName(Class<?> elementClass) {
Element annotation = elementClass.getAnnotation(Element.class);
if (annotation == null) {
return "*";
}
return annotation.value();
}
public static <T extends TestBenchElement> List<T> findByProperty(HasElementQuery context, Class<T> elementClass, String propertyName, String propertyValue) {
// workaround for https://github.com/vaadin/testbench/issues/1881
final String tagName = getTagName(elementClass);
final TestBenchElement element;
final String searchScope;
if (context instanceof TestBenchElement) {
element = (TestBenchElement) context;
searchScope = "arguments[0]";
} else {
element = null;
searchScope = "document";
}
final List<TestBenchElement> matchingElements = (List<TestBenchElement>) ((HasTestBenchCommandExecutor) context).getCommandExecutor().
executeScript("var propertyName = arguments[2]; var propertyValue = arguments[3]; var tagName = arguments[1]; return Array.from(" + searchScope + ".querySelectorAll(tagName)).filter(node => node[propertyName] == propertyValue)", element, tagName, propertyName, propertyValue);
return matchingElements.stream().map(e -> e.wrap(elementClass)).toList();
}
public static <T extends TestBenchElement> List<T> caption(HasElementQuery context, Class<T> elementClass, String caption) {
if (ButtonElement.class.isAssignableFrom(elementClass)) {
// this is fast enough
final List<WebElement> buttons = context.getContext().findElements(By.xpath("//vaadin-button[text()='" + caption + "']"));
// this is much slower, but might be more accurate.
// final List<T> buttons = $(clazz).all().stream().filter(it -> it.getText().equals(caption)).toList();
final List<T> result = buttons.stream().map(it -> ((TestBenchElement)it).wrap(elementClass)).toList();
return result;
}
// first, try a quick search, maybe we'll get lucky.
final List<T> matchingElements = findByProperty(context, elementClass, "label", caption);
if (!matchingElements.isEmpty()) {
return matchingElements;
}
// @todo VAADIN-FIXME this will be very slow. We need a custom query which filters components in the browser.
// filed upstream: https://github.com/vaadin/testbench/issues/1883
// no luck, fall back to the slow way.
final List<T> all = context.$(elementClass).all();
return all.stream()
.filter(it -> caption.equals(getCaption(it)))
.collect(Collectors.toList());
}
}