/*==========================================================================*\ | $Id: GUITestCase.java,v 1.15 2011/06/09 15:29:58 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2007-2010 Virginia Tech | | This file is part of the Student-Library. | | The Student-Library is free software; you can redistribute it and/or | modify it under the terms of the GNU Lesser General Public License as | published by the Free Software Foundation; either version 3 of the | License, or (at your option) any later version. | | The Student-Library 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 Lesser General Public License for more details. | | You should have received a copy of the GNU Lesser General Public License | along with the Student-Library; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package student; import static student.testingsupport.ReflectionSupport.invoke; import abbot.finder.BasicFinder; import abbot.finder.ComponentFinder; import abbot.finder.ComponentNotFoundException; import abbot.finder.Hierarchy; import abbot.finder.Matcher; import abbot.finder.MultiMatcher; import abbot.finder.MultipleComponentsFoundException; import abbot.finder.TestHierarchy; import abbot.tester.ComponentLocation; import abbot.tester.ComponentTester; import abbot.tester.JComboBoxTester; import abbot.tester.JListTester; import abbot.tester.JMenuItemTester; import abbot.tester.JTextComponentTester; import abbot.tester.Robot; import abbot.tester.WindowTracker; import abbot.util.AWTFixtureHelper; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Frame; import java.awt.Point; import java.awt.Window; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.io.File; import java.io.PrintStream; import java.io.PrintWriter; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.swing.JButton; import javax.swing.JColorChooser; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JList; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.text.JTextComponent; import student.testingsupport.GUIFilter; //------------------------------------------------------------------------- /** * This class provides enhancements to {@link student.TestCase} to support * testing of custom Swing components, panels, and main programs * with graphical user interfaces (GUIs). GUI testing support is based * on abbot's testing infrastructure, and works equally well for * Swing and/or AWT components. * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.15 $, $Date: 2011/06/09 15:29:58 $ */ public class GUITestCase extends TestCase { //~ Constants ............................................................. /** * This field re-exports the <code>where</code> operator from * {@link student.testingsupport.GUIFilter} so that it is available * in test methods without requiring a static import. */ public final GUIFilter.Operator where = GUIFilter.ClientImports.where; //~ Instance/static variables ............................................. /** * Any member data derived from these classes will be automatically set * to <code>null</code> after the test has run. This enables GC of said * classes without GC of the test itself (the default JUnit runners never * release their references to the tests) or requiring explicit * <code>null</code>-setting in the {@link TestCase#tearDown()} method. */ protected static final Class<?>[] DISPOSE_CLASSES = { Component.class, ComponentTester.class }; private AWTFixtureHelper fixtureHelper; private Throwable edtException; private long edtExceptionTime; private ComponentFinder finder; private Hierarchy hierarchy; //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Creates a new GUITestCase object. */ public GUITestCase() { super(); } // ---------------------------------------------------------- /** * Creates a new GUITestCase object. * @param name The name of this test case. */ public GUITestCase(String name) { super(name); } //~ Public Methods ........................................................ // ---------------------------------------------------------- /** * Displays a specific component that you wish to test, such as a panel, * a custom text field, or some other component, wrapping it in a * {@link Frame}. The frame's size will be its preferred size. This * method will return when the enclosing {@link Frame} is showing and * ready for input. * @param component The component to display. * @return The newly created, visible frame containing the component. */ public Frame showInFrame(Component component) { return showInFrame(component, null); } // ---------------------------------------------------------- /** * Displays a specific component that you wish to test, such as a panel, * a custom text field, or some other component, wrapping it in a * {@link Frame} with the size you specify. This method will return when * the enclosing {@link Frame} is showing and ready for input. * @param component The component to display. * @param size The desired size of the enclosing frame, or * <code>null</code> to use the component's preferred size. * @return The newly created, visible frame containing the component */ public Frame showInFrame(Component component, Dimension size) { return fixtureHelper.showFrame(component, size, getName()); } // ---------------------------------------------------------- /** * Safely display a window with proper EDT synchronization. This method * blocks until the {@link Window} is showing and ready for input. * @param window The window to show. */ public void showWindow(Window window) { showWindow(window, null); } // ---------------------------------------------------------- /** * Safely display a window with proper EDT synchronization. This method * blocks until the {@link Window} is showing and ready for input. * @param window The window to show. * @param size The desired size of the enclosing frame, or * <code>null</code> to use the component's preferred size. */ public void showWindow(Window window, Dimension size) { fixtureHelper.showWindow(window, size, true); } // ---------------------------------------------------------- /** * Set any key modifiers for future events. This method's effects will be * automatically undone at the end of the test. * @param modifiers A mask indicating which modifier keys to use. * @param pressed Whether the modifiers should be in the pressed state. */ public void setModifiers(int modifiers, boolean pressed) { fixtureHelper.setModifiers(modifiers, pressed); } // ---------------------------------------------------------- /** * Hook into the custom test execution machinery provided by MixRunner * to ensure proper test harness setup and tear down that won't * likely be accidentally overridden by a derived class. * <p> * If any exceptions are thrown on the event dispatch thread, they count * as errors. They will not, however supersede any failures/errors * thrown by the test itself unless thrown prior to the main test * failure. * * @param statement The (JUnit4-style) test method to be executed. * * @throws Throwable If any exception occurs in the test case or on the * event dispatch thread. */ protected void runTestMethod(org.junit.runners.model.Statement statement) throws Throwable { if (Boolean.getBoolean("abbot.skip_ui_tests")) { return; } Throwable exception = null; long exceptionTime = -1; try { try { fixtureSetUp(); statement.evaluate(); } catch (Throwable e) { exception = e; } finally { try { fixtureTearDown(); } catch (Throwable tearingDown) { if (exception == null) { exception = tearingDown; } } } if (exception != null) { throw exception; } } catch (Throwable e) { exceptionTime = System.currentTimeMillis(); exception = e; } finally { // Cf. StepRunner.runStep() // Any EDT exception which occurred *prior* to when the // exception on the main thread was thrown should be used // instead. if (edtException != null && (exception == null || edtExceptionTime < exceptionTime)) { exception = new EventDispatchException(edtException); } } if (exception != null) { throw exception; } } public void runBare() throws Throwable { if (Boolean.getBoolean("abbot.skip_ui_tests")) { return; } Throwable exception = null; long exceptionTime = -1; try { try { fixtureSetUp(); super.runBare(); } catch (Throwable e) { exception = e; } finally { try { fixtureTearDown(); } catch (Throwable tearingDown) { if (exception == null) { exception = tearingDown; } } } if (exception != null) { throw exception; } } catch (Throwable e) { exceptionTime = System.currentTimeMillis(); exception = e; } finally { // Cf. StepRunner.runStep() // Any EDT exception which occurred *prior* to when the // exception on the main thread was thrown should be used // instead. if (edtException != null && (exception == null || edtExceptionTime < exceptionTime)) { exception = new EventDispatchException(edtException); } } if (exception != null) { throw exception; } } // ---------------------------------------------------------- /** * The "not" operator for negating an existing filter, when the not * operation is at the very beginning of the expression, re-exported * from {@link student.testingsupport.GUIFilter} so that it is * available in test methods without requiring a static import. This * method is designed to be used in expressions like * <code>not(where.enabledIs(true).or.hasFocusIs(true))</code>. * * @param otherFilter The filter to negate * @return A new filter that represents a combination of the left * filter with "NOT otherFilter". */ public static GUIFilter not(final GUIFilter otherFilter) { return GUIFilter.ClientImports.not(otherFilter); } // ---------------------------------------------------------- /** * Look up a component in the GUI being tested by specifying its class. * This method expects the given class to identify a unique component, * meaning that there should only be one instance of the given class * in the entire GUI. This can be useful if, for example, you are * trying to retrieve a custom panel object created from a class you * only instantiate once. * <p> * If no matching component exists, the test case will fail with an * appropriate message. If more than one matching component exists, * the test case will fail with an appropriate message. * test case failure results). * @param <T> This method is a template method, and the type T used for * the return value is implicitly deduced from the provided * argument <code>type</code>. * @param type The type (class) of the component you wish to retrieve, and * also the way you specify the return type of this method. * @return The single component of the desired type that was found * (otherwise, a test case failure results). * @see #getFirstComponentMatching(Class) * @see #getAllComponentsMatching(Class) */ public <T extends Component> T getComponent(Class<T> type) { @SuppressWarnings("unchecked") T result = (T)getComponent(where.typeIs(type)); return result; } // ---------------------------------------------------------- /** * Look up a component in the GUI being tested by specifying its class * and its name (as returned by the component's * {@link Component#getName() getName()} method). * This method expects the given class and name together to identify a * unique component, meaning that there should only be one instance of * the given class with the given name in the entire GUI. Normally, * that will always be the case, since names are used as unique * identifiers for testing purposes. * <p> * If no matching component exists, the test case will fail with an * appropriate message. If more than one matching component exists, * the test case will fail with an appropriate message. * test case failure results). * @param <T> This method is a template method, and the type T used for * the return value is implicitly deduced from the provided * argument <code>type</code>. * @param type The type (class) of the component you wish to retrieve, and * also the way you specify the return type of this method. * @param name The name of the desired component * @return The single component matching the criteria specified * (otherwise, a test case failure results). */ public <T extends Component> T getComponent(Class<T> type, String name) { @SuppressWarnings("unchecked") GUIFilter nameFilter = where.typeIs(type).and.nameIs(name); T result = null; List<Component> results = getAllComponentsMatching(nameFilter); if(results.size() == 0) { GUIFilter textFilter = where.typeIs(type).and.textIs(name); results = getAllComponentsMatching(textFilter); } if(results.size() == 1) { result = (T)results.get(0); } else if(results.size() > 1) { fail("Found " + results.size() + " components matching: " + nameFilter); } else fail("Cannot find component matching: " + nameFilter); return result; } // ---------------------------------------------------------- /** * Look up a component in the GUI being tested by specifying its class * and a {@link GUIFilter}. * This method expects exactly one component to match your criteria. * If no matching component exists, the test case will fail with an * appropriate message. If more than one matching component exists, * the test case will fail with an appropriate message. * test case failure results). * @param <T> This method is a template method, and the type T used for * the return value is implicitly deduced from the provided * argument <code>type</code>. * @param type The type (class) of the component you wish to retrieve, and * also the way you specify the return type of this method. * @param filter The search criteria. * @return The single component matching the criteria specified * (otherwise, a test case failure results). * @see #getFirstComponentMatching(Class,GUIFilter) * @see #getAllComponentsMatching(Class,GUIFilter) */ public <T extends Component> T getComponent(Class<T> type, GUIFilter filter) { @SuppressWarnings("unchecked") T result = (T)getComponent(where.typeIs(type).and(filter)); return result; } // ---------------------------------------------------------- /** * Look up a component in the GUI being tested, using a filter to * specify which component you want. This method is more general * than {@link #getComponent(Class,GUIFilter)}, since no class needs to be * specified, but that also means the return type is less specific * (it is always <code>Component</code>). * This method expects the given filter * to identify a unique component. If no matching component exists, * the test case will fail with an appropriate message. If more than * one matching component exists, the test case will fail with an * appropriate message. * @param filter The search criteria. * @return The single component matching the provided filter (otherwise, a * test case failure results). * @see #getFirstComponentMatching(GUIFilter) * @see #getAllComponentsMatching(GUIFilter) */ public Component getComponent(GUIFilter filter) { Component result = null; try { result = getFinder().find(filter2matcher(filter)); } catch (ComponentNotFoundException e) { fail("Cannot find component matching: " + filter); } catch (MultipleComponentsFoundException e) { Component[] comps = e.getComponents(); int visibleCount = 0; for (Component c: comps) { if(c.isVisible()) { visibleCount++; result = c; } } if (visibleCount != 1) { fail("Found " + e.getComponents().length + " components matching: " + filter); } } return result; } // ---------------------------------------------------------- /** * Look up a component in the GUI being tested by specifying its class. * This method expects the given class to identify at least one such * component. If no matching component exists, the test case will fail * with an appropriate message. If more than one matching component * exists, the first one found will be returned (although client code * should not expect a specific search order). * @param <T> This method is a template method, and the type T used for * the return value is implicitly deduced from the provided * argument <code>type</code>. * @param type The type (class) of the component you wish to retrieve, and * also the way you specify the return type of this method. * @return The first component of the desired type that was found * (a test case failure results if there are none). * @see #getComponent(Class) * @see #getAllComponentsMatching(Class) */ public <T extends Component> T getFirstComponentMatching(Class<T> type) { @SuppressWarnings("unchecked") T result = (T)getFirstComponentMatching(where.typeIs(type)); return result; } // ---------------------------------------------------------- /** * Look up a component in the GUI being tested by specifying its class * and a {@link GUIFilter}. * This method expects the given criteria to identify at least one such * component. If no matching component exists, the test case will fail * with an appropriate message. If more than one matching component * exists, the first one found will be returned (although client code * should not expect a specific search order). * @param <T> This method is a template method, and the type T used for * the return value is implicitly deduced from the provided * argument <code>type</code>. * @param type The type (class) of the component you wish to retrieve, and * also the way you specify the return type of this method. * @param filter The search criteria. * @return The first component that was found matching the criteria * specified (a test case failure results if there are none). * @see #getComponent(Class,GUIFilter) * @see #getAllComponentsMatching(Class,GUIFilter) */ public <T extends Component> T getFirstComponentMatching( Class<T> type, GUIFilter filter) { @SuppressWarnings("unchecked") T result = (T)getFirstComponentMatching(where.typeIs(type).and(filter)); return result; } // ---------------------------------------------------------- /** * Look up a component in the GUI being tested by specifying * a {@link GUIFilter}. This method is more general * than {@link #getFirstComponentMatching(Class,GUIFilter)}, since no * class needs to be specified, but that also means the return type * is less specific (it is always <code>Component</code>). * This method expects the given criteria to identify at least one such * component. If no matching component exists, the test case will fail * with an appropriate message. If more than one matching component * exists, the first one found will be returned (although client code * should not expect a specific search order). * @param filter The search criteria. * @return The first component that was found matching the criteria * specified (a test case failure results if there are none). * @see #getComponent(GUIFilter) * @see #getAllComponentsMatching(GUIFilter) */ public Component getFirstComponentMatching(GUIFilter filter) { Component result = null; try { result = getFinder().find(filter2matcher(filter)); } catch (ComponentNotFoundException e) { fail("Cannot find component matching: " + filter); } catch (MultipleComponentsFoundException e) { result = e.getComponents()[0]; } return result; } // ---------------------------------------------------------- /** * Look up all components in the GUI being tested by specifying their * class. All matching objects are returned in a list. * @param <T> This method is a template method, and the type T used as * the <code>List</code> element type in * the return value is implicitly deduced from the provided * argument <code>type</code>. * @param type The type (class) of the components you wish to retrieve, * and also the way you specify the type of elements in * the list returned by this method. * @return A list of all components of the desired type that were found. * This will be an empty list (not null) if no matching components * are found. * @see #getComponent(Class) * @see #getFirstComponentMatching(Class) */ public <T extends Component> List<T> getAllComponentsMatching( Class<T> type) { @SuppressWarnings("unchecked") List<T> result = (List<T>)getAllComponentsMatching(where.typeIs(type)); return result; } // ---------------------------------------------------------- /** * Look up all components in the GUI being tested by specifying their * class and a {@link GUIFilter}. All matching objects are returned in * a list. * @param <T> This method is a template method, and the type T used as * the <code>List</code> element type in * the return value is implicitly deduced from the provided * argument <code>type</code>. * @param type The type (class) of the components you wish to retrieve, * and also the way you specify the type of elements in * the list returned by this method. * @param filter The search criteria. * @return A list of all components found matching the criteria specified. * This will be an empty list (not null) if no matching components * are found. * @see #getComponent(Class,GUIFilter) * @see #getAllComponentsMatching(Class,GUIFilter) */ public <T extends Component> List<T> getAllComponentsMatching( Class<T> type, GUIFilter filter) { @SuppressWarnings("unchecked") List<T> result = (List<T>)getAllComponentsMatching( where.typeIs(type).and(filter)); return result; } // ---------------------------------------------------------- /** * Look up all components in the GUI being tested by specifying * a {@link GUIFilter}. * All matching objects are returned in a list. * This method is more general than * {@link #getAllComponentsMatching(Class,GUIFilter)}, since no * class needs to be specified, but that also means the return type * is less specific (it is always <code>List<Component></code>). * @param filter The search criteria. * @return A list of all components found matching the criteria specified. * This will be an empty list (not null) if no matching components * are found. * @see #getComponent(GUIFilter) * @see #getAllComponentsMatching(GUIFilter) */ public List<Component> getAllComponentsMatching(GUIFilter filter) { List<Component> result = null; try { Component single = getFinder().find(filter2matcher(filter)); result = new ArrayList<Component>(1); result.add(single); } catch (ComponentNotFoundException e) { result = new ArrayList<Component>(); } catch (MultipleComponentsFoundException e) { result = Arrays.asList(e.getComponents()); } return result; } //~ Basic Component Action Methods ........................................ // ---------------------------------------------------------- /** * Left-click on the center of the component (mouse button 1 press and * release). * @param component The component to click on. */ public void click(Component component) { if(switchFocus) focus(component); ComponentTester.getTester(component).actionClick(component); } // ---------------------------------------------------------- /** * Left-click at the given location on a component (mouse button 1 * press and release). * Coordinates are specified relative to the component being clicked. * @param component The component to click on. * @param x The x-coordinate of the click location. * @param y The y-coordinate of the click location. */ public void click(Component component, int x, int y) { if(switchFocus) focus(component); ComponentTester.getTester(component).actionClick(component, x, y); } // ---------------------------------------------------------- /** * Left-double-click on the center of the component (mouse button 1 * press and release twice). * @param component The component to click on. */ public void doubleClick(Component component) { if(switchFocus) focus(component); ComponentTester.getTester(component).actionClick( component, new ComponentLocation(), InputEvent.BUTTON1_MASK, 2); } // ---------------------------------------------------------- /** * Left-double-click at the given location on a component (mouse button 1 * press and release twice). * Coordinates are specified relative to the component being clicked. * @param component The component to click on. * @param x The x-coordinate of the click location. * @param y The y-coordinate of the click location. */ public void doubleClick(Component component, int x, int y) { if(switchFocus) focus(component); click(component, x, y, InputEvent.BUTTON1_MASK, 2); } // ---------------------------------------------------------- /** * Right-click on the center of the component (mouse button 3 press * and release). * @param component The component to click on. */ public void rightClick(Component component) { if(switchFocus) focus(component); ComponentTester.getTester(component).actionClick( component, new ComponentLocation(), InputEvent.BUTTON3_MASK); } // ---------------------------------------------------------- /** * Right-click at the given location on a component (mouse button 3 * press and release). * Coordinates are specified relative to the component being clicked. * @param component The component to click on. * @param x The x-coordinate of the click location. * @param y The y-coordinate of the click location. */ public void rightClick(Component component, int x, int y) { if(switchFocus) focus(component); click(component, x, y, InputEvent.BUTTON3_MASK, 1); } // ---------------------------------------------------------- /** * Click on a component, specifying the location, specific mouse buttons, * and click count. * Coordinates are specified relative to the component being clicked. * @param component The component to click on. * @param x The x-coordinate of the click location. * @param y The y-coordinate of the click location. * @param buttons The button mask indicating which button(s) are being * clicked. Use {@link InputEvent} constants (just like * in a mouse listener) to identify which button(s) to * click simultaneously. * @param count The number of clicks for this event * (single, double, more ...). */ public void click(Component component, int x, int y, int buttons, int count) { if(switchFocus) focus(component); ComponentTester.getTester(component).actionClick( component, x, y, buttons, count); } // ---------------------------------------------------------- /** * Press the left mouse button at the given location on a component * (mouse button 1). * Coordinates are specified relative to the component being clicked. * @param component The component to press the mouse button on. * @param x The x-coordinate of the click location. * @param y The y-coordinate of the click location. */ public void mousePress(Component component, int x, int y) { ComponentTester.getTester(component).actionMousePress( component, new ComponentLocation(new Point(x, y))); } // ---------------------------------------------------------- /** * Press one or more mouse buttons at the given location on a component. * Coordinates are specified relative to the component being clicked. * @param component The component to press the mouse button on. * @param x The x-coordinate of the click location. * @param y The y-coordinate of the click location. * @param buttons The button mask indicating which button(s) are being * clicked. Use {@link InputEvent} constants (just like * in a mouse listener) to identify which button(s) to * click simultaneously. */ public void mousePress(Component component, int x, int y, int buttons) { ComponentTester.getTester(component).actionMousePress( component, new ComponentLocation(new Point(x, y)), buttons); } // ---------------------------------------------------------- /** * Release any currently held mouse buttons. */ public void mouseRelease() { ComponentTester.getTester(Component.class).actionMouseRelease(); } // ---------------------------------------------------------- /** * Move the mouse pointer to the given location on the specified * component. Note that a <code>mouseEntered</code> event will be * automatically generated as well, if the mouse was not already * over the specified component before the move. Further, if the mouse * is already inside the component and the coordinates specified lie * outside it, a <code>mouseExited</code> event will be generated * automatically. * @param component The component to move the mouse button on. * @param x The x-coordinate of the destination. * @param y The y-coordinate of the destination. */ public void mouseMove(Component component, int x, int y) { ComponentTester.getTester(component).actionMouseMove( component, new ComponentLocation(new Point(x, y))); } // ---------------------------------------------------------- /** * Move the mouse pointer out of the given component. This is achieved * by moving the mouse to (width + 1, height + 1) relative to the * component, forcing a <code>mouseExited</code> event to be generated. * This might also cause a <code>mouseEntered</code> event in a surrounding * or neighboring component--use <code>mouseMove()</code> with other * specific coordinates if you want a different effect. * @param component The component to exit from. */ public void mouseExit(Component component) { mouseMove( component, component.getWidth() + 1, component.getHeight() + 1); } // ---------------------------------------------------------- /** * Initiate a drag action at a specific location on a component. * @param component The component to begin the drag from * @param x The x-coordinate for the mouse press that initiates * the drag, which is specified relative to the * <code>component</code>. * @param y The y-coordinate for the mouse press that initiates * the drag, which is specified relative to the * <code>component</code>. */ public void mouseDragFrom(Component component, int x, int y) { ComponentTester.getTester(component).actionDrag( component, new ComponentLocation(new Point(x, y))); } // ---------------------------------------------------------- /** * Initiate a drag action at a specific location on a component, with * a specified mouse button. * @param component The component to begin the drag from * @param x The x-coordinate for the mouse press that initiates * the drag, which is specified relative to the * <code>component</code>. * @param y The y-coordinate for the mouse press that initiates * the drag, which is specified relative to the * <code>component</code>. * @param buttons The button mask indicating which button(s) are * pressed to initiate this mouse drag action. Use * {@link InputEvent} constants (just like * in a mouse listener) to identify which button(s) to * press simultaneously. */ public void mouseDragFrom(Component component, int x, int y, int buttons) { ComponentTester.getTester(component).actionDrag( component, new ComponentLocation(new Point(x, y)), buttons); } // ---------------------------------------------------------- /** * Drag the currently dragged object over the given location on * the specified component. Use this method to simulate intermediate * parts of a drag--that is, movements after the drag has started * (via <code>startDragFrom()</code>) and before the "drop" (mouse * release). * @param component The component to drag over. * @param x The x-coordinate of the location to drag over, * relative to the given <code>component</code>. * @param y The y-coordinate of the location to drag over, * relative to the given <code>component</code>. */ public void mouseDragOver(Component component, int x, int y) { ComponentTester.getTester(component).actionDragOver( component, new ComponentLocation(new Point(x, y))); } // ---------------------------------------------------------- /** * End a drag operation currently in progress by "dropping" at the * in the center of the specified component. This operation presumes * you have already started dragging using <code>startDragFrom()</code>. * Calling this method implicitly causes a <code>mouseMoved</code> * event to get to the center of the given component, followed by a * mouse release of whatever mouse buttons are currently pressed as * part of the drag operation that was in progress. * @param destination The component to drop on. */ public void mouseDropOn(Component destination) { ComponentTester.getTester(destination).actionDrop(destination); } // ---------------------------------------------------------- /** * End a drag operation currently in progress by "dropping" at the * specified coordinates on the given component. This operation presumes * you have already started dragging using <code>startDragFrom()</code>. * Calling this method implicitly causes a <code>mouseMoved</code> * event to get to the specified coordinates, followed by a * mouse release of whatever mouse buttons are currently pressed as * part of the drag operation that was in progress. * @param destination The component to drop on. * @param x The x-coordinate of the drop location, * relative to the given <code>destination</code>. * @param y The y-coordinate of the drop location, * relative to the given <code>destination</code>. */ public void mouseDropOn(Component destination, int x, int y) { ComponentTester.getTester(destination).actionDrop(destination, x, y); } // ---------------------------------------------------------- /** * Give the focus to a specific component. * @param component The component to receive focus. */ public void focus(Component component) { ComponentTester.getTester(component).actionFocus(component); } // ---------------------------------------------------------- /** * Type the given text into the given component, replacing any * existing text already there. If the empty string or * <code>null</code> is given, then this method * simply removes all existing text. * @param component The component to enter text on. * @param text The text to enter on the component. */ public void enterText(JTextComponent component, String text) { JTextComponentTester tester = (JTextComponentTester)ComponentTester.getTester(component); tester.actionEnterText(component, text); } // ---------------------------------------------------------- /** * Send a single keystroke to a component. A keystroke consists of * a key press/key release, with no modifiers. * @param component The component that will receive the keystroke--focus * will be automatically given to this component first, * before the keystroke. * @param keyCode The keystroke to send. Use the * {@link KeyEvent} <code>VK_*</code> constants. */ public void keyStroke(Component component, int keyCode) { ComponentTester.getTester(component) .actionKeyStroke(component, keyCode); } // ---------------------------------------------------------- /** * Send a single keystroke to a component. A keystroke consists of * a key press/key release, with the specified modifiers. * @param component The component that will receive the keystroke--focus * will be automatically given to this component first, * before the keystroke. * @param keyCode The keystroke to send. Use the * {@link KeyEvent} <code>VK_*</code> constants. * @param modifiers The keyboard modifier keys to press simultaneously * (e.g., shift, control, etc.). For modifiers, * use the {@link InputEvent} modifier mask constants. */ public void keyStroke(Component component, int keyCode, int modifiers) { ComponentTester.getTester(component) .actionKeyStroke(component, keyCode, modifiers); } // ---------------------------------------------------------- /** * Send a single key press (key down action) to a component. * @param component The component that will receive the key press--focus * will be automatically given to this component first, * before the key is pressed. * @param keyCode The key to press. Use the * {@link KeyEvent} <code>VK_*</code> constants. */ public void keyPress(Component component, int keyCode) { ComponentTester.getTester(component) .actionKeyPress(component, keyCode); } // ---------------------------------------------------------- /** * Send a single key release (key up action) to a component. * @param component The component that will receive the key release--focus * will be automatically given to this component first, * before the key is released. * @param keyCode The key to release. Use the * {@link KeyEvent} <code>VK_*</code> constants. */ public void keyRelease(Component component, int keyCode) { ComponentTester.getTester(component) .actionKeyRelease(component, keyCode); } // ---------------------------------------------------------- /** * Type a sequence of characters on the given component, where * the contents are specified as a string. This method * will send the events required to generate the given string on * the given component. * @param component The component to type on (or to). It will receive * focus first, if necessary. * @param sequence The content to type on the component. */ public void keyString(Component component, String sequence) { ComponentTester.getTester(component) .actionKeyString(component, sequence); } // ---------------------------------------------------------- /** * Select a specific item in a combo box or selection list. * @param component The combo box to select from. * @param item The item to select. */ public void selectItem(JComboBox component, String item) { JComboBoxTester tester = (JComboBoxTester)ComponentTester.getTester(component); tester.actionSelectItem(component, item); } // ---------------------------------------------------------- /** * Select a specific item in a selection list. * @param component The list to select from. * @param item The item to select. */ public void selectItem(JList component, String item) { JListTester tester = (JListTester)ComponentTester.getTester(component); tester.actionSelectItem(component, item); } // ---------------------------------------------------------- /** * Get the contents of a combo box, in the form of a list of strings. * @param component The combo box. * @return The component's contents as a list (never null). */ public List<String> getContents(JComboBox component) { JComboBoxTester tester = (JComboBoxTester)ComponentTester.getTester(component); String[] result = tester.getContents(component); if (result == null) { return new ArrayList<String>(); } else { return Arrays.asList(result); } } // ---------------------------------------------------------- /** * Get the contents of a list control, in the form of a list of strings. * @param component The list. * @return The component's contents as a list (never null). */ public List<String> getContents(JList component) { JListTester tester = (JListTester)ComponentTester.getTester(component); String[] result = tester.getContents(component); if (result == null) { return new ArrayList<String>(); } else { return Arrays.asList(result); } } // ---------------------------------------------------------- /** * Select a given menu item. * @param item The menu item to select. */ public void selectMenuItem(JMenuItem item) { JMenuItemTester tester = (JMenuItemTester)ComponentTester.getTester(item); tester.actionSelectMenuItem(item); } // ---------------------------------------------------------- /** * Get the hierarchical path to the given component, starting from the * outermost component in which it is contained, * e.g., "JFrame:JRootPane:JPanel:JButton". * @param component The component * @return A string representation of the given component's location * in the component hierarchy. */ public String getPathFor(Component component) { return Robot.toHierarchyPath(component); } // ---------------------------------------------------------- /** * A shorter, simpler way to get a human-readable representation of a * component. The <code>toString()</code> method on {@link Component} * prints a huge amount of information, and ironically often makes it * difficult to understand exactly which component you've printed. * This version of <code>toString()</code> prints only the component's * class, name, and hash code. * @param c The component * @return A string representation of the component of the form * <code>JButton[button1, 6546787]</code>. */ public String toString(Component c) { return c.getClass().getSimpleName() + "[" + c.getName() + ", " + c.hashCode() + "]"; } // ---------------------------------------------------------- /** * Get a human-reable representation of an array of components using * {@link #toString(Component)}. * @param components The array * @return A string representation of the array of the form * <code>(JButton[button1, 6546787], JTextField[input, 6881863])</code>. */ public String toString(Component[] components) { String result = "("; for (int i = 0; i < components.length; i++) { if (i != 0) { result += ", "; } result += toString(components[i]); } return result + ")"; } // ---------------------------------------------------------- /** * Get a human-readable representation of a List of components using * {@link #toString(Component)}. * @param <T> This method is a template method, and the type T is * implicitly deduced from the type of elements in the * provided <code>List</code> argument. It represents the * specific subtype of <code>Component</code> in the * <code>List</code>. * @param components The list * @return A string representation of the list of the form * <code>(JButton[button1, 6546787], JTextField[input, 6881863])</code>. */ public <T extends Component> String toString(List<T> components) { String result = "("; for (int i = 0; i < components.size(); i++) { if (i != 0) { result += ", "; } result += toString(components.get(i)); } return result + ")"; } // ---------------------------------------------------------- /** * Call a method that involves window-based I/O, so that the * method is executed on the GUI event thread. This version is * intended for calling void methods that do not return any results. * @param reciever The object to which the method belongs. * @param methodName The name of the method to call. * @param params A list of zero or more parameters to pass to the method. */ public void callGUIIOMethod( final Object reciever, final String methodName, final Object ... params) { callGUIIOMethod(new Runnable() { public void run() { invoke(reciever, methodName, params); } }); } // ---------------------------------------------------------- /** * Call a method that involves window-based I/O, so that the * method is executed on the GUI event thread, and return its value. * This version is intended for calling methods that have a non-void * return type. * @param <T> This method is a template method, and the type T is * implicitly deduced from the <code>returnType</code> * argument. * @param receiver The object to which the method belongs. * @param returnType The expected type of the method's return value. * @param methodName The name of the method to call. * @param params A list of zero or more parameters to pass to the method. * @return The return value of the method. */ public <T> T callGUIIOMethod( final Object receiver, final Class<T> returnType, final String methodName, final Object ... params) { class RunnableWithResult implements Runnable { private T result; public void run() { result = invoke(receiver, returnType, methodName, params); } public T getResult() { return result; } } RunnableWithResult runnable = new RunnableWithResult(); callGUIIOMethod(runnable); return runnable.getResult(); } // ---------------------------------------------------------- /** * A more primitive version of <code>callGUIIOMethod()</code> that * takes a {@link Runnable} instead of a receiver, method name, and * parameters. Students should use one of the other versions instead. * @param r The runnable to invoke. */ public void callGUIIOMethod(Runnable r) { SwingUtilities.invokeLater(r); getRobot().waitForIdle(); } // ---------------------------------------------------------- /** * Assuming that a JFileChoosers is currently open, selects the given * file and closes the JFileChooser * @param fileName the name of the file to choose */ public void selectFileInChooser(String fileName) { final JFileChooser chooser = getComponent(JFileChooser.class); final File file = new File(fileName); callGUIIOMethod(new Runnable() { public void run() { chooser.setSelectedFile(file); chooser.approveSelection(); } }); } /** * Assuming that a JFileChoosers is currently open, selects the given * files and closes the JFileChooser * @param fileName the name of the file to choose */ public void selectFilesInChooser(String ... files) { final JFileChooser chooser = getComponent(JFileChooser.class); final File[] chooserFiles = new File[files.length]; for(int i = 0; i < files.length; i++) chooserFiles[i] = new File(files[i]); callGUIIOMethod(new Runnable() { public void run() { chooser.setSelectedFiles(chooserFiles); chooser.approveSelection(); } }); } // ---------------------------------------------------------- /** * Assuming that a JColorChooser is currently open, chooses the given * color and closes the JColorChooser * @param color the color to choose in the JColorChooser */ public void selectColorInChooser(final Color color) { // get the JColorChooser itself final JColorChooser chooser = getComponent(JColorChooser.class); // the button panel is actually a sibling component of the color // chooser, so first get the color chooser's parent JPanel chooserParent = (JPanel)chooser.getParent(); // then find the button panel by using chooserParent JPanel buttonPanel = getComponent(JPanel.class, where.parentIs(chooserParent)); // finally, find the button labeled "OK" on the buttonPanel final JButton okButton = getComponent(JButton.class, where.textIs("OK").and.parentIs(buttonPanel)); callGUIIOMethod(new Runnable() { public void run() { chooser.setColor(color); okButton.doClick(); } }); } /** * Assuming that a Dialog of some sort is currently * open, this method clicks the specified button. * @param buttonText The text on the button to click. */ public void clickDialogButton(final String buttonText) { try { JDialog d = (JDialog)getFinder().find(filter2matcher(where.typeIs(JDialog.class))); final JButton b = getComponent(JButton.class, where.textIs(buttonText).and.ancestorIs(d)); callGUIIOMethod(new Runnable() { public void run() { b.doClick(); } }); } catch ( ComponentNotFoundException e ) { JPanel panel = getComponent(JPanel.class, where.nameIs("OptionPane.buttonArea")); final JButton b = getComponent(JButton.class, where.textIs(buttonText).and.ancestorIs(panel)); callGUIIOMethod(new Runnable() { public void run() { b.doClick(); } }); } catch ( MultipleComponentsFoundException e ) { fail("Found " + e.getComponents().length + " components matching search criteria."); } } // ---------------------------------------------------------- /** * Assuming a {@link JOptionPane} is currently open and waiting for * input, this method enters the given text into the pane's text * field and then closes the JOptionPane. * @param text The text to enter into the JOptionPane. */ public void setInputDialogText(final String text) { // grab the text field on the JOptionPane by name JTextField f = getComponent( JTextField.class, where.nameIs("OptionPane.textField")); f.setText(text); // grab the panel that holds the buttons by name // we need this so we can use the parentIs() filter to find the button // in case there are other buttons with text "OK" JPanel panel = getComponent( JPanel.class, where.nameIs("OptionPane.buttonArea")); click(getComponent( JButton.class, where.textIs("OK").and.parentIs(panel))); } public void setSwitchFocus(boolean switchFocus) { this.switchFocus = switchFocus; } //~ Protected Methods/Declarations ........................................ // ---------------------------------------------------------- /** * Return an Abbot {@link abbot.tester.Robot} for basic event * generation. * @return the robot used for this test */ protected Robot getRobot() { return fixtureHelper.getRobot(); } // ---------------------------------------------------------- /** * Return a WindowTracker instance. * @return the window tracker for this test */ protected WindowTracker getWindowTracker() { return fixtureHelper.getWindowTracker(); } // ---------------------------------------------------------- /** * Convenience method to sleep for a UI interval * (same as getRobot().sleep()). */ protected void sleep() { getRobot().sleep(); } // ---------------------------------------------------------- /** * Ensure proper test harness setup that won't * be inadvertently overridden by a derived class. */ protected void fixtureSetUp() { hierarchy = createHierarchy(); finder = new BasicFinder(hierarchy); fixtureHelper = new AWTFixtureHelper(hierarchy) { // ---------------------------------------------------------- /** * Dispose of all extant windows. */ protected void disposeAll() { java.util.Iterator<?> iter = hierarchy.getRoots().iterator(); while (iter.hasNext()) { hierarchy.dispose((Window)iter.next()); } } }; } // ---------------------------------------------------------- /** * Handles restoration of system state. Automatically disposes of any * Components used in the test. */ protected void fixtureTearDown() { edtExceptionTime = fixtureHelper.getEventDispatchErrorTime(); edtException = fixtureHelper.getEventDispatchError(); fixtureHelper.dispose(); fixtureHelper = null; clearTestFields(); ComponentTester.clearTesterCache(); // Explicitly set these null, since the test fixture instance may // be kept around by the test runner hierarchy = null; finder = null; } // ---------------------------------------------------------- /** * Obtain a component finder to look up components. * @return a component finder for this test class. */ protected ComponentFinder getFinder() { return finder; } // ---------------------------------------------------------- /** * Create the component hierarchy object that will be used in this * test class. This method allows derived classes to provide their * own {@link Hierarchy} if necessary. * @return A new hierarchy object for use in this test class. */ protected Hierarchy createHierarchy() { return new TestHierarchy(); } // ---------------------------------------------------------- /** * Get the hierarchy used by finders in this test class. * @return The hierarchy currently in use */ protected Hierarchy getHierarchy() { return hierarchy; } // ---------------------------------------------------------- /** * Represents an exception that occurred on the event dispatch thread. */ protected static class EventDispatchException extends InvocationTargetException { private static final long serialVersionUID = 8199841262670518931L; // ---------------------------------------------------------- /** * Create a new exception object. * @param t The throwable that was thrown in the event dispatch thread */ public EventDispatchException(Throwable t) { super(t, "An exception was thrown on the event dispatch thread: " + t.toString()); } // ---------------------------------------------------------- public void printStackTrace() { getTargetException().printStackTrace(); } // ---------------------------------------------------------- public void printStackTrace(PrintStream p) { getTargetException().printStackTrace(p); } // ---------------------------------------------------------- public void printStackTrace(PrintWriter p) { getTargetException().printStackTrace(p); } } //~ Private Methods/Declarations .......................................... // ---------------------------------------------------------- /** * Clears all non-static {@link TestCase} fields which are instances of * any class found in {@link #DISPOSE_CLASSES}. */ private void clearTestFields() { Field fieldForException = null; try { for (Field field : getClass().getDeclaredFields()) { fieldForException = field; if (!Modifier.isStatic(field.getModifiers())) { field.setAccessible(true); for (Class<?> cls : DISPOSE_CLASSES) { if (cls.isAssignableFrom(field.getType())) { field.set(this, null); } } } } } catch(Exception e) { if (fieldForException != null) { System.err.println("Unable to automatically clear field " + fieldForException + " during fixture tearDown()"); } e.printStackTrace(System.err); } } // ---------------------------------------------------------- private Matcher filter2matcher(final GUIFilter filter) { return new MultiMatcher() { public boolean matches(Component comp) { return filter.test(comp); } public Component bestMatch(Component[] components) throws MultipleComponentsFoundException { throw new MultipleComponentsFoundException(components); } }; } // Force AWT mode, if no preference has been set in the environment static { boolean forceAWT = AccessController.doPrivileged( new PrivilegedAction<Boolean>() { public Boolean run() { return System.getProperty("abbot.robot.mode") != null; } }); if (forceAWT) { Robot.setEventMode(Robot.EM_AWT); } } private static boolean switchFocus; }