Docs

Test a Custom Component

Test a component without a route, add a reusable component tester, and encapsulate interactions in a custom locator.

Add the dependencies from the plain Java setup guide before using the standalone component API. This guide constructs components directly without a dependency injection container. The standalone factories do not apply the CDI setup or resolve @Inject dependencies. For a CDI-managed component, test it in a registered CDI view using AbstractCdiViewTest.

The examples use illustrative application components: replace MyForm with your form and place the PhoneNumberField below inside PersonFormView.

Testing 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, route-free test environment, 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 Multi-User and Multi-Window Testing 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());
}

Building Custom Testers

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.

Defining a Custom Tester

Place this tester in the same package as PersonFormView so it can access the child fields.

Source code
Java
// 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();
    }

}
Source code
PhoneNumberField Nested in PersonFormView
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] : "");
    }
}

Custom testers can use other testers internally, as shown above with ComboBoxTester and TextFieldTester.

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").

Registering Custom Testers

Keep your tester in an application-owned package and annotate the test class with @ComponentTesterPackages:

Source code
Java
@ComponentTesterPackages("com.example.application.views.personform")
class PersonFormViewTest extends BrowserlessTest {
}

Custom Locators

The following example assumes a PersonForm with fields identified by pf-name and pf-email, and a button identified by pf-submit. Use it inside a test window containing that form.

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:

Source code
Java
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>):

Source code
Java
window.find(PersonFormLocator::new)
        .fillIn("Ada", "ada@example.com")
        .submit();

93145B04-239A-434F-A5B8-C95EAC71013B

Updated