1

目前我正在研究 TextField 和默认/取消按钮的问题。在使用 TestFX 测试假定的修复程序时,我遇到了事件调度 (?) 的差异,这会导致测试失败,而应用程序似乎正在运行。

下面是一个非常简化的版本:

  • 只是一个简单的 ui,由一个框内的 textField 组成,用于 Application/Test
  • textField 有一个 (key-) 处理程序,当按下a时会触发 actionEvent
  • textField 有一个使用 actionEvent 的操作处理程序
  • (key-) 处理程序检查操作是否已被使用(这里:简单日志,在实际上下文中,如果操作被使用,则必须使用 (key-) 事件)

修复的关键部分是使用 textField 作为源和目标创建 actionEvent(以防止在调度期间将事件复制到新实例中):

ActionEvent action = new ActionEvent(field, field);

运行应用程序时,这似乎足以使其工作。运行测试时,失败 - 事件被复制到另一个实例,因此被消耗的事件与在调度中传递的事件是不同的实例(只能在调试期间看到)。无法确定发生这种情况的确切地点/原因。

问题是:这种差异是预期的,如果是,为什么/在哪里?或者我做错了什么(远非零概率)?

重现

  • 运行应用程序,按 a:注意说明已触发的 actionEvent 已被消耗的日志
  • 运行测试,注意日志说明触发的 actionEvent 没有被消耗

编码:

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

}

考试:

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

我的环境是 2018 年 10 月在 win10 上的 fx11 和 TestFX。仅供参考:在 testFX 中打开了一个问题

4

1 回答 1

2

不同之处在于 TestFxEventType.ROOT在存储所有触发事件的舞台上注入了一个 eventFilter。破解方法是移除那个过滤器,像这样变脏:

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

虽然这绕过了这个特定的上下文,但它在一般情况下并没有可靠的帮助:只要在父层次结构中的任何地方都有一个 eventFilter(与触发的事件相同或超级 eventType),就会发生同样的情况。根本原因似乎是事件分派在为 eventFilters 分派事件时创建了新的事件实例(通过 event.copyFor)。因此,在动作处理程序中使用的事件实例与火发出的实例不同。

更新:

  • 注意到简单地删除firedEvents过滤器并没有帮助(不知道为什么它看起来像几天前那样......;):一旦有一个过滤器,事件就会永远分派到它的包含处理程序,即使它是后来清空了。
  • 提出问题
于 2019-08-10T12:48:34.337 回答