Test a Custom Component
The examples use the plain Java setup from Set Up Browserless Tests in Plain Java and construct components directly, without a dependency injection container.
To test a component that needs injected dependencies, test it inside a view with your framework’s setup.
MyForm and PersonForm stand for your own forms, and PhoneNumberField is a custom field nested in PersonFormView.
Test a Component Without a View
A single component — for example, a form or a custom field — can be tested in isolation, without wrapping it in an @Route view.
BrowserlessUIContext.forComponent() builds a self-contained test environment without routes, attaches the component to a window’s UI, and tears everything down when the window closes, so a single try-with-resources is enough:
Source code
Java
try (var window = BrowserlessUIContext.forComponent(new MyForm())) {
window.findTextField().withLabel("Name").setValue("Ada");
Assertions.assertEquals("Ada",
window.findTextField().withLabel("Name").component().getValue());
}The returned window is a BrowserlessUIContext, so the full testing DSL is available: find(), test(), and the typed locator entry points such as findButton(). See Application, User, and Window Contexts for the context API.
If the component’s constructor needs UI.getCurrent() or the session, pass a factory instead: BrowserlessUIContext.forComponent(MyForm::new). The factory runs after the Vaadin thread-locals are installed, so the constructor observes the live environment.
For tests that need the same standalone component in several windows or for several users, use BrowserlessApplicationContext.forComponent(Supplier). It returns the application context, and every window created from it gets a fresh component instance from the factory:
Source code
Java
try (var app = BrowserlessApplicationContext.forComponent(MyForm::new)) {
var w1 = app.newUser().newWindow();
var w2 = app.newUser().newWindow();
w1.findTextField().withLabel("Name").setValue("Ada");
// w2 holds its own MyForm instance
Assertions.assertEquals("", w2.findTextField().withLabel("Name")
.component().getValue());
}Build a Custom Tester
When you create custom components, you can build testers for them too. Custom testers extend ComponentTester and use the @Tests annotation to declare which component they test.
Create the Component
This view contains a phone number field built from a country code combo box and a number field:
Source code
PersonFormView.java
PersonFormView.javaimport com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.customfield.CustomField;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.Route;
@Route("person-form")
public class PersonFormView extends Div {
final PhoneNumberField phone = new PhoneNumberField();
public PersonFormView() {
add(phone);
}
public static class PhoneNumberField extends CustomField<String> {
final ComboBox<String> countryCode = new ComboBox<>();
final TextField number = new TextField();
public PhoneNumberField() {
countryCode.setItems("+1", "+358");
add(countryCode, number);
countryCode.addValueChangeListener(event -> updateValue());
number.addValueChangeListener(event -> updateValue());
}
@Override
protected String generateModelValue() {
return (countryCode.getValue() == null ? "" : countryCode.getValue())
+ " " + number.getValue();
}
@Override
protected void setPresentationValue(String value) {
if (value == null || value.isBlank()) {
countryCode.clear();
number.clear();
return;
}
String[] parts = value.split(" ", 2);
countryCode.setValue(parts[0]);
number.setValue(parts.length > 1 ? parts[1] : "");
}
}
}Define the Tester
Place this tester in the same package as PersonFormView so it can access the child fields of PhoneNumberField:
Source code
PhoneNumberFieldTester.java
PhoneNumberFieldTester.javaimport java.util.List;
import com.vaadin.browserless.ComponentTester;
import com.vaadin.browserless.Tests;
import com.vaadin.flow.component.combobox.ComboBox;
import com.vaadin.flow.component.combobox.ComboBoxTester;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.component.textfield.TextFieldTester;
// Tests defines the components this tester should be used for automatically
@Tests(PersonFormView.PhoneNumberField.class)
public class PhoneNumberFieldTester extends ComponentTester<PersonFormView.PhoneNumberField> {
// Other testers can be used inside the custom tester
final ComboBoxTester<ComboBox<String>, String> combo_;
final TextFieldTester<TextField, String> number_;
public PhoneNumberFieldTester(PersonFormView.PhoneNumberField component) {
super(component);
combo_ = new ComboBoxTester<>(
getComponent().countryCode);
number_ = new TextFieldTester<>(getComponent().number);
}
public List<String> getCountryCodes() {
return combo_.getSuggestionItems();
}
public void setCountryCode(String code) {
ensureComponentIsUsable();
if (!getCountryCodes().contains(code)) {
throw new IllegalArgumentException("Given code isn't available for selection");
}
combo_.selectItem(code);
}
public void setNumber(String number) {
ensureComponentIsUsable();
number_.setValue(number);
}
public String getValue() {
return getComponent().getValue();
}
}The tester uses ComboBoxTester and TextFieldTester internally; custom testers can build on other testers this way.
|
Tip
|
Generic Components
The @Tests annotation also has an fqn attribute that accepts fully qualified class names as strings. Use this when the component type uses generics that prevent it from being passed as a class literal:
@Tests(fqn = "com.example.MyField").
|
Register and Use the Tester
Keep your tester in an application-owned package and annotate the test class with @ComponentTesterPackages.
The test() method then returns the custom tester for the field:
Source code
PersonFormViewTest.java
PersonFormViewTest.javaimport org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import com.vaadin.browserless.BrowserlessTest;
import com.vaadin.browserless.ComponentTesterPackages;
@ComponentTesterPackages("com.example.application.views.personform")
class PersonFormViewTest extends BrowserlessTest {
@Test
void enterPhoneNumber() {
PersonFormView view = navigate(PersonFormView.class);
PhoneNumberFieldTester phone = test(view.phone);
phone.setCountryCode("+358");
phone.setNumber("40 1234567");
Assertions.assertEquals("+358 40 1234567", phone.getValue());
}
}Create a Custom Locator
For composites, page objects, or domain-specific widgets, subclass Locator with the recursive self-type so filter steps stay fluent, and expose the actions you want the test to see.
Scope inner queries with inside(this) so they only match descendants of the resolved composite.
The following example assumes a PersonForm with fields identified by pf-name and pf-email, and a button identified by pf-submit:
Source code
PersonFormLocator.java
PersonFormLocator.javaimport com.vaadin.browserless.locator.Locator;
import com.vaadin.flow.component.button.ButtonLocator;
import com.vaadin.flow.component.textfield.TextFieldLocator;
public class PersonFormLocator
extends Locator<PersonForm, PersonFormLocator> {
public PersonFormLocator() {
super(PersonForm.class);
}
public PersonFormLocator fillIn(String name, String email) {
new TextFieldLocator().withId("pf-name").inside(this).setValue(name);
new TextFieldLocator().withId("pf-email").inside(this).setValue(email);
return this;
}
public void submit() {
new ButtonLocator().withId("pf-submit").inside(this).click();
}
}Tests reach the custom locator through find(Supplier<L>), for example in a test window that contains the form:
Source code
Java
window.find(PersonFormLocator::new)
.fillIn("Ada", "ada@example.com")
.submit();93145B04-239A-434F-A5B8-C95EAC71013B