kleopatra
kleopatra

Reputation: 51525

Application vs ApplicationTest: different event dispatch?

Currently I'm digging into issues with TextField and default/cancel button. While testing a presumed fix with TestFX, I ran into a difference in event dispatch (?) that make the test fail while the application appears to be working.

Below is a very simplified version:

The crucial part of the fix is to create the actionEvent with the textField as both source and target (to prevent copying the event into a new instance during dispatch):

ActionEvent action = new ActionEvent(field, field);

When running the app, this seems to be enough to make it work. When running the test, fails - the event is copied to another instance such that the event that was consumed is a different instance than the one that's passed around in the dispatch (can be seen only during debugging). Couldn't nail the exact point where/why that happens.

The question: is that difference expected, and if so why/where? Or what I'm doing wrong (a far from zero probability)?

To reproduce

The code:

public class ActionApp extends Application {

    // create a simple ui - static because must be same for ActionTest
    public static Parent createContent() {
        TextField field = new TextField();
        // some handler to fire an actionEvent
        field.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
            if (e.getCode() == KeyCode.A) {
                ActionEvent action = new ActionEvent(field, field);
                field.fireEvent(action);
                LOG.info("action/consumed? " + action + action.isConsumed());
            }
        });
        // another handler to consume the fired action
        field.addEventHandler(ActionEvent.ACTION, e -> {
            e.consume();
            LOG.info("action received " + e + e.isConsumed());
        });

        VBox actionUI = new VBox(field);
        return actionUI;
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setTitle(FXUtils.version());
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(ActionApp.class.getName());

}

The test:

public class ActionTest extends  ApplicationTest {

    /**
     * Does not really test anything, just to see the output.
     */
    @Test
    public void testConsumeA() {
        // sanity: focused to receive the key
        verifyThat(".text-field", NodeMatchers.isFocused());
        press(KeyCode.A);
    }

    @Override
    public void start(Stage stage) {
        Parent root = ActionApp.createContent();
        Scene scene = new Scene(root, 100, 100);
        stage.setScene(scene);
        stage.show();
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(ActionTest.class.getName());
}

My environment is fx11 and TestFX from Oct 2018 on win10. FYI: Opened an issue in testFX

Upvotes: 0

Views: 292

Answers (1)

kleopatra
kleopatra

Reputation: 51525

The difference is that TestFx injects an eventFilter for EventType.ROOT on the stage that stores all fired events. The hack is to remove that filter, going dirty like:

public static void stopStoringFiredEvents() {
    FxToolkitContext context = FxToolkit.toolkitContext();
    // reflectively access the private field of the context
    FiredEvents fired =(FiredEvents) FXUtils.invokeGetFieldValue(FxToolkitContext.class, context, "firedEvents");
    // stop recording
    fired.stopStoringFiredEvents();
}

/**
 * Updated hack, now reaaally dirty: need to manually clear the handler map :(
 */
public static void stopStoringFiredEvents(Stage stage) {
    // remove the event-logging filter
    stopStoringFiredEvents();
    // really cleanup: 
    // removing the filter only nulls the eventHandler in CompositeEventHandler
    // but does not remove the Composite from EventHandlerManager.handlerMap
    // as a result, handlerManager.dispatchCapturingEvent runs into the fixForSource
    // block which copies the event even though there is no filter
    WindowEventDispatcher windowDispatcher = (WindowEventDispatcher) stage.getEventDispatcher();
    EventHandlerManager manager = windowDispatcher.getEventHandlerManager();
    Map<?, ?> handlerMap = (Map<?, ?>) FXUtils.invokeGetFieldValue(EventHandlerManager.class, manager, "eventHandlerMap");
    handlerMap.clear();
}

While that hacks around this particular context, it does not reliably help in the general case: the same happens whenever there is an eventFilter (of the same or super eventType than the fired event) anywhere in the parent hierarchy. The underlying reason seems to be that the event dispatch creates new instances of events (via event.copyFor) when dispatching them for eventFilters. As a consequence, the instance of the event that's consumed in the action handler is not the same instance that's sent out by the fire.

Update:

  • noticed that simply removing the firedEvents filter doesn't help (no idea why it looked like it did a couple of days ago .. ;): once there has been an filter, events are dispatched to its containing handler eternally, even if it is emptied later.
  • filed an issue

Upvotes: 3

Related Questions