1/* 2 * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23package org.jemmy2ext; 24 25import java.awt.Component; 26import java.awt.EventQueue; 27import java.awt.Frame; 28import java.awt.Graphics; 29import java.awt.Rectangle; 30import java.awt.Robot; 31import java.awt.Window; 32import java.awt.image.BufferedImage; 33import java.io.BufferedOutputStream; 34import java.io.File; 35import java.io.FileNotFoundException; 36import java.io.FileOutputStream; 37import java.io.IOException; 38import java.lang.reflect.InvocationTargetException; 39import java.util.ArrayList; 40import java.util.Collections; 41import java.util.List; 42import java.util.function.Function; 43import java.util.logging.Level; 44import java.util.logging.Logger; 45import java.util.stream.IntStream; 46import javax.imageio.ImageIO; 47import javax.swing.JButton; 48import javax.swing.JComponent; 49import javax.swing.JPanel; 50import javax.swing.JWindow; 51import javax.swing.border.Border; 52import javax.swing.border.CompoundBorder; 53import javax.swing.border.TitledBorder; 54import org.netbeans.jemmy.ComponentChooser; 55import org.netbeans.jemmy.DefaultCharBindingMap; 56import org.netbeans.jemmy.QueueTool; 57import org.netbeans.jemmy.TimeoutExpiredException; 58import org.netbeans.jemmy.Waitable; 59import org.netbeans.jemmy.Waiter; 60import org.netbeans.jemmy.drivers.scrolling.JSpinnerDriver; 61import org.netbeans.jemmy.image.StrictImageComparator; 62import org.netbeans.jemmy.operators.ComponentOperator; 63import org.netbeans.jemmy.operators.ContainerOperator; 64import org.netbeans.jemmy.operators.FrameOperator; 65import org.netbeans.jemmy.operators.JButtonOperator; 66import org.netbeans.jemmy.operators.JFrameOperator; 67import org.netbeans.jemmy.operators.JLabelOperator; 68import org.netbeans.jemmy.operators.Operator; 69import org.netbeans.jemmy.util.Dumper; 70import org.netbeans.jemmy.util.PNGEncoder; 71import static org.testng.AssertJUnit.*; 72 73/** 74 * This class solves two tasks: 1. It adds functionality that is missing in 75 * Jemmy 2. It references all the Jemmy API that is needed by tests so that they 76 * can just @build JemmyExt class and do not worry about Jemmy 77 * 78 * @author akouznet 79 */ 80public class JemmyExt { 81 82 /** 83 * Statically referencing all the classes that are needed by tests so that 84 * they're compiled by jtreg 85 */ 86 static final Class<?>[] DEPENDENCIES = { 87 JSpinnerDriver.class, 88 DefaultCharBindingMap.class 89 }; 90 91 public static void assertNotBlack(BufferedImage image) { 92 int w = image.getWidth(); 93 int h = image.getHeight(); 94 try { 95 assertFalse("All pixels are not black", IntStream.range(0, w).parallel().allMatch(x 96 -> IntStream.range(0, h).allMatch(y -> (image.getRGB(x, y) & 0xffffff) == 0) 97 )); 98 } catch (Throwable t) { 99 save(image, "allPixelsAreBlack.png"); 100 throw t; 101 } 102 } 103 104 public static void waitArmed(JButtonOperator button) { 105 button.waitState(new ComponentChooser() { 106 107 @Override 108 public boolean checkComponent(Component comp) { 109 return isArmed(button); 110 } 111 112 @Override 113 public String getDescription() { 114 return "Button is armed"; 115 } 116 }); 117 } 118 119 public static boolean isArmed(JButtonOperator button) { 120 return button.getQueueTool().invokeSmoothly(new QueueTool.QueueAction<Boolean>("getModel().isArmed()") { 121 122 @Override 123 public Boolean launch() throws Exception { 124 return ((JButton) button.getSource()).getModel().isArmed(); 125 } 126 }); 127 } 128 129 public static void waitPressed(JButtonOperator button) { 130 button.waitState(new ComponentChooser() { 131 132 @Override 133 public boolean checkComponent(Component comp) { 134 return isPressed(button); 135 } 136 137 @Override 138 public String getDescription() { 139 return "Button is pressed"; 140 } 141 }); 142 } 143 144 public static boolean isPressed(JButtonOperator button) { 145 return button.getQueueTool().invokeSmoothly(new QueueTool.QueueAction<Boolean>("getModel().isPressed()") { 146 147 @Override 148 public Boolean launch() throws Exception { 149 return ((JButton) button.getSource()).getModel().isPressed(); 150 } 151 }); 152 } 153 154 public static void assertEquals(String string, StrictImageComparator comparator, BufferedImage expected, BufferedImage actual) { 155 try { 156 assertTrue(string, comparator.compare(expected, actual)); 157 } catch (Error err) { 158 save(expected, "expected.png"); 159 save(actual, "actual.png"); 160 throw err; 161 } 162 } 163 164 public static void assertNotEquals(String string, StrictImageComparator comparator, BufferedImage notExpected, BufferedImage actual) { 165 try { 166 assertFalse(string, comparator.compare(notExpected, actual)); 167 } catch (Error err) { 168 save(notExpected, "notExpected.png"); 169 save(actual, "actual.png"); 170 throw err; 171 } 172 } 173 174 public static void save(BufferedImage image, String filename) { 175 String filepath = filename; 176 try { 177 filepath = new File(filename).getCanonicalPath(); 178 System.out.println("Saving screenshot to " + filepath); 179 BufferedOutputStream file = new BufferedOutputStream(new FileOutputStream(filepath)); 180 new PNGEncoder(file, PNGEncoder.COLOR_MODE).encode(image); 181 } catch (IOException ioe) { 182 throw new RuntimeException("Failed to save image to " + filepath, ioe); 183 } 184 } 185 186 public static void waitImageIsStill(Robot rob, ComponentOperator operator) { 187 operator.waitState(new ComponentChooser() { 188 189 private BufferedImage previousImage = null; 190 private int index = 0; 191 private final StrictImageComparator sComparator = new StrictImageComparator(); 192 193 @Override 194 public boolean checkComponent(Component comp) { 195 BufferedImage currentImage = capture(rob, operator); 196 save(currentImage, "waitImageIsStill" + index + ".png"); 197 index++; 198 boolean compareResult = previousImage == null ? false : sComparator.compare(currentImage, previousImage); 199 previousImage = currentImage; 200 return compareResult; 201 } 202 203 @Override 204 public String getDescription() { 205 return "Image of " + operator + " is still"; 206 } 207 }); 208 } 209 210 private static class ThrowableHolder { 211 212 volatile Throwable t; 213 } 214 215 public static void waitFor(String description, RunnableWithException r) throws Exception { 216 Waiter<Boolean, ThrowableHolder> waiter = new Waiter<>(new Waitable<Boolean, ThrowableHolder>() { 217 218 @Override 219 public Boolean actionProduced(ThrowableHolder obj) { 220 try { 221 r.run(); 222 return true; 223 } catch (Throwable t) { 224 obj.t = t; 225 return null; 226 } 227 } 228 229 @Override 230 public String getDescription() { 231 return description; 232 } 233 }); 234 ThrowableHolder th = new ThrowableHolder(); 235 try { 236 waiter.waitAction(th); 237 } catch (TimeoutExpiredException tee) { 238 Throwable t = th.t; 239 if (t != null) { 240 t.addSuppressed(tee); 241 if (t instanceof Exception) { 242 throw (Exception) t; 243 } else if (t instanceof Error) { 244 throw (Error) t; 245 } else if (t instanceof RuntimeException) { 246 throw (RuntimeException) t; 247 } else { 248 throw new IllegalStateException("Unexpected exception type", t); 249 } 250 } 251 } 252 } 253 254 public static BufferedImage capture(Robot rob, ComponentOperator operator) { 255 Rectangle boundary = new Rectangle(operator.getLocationOnScreen(), 256 operator.getSize()); 257 return rob.createScreenCapture(boundary); 258 } 259 260 /** 261 * Dispose all AWT/Swing windows causing event thread to stop 262 */ 263 public static void disposeAllWindows() { 264 System.out.println("disposeAllWindows"); 265 try { 266 EventQueue.invokeAndWait(() -> { 267 Window[] windows = Window.getWindows(); 268 for (Window w : windows) { 269 w.dispose(); 270 } 271 }); 272 } catch (InterruptedException | InvocationTargetException ex) { 273 Logger.getLogger(JemmyExt.class.getName()).log(Level.SEVERE, "Failed to dispose all windows", ex); 274 } 275 } 276 277 /** 278 * This is a helper class which allows to catch throwables thrown in other 279 * threads and throw them in the main test thread 280 */ 281 public static class MultiThreadedTryCatch { 282 283 private final List<Throwable> throwables 284 = Collections.synchronizedList(new ArrayList<>()); 285 286 /** 287 * Throws registered throwables. If the list of the registered 288 * throwables is not empty, it re-throws the first throwable in the list 289 * adding all others into its suppressed list. Can be used in any 290 * thread. 291 * 292 * @throws Exception 293 */ 294 public void throwRegistered() throws Exception { 295 Throwable root = null; 296 synchronized (throwables) { 297 if (!throwables.isEmpty()) { 298 root = throwables.remove(0); 299 while (!throwables.isEmpty()) { 300 root.addSuppressed(throwables.remove(0)); 301 } 302 } 303 } 304 if (root != null) { 305 if (root instanceof Error) { 306 throw (Error) root; 307 } else if (root instanceof Exception) { 308 throw (Exception) root; 309 } else { 310 throw new AssertionError("Unexpected exception type: " + root.getClass() + " (" + root + ")"); 311 } 312 } 313 } 314 315 /** 316 * Registers a throwable and adds it to the list of throwables. Can be 317 * used in any thread. 318 * 319 * @param t 320 */ 321 public void register(Throwable t) { 322 t.printStackTrace(); 323 throwables.add(t); 324 } 325 326 /** 327 * Registers a throwable and adds it as the first item of the list of 328 * catched throwables. 329 * 330 * @param t 331 */ 332 public void registerRoot(Throwable t) { 333 t.printStackTrace(); 334 throwables.add(0, t); 335 } 336 } 337 338 /** 339 * Trying to capture as much information as possible. Currently it includes 340 * full dump and a screenshot of the whole screen. 341 */ 342 public static void captureAll() { 343 PNGEncoder.captureScreen("failure.png", PNGEncoder.COLOR_MODE); 344 try { 345 Dumper.dumpAll("dumpAll.xml"); 346 } catch (FileNotFoundException ex) { 347 Logger.getLogger(JemmyExt.class.getName()).log(Level.SEVERE, null, ex); 348 } 349 captureWindows(); 350 } 351 352 /** 353 * Captures each showing window image using Window.paint() method. 354 */ 355 private static void captureWindows() { 356 try { 357 EventQueue.invokeAndWait(() -> { 358 Window[] windows = Window.getWindows(); 359 int index = 0; 360 for (Window w : windows) { 361 if (!w.isShowing()) { 362 continue; 363 } 364 BufferedImage img = new BufferedImage(w.getWidth(), w.getHeight(), BufferedImage.TYPE_INT_ARGB); 365 Graphics g = img.getGraphics(); 366 w.paint(g); 367 g.dispose(); 368 369 try { 370 ImageIO.write(img, "png", new File("window" + index++ + ".png")); 371 } catch (IOException e) { 372 e.printStackTrace(); 373 } 374 } 375 }); 376 } catch (InterruptedException | InvocationTargetException ex) { 377 Logger.getLogger(JemmyExt.class.getName()).log(Level.SEVERE, null, ex); 378 } 379 } 380 381 public static interface RunnableWithException { 382 383 public void run() throws Exception; 384 } 385 386 public static void waitIsFocused(JFrameOperator jfo) { 387 jfo.waitState(new ComponentChooser() { 388 389 @Override 390 public boolean checkComponent(Component comp) { 391 return jfo.isFocused(); 392 } 393 394 @Override 395 public String getDescription() { 396 return "JFrame is focused"; 397 } 398 }); 399 } 400 401 public static int getJWindowCount() { 402 return new QueueTool().invokeAndWait(new QueueTool.QueueAction<Integer>(null) { 403 404 @Override 405 public Integer launch() throws Exception { 406 Window[] windows = Window.getWindows(); 407 int windowCount = 0; 408 for (Window w : windows) { 409 if (w.getClass().equals(JWindow.class)) { 410 windowCount++; 411 } 412 } 413 return windowCount; 414 } 415 }); 416 } 417 418 public static JWindow getJWindow() { 419 return getJWindow(0); 420 } 421 422 public static JWindow getJWindow(int index) { 423 return new QueueTool().invokeAndWait(new QueueTool.QueueAction<JWindow>(null) { 424 425 @Override 426 public JWindow launch() throws Exception { 427 Window[] windows = Window.getWindows(); 428 int windowIndex = 0; 429 for (Window w : windows) { 430 if (w.getClass().equals(JWindow.class)) { 431 if (windowIndex == index) { 432 return (JWindow) w; 433 } 434 windowIndex++; 435 } 436 } 437 return null; 438 } 439 }); 440 } 441 442 public static boolean isIconified(FrameOperator frameOperator) { 443 return frameOperator.getQueueTool().invokeAndWait(new QueueTool.QueueAction<Boolean>("Frame is iconified") { 444 445 @Override 446 public Boolean launch() throws Exception { 447 return (((Frame) frameOperator.getSource()).getState() & Frame.ICONIFIED) != 0; 448 } 449 }); 450 } 451 452 public static final Operator.DefaultStringComparator EXACT_STRING_COMPARATOR 453 = new Operator.DefaultStringComparator(true, true); 454 455 /** 456 * Finds a label with the exact labelText and returns the operator for its 457 * parent container. 458 * 459 * @param container 460 * @param labelText 461 * @return 462 */ 463 public static ContainerOperator<?> getLabeledContainerOperator(ContainerOperator<?> container, String labelText) { 464 465 container.setComparator(EXACT_STRING_COMPARATOR); 466 467 JLabelOperator jLabelOperator = new JLabelOperator(container, labelText); 468 469 assert labelText.equals(jLabelOperator.getText()); 470 471 return new ContainerOperator<>(jLabelOperator.getParent()); 472 } 473 474 /** 475 * Finds a JPanel with exact title text. 476 * 477 * @param container 478 * @param titleText 479 * @return 480 */ 481 public static ContainerOperator<?> getBorderTitledJPanelOperator(ContainerOperator<?> container, String titleText) { 482 return new ContainerOperator<>(container, new JPanelByBorderTitleFinder(titleText, EXACT_STRING_COMPARATOR)); 483 } 484 485 public static final QueueTool QUEUE_TOOL = new QueueTool(); 486 487 /** 488 * Allows to find JPanel by the title text in its border. 489 */ 490 public static class JPanelByBorderTitleFinder implements ComponentChooser { 491 492 String titleText; 493 Operator.StringComparator comparator; 494 495 /** 496 * @param titleText title text pattern 497 * @param comparator specifies string comparison algorithm. 498 */ 499 public JPanelByBorderTitleFinder(String titleText, Operator.StringComparator comparator) { 500 this.titleText = titleText; 501 this.comparator = comparator; 502 } 503 504 /** 505 * @param titleText title text pattern 506 */ 507 public JPanelByBorderTitleFinder(String titleText) { 508 this(titleText, Operator.getDefaultStringComparator()); 509 } 510 511 @Override 512 public boolean checkComponent(Component comp) { 513 assert EventQueue.isDispatchThread(); 514 if (comp instanceof JPanel) { 515 return checkBorder(((JPanel) comp).getBorder()); 516 } 517 return false; 518 } 519 520 public boolean checkBorder(Border border) { 521 if (border instanceof TitledBorder) { 522 String title = ((TitledBorder) border).getTitle(); 523 return comparator.equals(title, titleText); 524 } else if (border instanceof CompoundBorder) { 525 CompoundBorder compoundBorder = (CompoundBorder) border; 526 return checkBorder(compoundBorder.getInsideBorder()) || checkBorder(compoundBorder.getOutsideBorder()); 527 } else { 528 return false; 529 } 530 } 531 532 @Override 533 public String getDescription() { 534 return ("JPanel with border title text \"" + titleText + "\" with comparator " + comparator); 535 } 536 } 537 538 public static class ByClassSimpleNameChooser implements ComponentChooser { 539 540 private final String className; 541 542 public ByClassSimpleNameChooser(String className) { 543 this.className = className; 544 } 545 546 @Override 547 public boolean checkComponent(Component comp) { 548 return comp.getClass().getSimpleName().equals(className); 549 } 550 551 @Override 552 public String getDescription() { 553 return "Component with the simple class name of " + className; 554 } 555 556 } 557 558 public static class ByClassChooser implements ComponentChooser { 559 560 private final Class<?> clazz; 561 562 public ByClassChooser(Class<?> clazz) { 563 this.clazz = clazz; 564 } 565 566 @Override 567 public boolean checkComponent(Component comp) { 568 return comp.getClass().equals(clazz); 569 } 570 571 @Override 572 public String getDescription() { 573 return "Component with the class of " + clazz; 574 } 575 576 } 577 578 public static class ByToolTipChooser implements ComponentChooser { 579 580 private final String tooltip; 581 582 public ByToolTipChooser(String tooltip) { 583 if (tooltip == null) { 584 throw new NullPointerException("Tooltip cannot be null"); 585 } 586 this.tooltip = tooltip; 587 } 588 589 @Override 590 public boolean checkComponent(Component comp) { 591 return (comp instanceof JComponent) 592 ? tooltip.equals(((JComponent) comp).getToolTipText()) 593 : false; 594 } 595 596 @Override 597 public String getDescription() { 598 return "JComponent with the tooltip '" + tooltip + "'"; 599 } 600 601 } 602 603 @SuppressWarnings(value = "unchecked") 604 public static <R, O extends Operator, S extends Component> R getUIValue(O operator, Function<S, R> getter) { 605 return operator.getQueueTool().invokeSmoothly(new QueueTool.QueueAction<R>("getting UI value through the queue using " + getter) { 606 607 @Override 608 public R launch() throws Exception { 609 return getter.apply((S) operator.getSource()); 610 } 611 }); 612 } 613} 614