Reputation: 51525
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
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:
Upvotes: 3