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