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 * Gets a shell by text 218 * 219 * @param text 220 * the text string to lookup the shell with 221 * @return the associated shell 222 */ 223 protected static Wrap<? extends Shell> getShellByText(String text) { 224 return Shells.SHELLS.lookup(Shell.class, new ByTextShell<>(text)).wrap(); 225 } 226 227 /** 228 * Tries to set focus on Mission Control 229 */ 230 public static void focusMc() { 231 getShell().as(Focusable.class).focuser().focus(); 232 } 233 234 /** 235 * Checks if supplied control is identical to our control 236 * 237 * @param otherControl 238 * the control shall be compared with this control 239 * @return {@code true} if the controls are equal 240 */ 241 public Boolean controlsAreEqual(Control otherControl) { 242 return (otherControl.equals(this.control.getControl())); 243 } 244 245 /** 246 * Determines if this control has a specific control as an ancestor or not 247 * 248 * @param possibleAncestor 249 * the control that should be checked for among the ancestors 250 * @return {@code true} if the control provided as parameter is an ancestor of this control 251 */ 252 public Boolean hasAsAncestor(MCJemmyBase possibleAncestor) { 253 final Control control = this.control.getControl(); 254 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 255 @Override 256 public void run() { 257 Boolean parentFound = false; 258 try { 259 Composite ancestor = control.getParent(); 260 while (!(ancestor instanceof Shell)) { 261 if (possibleAncestor.controlsAreEqual(ancestor)) { 262 parentFound = true; 263 break; 264 } 265 ancestor = ancestor.getParent(); 266 } 267 } catch (SWTException e) { 268 suppressWidgetDisposedException(e); 269 } 270 setOutput(parentFound); 271 } 272 }; 273 Display.getDefault().syncExec(fetcher); 274 return fetcher.getOutput(); 275 } 276 277 /** 278 * Does a lookup waiting for the editor with the supplied title to show up before returning 279 * 280 * @param title 281 * the title of the editor 282 * @return {@code true} if the editor was found, otherwise {@code false} 283 */ 284 public static boolean waitForEditor(String title) { 285 return waitForEditor(EDITOR_LOAD_WAIT_TIMEOUT_MS, title); 286 } 287 288 /** 289 * Search for an editor with a time limit on retries 290 * 291 * @param maxWait 292 * the max number of milliseconds to repeat the lookup of the editor (unless found). 293 * @param title 294 * the title of the editor to find (exact match) 295 * @return {@code true} if the editor was found, otherwise {@code false} 296 */ 297 public static boolean waitForEditor(long maxWait, String title) { 298 return waitForEditor(title, StringComparePolicy.EXACT, maxWait); 299 } 300 301 /** 302 * Search for an editor with a retry count 303 * 304 * @param title 305 * the title of the editor to find (exact match) 306 * @param maxRetries 307 * The max number of times to retry the lookup of the editor. Each retry might take a 308 * considerable amount of time so use this wisely. 309 * @return {@code true} if the editor was found, otherwise {@code false} 310 */ 311 public static boolean waitForEditor(String title, int maxRetries) { 312 return waitForEditor(title, StringComparePolicy.EXACT, maxRetries); 313 } 314 315 /** 316 * Search for an editor with a retry count 317 * 318 * @param title 319 * the title of the editor to find 320 * @param policy 321 * a {@link StringComparePolicy} used for matching the title text 322 * @param maxRetries 323 * The max number of times to retry the lookup of the editor. Each retry might take a 324 * considerable amount of time so use this wisely. 325 * @return {@code true} if the editor was found, otherwise {@code false} 326 */ 327 public static boolean waitForEditor(String title, StringComparePolicy policy, int maxRetries) { 328 boolean found = false; 329 while (!found && maxRetries > 0) { 330 found = isEditorLoadComplete(getEditorLookup(title, policy)); 331 if (!found) { 332 sleep(LOOKUP_SLEEP_TIME_MS); 333 } 334 maxRetries--; 335 } 336 return found; 337 } 338 339 /** 340 * Search for an editor with a time limit on retries 341 * 342 * @param title 343 * the title of the editor to find 344 * @param policy 345 * a {@link StringComparePolicy} used for matching the title with the title text 346 * @param maxWait 347 * the max number of milliseconds to repeat the lookup of the editor (unless found) 348 * @return {@code true} if the editor was found, otherwise {@code false} 349 */ 350 public static boolean waitForEditor(String title, StringComparePolicy policy, long maxWait) { 351 boolean found = false; 352 long maxTimeStamp = System.currentTimeMillis() + maxWait; 353 while (!found && System.currentTimeMillis() < maxTimeStamp) { 354 found = isEditorLoadComplete(getEditorLookup(title, policy)); 355 if (!found) { 356 sleep(LOOKUP_SLEEP_TIME_MS); 357 } 358 } 359 return found; 360 } 361 362 @SuppressWarnings("unchecked") 363 private static Lookup<CTabFolder> getEditorLookup(String title, StringComparePolicy policy) { 364 return getShell().as(Parent.class, CTabFolder.class).lookup(CTabFolder.class, 365 new ByItemLookup<CTabFolder>(title, policy)); 366 } 367 368 private static boolean isEditorLoadComplete(Lookup<CTabFolder> editorLookup) { 369 boolean hasProgressIndicator = false; 370 int numOfEditors = editorLookup.size(); 371 if (numOfEditors == 1) { 372 MCTabFolder tf = new MCTabFolder(editorLookup.wrap(), getShell()); 373 List<MCProgressIndicator> jpis = MCProgressIndicator.getVisible(getShell()); 374 for (MCProgressIndicator jpi : jpis) { 375 try { 376 if (jpi.hasAsAncestor(tf)) { 377 hasProgressIndicator = true; 378 break; 379 } 380 } catch (SWTException e) { 381 suppressWidgetDisposedException(e); 382 } 383 } 384 } 385 return numOfEditors == 1 && !hasProgressIndicator; 386 } 387 388 /** 389 * Search for an editor with a retry count 390 * 391 * @param title 392 * the title of the editor to find (substring match) 393 * @param maxRetries 394 * The max number of times to retry the lookup of the editor. Each retry might take a 395 * considerable amount of time so use this wisely. 396 * @return {@code true} if found, otherwise {@code false} 397 */ 398 public static boolean waitForSubstringMatchedEditor(String title, int maxRetries) { 399 return waitForEditor(title, StringComparePolicy.SUBSTRING, maxRetries); 400 } 401 402 /** 403 * Search for an editor with a time limit on retries 404 * 405 * @param maxWait 406 * the max number of milliseconds to repeat the lookup of the editor. 407 * @param title 408 * the title of the editor to find (substring match) 409 * @return {@code true} if found, otherwise {@code false} 410 */ 411 public static boolean waitForSubstringMatchedEditor(long maxWait, String title) { 412 return waitForEditor(title, StringComparePolicy.SUBSTRING, maxWait); 413 } 414 415 /** 416 * Search for an editor with a time limit (defined by {@code EDITOR_LOAD_WAIT_TIMEOUT_MS}) on 417 * retries 418 * 419 * @param title 420 * the title of the editor to find (substring match) 421 * @return {@code true} if found, otherwise {@code false} 422 */ 423 public static boolean waitForSubstringMatchedEditor(String title) { 424 return waitForSubstringMatchedEditor(EDITOR_LOAD_WAIT_TIMEOUT_MS, title); 425 } 426 427 /** 428 * Convenience method to put test execution on hold by putting the thread to sleep 429 * 430 * @param millis 431 * the time to sleep in milliseconds. 432 * @return The actual time slept in milliseconds. Should be equal to millis unless an 433 * {@link InterruptedException} was thrown. 434 */ 435 public static long sleep(long millis) { 436 long time = System.currentTimeMillis(); 437 long slept = millis; 438 try { 439 Thread.sleep(millis); 440 } catch (InterruptedException e) { 441 slept = System.currentTimeMillis() - time; 442 } 443 return slept; 444 } 445 446 /** 447 * This method finds a widget that may or may not exist by means of doing a lookup and then 448 * walking through the results to see if there is a match. The difference to lookups with 449 * specific criterion is that this doesn't timeout and throw an exception if not found. Instead 450 * null is returned. Note: This method expects the property to hold a String or a list of 451 * Strings 452 * 453 * @param clazz 454 * the class of the widget to look for 455 * @param text 456 * the string value used for matching 457 * @param property 458 * the property to match with the supplied text parameter (has to return a String or 459 * List of String to be properly matched) 460 * @param policy 461 * a {@link StringComparePolicy} to use when matching the widget 462 * @return the control searched for or {@code null} if not found 463 */ 464 @SuppressWarnings("unchecked") 465 protected static <T> Wrap<? extends T> findWrap( 466 Class<T> clazz, String text, String property, StringComparePolicy policy) { 467 Lookup<? extends T> lookup = getShell().as(Parent.class, Control.class).lookup(clazz); 468 Wrap<? extends T> result = null; 469 for (int i = 0; i < lookup.size() && result == null; i++) { 470 Wrap<? extends T> wrap = lookup.wrap(i); 471 Object object = wrap.getProperty(property); 472 if (object instanceof List) { 473 List<String> values = new ArrayList<String>().getClass().cast(object); 474 for (String value : values) { 475 if (policy.compare(value, text)) { 476 result = wrap; 477 break; 478 } 479 } 480 } else { 481 if (policy.compare(String.class.cast(object), text)) { 482 result = wrap; 483 } 484 } 485 } 486 return result; 487 } 488 489 /** 490 * Convenience method to find out if a widget is disposed 491 * 492 * @param widgetWrap 493 * the wrapped widget to check 494 * @return {@code true} if disposed (or disposing), otherwise {@code false} 495 */ 496 protected static boolean isDisposed(final Wrap<? extends Widget> widgetWrap) { 497 return isDisposed(widgetWrap.getControl()); 498 } 499 500 /** 501 * Convenience method to find out if a widget is disposed 502 * 503 * @param widget 504 * the widget to check 505 * @return {@code true} if disposed (or disposing), otherwise {@code false} 506 */ 507 protected static boolean isDisposed(final Widget widget) { 508 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 509 @Override 510 public void run() { 511 if (widget != null) { 512 setOutput(widget.isDisposed()); 513 } else { 514 setOutput(true); 515 } 516 } 517 }; 518 Display.getDefault().syncExec(fetcher); 519 return fetcher.getOutput(); 520 } 521 522 /** 523 * Convenience method to find out if the widget of this wrapper is disposed 524 * 525 * @return {@code true} if disposed (or disposing), otherwise {@code false} 526 */ 527 public boolean isDisposed() { 528 return isDisposed(control); 529 } 530 531 /** 532 * Convenience method to find out if this control is enabled 533 * 534 * @return {@code true} if enabled, otherwise {@code false} 535 */ 536 public boolean isEnabled() { 537 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 538 @Override 539 public void run() { 540 setOutput(control.getControl().isEnabled()); 541 } 542 }; 543 Display.getDefault().syncExec(fetcher); 544 return fetcher.getOutput(); 545 } 546 547 /** 548 * Opens the context menu by right-clicking at a provided point 549 * 550 * @param p 551 * origin point of context (right-click) 552 */ 553 private void openContextMenuAtPoint(Point p) { 554 Display.getDefault().syncExec(() -> { 555 control.mouse().click(1, p, MouseButtons.BUTTON3); 556 }); 557 } 558 559 /** 560 * Checks the isEnabled value for a menu item 561 * 562 * @param p 563 * the point on the screen at which to open the context menu 564 * @param menuItemText 565 * the menu item of interest 566 * @return the isEnabled value for the menu item of interest 567 */ 568 public boolean isContextMenuItemEnabled(Point p, String menuItemText) { 569 openContextMenuAtPoint(p); 570 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 571 @Override 572 public void run() { 573 Menu menu = control.getControl().getMenu(); 574 for (MenuItem item : menu.getItems()) { 575 if(menuItemText.equals(item.getText())) { 576 setOutput(item.isEnabled()); 577 break; 578 } 579 } 580 } 581 }; 582 Display.getDefault().syncExec(fetcher); 583 return (fetcher.getOutput() == null) ? false : fetcher.getOutput(); 584 } 585 586 /** 587 * Convenience method to find out if a control is visible 588 * 589 * @param controlWrap 590 * the control to check 591 * @return {@code true} if visible, otherwise {@code false} 592 */ 593 protected static boolean isVisible(final Wrap<? extends Control> controlWrap) { 594 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 595 @Override 596 public void run() { 597 try { 598 setOutput(controlWrap.getControl().isVisible()); 599 } catch (SWTException e) { 600 suppressWidgetDisposedException(e); 601 } 602 } 603 }; 604 Display.getDefault().syncExec(fetcher); 605 return (fetcher.getOutput() == null) ? false : fetcher.getOutput(); 606 } 607 608 /** 609 * Convenience method to find out if this control is visible 610 * 611 * @return {@code true} if visible, otherwise {@code false} 612 */ 613 public boolean isVisible() { 614 return isVisible(control); 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 * @return a list of all visible wraps found by the lookup 623 */ 624 protected static <T extends Control> List<Wrap<? extends T>> getVisible(Lookup<T> lookup) { 625 return getVisible(lookup, true); 626 } 627 628 /** 629 * Iterates through the wraps found by a lookup and returns a list of all visible wraps. 630 * 631 * @param lookup 632 * the {@link Lookup} to search through 633 * @param waitForIdle 634 * {@code true} if "UI-update queue" should be empty before looking for controls 635 * @return a list of all visible wraps found by the {@link Lookup} 636 */ 637 protected static <T extends Control> List<Wrap<? extends T>> getVisible(Lookup<T> lookup, boolean waitForIdle) { 638 return getVisible(lookup, waitForIdle, VISIBLE_LOOKUP_TIMEOUT_MS); 639 } 640 641 /** 642 * Iterates through the wraps found by a lookup and returns a list of all visible wraps. 643 * 644 * @param lookup 645 * the {@link Lookup} to search through 646 * @param waitForIdle 647 * {@code true} if "UI-update queue" should be empty before looking for controls 648 * @param maxWaitMs 649 * the timeout in milliseconds before ending the lookup 650 * @return a list of all visible wraps found by the {@link Lookup} 651 */ 652 protected static <T extends Control> List<Wrap<? extends T>> getVisible( 653 Lookup<T> lookup, boolean waitForIdle, long maxWaitMs) { 654 return getVisible(lookup, waitForIdle, maxWaitMs, true); 655 } 656 657 /** 658 * Iterates through the wraps found by a lookup and returns a list of all visible wraps. Will 659 * retry a maximum of {@code VISIBLE_LOOKUP_MAX_RETRY_COUNT} times with a 660 * {@code VISIBLE_LOOKUP_SLEEP_TIME_MS} ms sleep in between if lookup has zero length 661 * 662 * @param lookup 663 * the {@link Lookup} to search through 664 * @param waitForIdle 665 * {@code true} if "UI-update queue" should be empty before looking for controls 666 * @param maxWaitMs 667 * the timeout in milliseconds before ending the lookup 668 * @param assertEmpty 669 * if {@code true} will assert the the resulting list isn't empty assertion will be 670 * done 671 * @return a list of all visible wraps found by the {@link Lookup} 672 */ 673 @SuppressWarnings("unchecked") 674 protected static <T extends Control> List<Wrap<? extends T>> getVisible( 675 Lookup<T> lookup, boolean waitForIdle, long maxWaitMs, boolean assertEmpty) { 676 if (waitForIdle) { 677 waitForIdle(); 678 } 679 List<Wrap<? extends T>> list = new ArrayList<>(); 680 long lookupEndTime = System.currentTimeMillis() + maxWaitMs; 681 do { 682 for (int i = 0; i < lookup.size(); i++) { 683 Wrap<T> wrap = (Wrap<T>) lookup.wrap(i); 684 if (isVisible(wrap)) { 685 list.add(wrap); 686 } 687 } 688 if (list.size() == 0) { 689 sleep(LOOKUP_SLEEP_TIME_MS); 690 } 691 } while (list.size() == 0 && lookupEndTime > System.currentTimeMillis()); 692 if (assertEmpty) { 693 Assert.assertTrue("No visible controls found", list.size() > 0); 694 } 695 return list; 696 } 697 698 /** 699 * Iterates through the wraps found by a lookup and returns a list of all visible wraps. Will 700 * retry a maximum of {@code VISIBLE_LOOKUP_MAX_RETRY_COUNT} times with a 701 * {@code VISIBLE_LOOKUP_SLEEP_TIME_MS} ms sleep in between if lookup has zero length 702 * 703 * @param lookup 704 * the {@link Lookup} to search through 705 * @param waitForIdle 706 * {@code true} if "UI-update queue" should empty before looking for controls 707 * @param assertEmpty 708 * if {@code true} will assert the the resulting list isn't empty. Otherwise no 709 * assertion will be done 710 * @return a list of all visible wraps found by the {@link Lookup} 711 */ 712 protected static <T extends Control> List<Wrap<? extends T>> getVisible( 713 Lookup<T> lookup, boolean waitForIdle, boolean assertEmpty) { 714 return getVisible(lookup, waitForIdle, VISIBLE_LOOKUP_TIMEOUT_MS, assertEmpty); 715 } 716 717 /** 718 * Convenience method to find out if Mission Control UI is busy 719 * 720 * @return {@code true} if busy 721 */ 722 public static boolean isBusy() { 723 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 724 @Override 725 public void run() { 726 setOutput(!Job.getJobManager().isIdle()); 727 } 728 }; 729 Display.getDefault().syncExec(fetcher); 730 return fetcher.getOutput(); 731 } 732 733 /** 734 * Inspects a Widget (graphically) for updates by comparing two snapshots of it 735 * 736 * @param widget 737 * the widget to inspect for change (graphically) 738 * @param waitTimeMillis 739 * the time to wait between the snapshots of the Widget 740 * @return {@code true} if the Widget has changed 741 */ 742 public boolean isWidgetUpdating(Wrap<? extends Widget> widget, int waitTimeMillis) { 743 sleep(waitTimeMillis); 744 org.jemmy.image.Image firstImage = widget.getScreenImage(); 745 sleep(waitTimeMillis); 746 org.jemmy.image.Image secondImage = widget.getScreenImage(); 747 748 // Set the Comparator to be strict, first get and save the current 749 // Comparator for later restoral 750 ImageComparator current = AWTImage.getComparator(); 751 AWTImage.setComparator(new StrictImageComparator()); 752 // diff will be null if the Images are identical 753 org.jemmy.image.Image diff = secondImage.compareTo(firstImage); 754 AWTImage.setComparator(current); 755 756 return (diff == null) ? false : true; 757 } 758 759 /** 760 * Inspects this control for updates by comparing two snapshots of it 761 * 762 * @param waitTimeMillis 763 * the time to wait between the snapshots of the Widget 764 * @return {@code true} if the Widget has changed 765 */ 766 public boolean isWidgetUpdating(int waitTimeMillis) { 767 return isWidgetUpdating(control, waitTimeMillis); 768 } 769 770 /** 771 * Waits for background jobs to finish before executing a script line. Since background jobs 772 * post to the UI-thread asynchronously we must ensure they get a chance to run, so we we spin 773 * asynchronously in a count down process. IDLE_LOOP_COUNT specifies the number iterations 774 * without the job manager being activated as being enough to make sure we are in a calm/idle 775 * state. 776 */ 777 public static void waitForIdle() { 778 int counter = IDLE_LOOP_COUNT; 779 long startTimestamp = System.currentTimeMillis(); 780 while (counter > 0) { 781 if ((System.currentTimeMillis() - startTimestamp) > IDLE_LOOP_TIMEOUT_MS) { 782 break; 783 } 784 if (isBusy()) { 785 counter = IDLE_LOOP_COUNT; 786 } else { 787 counter--; 788 } 789 sleep(IDLE_LOOP_TIME_STEP); 790 } 791 } 792 793 /** 794 * Saves a picture of Mission Control's shell. If Mission Control is behind other applications, 795 * the rectangle will show what's really on top. 796 * 797 * @param imageName 798 * the name of the saved file, ".png" will be added at the end 799 */ 800 public static void saveMcImage(String imageName) { 801 Image pic = getMcImage(); 802 pic.save(imageName + ".png"); 803 } 804 805 /** 806 * Saves a picture of Mission Control's shell. If Mission Control is behind other applications, 807 * the rectangle will show what's really on top. The image will be given a name which is unique 808 * for the execution. 809 */ 810 public static void saveMcImage() { 811 unnamedImageCounter++; 812 saveMcImage("unnamed_mc_image_" + String.format("%03d", unnamedImageCounter) + ".png"); 813 } 814 815 /** 816 * Returns an image of the Mission Control shell 817 * 818 * @return an {@link Image} 819 */ 820 public static Image getMcImage() { 821 return getShell().getScreenImage(); 822 } 823 824 /** 825 * Returns an image of this control 826 * 827 * @return an {@link Image} of this control 828 */ 829 public Image getThisImage() { 830 return control.getScreenImage(); 831 } 832 833 /** 834 * Saves a picture of this control. If the control is behind other applications or Mission 835 * Control controls, the rectangle will show what's really on top. 836 * 837 * @param imageName 838 * the name of the saved file. ".png" will be added at the end 839 */ 840 public void saveThisImage(String imageName) { 841 Image pic = getThisImage(); 842 saveImage(imageName + ".png", pic); 843 } 844 845 /** 846 * Saves a picture of this control. If the control is behind other applications or Mission 847 * Control controls, the rectangle will show what's really on top. The image will be given a 848 * name which is unique for the execution. 849 */ 850 public void saveThisImage() { 851 unnamedImageCounter++; 852 saveThisImage("unnamed_mc_image_" + String.format("%03d", unnamedImageCounter) + ".png"); 853 } 854 855 /** 856 * Saves the image with the specified name 857 * 858 * @param fileName 859 * the name of the image file 860 * @param image 861 * the image 862 */ 863 public static void saveImage(String fileName, Image image) { 864 image.save(fileName); 865 } 866 867 /** 868 * Focuses on a specific section to use instead of the main shell. 869 * 870 * @param name 871 * the name of the section to focus on 872 */ 873 @SuppressWarnings("unchecked") 874 public static void focusSectionByName(String name) { 875 focusedSection = (Wrap<? extends Shell>) getVisible( 876 getShell().as(Parent.class, Control.class).lookup(new ByName<Shell>(name))).get(0); 877 } 878 879 /** 880 * Focuses on a specific section to use instead of the main shell. 881 * 882 * @param title 883 * the title of the section to focus on 884 */ 885 @SuppressWarnings("unchecked") 886 public static void focusSectionByTitle(String title) { 887 focusedSection = (Wrap<? extends Shell>) getVisible( 888 getShell().as(Parent.class, Control.class).lookup(new ByTextControlLookup<>(title))).get(0); 889 } 890 891 /** 892 * Focuses on a specific section to use instead of the main shell. 893 * 894 * @param title 895 * the title of the section to focus on 896 * @param waitForIdle 897 * if {@code true} will first wait for the UI to be idle before setting focus 898 */ 899 @SuppressWarnings("unchecked") 900 public static void focusSectionByTitle(String title, boolean waitForIdle) { 901 focusedSection = (Wrap<? extends Shell>) getVisible( 902 getShell().as(Parent.class, Control.class).lookup(new ByTextControlLookup<>(title)), waitForIdle) 903 .get(0); 904 } 905 906 /** 907 * House keeping: clearing the focusedSection reference so that it won't be used in further 908 * lookups 909 */ 910 public static void clearFocus() { 911 focusedSection = null; 912 } 913 914 /** 915 * @return a {@link List} of {@link MCTable} either in the currently focused section or 916 * globally in the shell 917 */ 918 public static List<MCTable> getTables() { 919 if (focusedSection != null) { 920 return MCTable.getAll(focusedSection); 921 } else { 922 return MCTable.getAll(getShell()); 923 } 924 } 925 926 /** 927 * Get all tables in the focused section (if set), otherwise from the Mission Control main shell 928 * 929 * @param waitForIdle 930 * {@code true} if "UI-update queue" should be empty before looking for controls 931 * @return a {@link List} of {@link MCTable} either in the currently focused section or 932 * globally in the shell 933 */ 934 public static List<MCTable> getTables(boolean waitForIdle) { 935 if (focusedSection != null) { 936 return MCTable.getAll(focusedSection, waitForIdle); 937 } else { 938 return MCTable.getAll(getShell(), waitForIdle); 939 } 940 } 941 942 /** 943 * Runs the method and returns the result if a matching method is found. If not, null will 944 * always be returned. Note that the method could return null as well if the operation succeeds 945 * so this needs to be handled in a proper way by the caller. 946 * 947 * @param returnType 948 * the type of the returned object 949 * @param object 950 * the object on which to run the method 951 * @param methodName 952 * the name of the method to run 953 * @param params 954 * an object array of parameters for the method. null if no parameters 955 * @return The result of running the method. null if no matching method is found (name, 956 * parameters and return type). Note that the method could return null as well if the 957 * operation succeeds so this needs to be handled in a proper way by the caller. 958 */ 959 public static <T> T runMethod(Class<T> returnType, Object object, String methodName, Object ... params) { 960 T result = null; 961 try { 962 Class<?>[] paramTypes = null; 963 964 if (params != null) { 965 paramTypes = new Class<?>[params.length]; 966 for (int i = 0; i < params.length; i++) { 967 paramTypes[i] = params[i].getClass(); 968 } 969 } 970 971 Method method = getCompatibleMethod(object, methodName, paramTypes); 972 Class<?> methodReturnType = method.getReturnType(); 973 974 if (methodReturnType.isPrimitive()) { 975 methodReturnType = primitiveMap.get(methodReturnType); 976 } 977 978 if (returnType.equals(Void.class) || returnType.isAssignableFrom(methodReturnType)) { 979 result = returnType.cast(method.invoke(object, params)); 980 } 981 } catch (Exception e) { 982 // do nothing, just return null 983 } 984 return result; 985 } 986 987 private static Method getCompatibleMethod(Object object, String methodName, Class<?> ... paramTypes) 988 throws SecurityException, NoSuchMethodException { 989 if (paramTypes != null) { 990 Method[] methods = object.getClass().getMethods(); 991 for (Method method : methods) { 992 Method m = method; 993 994 if (!m.getName().equals(methodName)) { 995 continue; 996 } 997 998 Class<?>[] actualTypes = m.getParameterTypes(); 999 if (actualTypes.length != paramTypes.length) { 1000 continue; 1001 } 1002 1003 boolean found = true; 1004 for (int j = 0; j < actualTypes.length; j++) { 1005 if (!actualTypes[j].isAssignableFrom(paramTypes[j])) { 1006 if (actualTypes[j].isPrimitive()) { 1007 found = primitiveMap.get(actualTypes[j]).equals(paramTypes[j]); 1008 } else if (paramTypes[j].isPrimitive()) { 1009 found = primitiveMap.get(paramTypes[j]).equals(actualTypes[j]); 1010 } else { 1011 found = false; 1012 } 1013 } 1014 1015 if (!found) { 1016 break; 1017 } 1018 } 1019 1020 if (found) { 1021 return m; 1022 } 1023 } 1024 1025 throw new NoSuchMethodException("Could not find method " + methodName + " with parameters " + paramTypes); 1026 } else { 1027 return object.getClass().getMethod(methodName); 1028 } 1029 } 1030 1031 /** 1032 * Returns the name of this control 1033 * 1034 * @return the name of this control. Null if no name has been set 1035 */ 1036 public String getName() { 1037 return control.getProperty(String.class, Wrap.NAME_PROP_NAME); 1038 } 1039 1040 /** 1041 * @return the text of this control 1042 */ 1043 public String getText() { 1044 return control.getProperty(String.class, Wrap.TEXT_PROP_NAME); 1045 } 1046 1047 /** 1048 * Clicks this control (once) 1049 */ 1050 public void click() { 1051 click(1); 1052 } 1053 1054 /** 1055 * Clicks this control {@code times} times 1056 * 1057 * @param times 1058 * the number of times to mouse click this control 1059 */ 1060 public void click(int times) { 1061 control.mouse().click(times); 1062 } 1063 1064 /** 1065 * Default implementation of a copy to clipboard for any wrapper 1066 */ 1067 public void copyToClipboard() { 1068 control.as(Focusable.class).focuser().focus(); 1069 sleep(BETWEEN_KEYSTROKES_SLEEP); 1070 control.keyboard().pushKey(KeyboardButtons.A, SHORTCUT_MODIFIER); 1071 sleep(BETWEEN_KEYSTROKES_SLEEP); 1072 control.keyboard().pushKey(KeyboardButtons.C, SHORTCUT_MODIFIER); 1073 sleep(BETWEEN_KEYSTROKES_SLEEP); 1074 } 1075 1076 /** 1077 * Gets the system clipboard contents as a string 1078 * 1079 * @return the clipboard contents 1080 */ 1081 public static String getStringFromClipboard() { 1082 return getFromClipBoard(String.class); 1083 } 1084 1085 /** 1086 * Finds out if content assist is present. The assumption is that content assist is present if 1087 * there is a Shell with a single Composite that in turn has a single child of type Table 1088 * 1089 * @return {@code true} if content assist is present, otherwise {@code false} 1090 */ 1091 public static boolean isContentAssistPresent() { 1092 boolean found = true; 1093 long endTime = System.currentTimeMillis() + 1000; 1094 while (found && System.currentTimeMillis() < endTime) { 1095 FetcherWithInput<List<Wrap<? extends Shell>>, Boolean> fetcher = new FetcherWithInput<List<Wrap<? extends Shell>>, Boolean>( 1096 getVisible(Shells.SHELLS.lookup(Shell.class), false, false)) { 1097 1098 @Override 1099 public void run() { 1100 boolean isPresent = false; 1101 for (Wrap<? extends Shell> shellWrap : getInput()) { 1102 Control[] shellChildren = shellWrap.getControl().getChildren(); 1103 if (shellChildren.length == 1 && shellChildren[0] instanceof Composite) { 1104 Control[] compositeChildren = Composite.class.cast(shellChildren[0]).getChildren(); 1105 if (compositeChildren.length == 1 && compositeChildren[0] instanceof Table) { 1106 isPresent = true; 1107 break; 1108 } 1109 } 1110 } 1111 setOutput(isPresent); 1112 } 1113 }; 1114 Display.getDefault().syncExec(fetcher); 1115 found = fetcher.getOutput(); 1116 if (found) { 1117 sleep(100); 1118 } 1119 } 1120 return found; 1121 } 1122 1123 private static <T> T getFromClipBoard(Class<T> returnType) { 1124 Transferable transferable = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null); 1125 DataFlavor thisFlavor = new DataFlavor(returnType, returnType.getName()); 1126 try { 1127 if (transferable != null && transferable.isDataFlavorSupported(thisFlavor)) { 1128 return returnType.cast(transferable.getTransferData(thisFlavor)); 1129 } 1130 } catch (UnsupportedFlavorException e) { 1131 System.out.println("Clipboard content flavor is not supported " + e.getMessage()); 1132 } catch (IOException e) { 1133 System.out.println("Clipboard content could not be retrieved " + e.getMessage()); 1134 } 1135 return null; 1136 } 1137 1138 private static void suppressWidgetDisposedException(SWTException e) { 1139 if (!e.getMessage().contains("Widget is disposed")) { 1140 // Unexpected exception. Re-throw it 1141 throw e; 1142 } 1143 } 1144 1145 /** 1146 * Setting focus on this control programmatically (if not already focused). 1147 */ 1148 protected void ensureFocus() { 1149 Fetcher<Boolean> fetcher = new Fetcher<Boolean>() { 1150 @Override 1151 public void run() { 1152 if (!control.getControl().isFocusControl()) { 1153 control.getControl().setFocus(); 1154 } 1155 setOutput(control.getControl().isFocusControl()); 1156 } 1157 }; 1158 Display.getDefault().syncExec(fetcher); 1159 if (!fetcher.getOutput()) { 1160 // fallback if the programmatic focusing didn't work 1161 control.as(Focusable.class).focuser().focus(); 1162 } 1163 } 1164 }