blob: 8063c60c9861f1c58b65dbd87eeab3cf4d3acac6 [file] [log] [blame]
/*
* Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.jemmy2ext;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Window;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.IntStream;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JWindow;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.TitledBorder;
import org.netbeans.jemmy.ComponentChooser;
import org.netbeans.jemmy.DefaultCharBindingMap;
import org.netbeans.jemmy.QueueTool;
import org.netbeans.jemmy.TimeoutExpiredException;
import org.netbeans.jemmy.Waitable;
import org.netbeans.jemmy.Waiter;
import org.netbeans.jemmy.drivers.scrolling.JSpinnerDriver;
import org.netbeans.jemmy.image.StrictImageComparator;
import org.netbeans.jemmy.operators.ComponentOperator;
import org.netbeans.jemmy.operators.ContainerOperator;
import org.netbeans.jemmy.operators.FrameOperator;
import org.netbeans.jemmy.operators.JButtonOperator;
import org.netbeans.jemmy.operators.JFrameOperator;
import org.netbeans.jemmy.operators.JLabelOperator;
import org.netbeans.jemmy.operators.Operator;
import org.netbeans.jemmy.util.Dumper;
import org.netbeans.jemmy.util.PNGEncoder;
import static org.testng.AssertJUnit.*;
/**
* This class solves two tasks: 1. It adds functionality that is missing in
* Jemmy 2. It references all the Jemmy API that is needed by tests so that they
* can just @build JemmyExt class and do not worry about Jemmy
*
* @author akouznet
*/
public class JemmyExt {
/**
* Statically referencing all the classes that are needed by tests so that
* they're compiled by jtreg
*/
static final Class<?>[] DEPENDENCIES = {
JSpinnerDriver.class,
DefaultCharBindingMap.class
};
public static void assertNotBlack(BufferedImage image) {
int w = image.getWidth();
int h = image.getHeight();
try {
assertFalse("All pixels are not black", IntStream.range(0, w).parallel().allMatch(x
-> IntStream.range(0, h).allMatch(y -> (image.getRGB(x, y) & 0xffffff) == 0)
));
} catch (Throwable t) {
save(image, "allPixelsAreBlack.png");
throw t;
}
}
public static void waitArmed(JButtonOperator button) {
button.waitState(new ComponentChooser() {
@Override
public boolean checkComponent(Component comp) {
return isArmed(button);
}
@Override
public String getDescription() {
return "Button is armed";
}
});
}
public static boolean isArmed(JButtonOperator button) {
return button.getQueueTool().invokeSmoothly(new QueueTool.QueueAction<Boolean>("getModel().isArmed()") {
@Override
public Boolean launch() throws Exception {
return ((JButton) button.getSource()).getModel().isArmed();
}
});
}
public static void waitPressed(JButtonOperator button) {
button.waitState(new ComponentChooser() {
@Override
public boolean checkComponent(Component comp) {
return isPressed(button);
}
@Override
public String getDescription() {
return "Button is pressed";
}
});
}
public static boolean isPressed(JButtonOperator button) {
return button.getQueueTool().invokeSmoothly(new QueueTool.QueueAction<Boolean>("getModel().isPressed()") {
@Override
public Boolean launch() throws Exception {
return ((JButton) button.getSource()).getModel().isPressed();
}
});
}
public static void assertEquals(String string, StrictImageComparator comparator, BufferedImage expected, BufferedImage actual) {
try {
assertTrue(string, comparator.compare(expected, actual));
} catch (Error err) {
save(expected, "expected.png");
save(actual, "actual.png");
throw err;
}
}
public static void assertNotEquals(String string, StrictImageComparator comparator, BufferedImage notExpected, BufferedImage actual) {
try {
assertFalse(string, comparator.compare(notExpected, actual));
} catch (Error err) {
save(notExpected, "notExpected.png");
save(actual, "actual.png");
throw err;
}
}
public static void save(BufferedImage image, String filename) {
String filepath = filename;
try {
filepath = new File(filename).getCanonicalPath();
System.out.println("Saving screenshot to " + filepath);
BufferedOutputStream file = new BufferedOutputStream(new FileOutputStream(filepath));
new PNGEncoder(file, PNGEncoder.COLOR_MODE).encode(image);
} catch (IOException ioe) {
throw new RuntimeException("Failed to save image to " + filepath, ioe);
}
}
public static void waitImageIsStill(Robot rob, ComponentOperator operator) {
operator.waitState(new ComponentChooser() {
private BufferedImage previousImage = null;
private int index = 0;
private final StrictImageComparator sComparator = new StrictImageComparator();
@Override
public boolean checkComponent(Component comp) {
BufferedImage currentImage = capture(rob, operator);
save(currentImage, "waitImageIsStill" + index + ".png");
index++;
boolean compareResult = previousImage == null ? false : sComparator.compare(currentImage, previousImage);
previousImage = currentImage;
return compareResult;
}
@Override
public String getDescription() {
return "Image of " + operator + " is still";
}
});
}
private static class ThrowableHolder {
volatile Throwable t;
}
public static void waitFor(String description, RunnableWithException r) throws Exception {
Waiter<Boolean, ThrowableHolder> waiter = new Waiter<>(new Waitable<Boolean, ThrowableHolder>() {
@Override
public Boolean actionProduced(ThrowableHolder obj) {
try {
r.run();
return true;
} catch (Throwable t) {
obj.t = t;
return null;
}
}
@Override
public String getDescription() {
return description;
}
});
ThrowableHolder th = new ThrowableHolder();
try {
waiter.waitAction(th);
} catch (TimeoutExpiredException tee) {
Throwable t = th.t;
if (t != null) {
t.addSuppressed(tee);
if (t instanceof Exception) {
throw (Exception) t;
} else if (t instanceof Error) {
throw (Error) t;
} else if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new IllegalStateException("Unexpected exception type", t);
}
}
}
}
public static BufferedImage capture(Robot rob, ComponentOperator operator) {
Rectangle boundary = new Rectangle(operator.getLocationOnScreen(),
operator.getSize());
return rob.createScreenCapture(boundary);
}
/**
* Dispose all AWT/Swing windows causing event thread to stop
*/
public static void disposeAllWindows() {
System.out.println("disposeAllWindows");
try {
EventQueue.invokeAndWait(() -> {
Window[] windows = Window.getWindows();
for (Window w : windows) {
w.dispose();
}
});
} catch (InterruptedException | InvocationTargetException ex) {
Logger.getLogger(JemmyExt.class.getName()).log(Level.SEVERE, "Failed to dispose all windows", ex);
}
}
/**
* This is a helper class which allows to catch throwables thrown in other
* threads and throw them in the main test thread
*/
public static class MultiThreadedTryCatch {
private final List<Throwable> throwables
= Collections.synchronizedList(new ArrayList<>());
/**
* Throws registered throwables. If the list of the registered
* throwables is not empty, it re-throws the first throwable in the list
* adding all others into its suppressed list. Can be used in any
* thread.
*
* @throws Exception
*/
public void throwRegistered() throws Exception {
Throwable root = null;
synchronized (throwables) {
if (!throwables.isEmpty()) {
root = throwables.remove(0);
while (!throwables.isEmpty()) {
root.addSuppressed(throwables.remove(0));
}
}
}
if (root != null) {
if (root instanceof Error) {
throw (Error) root;
} else if (root instanceof Exception) {
throw (Exception) root;
} else {
throw new AssertionError("Unexpected exception type: " + root.getClass() + " (" + root + ")");
}
}
}
/**
* Registers a throwable and adds it to the list of throwables. Can be
* used in any thread.
*
* @param t
*/
public void register(Throwable t) {
t.printStackTrace();
throwables.add(t);
}
/**
* Registers a throwable and adds it as the first item of the list of
* catched throwables.
*
* @param t
*/
public void registerRoot(Throwable t) {
t.printStackTrace();
throwables.add(0, t);
}
}
/**
* Trying to capture as much information as possible. Currently it includes
* full dump and a screenshot of the whole screen.
*/
public static void captureAll() {
String lookAndFeelClassName = UIManager.getLookAndFeel().getClass().getSimpleName();
PNGEncoder.captureScreen("failure_" + lookAndFeelClassName + ".png", PNGEncoder.COLOR_MODE);
try {
Dumper.dumpAll("dumpAll_" + lookAndFeelClassName + ".xml");
} catch (FileNotFoundException ex) {
Logger.getLogger(JemmyExt.class.getName()).log(Level.SEVERE, null, ex);
}
captureWindows(lookAndFeelClassName);
}
/**
* Captures each showing window image using Window.paint() method.
* @param lookAndFeelClassName
*/
private static void captureWindows(String lookAndFeelClassName) {
try {
EventQueue.invokeAndWait(() -> {
Window[] windows = Window.getWindows();
int index = 0;
for (Window w : windows) {
if (!w.isShowing()) {
continue;
}
BufferedImage img = new BufferedImage(w.getWidth(), w.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics g = img.getGraphics();
w.paint(g);
g.dispose();
try {
ImageIO.write(img, "png", new File("window_" + lookAndFeelClassName
+ "_" + index++ + ".png"));
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (InterruptedException | InvocationTargetException ex) {
Logger.getLogger(JemmyExt.class.getName()).log(Level.SEVERE, null, ex);
}
}
public static interface RunnableWithException {
public void run() throws Exception;
}
public static void waitIsFocused(JFrameOperator jfo) {
jfo.waitState(new ComponentChooser() {
@Override
public boolean checkComponent(Component comp) {
return jfo.isFocused();
}
@Override
public String getDescription() {
return "JFrame is focused";
}
});
}
public static int getJWindowCount() {
return new QueueTool().invokeAndWait(new QueueTool.QueueAction<Integer>(null) {
@Override
public Integer launch() throws Exception {
Window[] windows = Window.getWindows();
int windowCount = 0;
for (Window w : windows) {
if (w.getClass().equals(JWindow.class) && ((JWindow)w).isShowing()) {
windowCount++;
}
}
return windowCount;
}
});
}
public static JWindow getJWindow() {
return getJWindow(0);
}
public static JWindow getJWindow(int index) {
return new QueueTool().invokeAndWait(new QueueTool.QueueAction<JWindow>(null) {
@Override
public JWindow launch() throws Exception {
Window[] windows = Window.getWindows();
int windowIndex = 0;
for (Window w : windows) {
if (w.getClass().equals(JWindow.class) && ((JWindow)w).isShowing()) {
if (windowIndex == index) {
return (JWindow) w;
}
windowIndex++;
}
}
return null;
}
});
}
public static boolean isIconified(FrameOperator frameOperator) {
return frameOperator.getQueueTool().invokeAndWait(new QueueTool.QueueAction<Boolean>("Frame is iconified") {
@Override
public Boolean launch() throws Exception {
return (((Frame) frameOperator.getSource()).getState() & Frame.ICONIFIED) != 0;
}
});
}
public static final Operator.DefaultStringComparator EXACT_STRING_COMPARATOR
= new Operator.DefaultStringComparator(true, true);
/**
* Finds a label with the exact labelText and returns the operator for its
* parent container.
*
* @param container
* @param labelText
* @return
*/
public static ContainerOperator<?> getLabeledContainerOperator(ContainerOperator<?> container, String labelText) {
container.setComparator(EXACT_STRING_COMPARATOR);
JLabelOperator jLabelOperator = new JLabelOperator(container, labelText);
assert labelText.equals(jLabelOperator.getText());
return new ContainerOperator<>(jLabelOperator.getParent());
}
/**
* Finds a JPanel with exact title text.
*
* @param container
* @param titleText
* @return
*/
public static ContainerOperator<?> getBorderTitledJPanelOperator(ContainerOperator<?> container, String titleText) {
return new ContainerOperator<>(container, new JPanelByBorderTitleFinder(titleText, EXACT_STRING_COMPARATOR));
}
public static final QueueTool QUEUE_TOOL = new QueueTool();
/**
* Allows to find JPanel by the title text in its border.
*/
public static class JPanelByBorderTitleFinder implements ComponentChooser {
String titleText;
Operator.StringComparator comparator;
/**
* @param titleText title text pattern
* @param comparator specifies string comparison algorithm.
*/
public JPanelByBorderTitleFinder(String titleText, Operator.StringComparator comparator) {
this.titleText = titleText;
this.comparator = comparator;
}
/**
* @param titleText title text pattern
*/
public JPanelByBorderTitleFinder(String titleText) {
this(titleText, Operator.getDefaultStringComparator());
}
@Override
public boolean checkComponent(Component comp) {
assert EventQueue.isDispatchThread();
if (comp instanceof JPanel) {
return checkBorder(((JPanel) comp).getBorder());
}
return false;
}
public boolean checkBorder(Border border) {
if (border instanceof TitledBorder) {
String title = ((TitledBorder) border).getTitle();
return comparator.equals(title, titleText);
} else if (border instanceof CompoundBorder) {
CompoundBorder compoundBorder = (CompoundBorder) border;
return checkBorder(compoundBorder.getInsideBorder()) || checkBorder(compoundBorder.getOutsideBorder());
} else {
return false;
}
}
@Override
public String getDescription() {
return ("JPanel with border title text \"" + titleText + "\" with comparator " + comparator);
}
}
public static class ByClassSimpleNameChooser implements ComponentChooser {
private final String className;
public ByClassSimpleNameChooser(String className) {
this.className = className;
}
@Override
public boolean checkComponent(Component comp) {
return comp.getClass().getSimpleName().equals(className);
}
@Override
public String getDescription() {
return "Component with the simple class name of " + className;
}
}
public static class ByClassChooser implements ComponentChooser {
private final Class<?> clazz;
public ByClassChooser(Class<?> clazz) {
this.clazz = clazz;
}
@Override
public boolean checkComponent(Component comp) {
return comp.getClass().equals(clazz) && comp.isShowing();
}
@Override
public String getDescription() {
return "Component with the class of " + clazz;
}
}
public static class ByToolTipChooser implements ComponentChooser {
private final String tooltip;
public ByToolTipChooser(String tooltip) {
if (tooltip == null) {
throw new NullPointerException("Tooltip cannot be null");
}
this.tooltip = tooltip;
}
@Override
public boolean checkComponent(Component comp) {
return (comp instanceof JComponent)
? tooltip.equals(((JComponent) comp).getToolTipText())
: false;
}
@Override
public String getDescription() {
return "JComponent with the tooltip '" + tooltip + "'";
}
}
@SuppressWarnings(value = "unchecked")
public static <R, O extends Operator, S extends Component> R getUIValue(O operator, Function<S, R> getter) {
return operator.getQueueTool().invokeSmoothly(new QueueTool.QueueAction<R>("getting UI value through the queue using " + getter) {
@Override
public R launch() throws Exception {
return getter.apply((S) operator.getSource());
}
});
}
}