Forum Stats

  • 3,826,772 Users
  • 2,260,706 Discussions
  • 7,897,072 Comments

Discussions

[FX] trigger event loop?

TPD-Opitz
TPD-Opitz Member Posts: 2,465 Silver Trophy
edited Jan 13, 2016 11:11AM in Java 8 Questions

Hello

I have a class that starts a JavaFX Dialog (out of a Swing Action). The dialog creation is wrapped into a call to Platform.runLater(Runnable).

I have this alredy working.

Now I'm creating a UnitTest which checks that the DialogCreation method is called.

The problem is, that within my unittest the Runnable is called after the testmethod finised. Therefore the test cannot recognize the call to my dialog cration method.

here is a SSCCE:

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import org.junit.Rule;
import org.junit.Test;
import javafx.application.Platform;
/**
* test fails because Platform.runLater() is invokes after test has ended
* @author TPD-opitz
*/
public class FxDialogTest {
    public static class FxDialogOpenAction extends AbstractAction {

        private final DialogCreatorInterface _dialogCreatorInterface;

        public FxDialogOpenAction(DialogCreatorInterface dialogCreatorInterface) {
            _dialogCreatorInterface = dialogCreatorInterface;
        }

        @Override
        public void actionPerformed(ActionEvent arg0) {
            System.out.println("in starter thread, could be Swing");
            Platform.runLater(() -> {
                System.out.println("in FX thread, but too late");
                _dialogCreatorInterface.createFxDialog();
            });
        }
    }

    public static interface DialogCreatorInterface {
        void createFxDialog();
    }

    @Rule
    public JavaFXThreadingRule javafxRule = new JavaFXThreadingRule();

    @Test
    public void testActionPerformed_noPrecondition__callsCreateDialog() throws Exception {
        DialogCreatorInterface dialogCreatorInterface = mock(DialogCreatorInterface.class);
        Action action = new FxDialogOpenAction(dialogCreatorInterface);

        action.actionPerformed(mock(ActionEvent.class));

        Thread.sleep(2000);
        System.out.println("in tester thread after wait");

        verify(dialogCreatorInterface).createFxDialog();
    }
}


and the custom Rule used by the test:

import java.util.concurrent.CountDownLatch;
import javax.swing.SwingUtilities;
import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
/**
* A JUnit {@link Rule} for running tests on the JavaFX thread and performing JavaFX initialisation. To
* include in your test case, add the following code:
*
* <pre>
* {@literal @}Rule
* public JavaFXThreadingRule jfxRule = new JavaFXThreadingRule();
* </pre>
*
* @author Andy Till
*/
public class JavaFXThreadingRule implements TestRule {

    /**
     * Flag for setting up the JavaFX, we only need to do this once for all tests.
     */
    private static boolean jfxIsSetup;

    @Override
    public Statement apply(Statement statement, Description description) {

        return new OnJFXThreadStatement(statement);
    }

    private static class OnJFXThreadStatement extends Statement {

        private final Statement statement;

        public OnJFXThreadStatement(Statement aStatement) {
            statement = aStatement;
        }

        private Throwable rethrownException = null;

        @Override
        public void evaluate() throws Throwable {

            if (!jfxIsSetup) {
                setupJavaFX();

                jfxIsSetup = true;
            }

            final CountDownLatch countDownLatch = new CountDownLatch(1);

            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        statement.evaluate();
                    } catch (Throwable e) {
                        rethrownException = e;
                    }
                    countDownLatch.countDown();
                }
            });

            countDownLatch.await();

            // if an exception was thrown by the statement during evaluation,
            // then re-throw it to fail the test
            if (rethrownException != null) {
                throw rethrownException;
            }
        }

        protected void setupJavaFX() throws InterruptedException {

            long timeMillis = System.currentTimeMillis();

            final CountDownLatch latch = new CountDownLatch(1);

            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    // initializes JavaFX environment
                    new JFXPanel();

                    latch.countDown();
                }
            });

            System.out.println("javafx initialising...");
            latch.await();
            System.out.println("javafx is initialised in "
                    + (System.currentTimeMillis()
                            - timeMillis)
                    + "ms");
        }
    }
}


the output is:

javafx initialising...
javafx is initialised in 200ms
in starter thread, could be Swing
in tester thread after wait
in FX thread, but too late


how do I force the FX-event loop to be invokes in time?

bye

TPD

Tagged:

Answers

  • Unknown
    edited Jan 12, 2016 11:54AM
    The problem is, that within my unittest the Runnable is called after the testmethod finised. Therefore the test cannot recognize the call to my dialog cration method.
    

    Correct - that is what 'invokeLater' means - it will get executed 'later'. That might be a microsecond, millisecond or next week.

    how do I force the FX-event loop to be invokes in time?
    

    You can't and don't need to.

    You need to 'wait' some period of time to give the 'invokeLater' a chance to do it's job.

    Use a loop and sleep for a while then test. Wait a while longer and test again.

    See this 10 year old JavaWorld article

    Automate GUI tests for Swing applications | JavaWorld

    <span class="lit">104</span><span class="pln">   </span><span class="com">// The dialog box will show up shortly</span><span class="pln"><br/></span><span class="lit">105</span><span class="pln">   </span><span class="kwd">for</span><span class="pln"> </span><span class="pun">(</span><span class="kwd">int</span><span class="pln"> i </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0</span><span class="pun">;</span><span class="pln"> ok </span><span class="pun">==</span><span class="pln"> </span><span class="kwd">null</span><span class="pln"> </span><span class="pun">||</span><span class="pln"> message </span><span class="pun">==</span><span class="pln"> </span><span class="kwd">null</span><span class="pun">;</span><span class="pln"> </span><span class="pun">++</span><span class="pln">i</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln"><br/></span><span class="lit">106</span><span class="pln">   </span><span class="typ">Thread</span><span class="pun">.</span><span class="pln">sleep</span><span class="pun">(</span><span class="lit">200</span><span class="pun">);</span><span class="pln"><br/></span><span class="lit">107</span><span class="pln">  ok </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="typ">JButton</span><span class="pun">)</span><span class="typ">TestUtils</span><span class="pun">.</span><span class="pln">getChildIndexed</span><span class="pun">(</span><span class="pln">foo</span><span class="pun">,</span><span class="pln"> </span><span class="str">"JButton"</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">);</span><span class="pln"><br/></span><span class="lit">108</span><span class="pln">  message </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="typ">JTextArea</span><span class="pun">)</span><span class="typ">TestUtils</span><span class="pun">.</span><span class="pln">getChildIndexed</span><span class="pun">(</span><span class="pln">foo</span><span class="pun">,</span><span class="pln"> </span><span class="str">"JTextArea"</span><span class="pun">,</span><span class="pln"> </span><span class="lit">0</span><span class="pun">);</span><span class="pln"><br/></span><span class="lit">109</span><span class="pln">  assertTrue</span><span class="pun">(</span><span class="pln">i </span><span class="pun"><</span><span class="pln"> </span><span class="lit">10</span><span class="pun">);</span><span class="pln"><br/></span><span class="lit">110</span><span class="pln">   </span><span class="pun">}</span><span class="pln"><br/></span><span class="lit">111</span><span class="pln">  assertEquals</span><span class="pun">(</span><span class="pln"><br/></span><span class="lit">112</span><span class="pln">   </span><span class="typ">UIManager</span><span class="pun">.</span><span class="pln">getString</span><span class="pun">(</span><span class="str">"OptionPane.okButtonText"</span><span class="pun">),</span><span class="pln"> ok</span><span class="pun">.</span><span class="pln">getText</span><span class="pun">());</span><span class="pln"><br/></span><span class="lit">113</span><span class="pln">  assertEquals</span><span class="pun">(</span><span class="pln">testString </span><span class="pun">+</span><span class="pln"> </span><span class="str">"? ... done."</span><span class="pun">,</span><span class="pln"> message</span><span class="pun">.</span><span class="pln">getText</span><span class="pun">());</span><span class="pln"><br/></span><span class="lit">114</span><span class="pln">  <br/></span><span class="lit">115</span><span class="pln">  ok</span><span class="pun">.</span><span class="pln">doClick</span><span class="pun">();</span>
    

    Although the code you posted has the author's name it is customary to ALWAYS attribute any posts to the author with a link to the source.


    It looks like this may be the original source of that code:

    WONTFIX: JUnit Rule for JavaFX Controller Testing


  • TPD-Opitz
    TPD-Opitz Member Posts: 2,465 Silver Trophy
    edited Jan 13, 2016 6:44AM

    @rp0428 I know you as a well informed expert and a carefull reader. So I'm a bit surprized that you missed line 46 in my test case...

    bye

    TPD

  • Unknown
    edited Jan 13, 2016 11:11AM
    @rp0428 I know you as a well informed expert and a carefull reader. So I'm a bit surprized that you missed line 46 in my test case...
    
    

    Hmmm - no I didn't miss it. Did you miss this in my reply?

    Correct - that is what 'invokeLater' means - it will get executed 'later'. That might be a microsecond, millisecond or next week.
    
    

    That applies to that method

    https://docs.oracle.com/javase/7/docs/api/javax/swing/SwingUtilities.html#invokeLater%28java.lang.Runnable%29

    public static void invokeLater(
    Runnable doRun)
    Causes doRun.run() to be executed asynchronously on the AWT event dispatching thread. This will happen after all pending AWT events have been processed.
    
    

    And it also applies to the Platform.runLater method.

    https://docs.oracle.com/javase/7/docs/api/javax/swing/SwingUtilities.html#invokeLater%28java.lang.Runnable%29

    <strong><a href="https://docs.oracle.com/javafx/2/api/javafx/application/Platform.html#runLater%28java.lang.Runnable%29">runLater</a></strong>(java.lang.Runnable runnable)

    Run the specified Runnable on the JavaFX Application Thread at some unspecified time in the future.
    
    

    The key phrase is 'unspecified time in the future'.

    I posted the link to that 10 year old article so others would know this is NOT a new issue. That article's code uses a loop to just wait and wait.

    At line 46 you wait approximately 2 seconds.

            Thread.sleep(2000);  

    See the Java API for 'sleep' method in the Thread class.

    https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html

    <strong><a href="https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.html#sleep%28long%29">sleep</a></strong>(long millis)
    Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers.
    
    

    There are NO GUARANTEES as regards timing. What works in under 2 seconds one run might take a longer, or shorter, period of time the next run.

    When testing Swing or UI functionality you could wait for hours for a dialog button to be depressed if you are relying on a human to do it and they went to lunch right after the test started,

    Maybe there were no threads available, maybe the VM was in the middle of GC. Maybe  the JVM was out getting a donut and coffee. You know how slow some of those baristas can be.

    The obvious troubleshooting step would be to wait for a minute or two and see if that solves the problem.

    Post the results of that test.