// Copyright 2013 SICK AG. All rights reserved. package de.sick.guicheck.fx; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import javafx.application.Platform; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import com.sun.javafx.stage.StageHelper; import com.sun.javafx.tk.Toolkit; import de.sick.guicheck.GcAssertException; import de.sick.guicheck.GcException; import de.sick.guicheck.GcUtils; import de.sick.guicheck.GcUtils.IEvaluator; /** * General helpers for GUIcheck tests based on JavaFX. * * @author linggol (created) */ public final class GcUtilsFX { private static final int EVALUATION_RETRIES = 10; private static final int EVALUATION_DELAY = 50; private static final int IDLE_COUNT = 3; private static final int RUN_LATER_AND_WAIT_TIMEOUT = 500; // EasyMock needs a litte time for synchronisation between UI and mocked objects private static int ms_slowMotionFactor = 10; /** * Private method in quantum toolkit to get the current windowing thread. This method is used to detect if JavaFX is * fully initialized and running. */ private static final Method GET_FX_USER_THREAD_METHOD; static { try { GET_FX_USER_THREAD_METHOD = Toolkit.class.getDeclaredMethod("getFxUserThread"); GET_FX_USER_THREAD_METHOD.setAccessible(true); } catch (NoSuchMethodException | SecurityException e) { throw new GcException("Failed to initialize access to quantum toolkit", e); } } private GcUtilsFX() { // Prevent instantiation } public static void setSlowMotion(final int factor) { ms_slowMotionFactor = factor; } /** * Wait for the windowing thread to become idle. */ public static void waitForIdle() { waitForIdle(IDLE_COUNT, ms_slowMotionFactor); } /** * Wait the given sleep cycles for the windowing thread to become idle. */ public static void waitForIdle(final int count, final int sleep) { for (int i = 0; i < count; i++) { runLaterAndWait(GcUtils.NOOP_RUNNABLE); GcUtils.sleepAndIgnoreInterrupts(sleep); } } /** * Run the given runnable in the windowing thread and wait until its finished. */ public static void runLaterAndWait(final Runnable runnable) { final CountDownLatch l_latch = new CountDownLatch(1); Platform.runLater(new Runnable() { @Override public void run() { runnable.run(); l_latch.countDown(); } }); while (true) { try { // Always check if the platform is still alive, otherwise when closing the last stage, // this loop hangs forever. if (!isPlatformAlive() || l_latch.await(RUN_LATER_AND_WAIT_TIMEOUT, TimeUnit.MILLISECONDS)) { break; } } catch (final InterruptedException l_exception) { } } } static boolean isPlatformAlive() { try { return GET_FX_USER_THREAD_METHOD.invoke(null) != null; } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new GcException("Failed to check if platform is alive", e); } } /** * Use this method to stop the test at any point and wait until all stages get closed by the program or user. The * program will exit after waiting. This method is especially useful while debugging with GUI tests. */ public static void waitAndExitWhenAllStagesClosed() { while (StageHelper.getStages().size() > 0) { GcUtils.sleepAndIgnoreInterrupts(500); } Platform.exit(); System.exit(0); } public static void treeVisibleIs(final GcNodeFX gcNode, final boolean visible) { treeVisibleIs(gcNode.getNode(), visible); } /** * Check that the given {@link GcNodeFX} is visible. * <p> * The Node is visible, if itself and all of his parents are visible. * It's invisible if itself or any parent is invisible. */ public static void treeVisibleIs(final Node fxNode, final boolean visible) { if (fxNode.isVisible()) { final Parent l_parent = fxNode.getParent(); if (l_parent == null) { if (!visible) { throw new GcException("Unexpected value of treeVisible: Expected: false, Actual: true"); } } else { treeVisibleIs(l_parent, visible); } } else { if (visible) { throw new GcException("Unexpected value of treeVisible: Expected: true, Actual: false"); } } } /** * Evaluate the given evaluator with retries and timeouts. Retries are only done automatically if the evaluator * throws a {@link GcAssertException}. After each try this method waits for the windowing thread to become idle. */ public static <T> T eval(final IEvaluator<T> e) { return eval(e, EVALUATION_RETRIES, EVALUATION_DELAY); } /** * Evaluate the given evaluator with retries and timeouts. Retries are only done automatically if the evaluator * throws a {@link GcAssertException}. After each try this method waits for the windowing thread to become idle. */ public static <T> T eval(final IEvaluator<T> e, int evalRetries, int evalDelay) { for (int i = 0; i < evalRetries - 1; i++) { try { return e.eval(); } catch (final GcAssertException l_exception) { GcUtils.sleepAndIgnoreInterrupts(evalDelay); waitForIdle(); } } // The last time we try it without catching any exceptions return e.eval(); } /** * Check whether the given menu path exists. * * @param menuPath Path of menus and a menu item separated by / chars. * @throws GcAssertException Thrown if the path does not exist. */ static void menuPathExists(final Collection<? extends MenuItem> menuItems, final String menuPath) { GcUtilsFX.eval(new GcUtils.IEvaluator<Void>() { @Override public Void eval() { getMenuPath(menuItems, menuPath); return null; } }); } static ArrayList<MenuItem> getMenuPath(Collection<? extends MenuItem> menuItems, String menuPath) { ArrayList<MenuItem> l_result = new ArrayList<MenuItem>(); ArrayList<MenuItem> l_menus = new ArrayList<MenuItem>(menuItems); // Run down the menu path and collect the menus in the result collection for (String id : menuPath.split("/")) { boolean l_found = false; for (final MenuItem l_menu : l_menus) { if (id.equals(l_menu.getId())) { l_found = true; l_result.add(l_menu); l_menus.clear(); if (l_menu instanceof Menu) { l_menus.addAll(((Menu)l_menu).getItems()); } break; } } if (!l_found) { throw new GcAssertException("Cannot find menu path: " + menuPath); } } return l_result; } }