001: package org.uispec4j.interception;
002:
003: import junit.framework.Assert;
004: import org.uispec4j.Trigger;
005: import org.uispec4j.UISpec4J;
006: import org.uispec4j.Window;
007: import org.uispec4j.assertion.UISpecAssert;
008: import org.uispec4j.interception.handlers.*;
009: import org.uispec4j.interception.toolkit.UISpecDisplay;
010: import org.uispec4j.utils.ComponentUtils;
011: import org.uispec4j.utils.ExceptionContainer;
012: import org.uispec4j.utils.TriggerRunner;
013:
014: import java.util.ArrayList;
015: import java.util.Arrays;
016: import java.util.Iterator;
017: import java.util.List;
018:
019: /**
020: * Intercepts popped-up windows such as JFrame or JDialog. <p>
021: * There are two main usage scenarios for this class: intercepting "frames", i.e. non-modal windows,
022: * and intercepting modal dialogs.<p>
023: * Non-modal windows can be intercepted and used directly from within the test using the following
024: * construct:
025: * <pre>
026: * Window window = WindowInterceptor.run(panel.getButton("open").triggerClick());
027: * </pre>
028: * <p/>
029: * Modal dialogs cannot be intercepted this way, because the thread from which the test is run
030: * will likely be blocked in the production code until the dialog is closed.
031: * To intercept a sequence of popped-up windows, use the following construct:
032: * <pre>
033: * WindowInterceptor
034: * .init(new Trigger() {
035: * public void run() throws Exception {
036: * // ... trigger something that will cause the first window to be shown ...
037: * }
038: * })
039: * .process(new WindowHandler("first dialog") {
040: * public Trigger process(Window window) {
041: * // ... perform some operations on the first window ...
042: * return window.getButton("OK").triggerClick(); // return a trigger that will close it
043: * }
044: * })
045: * .process(new WindowHandler("second dialog") {
046: * public Trigger process(Window window) {
047: * // ... perform some operations on the second window ...
048: * return window.getButton("OK").triggerClick(); // return a trigger that will close it
049: * }
050: * })
051: * .run();
052: * </pre>
053: * </ul>
054: * This class uses a timeout (see {@link UISpec4J#setWindowInterceptionTimeLimit}) to make sure
055: * that windows appear within a given time limit, and that modal windows are closed before the
056: * end of the interception.
057: *
058: * @see <a href="http://www.uispec4j.org/interception.html">Intercepting windows</a>
059: */
060: public final class WindowInterceptor {
061:
062: private Trigger trigger;
063: private List handlers = new ArrayList();
064: private ExceptionContainer exceptionContainer = new ExceptionContainer();
065: private StackTraceElement[] stackReference;
066:
067: /**
068: * Starts the interception of a modal dialog. The returned interceptor must be used for
069: * processing the displayed window, for instance:
070: * <pre>
071: * WindowInterceptor
072: * .init(new Trigger() {
073: * public void run() throws Exception {
074: * // ... trigger something that will cause the first window to be shown ...
075: * }
076: * })
077: * .process(new WindowHandler("my dialog") {
078: * public Trigger process(Window window) {
079: * // ... perform some operations on the shown window ...
080: * return window.getButton("OK").triggerClick(); // return a trigger that will close it
081: * }
082: * })
083: * .run();
084: * </pre>
085: *
086: * @see #process(WindowHandler)
087: */
088: public static WindowInterceptor init(Trigger trigger) {
089: return new WindowInterceptor(trigger);
090: }
091:
092: private WindowInterceptor(Trigger trigger) {
093: this .trigger = trigger;
094: }
095:
096: /**
097: * Processes a modal dialog. The provided WindowHandler must return a trigger that will cause
098: * the window to be closed, in order to prevent the application to be stopped.
099: */
100: public WindowInterceptor process(WindowHandler handler) {
101: if (handler.getName() == null) {
102: handler.setName(Integer.toString(handlers.size() + 1));
103: }
104: handlers.add(handler);
105: return this ;
106: }
107:
108: /**
109: * Processes a modal dialog after having checked its title first.
110: *
111: * @see #process(WindowHandler)
112: */
113: public WindowInterceptor process(final String title,
114: final WindowHandler handler) {
115: handlers.add(new WindowHandler(title) {
116: public Trigger process(Window window) throws Exception {
117: UISpecAssert.assertTrue(window.titleEquals(title));
118: return handler.process(window);
119: }
120: });
121: return this ;
122: }
123:
124: /**
125: * Processes a sequence of dialogs (one handler per dialog).
126: *
127: * @see #process(WindowHandler)
128: */
129: public WindowInterceptor process(WindowHandler[] handlers) {
130: for (int i = 0; i < handlers.length; i++) {
131: process(handlers[i]);
132: }
133: return this ;
134: }
135:
136: /**
137: * Processes a dialog that will be closed automatically, and checks its name.
138: */
139: public WindowInterceptor processTransientWindow(final String title) {
140: return process(new WindowHandler("Transient window: " + title) {
141: public Trigger process(Window window) {
142: checkTitle(window, title);
143: return Trigger.DO_NOTHING;
144: }
145: });
146: }
147:
148: /**
149: * Processes a dialog that will be closed automatically.
150: */
151: public WindowInterceptor processTransientWindow() {
152: return process(new WindowHandler("Transient window") {
153: public Trigger process(Window window) {
154: return Trigger.DO_NOTHING;
155: }
156: });
157: }
158:
159: /**
160: * Processes a dialog by clicking on a given button. This a shortcut that prevents you
161: * from implementing your own WindowHandler for simple cases such as confirmation dialogs.
162: *
163: * @see #process(WindowHandler)
164: */
165: public WindowInterceptor processWithButtonClick(
166: final String buttonName) {
167: handlers.add(new WindowHandler("buttonClickHandler") {
168: public Trigger process(Window window) throws Exception {
169: return window.getButton(buttonName).triggerClick();
170: }
171: });
172: return this ;
173: }
174:
175: /**
176: * Processes a dialog by checking its title and clicking on a given button.
177: *
178: * @see #processWithButtonClick(String)
179: */
180: public WindowInterceptor processWithButtonClick(final String title,
181: final String buttonName) {
182: handlers.add(new WindowHandler("buttonClickHandler") {
183: public Trigger process(Window window) throws Exception {
184: checkTitle(window, title);
185: return window.getButton(buttonName).triggerClick();
186: }
187: });
188: return this ;
189: }
190:
191: /**
192: * Starts the interception prepared with the
193: * {@link #init(Trigger)}/{@link #process(WindowHandler)} call sequence.
194: * This method will fail if no window was shown by the trigger under the time limit.
195: * defined with {@link UISpec4J#setWindowInterceptionTimeLimit(long)}.
196: */
197: public void run() {
198: if (handlers.isEmpty()) {
199: Assert.fail("You must add at least one handler");
200: }
201: initStackReference();
202: try {
203: run(trigger, new InterceptionHandlerAdapter(handlers
204: .iterator()));
205: } catch (Throwable e) {
206: storeException(e,
207: handlers.size() > 1 ? "Error in first handler: "
208: : "");
209: }
210: exceptionContainer.rethrowIfNeeded();
211: UISpecDisplay.instance().rethrowIfNeeded();
212: }
213:
214: private void checkTitle(Window window, final String title) {
215: Assert.assertEquals("Invalid window title -", title, window
216: .getTitle());
217: }
218:
219: private void initStackReference() {
220: Exception dummyException = new Exception();
221: StackTraceElement[] trace = dummyException.getStackTrace();
222: List list = new ArrayList(Arrays.asList(trace));
223: list.remove(0);
224: list.remove(0);
225: this .stackReference = (StackTraceElement[]) list
226: .toArray(new StackTraceElement[list.size()]);
227: }
228:
229: private void storeException(Throwable e, String messagePrefix) {
230: if (!exceptionContainer.isSet()) {
231: String message = messagePrefix + e.getMessage();
232: InterceptionError error = new InterceptionError(message, e);
233: error.setStackTrace(stackReference);
234: exceptionContainer.set(error);
235: }
236: }
237:
238: private class InterceptionHandlerAdapter implements
239: InterceptionHandler {
240: private Iterator handlersIterator;
241: private WindowHandler handler;
242:
243: public InterceptionHandlerAdapter(Iterator handlersIterator) {
244: this .handlersIterator = handlersIterator;
245: }
246:
247: public void process(Window window) {
248: handler = getNextHandler();
249: String name = handler.getName();
250: try {
251: Trigger trigger = handler.process(window);
252: if (handlersIterator.hasNext()) {
253: WindowInterceptor.run(trigger, this );
254: } else {
255: TriggerRunner.runInCurrentThread(trigger);
256: }
257: } catch (WindowNotClosedError e) {
258: storeException(e, computeMessagePrefix(handler
259: .getName()));
260: } catch (Throwable e) {
261: storeException(e, computeMessagePrefix(name));
262: } finally {
263: if (exceptionContainer.isSet()) {
264: ComponentUtils.close(window);
265: }
266: }
267: }
268:
269: private String computeMessagePrefix(String handlerName) {
270: return handlers.size() > 1 ? "Error in handler '"
271: + handlerName + "': " : "";
272: }
273:
274: private WindowHandler getNextHandler() {
275: return (WindowHandler) handlersIterator.next();
276: }
277: }
278:
279: /**
280: * Intercepts a non-modal window by running a trigger and returning the displayed window.
281: * This method will fail if no window was shown by the trigger under the time limit
282: * defined with {@link UISpec4J#setWindowInterceptionTimeLimit(long)},
283: * or if it is used with a modal dialog (modal dialogs should be intercepted using
284: * {@link #init(Trigger)}). <p>
285: * Note: the trigger is run in the current thread.
286: */
287: public static Window run(Trigger trigger) {
288: return run(trigger, false);
289:
290: }
291:
292: /**
293: * Performs a "quick&dirty" interception of a modal dialog.<p>
294: * <em>Warning</em>: This method should be handled with care and especially avoided in cases
295: * where the application code is blocked while the dialog is displayed,
296: * because it could result in deadlocks. <p>
297: * Modal dialogs should rather be intercepted using {@link #init(Trigger)} <p/>
298: * This method will fail if no window was shown by the trigger under the time limit, or if it is
299: * used with a non-modal window. <p>
300: */
301: public static Window getModalDialog(Trigger trigger) {
302: return run(trigger, true);
303: }
304:
305: private static Window run(final Trigger trigger,
306: boolean shouldBeModal) {
307: UISpecDisplay.instance().rethrowIfNeeded();
308: ShownInterceptionCollectorHandler collector = new ShownInterceptionCollectorHandler();
309: ShownInterceptionDetectionHandler showDetector = new ShownInterceptionDetectionHandler(
310: collector, UISpec4J.getWindowInterceptionTimeLimit());
311: NewThreadInterceptionHandlerDecorator newThreadHandler = new NewThreadInterceptionHandlerDecorator(
312: showDetector);
313: ModalInterceptionCheckerHandler modalChecker = new ModalInterceptionCheckerHandler(
314: newThreadHandler, shouldBeModal);
315: UISpecDisplay.instance().add(modalChecker);
316:
317: try {
318: if (shouldBeModal) {
319: TriggerRunner.runInUISpecThread(trigger);
320: } else {
321: TriggerRunner.runInCurrentThread(trigger);
322: }
323:
324: UISpecDisplay.instance().rethrowIfNeeded();
325: showDetector.waitWindow();
326: return collector.getWindow();
327: } finally {
328: UISpecDisplay.instance().remove(modalChecker);
329: newThreadHandler.join();
330: UISpecDisplay.instance().rethrowIfNeeded();
331: }
332: }
333:
334: private static void run(Trigger trigger, InterceptionHandler handler) {
335: ShownInterceptionDetectionHandler showDetector = new ShownInterceptionDetectionHandler(
336: handler, UISpec4J.getWindowInterceptionTimeLimit());
337: ClosedInterceptionDetectionHandler closeDetector = new ClosedInterceptionDetectionHandler(
338: showDetector, UISpec4J.getWindowInterceptionTimeLimit());
339: NewThreadInterceptionHandlerDecorator newThreadHandler = new NewThreadInterceptionHandlerDecorator(
340: closeDetector);
341: UISpecDisplay.instance().add(newThreadHandler);
342: try {
343: TriggerRunner.runInCurrentThread(trigger);
344: showDetector.waitWindow();
345: newThreadHandler.complete();
346: closeDetector.checkWindowWasClosed();
347: } finally {
348: UISpecDisplay.instance().remove(newThreadHandler);
349: newThreadHandler.join();
350: closeDetector.stop();
351: }
352: }
353: }
|