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 */ |
|
23 package org.jemmy2ext; |
|
24 |
|
25 import java.awt.Component; |
|
26 import java.awt.EventQueue; |
|
27 import java.awt.Frame; |
|
28 import java.awt.Graphics; |
|
29 import java.awt.Rectangle; |
|
30 import java.awt.Robot; |
|
31 import java.awt.Window; |
|
32 import java.awt.image.BufferedImage; |
|
33 import java.io.BufferedOutputStream; |
|
34 import java.io.File; |
|
35 import java.io.FileNotFoundException; |
|
36 import java.io.FileOutputStream; |
|
37 import java.io.IOException; |
|
38 import java.lang.reflect.InvocationTargetException; |
|
39 import java.util.ArrayList; |
|
40 import java.util.Collections; |
|
41 import java.util.List; |
|
42 import java.util.function.Function; |
|
43 import java.util.logging.Level; |
|
44 import java.util.logging.Logger; |
|
45 import java.util.stream.IntStream; |
|
46 import javax.imageio.ImageIO; |
|
47 import javax.swing.JButton; |
|
48 import javax.swing.JComponent; |
|
49 import javax.swing.JPanel; |
|
50 import javax.swing.JWindow; |
|
51 import javax.swing.border.Border; |
|
52 import javax.swing.border.CompoundBorder; |
|
53 import javax.swing.border.TitledBorder; |
|
54 import org.netbeans.jemmy.ComponentChooser; |
|
55 import org.netbeans.jemmy.DefaultCharBindingMap; |
|
56 import org.netbeans.jemmy.QueueTool; |
|
57 import org.netbeans.jemmy.TimeoutExpiredException; |
|
58 import org.netbeans.jemmy.Waitable; |
|
59 import org.netbeans.jemmy.Waiter; |
|
60 import org.netbeans.jemmy.drivers.scrolling.JSpinnerDriver; |
|
61 import org.netbeans.jemmy.image.StrictImageComparator; |
|
62 import org.netbeans.jemmy.operators.ComponentOperator; |
|
63 import org.netbeans.jemmy.operators.ContainerOperator; |
|
64 import org.netbeans.jemmy.operators.FrameOperator; |
|
65 import org.netbeans.jemmy.operators.JButtonOperator; |
|
66 import org.netbeans.jemmy.operators.JFrameOperator; |
|
67 import org.netbeans.jemmy.operators.JLabelOperator; |
|
68 import org.netbeans.jemmy.operators.Operator; |
|
69 import org.netbeans.jemmy.util.Dumper; |
|
70 import org.netbeans.jemmy.util.PNGEncoder; |
|
71 import 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 */ |
|
80 public 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 * Wraps the test code so that in case of any failure as much information as |
|
262 * possible is captured |
|
263 * |
|
264 * @param r test code Runnable |
|
265 * @throws Exception whatever exception the test may throw |
|
266 */ |
|
267 public static void captureDebugInfoOnFail(RunnableWithException r) throws Exception { |
|
268 // TODO: Remove this once https://bugs.openjdk.java.net/browse/JDK-8151671 is fixed |
|
269 try { |
|
270 r.run(); |
|
271 System.out.println("TEST PASSED"); |
|
272 } catch (Throwable t) { |
|
273 captureAll(); |
|
274 throw t; |
|
275 } |
|
276 } |
|
277 |
|
278 /** |
|
279 * This is a helper class which allows to catch throwables thrown in other |
|
280 * threads and throw them in the main test thread |
|
281 */ |
|
282 public static class MultiThreadedTryCatch { |
|
283 |
|
284 private final List<Throwable> throwables |
|
285 = Collections.synchronizedList(new ArrayList<>()); |
|
286 |
|
287 /** |
|
288 * Throws registered throwables. If the list of the registered |
|
289 * throwables is not empty, it re-throws the first throwable in the list |
|
290 * adding all others into its suppressed list. Can be used in any |
|
291 * thread. |
|
292 * |
|
293 * @throws Exception |
|
294 */ |
|
295 public void throwRegistered() throws Exception { |
|
296 Throwable root = null; |
|
297 synchronized (throwables) { |
|
298 if (!throwables.isEmpty()) { |
|
299 root = throwables.remove(0); |
|
300 while (!throwables.isEmpty()) { |
|
301 root.addSuppressed(throwables.remove(0)); |
|
302 } |
|
303 } |
|
304 } |
|
305 if (root != null) { |
|
306 if (root instanceof Error) { |
|
307 throw (Error) root; |
|
308 } else if (root instanceof Exception) { |
|
309 throw (Exception) root; |
|
310 } else { |
|
311 throw new AssertionError("Unexpected exception type: " + root.getClass() + " (" + root + ")"); |
|
312 } |
|
313 } |
|
314 } |
|
315 |
|
316 /** |
|
317 * Registers a throwable and adds it to the list of throwables. Can be |
|
318 * used in any thread. |
|
319 * |
|
320 * @param t |
|
321 */ |
|
322 public void register(Throwable t) { |
|
323 t.printStackTrace(); |
|
324 throwables.add(t); |
|
325 } |
|
326 |
|
327 /** |
|
328 * Registers a throwable and adds it as the first item of the list of |
|
329 * catched throwables. |
|
330 * |
|
331 * @param t |
|
332 */ |
|
333 public void registerRoot(Throwable t) { |
|
334 t.printStackTrace(); |
|
335 throwables.add(0, t); |
|
336 } |
|
337 } |
|
338 |
|
339 /** |
|
340 * Trying to capture as much information as possible. Currently it includes |
|
341 * full dump and a screenshot of the whole screen. |
|
342 */ |
|
343 public static void captureAll() { |
|
344 PNGEncoder.captureScreen("failure.png", PNGEncoder.COLOR_MODE); |
|
345 try { |
|
346 Dumper.dumpAll("dumpAll.xml"); |
|
347 } catch (FileNotFoundException ex) { |
|
348 Logger.getLogger(JemmyExt.class.getName()).log(Level.SEVERE, null, ex); |
|
349 } |
|
350 captureWindows(); |
|
351 } |
|
352 |
|
353 /** |
|
354 * Captures each showing window image using Window.paint() method. |
|
355 */ |
|
356 private static void captureWindows() { |
|
357 try { |
|
358 EventQueue.invokeAndWait(() -> { |
|
359 Window[] windows = Window.getWindows(); |
|
360 int index = 0; |
|
361 for (Window w : windows) { |
|
362 if (!w.isShowing()) { |
|
363 continue; |
|
364 } |
|
365 BufferedImage img = new BufferedImage(w.getWidth(), w.getHeight(), BufferedImage.TYPE_INT_ARGB); |
|
366 Graphics g = img.getGraphics(); |
|
367 w.paint(g); |
|
368 g.dispose(); |
|
369 |
|
370 try { |
|
371 ImageIO.write(img, "png", new File("window" + index++ + ".png")); |
|
372 } catch (IOException e) { |
|
373 e.printStackTrace(); |
|
374 } |
|
375 } |
|
376 }); |
|
377 } catch (InterruptedException | InvocationTargetException ex) { |
|
378 Logger.getLogger(JemmyExt.class.getName()).log(Level.SEVERE, null, ex); |
|
379 } |
|
380 } |
|
381 |
|
382 public static interface RunnableWithException { |
|
383 |
|
384 public void run() throws Exception; |
|
385 } |
|
386 |
|
387 public static void waitIsFocused(JFrameOperator jfo) { |
|
388 jfo.waitState(new ComponentChooser() { |
|
389 |
|
390 @Override |
|
391 public boolean checkComponent(Component comp) { |
|
392 return jfo.isFocused(); |
|
393 } |
|
394 |
|
395 @Override |
|
396 public String getDescription() { |
|
397 return "JFrame is focused"; |
|
398 } |
|
399 }); |
|
400 } |
|
401 |
|
402 public static int getJWindowCount() { |
|
403 return new QueueTool().invokeAndWait(new QueueTool.QueueAction<Integer>(null) { |
|
404 |
|
405 @Override |
|
406 public Integer launch() throws Exception { |
|
407 Window[] windows = Window.getWindows(); |
|
408 int windowCount = 0; |
|
409 for (Window w : windows) { |
|
410 if (w.getClass().equals(JWindow.class)) { |
|
411 windowCount++; |
|
412 } |
|
413 } |
|
414 return windowCount; |
|
415 } |
|
416 }); |
|
417 } |
|
418 |
|
419 public static JWindow getJWindow() { |
|
420 return getJWindow(0); |
|
421 } |
|
422 |
|
423 public static JWindow getJWindow(int index) { |
|
424 return new QueueTool().invokeAndWait(new QueueTool.QueueAction<JWindow>(null) { |
|
425 |
|
426 @Override |
|
427 public JWindow launch() throws Exception { |
|
428 Window[] windows = Window.getWindows(); |
|
429 int windowIndex = 0; |
|
430 for (Window w : windows) { |
|
431 if (w.getClass().equals(JWindow.class)) { |
|
432 if (windowIndex == index) { |
|
433 return (JWindow) w; |
|
434 } |
|
435 windowIndex++; |
|
436 } |
|
437 } |
|
438 return null; |
|
439 } |
|
440 }); |
|
441 } |
|
442 |
|
443 public static boolean isIconified(FrameOperator frameOperator) { |
|
444 return frameOperator.getQueueTool().invokeAndWait(new QueueTool.QueueAction<Boolean>("Frame is iconified") { |
|
445 |
|
446 @Override |
|
447 public Boolean launch() throws Exception { |
|
448 return (((Frame) frameOperator.getSource()).getState() & Frame.ICONIFIED) != 0; |
|
449 } |
|
450 }); |
|
451 } |
|
452 |
|
453 public static final Operator.DefaultStringComparator EXACT_STRING_COMPARATOR |
|
454 = new Operator.DefaultStringComparator(true, true); |
|
455 |
|
456 /** |
|
457 * Finds a label with the exact labelText and returns the operator for its |
|
458 * parent container. |
|
459 * |
|
460 * @param container |
|
461 * @param labelText |
|
462 * @return |
|
463 */ |
|
464 public static ContainerOperator<?> getLabeledContainerOperator(ContainerOperator<?> container, String labelText) { |
|
465 |
|
466 container.setComparator(EXACT_STRING_COMPARATOR); |
|
467 |
|
468 JLabelOperator jLabelOperator = new JLabelOperator(container, labelText); |
|
469 |
|
470 assert labelText.equals(jLabelOperator.getText()); |
|
471 |
|
472 return new ContainerOperator<>(jLabelOperator.getParent()); |
|
473 } |
|
474 |
|
475 /** |
|
476 * Finds a JPanel with exact title text. |
|
477 * |
|
478 * @param container |
|
479 * @param titleText |
|
480 * @return |
|
481 */ |
|
482 public static ContainerOperator<?> getBorderTitledJPanelOperator(ContainerOperator<?> container, String titleText) { |
|
483 return new ContainerOperator<>(container, new JPanelByBorderTitleFinder(titleText, EXACT_STRING_COMPARATOR)); |
|
484 } |
|
485 |
|
486 public static final QueueTool QUEUE_TOOL = new QueueTool(); |
|
487 |
|
488 /** |
|
489 * Allows to find JPanel by the title text in its border. |
|
490 */ |
|
491 public static class JPanelByBorderTitleFinder implements ComponentChooser { |
|
492 |
|
493 String titleText; |
|
494 Operator.StringComparator comparator; |
|
495 |
|
496 /** |
|
497 * @param titleText title text pattern |
|
498 * @param comparator specifies string comparison algorithm. |
|
499 */ |
|
500 public JPanelByBorderTitleFinder(String titleText, Operator.StringComparator comparator) { |
|
501 this.titleText = titleText; |
|
502 this.comparator = comparator; |
|
503 } |
|
504 |
|
505 /** |
|
506 * @param titleText title text pattern |
|
507 */ |
|
508 public JPanelByBorderTitleFinder(String titleText) { |
|
509 this(titleText, Operator.getDefaultStringComparator()); |
|
510 } |
|
511 |
|
512 @Override |
|
513 public boolean checkComponent(Component comp) { |
|
514 assert EventQueue.isDispatchThread(); |
|
515 if (comp instanceof JPanel) { |
|
516 return checkBorder(((JPanel) comp).getBorder()); |
|
517 } |
|
518 return false; |
|
519 } |
|
520 |
|
521 public boolean checkBorder(Border border) { |
|
522 if (border instanceof TitledBorder) { |
|
523 String title = ((TitledBorder) border).getTitle(); |
|
524 return comparator.equals(title, titleText); |
|
525 } else if (border instanceof CompoundBorder) { |
|
526 CompoundBorder compoundBorder = (CompoundBorder) border; |
|
527 return checkBorder(compoundBorder.getInsideBorder()) || checkBorder(compoundBorder.getOutsideBorder()); |
|
528 } else { |
|
529 return false; |
|
530 } |
|
531 } |
|
532 |
|
533 @Override |
|
534 public String getDescription() { |
|
535 return ("JPanel with border title text \"" + titleText + "\" with comparator " + comparator); |
|
536 } |
|
537 } |
|
538 |
|
539 public static class ByClassSimpleNameChooser implements ComponentChooser { |
|
540 |
|
541 private final String className; |
|
542 |
|
543 public ByClassSimpleNameChooser(String className) { |
|
544 this.className = className; |
|
545 } |
|
546 |
|
547 @Override |
|
548 public boolean checkComponent(Component comp) { |
|
549 return comp.getClass().getSimpleName().equals(className); |
|
550 } |
|
551 |
|
552 @Override |
|
553 public String getDescription() { |
|
554 return "Component with the simple class name of " + className; |
|
555 } |
|
556 |
|
557 } |
|
558 |
|
559 public static class ByClassChooser implements ComponentChooser { |
|
560 |
|
561 private final Class<?> clazz; |
|
562 |
|
563 public ByClassChooser(Class<?> clazz) { |
|
564 this.clazz = clazz; |
|
565 } |
|
566 |
|
567 @Override |
|
568 public boolean checkComponent(Component comp) { |
|
569 return comp.getClass().equals(clazz); |
|
570 } |
|
571 |
|
572 @Override |
|
573 public String getDescription() { |
|
574 return "Component with the class of " + clazz; |
|
575 } |
|
576 |
|
577 } |
|
578 |
|
579 public static class ByToolTipChooser implements ComponentChooser { |
|
580 |
|
581 private final String tooltip; |
|
582 |
|
583 public ByToolTipChooser(String tooltip) { |
|
584 if (tooltip == null) { |
|
585 throw new NullPointerException("Tooltip cannot be null"); |
|
586 } |
|
587 this.tooltip = tooltip; |
|
588 } |
|
589 |
|
590 @Override |
|
591 public boolean checkComponent(Component comp) { |
|
592 return (comp instanceof JComponent) |
|
593 ? tooltip.equals(((JComponent) comp).getToolTipText()) |
|
594 : false; |
|
595 } |
|
596 |
|
597 @Override |
|
598 public String getDescription() { |
|
599 return "JComponent with the tooltip '" + tooltip + "'"; |
|
600 } |
|
601 |
|
602 } |
|
603 |
|
604 @SuppressWarnings(value = "unchecked") |
|
605 public static <R, O extends Operator, S extends Component> R getUIValue(O operator, Function<S, R> getter) { |
|
606 return operator.getQueueTool().invokeSmoothly(new QueueTool.QueueAction<R>("getting UI value through the queue using " + getter) { |
|
607 |
|
608 @Override |
|
609 public R launch() throws Exception { |
|
610 return getter.apply((S) operator.getSource()); |
|
611 } |
|
612 }); |
|
613 } |
|
614 } |
|