Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ComponentEvent.isFromClient() is false despite using TestWrappers.test() in a SpringUIUnitTest #1814

Open
DennisSuffel opened this issue Jul 26, 2024 · 3 comments
Labels
enhancement UITest JUnit testing the UI

Comments

@DennisSuffel
Copy link

I expect ComponentEvent.isFromClient() to return true when using TestWrappers.test() in a SpringUIUnitTest to execute the action, that triggers the ComponentEvent.
Instead it returns false.

Steps to reproduce

MainView.java

package org.vaadin.example;

import com.vaadin.flow.component.html.NativeLabel;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.router.Route;

@Route
public class MainView extends VerticalLayout {

  static final String TEXT_FIELD = "textField";
  static final String NATIVE_LABEL = "nativeLabel";

  public MainView() {
    TextField textField = new TextField();
    textField.setId(TEXT_FIELD);

    NativeLabel nativeLabel = new NativeLabel();
    nativeLabel.setId(NATIVE_LABEL);

    textField.addValueChangeListener(event -> {
      if (event.isFromClient()) {
        nativeLabel.setText(event.getValue());
      }
    });

    add(textField, nativeLabel);
  }
}

MainViewTest.java

package org.vaadin.example;

import static org.assertj.core.api.Assertions.*;
import static org.vaadin.example.MainView.NATIVE_LABEL;
import static org.vaadin.example.MainView.TEXT_FIELD;

import com.vaadin.flow.component.html.NativeLabel;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.testbench.unit.SpringUIUnitTest;
import org.junit.jupiter.api.Test;

class MainViewTest extends SpringUIUnitTest {

  @Test
  void whenSetTextThenSetLabel() {
    navigate(MainView.class);

    test($(TextField.class).id(TEXT_FIELD)).setValue("new value");

    String label = $(NativeLabel.class).id(NATIVE_LABEL).getText();
    assertThat(label).isEqualTo("new value");
  }
}

Expected Result

event.isFromClient() should return true, when TestWrappers.test() is used to set the value of a TextField.
Then MainViewTest#whenSetTextThenSetLabel would complete successfuly.

Actual Result

MainViewTest#whenSetTextThenSetLabel fails with this AssertionError:

org.opentest4j.AssertionFailedError:
expected: "new value"
but was: ""

Versions

Vaadin 24.4.6
Java 17
macOS 14.5

@TatuLund TatuLund added the bug label Jul 26, 2024
@TatuLund
Copy link
Contributor

True, the problem is here

https://github.com/vaadin/testbench/blob/main/vaadin-testbench-unit-shared/src/main/java/com/vaadin/flow/component/textfield/TextFieldTester.java#L57

The tester should call with introspection setModelValue(value, true) instead.

@TatuLund
Copy link
Contributor

TatuLund commented Aug 1, 2024

I checked this further ... It looks like it is problem with all the fields. The trouble is that not all of them are as easy to fix as the fields directly using AbstractField.setValue(..). Some other fields like DatePicker, ComboBox, Select ... are overriding this method and for those fields using the setModelValue(value, true) is not enough as it is missing necessary things.

I first thought that I can simply write utility method that is used by all field Testers to set value, like below:

public class Utils {

    public static <V> void setValueAsUser(AbstractField<?, V> component, V value) {
        component.setValue(value);
        Class<?> clazz = component.getClass();
        while (!clazz.equals(AbstractField.class)) {
            clazz = clazz.getSuperclass();
        }
        try {
            Method setValueMethod = clazz.getDeclaredMethod("setModelValue",
                    Object.class, Boolean.TYPE);
            setValueMethod.setAccessible(true);
            setValueMethod.invoke(component, value, true);
        } catch (NoSuchMethodException | SecurityException
                | IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            e.printStackTrace();
        }        
    }
}

Which would allow me to rewrite TextFieldTester as

    public void setValue(V value) {
        ensureComponentIsUsable();

        if (value == null && getComponent().getEmptyValue() != null) {
            throw new IllegalArgumentException(
                    "Field doesn't allow null values");
        }

        if (hasValidation() && value != null
                && getValidationSupport().isInvalid(value.toString())) {
            if (getComponent().isPreventInvalidInputBoolean()) {
                throw new IllegalArgumentException(
                        "Given value doesn't pass field value validation. Check validation settings for field.");
            }
            LoggerFactory.getLogger(TextFieldTester.class).warn(
                    "Gave invalid input, but value set as invalid input is not prevented.");
        }

        Utils.setValueAsUser(getComponent(), value);
    }

This works for TextFieldTester, NumberFieldTester and TextAreaTester ... But I think not for the others.

@mvysny
Copy link
Member

mvysny commented Aug 1, 2024

I've used even wilder reflection which works for all AbstractFields:

public class FlowUtils {
    @NotNull
    private static <V> AbstractFieldSupport<?, V> getFieldSupport(@NotNull HasValue<?, V> component) {
        try {
            final Field javaField = AbstractField.class.getDeclaredField("fieldSupport");
            javaField.setAccessible(true);
            return (AbstractFieldSupport<?, V>) javaField.get(component);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Sets the value to given component. Supports pretending that the value came from the browser.
     * @param component the component to set the value to, not null.
     * @param value the new value, may be null.
     * @param isFromClient if true, we'll pretend that the value came from the browser. This causes the value change event to be fired
     *                     with `isFromClient` set to true.
     * @param <V> the value type
     */
    public static <V> void setValue(@NotNull HasValue<?, V> component, @Nullable V value, boolean isFromClient) {
        if (!isFromClient) {
            component.setValue(value);
            return;
        }
        if (component instanceof AbstractField) {
            final AbstractFieldSupport<?, V> fs = getFieldSupport(component);
            try {
                final Method m = AbstractFieldSupport.class.getDeclaredMethod("setValue", Object.class, boolean.class, boolean.class);
                m.setAccessible(true);
                m.invoke(fs, value, false, isFromClient);
            } catch (NoSuchMethodException | IllegalAccessException |
                     InvocationTargetException e) {
                throw new RuntimeException(e);
            }
            return;
        }
        throw new IllegalArgumentException("Parameter component: invalid value " + component + ": unsupported type of HasValue: " + component.getClass());
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement UITest JUnit testing the UI
Development

No branches or pull requests

4 participants