> Markdown version of [Test a Custom Component](https://vaadin.com/docs/next/building-apps/testing/browserless/test-custom-components). Section index: [llms.txt](https://vaadin.com/docs/next/building-apps/llms.txt)

# Test a Custom Component

The examples use the plain Java setup from [Set Up Browserless Tests in Plain Java](https://vaadin.com/docs/next/building-apps/testing/browserless/setup-without-spring.md) 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`.

## <a id="test-a-component-without-a-view"></a>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:

```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](https://vaadin.com/docs/next/flow/testing/browserless/multi-user.md) 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:

```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());
}
```

## <a id="build-a-custom-tester"></a>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.

### <a id="create-the-component"></a>Create the Component

This view contains a phone number field built from a country code combo box and a number field:

`PersonFormView.java`

```java
import 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] : "");
        }
    }
}
```

### <a id="define-the-tester"></a>Define the Tester

Place this tester in the same package as `PersonFormView` so it can access the child fields of `PhoneNumberField`:

`PhoneNumberFieldTester.java`

```java
import 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")`.

### <a id="register-and-use-the-tester"></a>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:

`PersonFormViewTest.java`

```java
import 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());
    }
}
```

## <a id="create-a-custom-locator"></a>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`:

`PersonFormLocator.java`

```java
import 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:

```java
window.find(PersonFormLocator::new)
        .fillIn("Ada", "ada@example.com")
        .submit();
```

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