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