001: package junit.extensions.abbot;
002:
003: import java.awt.*;
004: import java.awt.event.*;
005: import java.io.*;
006: import java.lang.reflect.*;
007: import java.util.Iterator;
008: import javax.swing.*;
009: import javax.swing.border.EmptyBorder;
010:
011: import junit.framework.TestCase;
012: import abbot.*;
013: import abbot.finder.*;
014: import abbot.finder.matchers.*;
015: import abbot.tester.*;
016: import abbot.tester.Robot;
017: import abbot.util.*;
018:
019: /** Fixture for testing AWT and/or JFC/Swing components under JUnit. Ensures
020: * proper setup and cleanup for a GUI environment. Provides methods for
021: * automatically placing a GUI component within a frame and properly handling
022: * Window showing/hiding (including modal dialogs). Catches exceptions thrown
023: * on the event dispatch thread and rethrows them as test failures.<p>
024: * Use {@link #showFrame(Component)}
025: * when testing individual components, or
026: * {@link #showWindow(Window)}
027: * when testing a {@link Frame}, {@link Dialog}, or {@link Window}.<p>
028: * Any member fields you define which are classes derived from any of the
029: * classes in {@link #DISPOSE_CLASSES} will be automatically set to null after
030: * the test is run.<p>
031: * <bold>WARNING:</bold> Any tests which use significant or scarce resources
032: * and reference them in member fields should explicitly null those fields in
033: * the tearDown method if those classes are not included or derived from those
034: * in {@link #DISPOSE_CLASSES}. Otherwise the resources will not be subject
035: * to GC until the {@link TestCase} itself and any containing
036: * {@link junit.framework.TestSuite} is
037: * disposed (which, in the case of the standard JUnit test runners, is
038: * <i>never</i>).
039: */
040: public abstract class ComponentTestFixture extends TestCase {
041:
042: public class EventDispatchException extends
043: InvocationTargetException {
044: private EventDispatchException(Throwable t) {
045: super (t,
046: "An exception was thrown on the event dispatch thread: "
047: + t.toString());
048: }
049:
050: public void printStackTrace() {
051: getTargetException().printStackTrace();
052: }
053:
054: public void printStackTrace(PrintStream p) {
055: getTargetException().printStackTrace(p);
056: }
057:
058: public void printStackTrace(PrintWriter p) {
059: getTargetException().printStackTrace(p);
060: }
061: }
062:
063: /** Simple matcher that may be used to verify that a specific component is
064: found by a given ComponentFinder.
065: */
066: protected class ComponentMatcher implements Matcher {
067: private Component component;
068:
069: public ComponentMatcher(Component c) {
070: component = c;
071: }
072:
073: public boolean matches(Component c) {
074: return c == component;
075: }
076: }
077:
078: /** Typical delay to wait for a robot event to be translated into a Java
079: event. */
080: public static final int EVENT_GENERATION_DELAY = AWTFixtureHelper.EVENT_GENERATION_DELAY;
081: public static final int WINDOW_DELAY = AWTFixtureHelper.WINDOW_DELAY;
082: public static final int POPUP_DELAY = AWTFixtureHelper.POPUP_DELAY;
083:
084: /** Any member data derived from these classes will be automatically set
085: to <code>null</code> after the test has run. This enables GC of said
086: classes without GC of the test itself (the default JUnit runners never
087: release their references to the tests) or requiring explicit
088: <code>null</code>-setting in the {@link TestCase#tearDown()} method.
089: */
090: protected static final Class[] DISPOSE_CLASSES = { Component.class,
091: ComponentTester.class };
092:
093: private AWTFixtureHelper fixtureHelper;
094: private Throwable edtException;
095: private long edtExceptionTime;
096: private ComponentFinder finder;
097: private Hierarchy hierarchy;
098:
099: /** Return an Abbot {@link abbot.tester.Robot} for basic event generation.
100: */
101: protected Robot getRobot() {
102: return fixtureHelper.getRobot();
103: }
104:
105: /** Return a WindowTracker instance. */
106: protected WindowTracker getWindowTracker() {
107: return fixtureHelper.getWindowTracker();
108: }
109:
110: /** Convenience method to sleep for a UI interval
111: * (same as getRobot().sleep()).
112: */
113: protected void sleep() {
114: getRobot().sleep();
115: }
116:
117: /** This method should be invoked to display the component under test.
118: * The frame's size will be its preferred size. This method will return
119: * with the enclosing {@link Frame} is showing and ready for input.
120: */
121: protected Frame showFrame(Component comp) {
122: return fixtureHelper.showFrame(comp, null, getName());
123: }
124:
125: /** This method should be invoked to display the component under test,
126: * when a specific size of frame is desired. The method will return when
127: * the enclosing {@link Frame} is showing and ready for input.
128: * @param comp
129: * @param size Desired size of the enclosing frame, or <code>null</code>
130: * to make no explicit adjustments to its size.
131: */
132: protected Frame showFrame(Component comp, Dimension size) {
133: return fixtureHelper.showFrame(comp, size, getName());
134: }
135:
136: /** Safely display a window with proper EDT synchronization. This method
137: * blocks until the {@link Window} is showing and ready for input.
138: */
139: protected void showWindow(Window w) {
140: showWindow(w, null, true);
141: }
142:
143: /** Safely display a window with proper EDT synchronization. This method
144: * blocks until the {@link Window} is showing and ready for input.
145: */
146: protected void showWindow(final Window w, final Dimension size) {
147: showWindow(w, size, true);
148: }
149:
150: /** Safely display a window with proper EDT synchronization. This method
151: * blocks until the window is showing. This method will return even when
152: * the window is a modal dialog, since the show method is called on the
153: * event dispatch thread. The window will be packed if the pack flag is
154: * set, and set to the given size if it is non-<code>null</code>.<p>
155: * Modal dialogs may be shown with this method without blocking.
156: */
157: protected void showWindow(final Window w, final Dimension size,
158: final boolean pack) {
159: fixtureHelper.showWindow(w, size, pack);
160: }
161:
162: /** Return when the window is ready for input or times out waiting.
163: * @param w
164: */
165: protected void waitForWindow(Window w, boolean visible) {
166: fixtureHelper.waitForWindow(w, visible);
167: }
168:
169: /** Synchronous, safe hide of a window. The window is ensured to be
170: * hidden ({@link java.awt.event.ComponentEvent#COMPONENT_HIDDEN} or
171: * equivalent has been posted) when this method returns. Note that this
172: * will <em>not</em> trigger a
173: * {@link java.awt.event.WindowEvent#WINDOW_CLOSING} event; use
174: * {@link abbot.tester.WindowTester#actionClose(Component)}
175: * if a window manager window close operation is required.
176: */
177: protected void hideWindow(final Window w) {
178: fixtureHelper.hideWindow(w);
179: }
180:
181: /** Synchronous, safe dispose of a window. The window is ensured to be
182: * disposed ({@link java.awt.event.WindowEvent#WINDOW_CLOSED} has been
183: * posted) when this method returns.
184: */
185: protected void disposeWindow(Window w) {
186: fixtureHelper.disposeWindow(w);
187: }
188:
189: /** Convenience method to set key modifiers. Using this method is
190: * preferred to invoking {@link Robot#setModifiers(int,boolean)} or
191: * {@link Robot#keyPress(int)}, since this method's effects will be
192: * automatically undone at the end of the test. If you use the
193: * {@link Robot} methods, you must remember to release any keys pressed
194: * during the test.
195: * @param modifiers mask indicating which modifier keys to use
196: * @param pressed whether the modifiers should be in the pressed state.
197: */
198: protected void setModifiers(int modifiers, boolean pressed) {
199: fixtureHelper.setModifiers(modifiers, pressed);
200: }
201:
202: /** Convenience for <code>getRobot().invokeAndWait(Runnable)</code>. */
203: protected void invokeAndWait(Runnable runnable) {
204: fixtureHelper.invokeAndWait(runnable);
205: }
206:
207: /** Convenience for <code>getRobot().invokeLater(Runnable)</code>. */
208: protected void invokeLater(Runnable runnable) {
209: fixtureHelper.invokeLater(runnable);
210: }
211:
212: /** Install the given popup on the given component. Takes care of
213: * installing the appropriate mouse handler to activate the popup.
214: */
215: protected void installPopup(Component invoker,
216: final JPopupMenu popup) {
217: fixtureHelper.installPopup(invoker, popup);
218: }
219:
220: /** Safely install and display a popup in the center of the given
221: * component, returning when it is visible. Does not install any mouse
222: * handlers not generate any mouse events.
223: */
224: protected void showPopup(final JPopupMenu popup,
225: final Component invoker) {
226: showPopup(popup, invoker, invoker.getWidth() / 2, invoker
227: .getHeight() / 2);
228: }
229:
230: /** Safely install and display a popup, returning when it is visible.
231: Does not install any mouse handlers not generate any mouse events.
232: */
233: protected void showPopup(final JPopupMenu popup,
234: final Component invoker, final int x, final int y) {
235: fixtureHelper.showPopup(popup, invoker, x, y);
236: }
237:
238: /** Display a modal dialog and wait for it to show. Useful for things
239: * like {@link JFileChooser#showOpenDialog(Component)} or
240: * {@link JOptionPane#showInputDialog(Component,Object)}, or any
241: * other instance where the dialog contents are not predefined and
242: * displaying the dialog involves anything more than
243: * {@link Window#setVisible(boolean) show()/setVisible(true}
244: * (if {@link Window#setVisible(boolean) show()/setVisible(true)} is all
245: * that is required, use the {@link #showWindow(Window)} method instead).<p>
246: * The given {@link Runnable} should contain the code which will show the
247: * modal {@link Dialog} (and thus block); it will be run on the event
248: * dispatch thread.<p>
249: * This method will return when a {@link Dialog} becomes visible which
250: * contains the given component (which may be any component which will
251: * appear on the {@link Dialog}), or the standard timeout (10s) is
252: * reached, at which point a {@link RuntimeException} will be thrown.<p>
253: * For example,<br>
254: <pre><code>
255: final Frame parent = ...;
256: Dialog d = showModalDialog(new Runnable) {
257: public void run() {
258: JOptionPane.showInputDialog(parent, "Hit me");
259: }
260: });
261: </code></pre>
262: @see #showWindow(java.awt.Window)
263: @see #showWindow(java.awt.Window,java.awt.Dimension)
264: @see #showWindow(java.awt.Window,java.awt.Dimension,boolean)
265: */
266: protected Dialog showModalDialog(final Runnable showAction)
267: throws ComponentSearchException {
268: return fixtureHelper.showModalDialog(showAction, getFinder());
269: }
270:
271: /** Similar to {@link #showModalDialog(Runnable)},
272: * but provides for the case where some of the {@link Dialog}'s contents
273: * are known beforehand.<p>
274: * @deprecated Use {@link #showModalDialog(Runnable)} instead.
275: */
276: protected Dialog showModalDialog(Runnable showAction,
277: Component contents) throws Exception {
278: return showModalDialog(showAction);
279: }
280:
281: /** Returns whether a Component is showing. The ID may be the component
282: * name or, in the case of a Frame or Dialog, the title. Regular
283: * expressions may be used, but must be delimited by slashes, e.g. /expr/.
284: * Returns if one or more matches is found.
285: */
286: protected boolean isShowing(String id) {
287: try {
288: getFinder().find(new WindowMatcher(id, true));
289: } catch (ComponentNotFoundException e) {
290: return false;
291: } catch (MultipleComponentsFoundException m) {
292: // Might not be the one you want, but that's what the docs say
293: }
294: return true;
295: }
296:
297: /** Construct a test case with the given name. */
298: public ComponentTestFixture(String name) {
299: super (name);
300: }
301:
302: /** Default Constructor. The name will be automatically set from the
303: selected test method.
304: */
305: public ComponentTestFixture() {
306: }
307:
308: /** Ensure proper test harness setup and teardown that won't
309: * be inadvertently overridden by a derived class.
310: */
311: protected void fixtureSetUp() throws Throwable {
312: hierarchy = createHierarchy();
313:
314: finder = new BasicFinder(hierarchy);
315:
316: fixtureHelper = new AWTFixtureHelper(hierarchy) {
317: // backward-compatibility
318: protected void disposeAll() {
319: ComponentTestFixture.this .disposeAll();
320: }
321: };
322: }
323:
324: /** Handles restoration of system state. Automatically disposes of any
325: Components used in the test.
326: */
327: protected void fixtureTearDown() throws Throwable {
328: edtExceptionTime = fixtureHelper.getEventDispatchErrorTime();
329: edtException = fixtureHelper.getEventDispatchError();
330: fixtureHelper.dispose();
331: fixtureHelper = null;
332: clearTestFields();
333: // Explicitly set these null, since the test fixture instance may
334: // be kept around by the test runner
335: hierarchy = null;
336: finder = null;
337: }
338:
339: /** Dispose of all extant windows.
340: * @deprecated This functionality is now deferred to AWTFixtureHelper
341: */
342: protected void disposeAll() {
343: Iterator iter = hierarchy.getRoots().iterator();
344: while (iter.hasNext()) {
345: hierarchy.dispose((Window) iter.next());
346: }
347: }
348:
349: /** Clears all non-static {@link TestCase} fields which are instances of
350: * any class found in {@link #DISPOSE_CLASSES}.
351: */
352: private void clearTestFields() {
353: try {
354: Field[] fields = getClass().getDeclaredFields();
355: for (int i = 0; i < fields.length; i++) {
356: if ((fields[i].getModifiers() & Modifier.STATIC) == 0) {
357: fields[i].setAccessible(true);
358: for (int c = 0; c < DISPOSE_CLASSES.length; c++) {
359: Class cls = DISPOSE_CLASSES[c];
360: if (cls.isAssignableFrom(fields[i].getType())) {
361: fields[i].set(this , null);
362: }
363: }
364: }
365: }
366: } catch (Exception e) {
367: Log.warn(e);
368: }
369: }
370:
371: /** Override the default <code>junit.framework.TestCase#RunBare()</code>
372: * to ensure proper test harness setup and teardown that won't
373: * likely be accidentally overridden by a derived class.
374: * <p>
375: * If any exceptions are thrown on the event dispatch thread, they count
376: * as errors. They will not, however supersede any failures/errors
377: * thrown by the test itself unless thrown prior to the main test
378: * failure.
379: */
380: public void runBare() throws Throwable {
381: if (Boolean.getBoolean("abbot.skip_ui_tests")) {
382: return;
383: }
384:
385: Throwable exception = null;
386: long exceptionTime = -1;
387: try {
388: try {
389: fixtureSetUp();
390: super .runBare();
391: } catch (Throwable e) {
392: exception = e;
393: } finally {
394: Log.log("tearing down fixture: " + getName());
395: try {
396: fixtureTearDown();
397: } catch (Throwable tearingDown) {
398: if (exception == null)
399: exception = tearingDown;
400: }
401: }
402: if (exception != null)
403: throw exception;
404: } catch (Throwable e) {
405: exceptionTime = System.currentTimeMillis();
406: exception = e;
407: } finally {
408: // Cf. StepRunner.runStep()
409: // Any EDT exception which occurred *prior* to when the
410: // exception on the main thread was thrown should be used
411: // instead.
412: if (edtException != null
413: && (exception == null || edtExceptionTime < exceptionTime)) {
414: exception = new EventDispatchException(edtException);
415: }
416: }
417: if (exception != null) {
418: throw exception;
419: }
420: }
421:
422: /** Provide for derived classes to provide their own Hierarchy. */
423: protected Hierarchy createHierarchy() {
424: return new TestHierarchy();
425: }
426:
427: /** Obtain a component finder to look up components. */
428: protected ComponentFinder getFinder() {
429: return finder;
430: }
431:
432: /** Obtain a consistent hierarchy. */
433: protected Hierarchy getHierarchy() {
434: return hierarchy;
435: }
436: }
|