001: package abbot.util;
002:
003: import java.awt.AWTEvent;
004: import java.awt.Component;
005: import java.awt.Dialog;
006: import java.awt.Dimension;
007: import java.awt.EventQueue;
008: import java.awt.Frame;
009: import java.awt.Window;
010: import java.awt.event.*;
011: import java.util.Iterator;
012: import javax.swing.JFrame;
013: import javax.swing.JPanel;
014: import javax.swing.JPopupMenu;
015: import javax.swing.SwingUtilities;
016: import javax.swing.border.EmptyBorder;
017:
018: import abbot.Log;
019: import abbot.finder.BasicFinder;
020: import abbot.finder.ComponentFinder;
021: import abbot.finder.ComponentNotFoundException;
022: import abbot.finder.ComponentSearchException;
023: import abbot.finder.Hierarchy;
024: import abbot.finder.Matcher;
025: import abbot.finder.MultipleComponentsFoundException;
026: import abbot.finder.TestHierarchy;
027: import abbot.finder.matchers.ClassMatcher;
028: import abbot.finder.matchers.WindowMatcher;
029: import abbot.tester.Robot;
030: import abbot.tester.WindowTracker;
031:
032: /** Provides various facilities for setting up, using, and tearing down
033: * a test involving UI components.
034: * Handles standardized AWTEvent logging and catching exceptions on the
035: * AWT event dispatch thread (EDT).
036: * This class should be used at setup and teardown of your chosen fixture.
037: * @see junit.extensions.abbot.ComponentTestFixture
038: * @see abbot.script.StepRunner
039: */
040:
041: public class AWTFixtureHelper {
042: /** Typical delay to wait for a robot event to be translated into a Java
043: event. */
044: public static final int EVENT_GENERATION_DELAY = 5000;
045: public static final int WINDOW_DELAY = 20000; // for slow systems
046: public static final int POPUP_DELAY = 10000;
047:
048: private AWTEventListener listener = null;
049: private SystemState state;
050: private Robot robot;
051: private WindowTracker tracker;
052: private Hierarchy hierarchy;
053: // modifiers set during this helper's lifetime (to be cleared on dispose)
054: private int modifiers;
055:
056: public AWTFixtureHelper() {
057: this (new TestHierarchy());
058: }
059:
060: /** Create an instance of AWTFixtureHelper which makes a snapshot of the
061: current VM state.
062: */
063: public AWTFixtureHelper(Hierarchy hierarchy) {
064: // Preserve all system properties to restore them later
065: state = new SystemState();
066: this .hierarchy = hierarchy;
067:
068: // Install our own event handler, which will forward events thrown on
069: // the event queue
070: try {
071: new EDTExceptionCatcher().install();
072: } catch (RuntimeException re) {
073: // Not fatal if we can't install, since most tests don't
074: // depend on it. We won't be able to throw errors that were
075: // generated on the event dispatch thread, though.
076: }
077: // Only enable event logging if debug is enabled for this class
078: // Facilitate debugging by logging all events
079: if (Boolean.getBoolean("abbot.fixture.log_events")) {
080: long mask = Properties
081: .getProperty(
082: "abbot.fixture.event_mask",
083: Long.MIN_VALUE,
084: Long.MAX_VALUE,
085: abbot.editor.recorder.EventRecorder.RECORDING_EVENT_MASK);
086:
087: Log.log("Using mask value " + mask);
088: listener = new AWTEventListener() {
089: public void eventDispatched(AWTEvent event) {
090: if (listener != null)
091: Log.log(Robot.toString(event));
092: }
093: };
094: new WeakAWTEventListener(listener, mask);
095: }
096: robot = new Robot();
097: tracker = WindowTracker.getTracker();
098:
099: SystemState.clearLockingKeys();
100: robot.reset();
101: if (Bugs.hasMultiClickFrameBug())
102: robot.delay(500);
103: }
104:
105: public Robot getRobot() {
106: return robot;
107: }
108:
109: public WindowTracker getWindowTracker() {
110: return tracker;
111: }
112:
113: public Hierarchy getHierarchy() {
114: return hierarchy;
115: }
116:
117: /** Returns the last exception thrown on the event dispatch thread, or
118: <code>null</code> if no such exception has been thrown.
119: */
120: public Throwable getEventDispatchError() {
121: return EDTExceptionCatcher.getThrowable();
122: }
123:
124: /** Returns the time of the last exception thrown on the event dispatch
125: thread.
126: */
127: public long getEventDispatchErrorTime() {
128: return EDTExceptionCatcher.getThrowableTime();
129: }
130:
131: /** Convenience method to set key modifiers. Using this method is
132: * preferred to invoking {@link Robot#setModifiers(int,boolean)} or
133: * {@link Robot#keyPress(int)}, since this method's effects will be
134: * automatically undone at the end of the test. If you use the
135: * {@link Robot} methods, you must remember to release any keys pressed
136: * during the test.
137: * @param modifiers mask indicating which modifier keys to use
138: * @param pressed whether the modifiers should be in the pressed state.
139: */
140: public void setModifiers(int modifiers, boolean pressed) {
141: if (pressed)
142: this .modifiers |= modifiers;
143: else
144: this .modifiers &= ~modifiers;
145: robot.setModifiers(modifiers, pressed);
146: robot.waitForIdle();
147: }
148:
149: protected void disposeAll() {
150: Iterator iter = hierarchy.getRoots().iterator();
151: while (iter.hasNext()) {
152: hierarchy.dispose((Window) iter.next());
153: }
154: }
155:
156: /** Restore the state that was preserved when this object was created.
157: */
158: public void restore() {
159: if (AWT.isAWTPopupMenuBlocking())
160: AWT.dismissAWTPopup();
161: state.restore();
162:
163: // Encourage GC of unused components, which reduces the load on
164: // future tests.
165: System.gc();
166: System.runFinalization();
167: }
168:
169: /** Dispose all windows created during this object's lifetime and restore
170: * the previous system/UI state, to the extent possible.
171: */
172: public void dispose() {
173: // WARNING: clear input state prior to disposing windows,
174: // otherwise native drag operations may be left stuck
175: if (robot != null) {
176: if (modifiers != 0) {
177: robot.setModifiers(modifiers, false);
178: modifiers = 0;
179: }
180: int buttons = Robot.getState().getButtons();
181: if (buttons != 0) {
182: Log.debug("release " + AWT.getMouseModifiers(buttons));
183: robot.mouseRelease(buttons);
184: }
185: if (robot.getState().isNativeDragActive()) {
186: robot.keyPress(KeyEvent.VK_ESCAPE);
187: robot.keyRelease(KeyEvent.VK_ESCAPE);
188: }
189: // TODO: release *any* keys that were pressed, not just modifiers
190: }
191: disposeAll();
192: restore();
193: }
194:
195: /** This method should be invoked to display the component under test.
196: * The frame's size will be its preferred size. This method will return
197: * with the enclosing {@link Frame} is showing and ready for input.
198: */
199: public Frame showFrame(Component comp) {
200: return showFrame(comp, null);
201: }
202:
203: /** This method should be invoked to display the component under test,
204: * when a specific size of frame is desired. The method will return when
205: * the enclosing {@link Frame} is showing and ready for input.
206: * @param comp
207: * @param size Desired size of the enclosing frame, or <code>null</code>
208: * to make no explicit adjustments to its size.
209: */
210: public Frame showFrame(Component comp, Dimension size) {
211: return showFrame(comp, size, "Test Frame");
212: }
213:
214: /** This method should be invoked to display the component under test,
215: * when a specific size of frame is desired. The method will return when
216: * the enclosing {@link Frame} is showing and ready for input.
217: * @param comp
218: * @param size Desired size of the enclosing frame, or <code>null</code>
219: * to make no explicit adjustments to its size.
220: * @param title Title of the wrapping frame
221: */
222: public Frame showFrame(Component comp, Dimension size, String title) {
223: JFrame frame = new JFrame(title);
224: frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
225: JPanel pane = (JPanel) frame.getContentPane();
226: pane.setBorder(new EmptyBorder(10, 10, 10, 10));
227: pane.add(comp);
228: showWindow(frame, size, true);
229: return frame;
230: }
231:
232: /** Safely display a window with proper EDT synchronization. This method
233: * blocks until the {@link Window} is showing and ready for input.
234: */
235: public void showWindow(Window w) {
236: showWindow(w, null, true);
237: }
238:
239: /** Safely display a window with proper EDT synchronization. This method
240: * blocks until the {@link Window} is showing and ready for input.
241: */
242: public void showWindow(final Window w, final Dimension size) {
243: showWindow(w, size, true);
244: }
245:
246: /** Safely display a window with proper EDT synchronization. This method
247: * blocks until the window is showing. This method will return even when
248: * the window is a modal dialog, since the show method is called on the
249: * event dispatch thread. The window will be packed if the pack flag is
250: * set, and set to the given size if it is non-<code>null</code>.<p>
251: * Modal dialogs may be shown with this method without blocking.
252: */
253: public void showWindow(final Window w, final Dimension size,
254: final boolean pack) {
255: EventQueue.invokeLater(new Runnable() {
256: public void run() {
257: if (pack) {
258: w.pack();
259: // Make sure the window is positioned away from
260: // any toolbars around the display borders
261: w.setLocation(100, 100);
262: }
263: if (size != null)
264: w.setSize(size.width, size.height);
265: w.setVisible(true);
266: }
267: });
268: // Ensure the window is visible before returning
269: waitForWindow(w, true);
270: }
271:
272: /** Return when the window is ready for input or times out waiting.
273: * @param w
274: */
275: public void waitForWindow(Window w, boolean visible) {
276: long start = System.currentTimeMillis();
277: while ((Robot.getEventMode() == Robot.EM_ROBOT && visible && !getWindowTracker()
278: .isWindowReady(w))
279: || w.isShowing() != visible) {
280: long elapsed = System.currentTimeMillis() - start;
281: if (elapsed > WINDOW_DELAY)
282: throw new RuntimeException(
283: "Timed out waiting for Window to "
284: + (visible ? "open" : "close") + " ("
285: + elapsed + "ms)");
286: getRobot().sleep();
287: }
288: }
289:
290: /** Synchronous, safe hide of a window. The window is ensured to be
291: * hidden ({@link java.awt.event.ComponentEvent#COMPONENT_HIDDEN} or
292: * equivalent has been posted) when this method returns. Note that this
293: * will <em>not</em> trigger a
294: * {@link java.awt.event.WindowEvent#WINDOW_CLOSING} event; use
295: * {@link abbot.tester.WindowTester#actionClose(Component)}
296: * if a window manager window close operation is required.
297: */
298: public void hideWindow(final Window w) {
299: EventQueue.invokeLater(new Runnable() {
300: public void run() {
301: w.setVisible(false);
302: }
303: });
304: waitForWindow(w, false);
305: // Not strictly required, but if a test is depending on a window
306: // event listener's actions on window hide/close, better to wait.
307: getRobot().waitForIdle();
308: }
309:
310: /** Synchronous, safe dispose of a window. The window is ensured to be
311: * disposed ({@link java.awt.event.WindowEvent#WINDOW_CLOSED} has been
312: * posted) when this method returns.
313: */
314: public void disposeWindow(Window w) {
315: w.dispose();
316: waitForWindow(w, false);
317: getRobot().waitForIdle();
318: }
319:
320: /** Convenience for <code>getRobot().invokeAndWait(Runnable)</code>. */
321: public void invokeAndWait(Runnable runnable) {
322: getRobot().invokeAndWait(runnable);
323: }
324:
325: /** Convenience for <code>getRobot().invokeLater(Runnable)</code>. */
326: public void invokeLater(Runnable runnable) {
327: getRobot().invokeLater(runnable);
328: }
329:
330: /** Install the given popup on the given component. Takes care of
331: * installing the appropriate mouse handler to activate the popup.
332: */
333: public void installPopup(Component invoker, final JPopupMenu popup) {
334: invoker.addMouseListener(new MouseAdapter() {
335: public void mousePressed(MouseEvent e) {
336: mouseReleased(e);
337: }
338:
339: public void mouseClicked(MouseEvent e) {
340: mouseReleased(e);
341: }
342:
343: public void mouseReleased(MouseEvent e) {
344: if (e.isPopupTrigger()) {
345: popup.show(e.getComponent(), e.getX(), e.getY());
346: }
347: }
348: });
349: }
350:
351: /** Safely install and display a popup in the center of the given
352: * component, returning when it is visible. Does not install any mouse
353: * handlers not generate any mouse events.
354: */
355: public void showPopup(final JPopupMenu popup,
356: final Component invoker) {
357: showPopup(popup, invoker, invoker.getWidth() / 2, invoker
358: .getHeight() / 2);
359: }
360:
361: /** Safely install and display a popup, returning when it is visible.
362: Does not install any mouse handlers not generate any mouse events.
363: */
364: public void showPopup(final JPopupMenu popup,
365: final Component invoker, final int x, final int y) {
366: EventQueue.invokeLater(new Runnable() {
367: public void run() {
368: popup.show(invoker, x, y);
369: }
370: });
371: long start = System.currentTimeMillis();
372: while (!popup.isShowing()) {
373: if (System.currentTimeMillis() - start > POPUP_DELAY)
374: throw new RuntimeException(
375: "Timed out waiting for popup to show");
376: robot.sleep();
377: }
378: waitForWindow(SwingUtilities.getWindowAncestor(popup), true);
379: }
380:
381: /** Display a modal dialog and wait for it to show. Useful for things
382: * like
383: * {@link javax.swing.JFileChooser#showOpenDialog(java.awt.Component)} or
384: * {@link javax.swing.JOptionPane#showInputDialog(Object)}, or any
385: * other instance where the dialog contents are not predefined and
386: * displaying the dialog involves anything more than
387: * {@link Window#setVisible(boolean) show()/setVisible(true}
388: * (if {@link Window#setVisible(boolean) show()/setVisible(true)} is all
389: * that is required, use the {@link #showWindow(Window)} method instead).<p>
390: * The given {@link Runnable} should contain the code which will show the
391: * modal {@link Dialog} (and thus block); it will be run on the event
392: * dispatch thread.<p>
393: * This method will return when a {@link Dialog} becomes visible which
394: * contains the given component (which may be any component which will
395: * appear on the {@link Dialog}), or the standard timeout (10s) is
396: * reached, at which point a {@link RuntimeException} will be thrown.<p>
397: * For example,<br>
398: <pre><code>
399: final Frame parent = ...;
400: Dialog d = showModalDialog(new Runnable) {
401: public void run() {
402: JOptionPane.showInputDialog(parent, "Hit me");
403: }
404: });
405: </code></pre>
406: @see #showWindow(java.awt.Window)
407: @see #showWindow(java.awt.Window,java.awt.Dimension)
408: @see #showWindow(java.awt.Window,java.awt.Dimension,boolean)
409: */
410: public Dialog showModalDialog(final Runnable showAction)
411: throws ComponentSearchException {
412: return showModalDialog(showAction, new BasicFinder(hierarchy));
413: }
414:
415: /** Same as {@link #showModalDialog(Runnable)}, but provides a custom
416: * {@link ComponentFinder} to find the dialog.
417: */
418: public Dialog showModalDialog(final Runnable showAction,
419: ComponentFinder finder) throws ComponentSearchException {
420: final boolean[] modalRun = { false };
421: final boolean[] invocationFinished = { false };
422: EventQueue.invokeLater(new Runnable() {
423: public void run() {
424: modalRun[0] = true;
425: try {
426: showAction.run();
427: } finally {
428: // Detect premature Runnable return
429: invocationFinished[0] = true;
430: }
431: }
432: });
433: while (!modalRun[0]) {
434: try {
435: Thread.sleep(10);
436: } catch (InterruptedException e) {
437: }
438: }
439: // Wait for any modal dialog to appear
440: Matcher matcher = new ClassMatcher(Dialog.class, true) {
441: public boolean matches(Component c) {
442: return super .matches(c) && ((Dialog) c).isModal()
443: && AWT.containsFocus(c);
444: }
445: };
446: long start = System.currentTimeMillis();
447: boolean finished = false;
448: while (true) {
449: try {
450: return (Dialog) finder.find(matcher);
451: } catch (ComponentSearchException e) {
452: if (invocationFinished[0]) {
453: // ensure we do one more check to see if the dialog is there
454: if (finished)
455: break;
456: finished = true;
457: }
458: if (System.currentTimeMillis() - start > 10000)
459: throw new ComponentSearchException(
460: "Timed out waiting for dialog to be ready");
461: robot.sleep();
462: }
463: }
464: throw new ComponentSearchException(
465: "No dialog was displayed (premature return=" + finished
466: + ")");
467: }
468:
469: /** Returns whether a Component is showing. The ID may be the component
470: * name or, in the case of a Frame or Dialog, the title. Regular
471: * expressions may be used, but must be delimited by slashes, e.g. /expr/.
472: * Returns if one or more matches is found.
473: */
474: public boolean isShowing(String id) {
475: return isShowing(id, new BasicFinder(hierarchy));
476: }
477:
478: /** Same as {@link #isShowing(String)}, but uses the given
479: * ComponentFinder to do the lookup.
480: */
481: public boolean isShowing(String id, ComponentFinder finder) {
482: try {
483: finder.find(new WindowMatcher(id, true));
484: } catch (ComponentNotFoundException e) {
485: return false;
486: } catch (MultipleComponentsFoundException m) {
487: // Might not be the one you want, but that's what the docs say
488: }
489: return true;
490: }
491: }
|