1 /* 2 * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. 3 * 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * The contents of this file are subject to the terms of either the Universal Permissive License 7 * v 1.0 as shown at http://oss.oracle.com/licenses/upl 8 * 9 * or the following license: 10 * 11 * Redistribution and use in source and binary forms, with or without modification, are permitted 12 * provided that the following conditions are met: 13 * 14 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions 15 * and the following disclaimer. 16 * 17 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of 18 * conditions and the following disclaimer in the documentation and/or other materials provided with 19 * the distribution. 20 * 21 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to 22 * endorse or promote products derived from this software without specific prior written permission. 23 * 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 25 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 26 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 30 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY 31 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 */ 33 package org.openjdk.jmc.test.jemmy.misc.base.wrappers; 34 35 import java.awt.Toolkit; 36 import java.awt.datatransfer.DataFlavor; 37 import java.awt.datatransfer.Transferable; 38 import java.awt.datatransfer.UnsupportedFlavorException; 39 import java.awt.im.InputContext; 40 import java.io.File; 41 import java.io.IOException; 42 import java.lang.reflect.Method; 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 48 import org.eclipse.core.runtime.jobs.Job; 49 import org.eclipse.swt.SWTException; 50 import org.eclipse.swt.custom.CTabFolder; 51 import org.eclipse.swt.widgets.Composite; 52 import org.eclipse.swt.widgets.Control; 53 import org.eclipse.swt.widgets.Display; 54 import org.eclipse.swt.widgets.Menu; 55 import org.eclipse.swt.widgets.MenuItem; 56 import org.eclipse.swt.widgets.Shell; 57 import org.eclipse.swt.widgets.Table; 58 import org.eclipse.swt.widgets.Widget; 59 import org.jemmy.Point; 60 import org.jemmy.action.AbstractExecutor; 61 import org.jemmy.control.Wrap; 62 import org.jemmy.env.Environment; 63 import org.jemmy.env.TestOut; 64 import org.jemmy.image.awt.AWTImage; 65 import org.jemmy.image.awt.AWTRobotCapturer; 66 import org.jemmy.image.awt.AverageDistanceImageComparator; 67 import org.jemmy.image.awt.FilesystemImageLoader; 68 import org.jemmy.image.awt.StrictImageComparator; 69 import org.jemmy.image.Image; 70 import org.jemmy.image.ImageComparator; 71 import org.jemmy.input.awt.AWTRobotInputFactory; 72 import org.jemmy.input.DefaultCharBindingMap; 73 import org.jemmy.interfaces.Focusable; 74 import org.jemmy.interfaces.Keyboard.KeyboardButton; 75 import org.jemmy.interfaces.Keyboard.KeyboardButtons; 76 import org.jemmy.interfaces.Keyboard.KeyboardModifiers; 77 import org.jemmy.interfaces.Mouse.MouseButtons; 78 import org.jemmy.interfaces.Parent; 79 import org.jemmy.lookup.AbstractLookup; 80 import org.jemmy.lookup.Lookup; 81 import org.jemmy.operators.Screen; 82 import org.jemmy.resources.StringComparePolicy; 83 import org.jemmy.swt.SWTMenu; 84 import org.jemmy.swt.Shells; 85 import org.jemmy.swt.lookup.ByItemLookup; 86 import org.jemmy.swt.lookup.ByName; 87 import org.jemmy.swt.lookup.ByTextControlLookup; 88 import org.jemmy.swt.lookup.ByTextShell; 89 import org.junit.Assert; 90 91 import org.openjdk.jmc.test.jemmy.misc.fetchers.Fetcher; 92 import org.openjdk.jmc.test.jemmy.misc.fetchers.FetcherWithInput; 93 import org.openjdk.jmc.test.jemmy.misc.wrappers.MCProgressIndicator; 94 import org.openjdk.jmc.test.jemmy.misc.wrappers.MCTabFolder; 95 import org.openjdk.jmc.test.jemmy.misc.wrappers.MCTable; 96 97 /** 98 * The base class for the Mission Control Jemmy wrappers 99 */ 100 public class MCJemmyBase { 101 private static final boolean VERBOSE_JEMMY_LOGGING = "true" 102 .equalsIgnoreCase(System.getProperty("mc.test.jemmy.verbose.logging")); 103 private static final long EDITOR_LOAD_WAIT_DEFAULT_TIMEOUT_MS = 30000; 104 private static final long EDITOR_LOAD_WAIT_TIMEOUT_MS = Long.getLong("jmc.test.editor.load.wait.timeout", 105 EDITOR_LOAD_WAIT_DEFAULT_TIMEOUT_MS); 106 public static final long VISIBLE_LOOKUP_DEFAULT_TIMEOUT_MS = 10000; 107 private static final long VISIBLE_LOOKUP_TIMEOUT_MS = Long.getLong("jmc.test.visible.lookup.timeout", 108 VISIBLE_LOOKUP_DEFAULT_TIMEOUT_MS); 109 private static final int BETWEEN_KEYSTROKES_SLEEP = 100; 110 public static final int LOOKUP_SLEEP_TIME_MS = 100; 111 protected static Wrap<? extends Shell> shell; 112 protected Wrap<? extends Control> control; 113 public static final KeyboardButtons SELECTION_BUTTON; 114 public static final KeyboardButtons EXPAND_BUTTON; 115 public static final KeyboardButtons COLLAPSE_BUTTON; 116 public static final KeyboardButtons CLOSE_BUTTON; 117 public static final KeyboardModifiers SHORTCUT_MODIFIER; 118 public static final String OS_NAME; 119 private static final int IDLE_LOOP_COUNT = 3; 120 private static final int IDLE_LOOP_TIME_STEP = 100; 121 private static final int IDLE_LOOP_TIMEOUT_MS = 10000; 122 private static Integer unnamedImageCounter = 0; 123 private static final Map<Class<?>, Class<?>> primitiveMap = new HashMap<>(); 124 protected static Wrap<? extends Shell> focusedSection; 125 126 static { 127 Environment.getEnvironment().setOutput(AbstractLookup.OUTPUT, 128 (VERBOSE_JEMMY_LOGGING) ? new TestOut() : TestOut.getNullOutput()); 129 Environment.getEnvironment().setOutput(AbstractExecutor.NON_QUEUE_ACTION_OUTPUT, 130 (VERBOSE_JEMMY_LOGGING) ? new TestOut() : TestOut.getNullOutput()); 131 Environment.getEnvironment().setOutput(AbstractExecutor.QUEUE_ACTION_OUTPUT, 132 (VERBOSE_JEMMY_LOGGING) ? new TestOut() : TestOut.getNullOutput()); 133 Environment.getEnvironment().setOutput(Wrap.OUTPUT, 134 (VERBOSE_JEMMY_LOGGING) ? new TestOut() : TestOut.getNullOutput()); 135 Environment.getEnvironment().setInputFactory(new AWTRobotInputFactory()); 136 Environment.getEnvironment().setImageCapturer(new AWTRobotCapturer()); 137 Environment.getEnvironment().setImageLoader(new FilesystemImageLoader()); 138 139 /** 140 * Overriding because on Linux it is wrongly assumed that SPACE does a proper selection 141 * (which is not the case with check or radio style where the menu isn't closed) 142 */ 143 Environment.getEnvironment().setProperty(KeyboardButton.class, SWTMenu.SELECTION_BUTTON_PROP, 144 KeyboardButtons.ENTER); 145 146 if ("sv".equals(InputContext.getInstance().getLocale().getLanguage())) { 147 Environment.getEnvironment().setProperty("LANG", "sv"); 148 } 149 AWTImage.setImageRoot(getResultDir()); 150 AWTImage.setComparator(new AverageDistanceImageComparator()); 151 Screen.SCREEN.getEnvironment().setInputFactory(new AWTRobotInputFactory()); 152 153 OS_NAME = System.getProperty("os.name").toLowerCase(); 154 SELECTION_BUTTON = OS_NAME.contains("linux") ? KeyboardButtons.SPACE : KeyboardButtons.ENTER; 155 EXPAND_BUTTON = OS_NAME.contains("os x") ? KeyboardButtons.RIGHT : KeyboardButtons.ADD; 156 COLLAPSE_BUTTON = OS_NAME.contains("os x") ? KeyboardButtons.LEFT : KeyboardButtons.SUBTRACT; 157 CLOSE_BUTTON = KeyboardButtons.W; 158 SHORTCUT_MODIFIER = OS_NAME.contains("os x") ? KeyboardModifiers.META_DOWN_MASK 159 : KeyboardModifiers.CTRL_DOWN_MASK; 160 161 Environment.getEnvironment().setProperty(Boolean.class, SWTMenu.SKIPS_DISABLED_PROP, 162 (OS_NAME.contains("windows")) ? false : true); 163 164 // keyboard re-mapping for Mac OS X with Swedish keyboard 165 if ("sv".equalsIgnoreCase(InputContext.getInstance().getLocale().getLanguage()) && OS_NAME.contains("os x")) { 166 // first making sure that the DefaultCharBindingMap has been loaded and initialized 167 getShell().keyboard(); 168 DefaultCharBindingMap map = (DefaultCharBindingMap) Environment.getEnvironment().getBindingMap(); 169 map.removeChar('+'); 170 map.addChar('+', KeyboardButtons.MINUS); 171 map.removeChar('-'); 172 map.addChar('-', KeyboardButtons.SLASH); 173 map.removeChar('_'); 174 map.addChar('_', KeyboardButtons.SLASH, KeyboardModifiers.SHIFT_DOWN_MASK); 175 map.removeChar('/'); 176 map.addChar('/', KeyboardButtons.D7, KeyboardModifiers.SHIFT_DOWN_MASK); 177 map.removeChar('\\'); 178 map.addChar('\\', KeyboardButtons.D7, KeyboardModifiers.SHIFT_DOWN_MASK, KeyboardModifiers.ALT_DOWN_MASK); 179 map.removeChar(':'); 180 map.addChar(':', KeyboardButtons.PERIOD, KeyboardModifiers.SHIFT_DOWN_MASK); 181 map.removeChar(';'); 182 map.addChar(';', KeyboardButtons.COMMA, KeyboardModifiers.SHIFT_DOWN_MASK); 183 map.removeChar('~'); 184 map.addChar('~', KeyboardButtons.CLOSE_BRACKET, KeyboardModifiers.ALT_DOWN_MASK); 185 map.removeChar('='); 186 map.addChar('=', KeyboardButtons.D0, KeyboardModifiers.SHIFT_DOWN_MASK); 187 } 188 189 primitiveMap.put(boolean.class, Boolean.class); 190 primitiveMap.put(byte.class, Byte.class); 191 primitiveMap.put(char.class, Character.class); 192 primitiveMap.put(short.class, Short.class); 193 primitiveMap.put(int.class, Integer.class); 194 primitiveMap.put(long.class, Long.class); 195 primitiveMap.put(float.class, Float.class); 196 primitiveMap.put(double.class, Double.class); 197 } 198 199 protected static File getResultDir() { 200 if (System.getProperty("results.dir") != null) { 201 return new File(System.getProperty("results.dir")); 202 } else { 203 return new File(System.getProperty("user.dir")); 204 } 205 } 206 207 /** 208 * Gets the main shell of Mission Control 209 * 210 * @return the main shell of Mission Control 211 */ 212 protected static Wrap<? extends Shell> getShell() { 213 return Shells.SHELLS.lookup(Shell.class, new ByTextShell<>("JDK Mission Control")).wrap(); 214 } 215 216 /** 217 * Tries to set focus on Mission Control 218 */ 219 public static void focusMc() { 220 getShell().as(Focusable.class).focuser().focus(); 221 } 222 223 /** 224 * Checks if supplied control is identical to our control 225 * 226 * @param otherControl 227 * the control shall be compared with this control 228 * @return {@code true} if the controls are equal 229 */ 230 public Boolean controlsAreEqual(Control otherControl) { 231 return (otherControl.equals(this.control.getControl())); 232 } 233 234 /** 235 * Determines if this control has a specific control as an ancestor or not 236 * 237 * @param possibleAncestor 238 * the control that should be checked for among the ancestors 239 * @return {@code true} if the control provided as parameter is an ancestor of this control 240 */ 241 public Boolean hasAsAncestor(MCJemmyBase possibleAncestor) { 242 final Control control = this.control.getControl(); 243 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 244 @Override 245 public void run() { 246 Boolean parentFound = false; 247 try { 248 Composite ancestor = control.getParent(); 249 while (!(ancestor instanceof Shell)) { 250 if (possibleAncestor.controlsAreEqual(ancestor)) { 251 parentFound = true; 252 break; 253 } 254 ancestor = ancestor.getParent(); 255 } 256 } catch (SWTException e) { 257 suppressWidgetDisposedException(e); 258 } 259 setOutput(parentFound); 260 } 261 }; 262 Display.getDefault().syncExec(fetcher); 263 return fetcher.getOutput(); 264 } 265 266 /** 267 * Does a lookup waiting for the editor with the supplied title to show up before returning 268 * 269 * @param title 270 * the title of the editor 271 * @return {@code true} if the editor was found, otherwise {@code false} 272 */ 273 public static boolean waitForEditor(String title) { 274 return waitForEditor(EDITOR_LOAD_WAIT_TIMEOUT_MS, title); 275 } 276 277 /** 278 * Search for an editor with a time limit on retries 279 * 280 * @param maxWait 281 * the max number of milliseconds to repeat the lookup of the editor (unless found). 282 * @param title 283 * the title of the editor to find (exact match) 284 * @return {@code true} if the editor was found, otherwise {@code false} 285 */ 286 public static boolean waitForEditor(long maxWait, String title) { 287 return waitForEditor(title, StringComparePolicy.EXACT, maxWait); 288 } 289 290 /** 291 * Search for an editor with a retry count 292 * 293 * @param title 294 * the title of the editor to find (exact match) 295 * @param maxRetries 296 * The max number of times to retry the lookup of the editor. Each retry might take a 297 * considerable amount of time so use this wisely. 298 * @return {@code true} if the editor was found, otherwise {@code false} 299 */ 300 public static boolean waitForEditor(String title, int maxRetries) { 301 return waitForEditor(title, StringComparePolicy.EXACT, maxRetries); 302 } 303 304 /** 305 * Search for an editor with a retry count 306 * 307 * @param title 308 * the title of the editor to find 309 * @param policy 310 * a {@link StringComparePolicy} used for matching the title text 311 * @param maxRetries 312 * The max number of times to retry the lookup of the editor. Each retry might take a 313 * considerable amount of time so use this wisely. 314 * @return {@code true} if the editor was found, otherwise {@code false} 315 */ 316 public static boolean waitForEditor(String title, StringComparePolicy policy, int maxRetries) { 317 boolean found = false; 318 while (!found && maxRetries > 0) { 319 found = isEditorLoadComplete(getEditorLookup(title, policy)); 320 if (!found) { 321 sleep(LOOKUP_SLEEP_TIME_MS); 322 } 323 maxRetries--; 324 } 325 return found; 326 } 327 328 /** 329 * Search for an editor with a time limit on retries 330 * 331 * @param title 332 * the title of the editor to find 333 * @param policy 334 * a {@link StringComparePolicy} used for matching the title with the title text 335 * @param maxWait 336 * the max number of milliseconds to repeat the lookup of the editor (unless found) 337 * @return {@code true} if the editor was found, otherwise {@code false} 338 */ 339 public static boolean waitForEditor(String title, StringComparePolicy policy, long maxWait) { 340 boolean found = false; 341 long maxTimeStamp = System.currentTimeMillis() + maxWait; 342 while (!found && System.currentTimeMillis() < maxTimeStamp) { 343 found = isEditorLoadComplete(getEditorLookup(title, policy)); 344 if (!found) { 345 sleep(LOOKUP_SLEEP_TIME_MS); 346 } 347 } 348 return found; 349 } 350 351 @SuppressWarnings("unchecked") 352 private static Lookup<CTabFolder> getEditorLookup(String title, StringComparePolicy policy) { 353 return getShell().as(Parent.class, CTabFolder.class).lookup(CTabFolder.class, 354 new ByItemLookup<CTabFolder>(title, policy)); 355 } 356 357 private static boolean isEditorLoadComplete(Lookup<CTabFolder> editorLookup) { 358 boolean hasProgressIndicator = false; 359 int numOfEditors = editorLookup.size(); 360 if (numOfEditors == 1) { 361 MCTabFolder tf = new MCTabFolder(editorLookup.wrap(), getShell()); 362 List<MCProgressIndicator> jpis = MCProgressIndicator.getVisible(getShell()); 363 for (MCProgressIndicator jpi : jpis) { 364 try { 365 if (jpi.hasAsAncestor(tf)) { 366 hasProgressIndicator = true; 367 break; 368 } 369 } catch (SWTException e) { 370 suppressWidgetDisposedException(e); 371 } 372 } 373 } 374 return numOfEditors == 1 && !hasProgressIndicator; 375 } 376 377 /** 378 * Search for an editor with a retry count 379 * 380 * @param title 381 * the title of the editor to find (substring match) 382 * @param maxRetries 383 * The max number of times to retry the lookup of the editor. Each retry might take a 384 * considerable amount of time so use this wisely. 385 * @return {@code true} if found, otherwise {@code false} 386 */ 387 public static boolean waitForSubstringMatchedEditor(String title, int maxRetries) { 388 return waitForEditor(title, StringComparePolicy.SUBSTRING, maxRetries); 389 } 390 391 /** 392 * Search for an editor with a time limit on retries 393 * 394 * @param maxWait 395 * the max number of milliseconds to repeat the lookup of the editor. 396 * @param title 397 * the title of the editor to find (substring match) 398 * @return {@code true} if found, otherwise {@code false} 399 */ 400 public static boolean waitForSubstringMatchedEditor(long maxWait, String title) { 401 return waitForEditor(title, StringComparePolicy.SUBSTRING, maxWait); 402 } 403 404 /** 405 * Search for an editor with a time limit (defined by {@code EDITOR_LOAD_WAIT_TIMEOUT_MS}) on 406 * retries 407 * 408 * @param title 409 * the title of the editor to find (substring match) 410 * @return {@code true} if found, otherwise {@code false} 411 */ 412 public static boolean waitForSubstringMatchedEditor(String title) { 413 return waitForSubstringMatchedEditor(EDITOR_LOAD_WAIT_TIMEOUT_MS, title); 414 } 415 416 /** 417 * Convenience method to put test execution on hold by putting the thread to sleep 418 * 419 * @param millis 420 * the time to sleep in milliseconds. 421 * @return The actual time slept in milliseconds. Should be equal to millis unless an 422 * {@link InterruptedException} was thrown. 423 */ 424 public static long sleep(long millis) { 425 long time = System.currentTimeMillis(); 426 long slept = millis; 427 try { 428 Thread.sleep(millis); 429 } catch (InterruptedException e) { 430 slept = System.currentTimeMillis() - time; 431 } 432 return slept; 433 } 434 435 /** 436 * This method finds a widget that may or may not exist by means of doing a lookup and then 437 * walking through the results to see if there is a match. The difference to lookups with 438 * specific criterion is that this doesn't timeout and throw an exception if not found. Instead 439 * null is returned. Note: This method expects the property to hold a String or a list of 440 * Strings 441 * 442 * @param clazz 443 * the class of the widget to look for 444 * @param text 445 * the string value used for matching 446 * @param property 447 * the property to match with the supplied text parameter (has to return a String or 448 * List of String to be properly matched) 449 * @param policy 450 * a {@link StringComparePolicy} to use when matching the widget 451 * @return the control searched for or {@code null} if not found 452 */ 453 @SuppressWarnings("unchecked") 454 protected static <T> Wrap<? extends T> findWrap( 455 Class<T> clazz, String text, String property, StringComparePolicy policy) { 456 Lookup<? extends T> lookup = getShell().as(Parent.class, Control.class).lookup(clazz); 457 Wrap<? extends T> result = null; 458 for (int i = 0; i < lookup.size() && result == null; i++) { 459 Wrap<? extends T> wrap = lookup.wrap(i); 460 Object object = wrap.getProperty(property); 461 if (object instanceof List) { 462 List<String> values = new ArrayList<String>().getClass().cast(object); 463 for (String value : values) { 464 if (policy.compare(value, text)) { 465 result = wrap; 466 break; 467 } 468 } 469 } else { 470 if (policy.compare(String.class.cast(object), text)) { 471 result = wrap; 472 } 473 } 474 } 475 return result; 476 } 477 478 /** 479 * Convenience method to find out if a widget is disposed 480 * 481 * @param widgetWrap 482 * the wrapped widget to check 483 * @return {@code true} if disposed (or disposing), otherwise {@code false} 484 */ 485 protected static boolean isDisposed(final Wrap<? extends Widget> widgetWrap) { 486 return isDisposed(widgetWrap.getControl()); 487 } 488 489 /** 490 * Convenience method to find out if a widget is disposed 491 * 492 * @param widget 493 * the widget to check 494 * @return {@code true} if disposed (or disposing), otherwise {@code false} 495 */ 496 protected static boolean isDisposed(final Widget widget) { 497 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 498 @Override 499 public void run() { 500 if (widget != null) { 501 setOutput(widget.isDisposed()); 502 } else { 503 setOutput(true); 504 } 505 } 506 }; 507 Display.getDefault().syncExec(fetcher); 508 return fetcher.getOutput(); 509 } 510 511 /** 512 * Convenience method to find out if the widget of this wrapper is disposed 513 * 514 * @return {@code true} if disposed (or disposing), otherwise {@code false} 515 */ 516 public boolean isDisposed() { 517 return isDisposed(control); 518 } 519 520 /** 521 * Convenience method to find out if this control is enabled 522 * 523 * @return {@code true} if enabled, otherwise {@code false} 524 */ 525 public boolean isEnabled() { 526 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 527 @Override 528 public void run() { 529 setOutput(control.getControl().isEnabled()); 530 } 531 }; 532 Display.getDefault().syncExec(fetcher); 533 return fetcher.getOutput(); 534 } 535 536 /** 537 * Opens the context menu by right-clicking at a provided point 538 * 539 * @param p 540 * origin point of context (right-click) 541 */ 542 private void openContextMenuAtPoint(Point p) { 543 Display.getDefault().syncExec(() -> { 544 control.mouse().click(1, p, MouseButtons.BUTTON3); 545 }); 546 } 547 548 /** 549 * Checks the isEnabled value for a menu item 550 * 551 * @param p 552 * the point on the screen at which to open the context menu 553 * @param menuItemText 554 * the menu item of interest 555 * @return the isEnabled value for the menu item of interest 556 */ 557 public boolean isContextMenuItemEnabled(Point p, String menuItemText) { 558 openContextMenuAtPoint(p); 559 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 560 @Override 561 public void run() { 562 Menu menu = control.getControl().getMenu(); 563 for (MenuItem item : menu.getItems()) { 564 if(menuItemText.equals(item.getText())) { 565 setOutput(item.isEnabled()); 566 break; 567 } 568 } 569 } 570 }; 571 Display.getDefault().syncExec(fetcher); 572 return (fetcher.getOutput() == null) ? false : fetcher.getOutput(); 573 } 574 575 /** 576 * Convenience method to find out if a control is visible 577 * 578 * @param controlWrap 579 * the control to check 580 * @return {@code true} if visible, otherwise {@code false} 581 */ 582 protected static boolean isVisible(final Wrap<? extends Control> controlWrap) { 583 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 584 @Override 585 public void run() { 586 try { 587 setOutput(controlWrap.getControl().isVisible()); 588 } catch (SWTException e) { 589 suppressWidgetDisposedException(e); 590 } 591 } 592 }; 593 Display.getDefault().syncExec(fetcher); 594 return (fetcher.getOutput() == null) ? false : fetcher.getOutput(); 595 } 596 597 /** 598 * Convenience method to find out if this control is visible 599 * 600 * @return {@code true} if visible, otherwise {@code false} 601 */ 602 public boolean isVisible() { 603 return isVisible(control); 604 } 605 606 /** 607 * Iterates through the wraps found by a lookup and returns a list of all visible wraps. 608 * 609 * @param lookup 610 * the {@link Lookup} to search through 611 * @return a list of all visible wraps found by the lookup 612 */ 613 protected static <T extends Control> List<Wrap<? extends T>> getVisible(Lookup<T> lookup) { 614 return getVisible(lookup, true); 615 } 616 617 /** 618 * Iterates through the wraps found by a lookup and returns a list of all visible wraps. 619 * 620 * @param lookup 621 * the {@link Lookup} to search through 622 * @param waitForIdle 623 * {@code true} if "UI-update queue" should be empty before looking for controls 624 * @return a list of all visible wraps found by the {@link Lookup} 625 */ 626 protected static <T extends Control> List<Wrap<? extends T>> getVisible(Lookup<T> lookup, boolean waitForIdle) { 627 return getVisible(lookup, waitForIdle, VISIBLE_LOOKUP_TIMEOUT_MS); 628 } 629 630 /** 631 * Iterates through the wraps found by a lookup and returns a list of all visible wraps. 632 * 633 * @param lookup 634 * the {@link Lookup} to search through 635 * @param waitForIdle 636 * {@code true} if "UI-update queue" should be empty before looking for controls 637 * @param maxWaitMs 638 * the timeout in milliseconds before ending the lookup 639 * @return a list of all visible wraps found by the {@link Lookup} 640 */ 641 protected static <T extends Control> List<Wrap<? extends T>> getVisible( 642 Lookup<T> lookup, boolean waitForIdle, long maxWaitMs) { 643 return getVisible(lookup, waitForIdle, maxWaitMs, true); 644 } 645 646 /** 647 * Iterates through the wraps found by a lookup and returns a list of all visible wraps. Will 648 * retry a maximum of {@code VISIBLE_LOOKUP_MAX_RETRY_COUNT} times with a 649 * {@code VISIBLE_LOOKUP_SLEEP_TIME_MS} ms sleep in between if lookup has zero length 650 * 651 * @param lookup 652 * the {@link Lookup} to search through 653 * @param waitForIdle 654 * {@code true} if "UI-update queue" should be empty before looking for controls 655 * @param maxWaitMs 656 * the timeout in milliseconds before ending the lookup 657 * @param assertEmpty 658 * if {@code true} will assert the the resulting list isn't empty assertion will be 659 * done 660 * @return a list of all visible wraps found by the {@link Lookup} 661 */ 662 @SuppressWarnings("unchecked") 663 protected static <T extends Control> List<Wrap<? extends T>> getVisible( 664 Lookup<T> lookup, boolean waitForIdle, long maxWaitMs, boolean assertEmpty) { 665 if (waitForIdle) { 666 waitForIdle(); 667 } 668 List<Wrap<? extends T>> list = new ArrayList<>(); 669 long lookupEndTime = System.currentTimeMillis() + maxWaitMs; 670 do { 671 for (int i = 0; i < lookup.size(); i++) { 672 Wrap<T> wrap = (Wrap<T>) lookup.wrap(i); 673 if (isVisible(wrap)) { 674 list.add(wrap); 675 } 676 } 677 if (list.size() == 0) { 678 sleep(LOOKUP_SLEEP_TIME_MS); 679 } 680 } while (list.size() == 0 && lookupEndTime > System.currentTimeMillis()); 681 if (assertEmpty) { 682 Assert.assertTrue("No visible controls found", list.size() > 0); 683 } 684 return list; 685 } 686 687 /** 688 * Iterates through the wraps found by a lookup and returns a list of all visible wraps. Will 689 * retry a maximum of {@code VISIBLE_LOOKUP_MAX_RETRY_COUNT} times with a 690 * {@code VISIBLE_LOOKUP_SLEEP_TIME_MS} ms sleep in between if lookup has zero length 691 * 692 * @param lookup 693 * the {@link Lookup} to search through 694 * @param waitForIdle 695 * {@code true} if "UI-update queue" should empty before looking for controls 696 * @param assertEmpty 697 * if {@code true} will assert the the resulting list isn't empty. Otherwise no 698 * assertion will be done 699 * @return a list of all visible wraps found by the {@link Lookup} 700 */ 701 protected static <T extends Control> List<Wrap<? extends T>> getVisible( 702 Lookup<T> lookup, boolean waitForIdle, boolean assertEmpty) { 703 return getVisible(lookup, waitForIdle, VISIBLE_LOOKUP_TIMEOUT_MS, assertEmpty); 704 } 705 706 /** 707 * Convenience method to find out if Mission Control UI is busy 708 * 709 * @return {@code true} if busy 710 */ 711 public static boolean isBusy() { 712 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 713 @Override 714 public void run() { 715 setOutput(!Job.getJobManager().isIdle()); 716 } 717 }; 718 Display.getDefault().syncExec(fetcher); 719 return fetcher.getOutput(); 720 } 721 722 /** 723 * Inspects a Widget (graphically) for updates by comparing two snapshots of it 724 * 725 * @param widget 726 * the widget to inspect for change (graphically) 727 * @param waitTimeMillis 728 * the time to wait between the snapshots of the Widget 729 * @return {@code true} if the Widget has changed 730 */ 731 public boolean isWidgetUpdating(Wrap<? extends Widget> widget, int waitTimeMillis) { 732 sleep(waitTimeMillis); 733 org.jemmy.image.Image firstImage = widget.getScreenImage(); 734 sleep(waitTimeMillis); 735 org.jemmy.image.Image secondImage = widget.getScreenImage(); 736 737 // Set the Comparator to be strict, first get and save the current 738 // Comparator for later restoral 739 ImageComparator current = AWTImage.getComparator(); 740 AWTImage.setComparator(new StrictImageComparator()); 741 // diff will be null if the Images are identical 742 org.jemmy.image.Image diff = secondImage.compareTo(firstImage); 743 AWTImage.setComparator(current); 744 745 return (diff == null) ? false : true; 746 } 747 748 /** 749 * Inspects this control for updates by comparing two snapshots of it 750 * 751 * @param waitTimeMillis 752 * the time to wait between the snapshots of the Widget 753 * @return {@code true} if the Widget has changed 754 */ 755 public boolean isWidgetUpdating(int waitTimeMillis) { 756 return isWidgetUpdating(control, waitTimeMillis); 757 } 758 759 /** 760 * Waits for background jobs to finish before executing a script line. Since background jobs 761 * post to the UI-thread asynchronously we must ensure they get a chance to run, so we we spin 762 * asynchronously in a count down process. IDLE_LOOP_COUNT specifies the number iterations 763 * without the job manager being activated as being enough to make sure we are in a calm/idle 764 * state. 765 */ 766 public static void waitForIdle() { 767 int counter = IDLE_LOOP_COUNT; 768 long startTimestamp = System.currentTimeMillis(); 769 while (counter > 0) { 770 if ((System.currentTimeMillis() - startTimestamp) > IDLE_LOOP_TIMEOUT_MS) { 771 break; 772 } 773 if (isBusy()) { 774 counter = IDLE_LOOP_COUNT; 775 } else { 776 counter--; 777 } 778 sleep(IDLE_LOOP_TIME_STEP); 779 } 780 } 781 782 /** 783 * Saves a picture of Mission Control's shell. If Mission Control is behind other applications, 784 * the rectangle will show what's really on top. 785 * 786 * @param imageName 787 * the name of the saved file, ".png" will be added at the end 788 */ 789 public static void saveMcImage(String imageName) { 790 Image pic = getMcImage(); 791 pic.save(imageName + ".png"); 792 } 793 794 /** 795 * Saves a picture of Mission Control's shell. If Mission Control is behind other applications, 796 * the rectangle will show what's really on top. The image will be given a name which is unique 797 * for the execution. 798 */ 799 public static void saveMcImage() { 800 unnamedImageCounter++; 801 saveMcImage("unnamed_mc_image_" + String.format("%03d", unnamedImageCounter) + ".png"); 802 } 803 804 /** 805 * Returns an image of the Mission Control shell 806 * 807 * @return an {@link Image} 808 */ 809 public static Image getMcImage() { 810 return getShell().getScreenImage(); 811 } 812 813 /** 814 * Returns an image of this control 815 * 816 * @return an {@link Image} of this control 817 */ 818 public Image getThisImage() { 819 return control.getScreenImage(); 820 } 821 822 /** 823 * Saves a picture of this control. If the control is behind other applications or Mission 824 * Control controls, the rectangle will show what's really on top. 825 * 826 * @param imageName 827 * the name of the saved file. ".png" will be added at the end 828 */ 829 public void saveThisImage(String imageName) { 830 Image pic = getThisImage(); 831 saveImage(imageName + ".png", pic); 832 } 833 834 /** 835 * Saves a picture of this control. If the control is behind other applications or Mission 836 * Control controls, the rectangle will show what's really on top. The image will be given a 837 * name which is unique for the execution. 838 */ 839 public void saveThisImage() { 840 unnamedImageCounter++; 841 saveThisImage("unnamed_mc_image_" + String.format("%03d", unnamedImageCounter) + ".png"); 842 } 843 844 /** 845 * Saves the image with the specified name 846 * 847 * @param fileName 848 * the name of the image file 849 * @param image 850 * the image 851 */ 852 public static void saveImage(String fileName, Image image) { 853 image.save(fileName); 854 } 855 856 /** 857 * Focuses on a specific section to use instead of the main shell. 858 * 859 * @param name 860 * the name of the section to focus on 861 */ 862 @SuppressWarnings("unchecked") 863 public static void focusSectionByName(String name) { 864 focusedSection = (Wrap<? extends Shell>) getVisible( 865 getShell().as(Parent.class, Control.class).lookup(new ByName<Shell>(name))).get(0); 866 } 867 868 /** 869 * Focuses on a specific section to use instead of the main shell. 870 * 871 * @param title 872 * the title of the section to focus on 873 */ 874 @SuppressWarnings("unchecked") 875 public static void focusSectionByTitle(String title) { 876 focusedSection = (Wrap<? extends Shell>) getVisible( 877 getShell().as(Parent.class, Control.class).lookup(new ByTextControlLookup<>(title))).get(0); 878 } 879 880 /** 881 * Focuses on a specific section to use instead of the main shell. 882 * 883 * @param title 884 * the title of the section to focus on 885 * @param waitForIdle 886 * if {@code true} will first wait for the UI to be idle before setting focus 887 */ 888 @SuppressWarnings("unchecked") 889 public static void focusSectionByTitle(String title, boolean waitForIdle) { 890 focusedSection = (Wrap<? extends Shell>) getVisible( 891 getShell().as(Parent.class, Control.class).lookup(new ByTextControlLookup<>(title)), waitForIdle) 892 .get(0); 893 } 894 895 /** 896 * House keeping: clearing the focusedSection reference so that it won't be used in further 897 * lookups 898 */ 899 public static void clearFocus() { 900 focusedSection = null; 901 } 902 903 /** 904 * @return a {@link List} of {@link MCTable} either in the currently focused section or 905 * globally in the shell 906 */ 907 public static List<MCTable> getTables() { 908 if (focusedSection != null) { 909 return MCTable.getAll(focusedSection); 910 } else { 911 return MCTable.getAll(getShell()); 912 } 913 } 914 915 /** 916 * Get all tables in the focused section (if set), otherwise from the Mission Control main shell 917 * 918 * @param waitForIdle 919 * {@code true} if "UI-update queue" should be empty before looking for controls 920 * @return a {@link List} of {@link MCTable} either in the currently focused section or 921 * globally in the shell 922 */ 923 public static List<MCTable> getTables(boolean waitForIdle) { 924 if (focusedSection != null) { 925 return MCTable.getAll(focusedSection, waitForIdle); 926 } else { 927 return MCTable.getAll(getShell(), waitForIdle); 928 } 929 } 930 931 /** 932 * Runs the method and returns the result if a matching method is found. If not, null will 933 * always be returned. Note that the method could return null as well if the operation succeeds 934 * so this needs to be handled in a proper way by the caller. 935 * 936 * @param returnType 937 * the type of the returned object 938 * @param object 939 * the object on which to run the method 940 * @param methodName 941 * the name of the method to run 942 * @param params 943 * an object array of parameters for the method. null if no parameters 944 * @return The result of running the method. null if no matching method is found (name, 945 * parameters and return type). Note that the method could return null as well if the 946 * operation succeeds so this needs to be handled in a proper way by the caller. 947 */ 948 public static <T> T runMethod(Class<T> returnType, Object object, String methodName, Object ... params) { 949 T result = null; 950 try { 951 Class<?>[] paramTypes = null; 952 953 if (params != null) { 954 paramTypes = new Class<?>[params.length]; 955 for (int i = 0; i < params.length; i++) { 956 paramTypes[i] = params[i].getClass(); 957 } 958 } 959 960 Method method = getCompatibleMethod(object, methodName, paramTypes); 961 Class<?> methodReturnType = method.getReturnType(); 962 963 if (methodReturnType.isPrimitive()) { 964 methodReturnType = primitiveMap.get(methodReturnType); 965 } 966 967 if (returnType.equals(Void.class) || returnType.isAssignableFrom(methodReturnType)) { 968 result = returnType.cast(method.invoke(object, params)); 969 } 970 } catch (Exception e) { 971 // do nothing, just return null 972 } 973 return result; 974 } 975 976 private static Method getCompatibleMethod(Object object, String methodName, Class<?> ... paramTypes) 977 throws SecurityException, NoSuchMethodException { 978 if (paramTypes != null) { 979 Method[] methods = object.getClass().getMethods(); 980 for (Method method : methods) { 981 Method m = method; 982 983 if (!m.getName().equals(methodName)) { 984 continue; 985 } 986 987 Class<?>[] actualTypes = m.getParameterTypes(); 988 if (actualTypes.length != paramTypes.length) { 989 continue; 990 } 991 992 boolean found = true; 993 for (int j = 0; j < actualTypes.length; j++) { 994 if (!actualTypes[j].isAssignableFrom(paramTypes[j])) { 995 if (actualTypes[j].isPrimitive()) { 996 found = primitiveMap.get(actualTypes[j]).equals(paramTypes[j]); 997 } else if (paramTypes[j].isPrimitive()) { 998 found = primitiveMap.get(paramTypes[j]).equals(actualTypes[j]); 999 } else { 1000 found = false; 1001 } 1002 } 1003 1004 if (!found) { 1005 break; 1006 } 1007 } 1008 1009 if (found) { 1010 return m; 1011 } 1012 } 1013 1014 throw new NoSuchMethodException("Could not find method " + methodName + " with parameters " + paramTypes); 1015 } else { 1016 return object.getClass().getMethod(methodName); 1017 } 1018 } 1019 1020 /** 1021 * Returns the name of this control 1022 * 1023 * @return the name of this control. Null if no name has been set 1024 */ 1025 public String getName() { 1026 return control.getProperty(String.class, Wrap.NAME_PROP_NAME); 1027 } 1028 1029 /** 1030 * @return the text of this control 1031 */ 1032 public String getText() { 1033 return control.getProperty(String.class, Wrap.TEXT_PROP_NAME); 1034 } 1035 1036 /** 1037 * Clicks this control (once) 1038 */ 1039 public void click() { 1040 click(1); 1041 } 1042 1043 /** 1044 * Clicks this control {@code times} times 1045 * 1046 * @param times 1047 * the number of times to mouse click this control 1048 */ 1049 public void click(int times) { 1050 control.mouse().click(times); 1051 } 1052 1053 /** 1054 * Default implementation of a copy to clipboard for any wrapper 1055 */ 1056 public void copyToClipboard() { 1057 control.as(Focusable.class).focuser().focus(); 1058 sleep(BETWEEN_KEYSTROKES_SLEEP); 1059 control.keyboard().pushKey(KeyboardButtons.A, SHORTCUT_MODIFIER); 1060 sleep(BETWEEN_KEYSTROKES_SLEEP); 1061 control.keyboard().pushKey(KeyboardButtons.C, SHORTCUT_MODIFIER); 1062 sleep(BETWEEN_KEYSTROKES_SLEEP); 1063 } 1064 1065 /** 1066 * Gets the system clipboard contents as a string 1067 * 1068 * @return the clipboard contents 1069 */ 1070 public static String getStringFromClipboard() { 1071 return getFromClipBoard(String.class); 1072 } 1073 1074 /** 1075 * Finds out if content assist is present. The assumption is that content assist is present if 1076 * there is a Shell with a single Composite that in turn has a single child of type Table 1077 * 1078 * @return {@code true} if content assist is present, otherwise {@code false} 1079 */ 1080 public static boolean isContentAssistPresent() { 1081 boolean found = true; 1082 long endTime = System.currentTimeMillis() + 1000; 1083 while (found && System.currentTimeMillis() < endTime) { 1084 FetcherWithInput<List<Wrap<? extends Shell>>, Boolean> fetcher = new FetcherWithInput<List<Wrap<? extends Shell>>, Boolean>( 1085 getVisible(Shells.SHELLS.lookup(Shell.class), false, false)) { 1086 1087 @Override 1088 public void run() { 1089 boolean isPresent = false; 1090 for (Wrap<? extends Shell> shellWrap : getInput()) { 1091 Control[] shellChildren = shellWrap.getControl().getChildren(); 1092 if (shellChildren.length == 1 && shellChildren[0] instanceof Composite) { 1093 Control[] compositeChildren = Composite.class.cast(shellChildren[0]).getChildren(); 1094 if (compositeChildren.length == 1 && compositeChildren[0] instanceof Table) { 1095 isPresent = true; 1096 break; 1097 } 1098 } 1099 } 1100 setOutput(isPresent); 1101 } 1102 }; 1103 Display.getDefault().syncExec(fetcher); 1104 found = fetcher.getOutput(); 1105 if (found) { 1106 sleep(100); 1107 } 1108 } 1109 return found; 1110 } 1111 1112 private static <T> T getFromClipBoard(Class<T> returnType) { 1113 Transferable transferable = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null); 1114 DataFlavor thisFlavor = new DataFlavor(returnType, returnType.getName()); 1115 try { 1116 if (transferable != null && transferable.isDataFlavorSupported(thisFlavor)) { 1117 return returnType.cast(transferable.getTransferData(thisFlavor)); 1118 } 1119 } catch (UnsupportedFlavorException e) { 1120 System.out.println("Clipboard content flavor is not supported " + e.getMessage()); 1121 } catch (IOException e) { 1122 System.out.println("Clipboard content could not be retrieved " + e.getMessage()); 1123 } 1124 return null; 1125 } 1126 1127 private static void suppressWidgetDisposedException(SWTException e) { 1128 if (!e.getMessage().contains("Widget is disposed")) { 1129 // Unexpected exception. Re-throw it 1130 throw e; 1131 } 1132 } 1133 1134 /** 1135 * Setting focus on this control programmatically (if not already focused). 1136 */ 1137 protected void ensureFocus() { 1138 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 1139 @Override 1140 public void run() { 1141 if (!control.getControl().isFocusControl()) { 1142 control.getControl().setFocus(); 1143 } 1144 setOutput(control.getControl().isFocusControl()); 1145 } 1146 }; 1147 Display.getDefault().syncExec(fetcher); 1148 if (!fetcher.getOutput()) { 1149 // fallback if the programmatic focusing didn't work 1150 control.as(Focusable.class).focuser().focus(); 1151 } 1152 } 1153 }