UI Technology

tapir is not restricted to the usage of Selenium. Selenium is just one framework you can use for automated interaction with a software under test.

The Selenium modules are not part of tapir’s core modules, so it’s easy to exchange them with your prefered automation framework.

tapir is not stucked on web applications. You can also test desktop applications if you have a suitable automation framework which is implemented in Java. tapir’s API is completely technology-independent.

In tapir this is the most complex task as you need to build a UI technology binding from scratch. It's time-consuming, but you will likely do this rarely or never.

How to Start?

These is a single point where you could integrate your custom UI technology module: Page objects. In your page object each field is bound to a specific UI technology implementation. This binding is established by an annotation which is annotated by @PageElementAnnotation. tapir’s Selenium module provides the @SeleniumElement annotation in order to bind the field to a html element which is located by Selenium. The annotated fields are processed and initialized by the bound PageObjectFieldInitializer. In case of Selenium the SeleniumPageObjectFieldInitializer solves this task.

tapir provides an experimental JavaFX module which is based on TestFX. We use this binding to exemplify the implementation of a custom UI technology.

As we don't want to get lost in Java FX implementation details we focus on the tapir API. Explanation of the JavaFX/TestFX API are omitted unless they are essential to understand the use case.


First of all you need an annotation which binds page fields as JavaFX elements. Each JavaFX element can have an (unique) id, which can be used to locate it.


@DynamicActive(processorRequired = false)
annotation JavaFXElement {
 String id


You have to implement your own PageObjectFieldInitializer in order to inject the fields of the page objects.


class JavaFXPageObjectFieldInitializer implements PageObjectFieldInitializer {
    JavaFxElementFactory javaFxElementFactory
    FxRobotInterface fxRobot
     * @since 2.0.0
    override void initializeFields(Object element) {
        // Get all fields in the element which are annotated with 'JavaFXElement'
        val allFields = element.class.declaredFields
        val annotatedFields = allFields.filter[isAnnotationPresent(JavaFXElement)]
        for (field : annotatedFields) {
            // Get the id from the annotation
            var javaFXElementAnnotation = field.getAnnotation(JavaFXElement)
            val id = javaFXElementAnnotation.id()
            val typeToken = TypeToken.of(field.genericType);
            // Get the (proxied) tapir element from the factory
            val tapirElement = javaFxElementFactory.getJavaFxElement(fxRobot.lookup(id).queryFirst, typeToken);
            // Inject the field value
            FieldUtils.writeField(field, element, tapirElement, true)

The initializer collects all fields which are annotated by JavaFXElement (line 13) and transforms them to tapir elements (line 20) and injects the TapirElement into the field (line 22).


Each field which is annotated by JavaFXElement needs a type which implements/extends TapirElement. It’s nearly the same as described in the chapter HTML Components with the difference that your implementation relies on TestFX instead of Selenium.

This is a example interface and its implementation:


public interface Button extends TapirElement, Displayable, Clickable, Enabable {


class DefaultJavaFXButton extends AbstractJavaFXElement implements Button {
    FxRobotInterface fxRobot

    override isDisplayed() {

    override click() {

    override isEnabled() {

Query lately

As decribed in the chapter Selenium Core module you should query elements as late as possible. Unfortunately JavaFX makes extensive use of final methods without providing interfaces. Therefore it’s impossible to use proxies. In this example we focus on proxying the NodeQuery interface which is provided by TestFX:


class JavaFxConfiguration {

    AutowireCapableBeanFactory beanFactory

    def ApplicationTestFixture applicationTestFixture(@Value("${javafx.headless:#{false}}") boolean headless) {
        System.setProperty("java.awt.headless", headless.toString)
        new ApplicationTestFixture
    def FxRobotInterface fxRobot(ApplicationTestFixture applicationTestFixture) {
        val searchContextMethodInterceptor = new NodeQueryMethodInterceptor[applicationTestFixture]
        beanFactory.autowireBeanProperties(searchContextMethodInterceptor, AutowireCapableBeanFactory.AUTOWIRE_NO, true)
        val proxyFactory = new ProxyFactory()
        proxyFactory.interfaces = FxRobotInterface
        val proxy = proxyFactory.getProxy(class.classLoader) as FxRobotInterface

Each FxRobot method call is interecepted by NodeQueryMethodInterceptor which is explained below:


class NodeQueryMethodInterceptor implements MethodInterceptor {
    extension NodeProxyFactory
    final Supplier<?> cache
    new(Supplier<?> nodeQuerySupplier) {
        cache = Suppliers.memoize(nodeQuerySupplier)
    def private getNodeQuery() {
    override invoke(MethodInvocation invocation) throws Throwable {
        val method = invocation.method
        val args = invocation.arguments
        val returnTypeToken = TypeToken.of(method.genericReturnType)
        if(returnTypeToken.isSubtypeOf(NodeQuery)) {
            return getNodeQueryProxy[
                method.invokeReflective(args) as NodeQuery
        return method.invokeReflective(args)
    def protected Object invokeReflective(Method method, Object... args) {
        val nodeQuery = getNodeQuery
        try {
            method.accessible = true
            return method.invoke(nodeQuery, args)
        } catch(InvocationTargetException e) {
            throw e.targetException

Whenever a method at NodeQuery is called which returns a subtype of NodeQuery, a NodeQueryProxy is built by NodeProxyFactory. All other method calls are forwarded to the proxied NodeQuery which is obtained lazily and cached by the cache field.

class NodeProxyFactory {
    private AutowireCapableBeanFactory beanFactory
    def NodeQuery getNodeQueryProxy(Supplier<NodeQuery> nodeQuerySupplier) {
        val nodeQueryMethodInterceptor = new NodeQueryMethodInterceptor(nodeQuerySupplier)
        beanFactory.autowireBeanProperties(nodeQueryMethodInterceptor, AutowireCapableBeanFactory.AUTOWIRE_NO, true)
        val proxyFactory = new ProxyFactory(NodeQuery, nodeQueryMethodInterceptor)
        val proxy = proxyFactory.getProxy(class.classLoader) as NodeQuery

The NodeProxyFactory just intercpets method calls to the given nodeQuerySupplier with the NodeQueryMethodInterceptor. This code is much the same as in JavaFxConfiguration, but using the interceptor again enables proxying nested NodeQuery calls like NodeQuery.lookup(String query) which returns a NodeQuery.


Integrating a new UI technology in tapir is a complex task, but most users never ever get in touch as you could rely on the UI technologies bindings that already exist.

If you have to get your handy dirty by implementing a UI technology binding, it’s definitely worth the effort as you benefit from all the great features tapir provides. Users which already use another tapir UI technology do not need time to learn the ropes as they are familiar with the API and all the concepts.

This is a Page Object based on the JavaFX module:


class MainPage {
    Button button

    TextField textField

    Label label

And an example test:


class MainTestClass {

    MainPage mainPage

    def void step1() {
        assertThat(mainPage.label.text, is('Button pressed'))

    def void step2() {
        mainPage.textField.text = 'Hallo world'