001 /*
002 * Created on Jan 26, 2008
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 *
014 * Copyright @2008-2010 the original author or authors.
015 */
016 package org.fest.swing.driver;
017
018 import static org.fest.assertions.Assertions.assertThat;
019 import static org.fest.swing.core.MouseButton.LEFT_BUTTON;
020 import static org.fest.swing.core.MouseButton.RIGHT_BUTTON;
021 import static org.fest.swing.driver.ComponentEnabledCondition.untilIsEnabled;
022 import static org.fest.swing.driver.ComponentPerformDefaultAccessibleActionTask.performDefaultAccessibleAction;
023 import static org.fest.swing.driver.ComponentStateValidator.validateIsEnabledAndShowing;
024 import static org.fest.swing.edt.GuiActionRunner.execute;
025 import static org.fest.swing.format.Formatting.format;
026 import static org.fest.swing.query.ComponentEnabledQuery.isEnabled;
027 import static org.fest.swing.query.ComponentHasFocusQuery.hasFocus;
028 import static org.fest.swing.query.ComponentSizeQuery.sizeOf;
029 import static org.fest.swing.query.ComponentVisibleQuery.isVisible;
030 import static org.fest.swing.timing.Pause.pause;
031 import static org.fest.swing.util.TimeoutWatch.startWatchWithTimeoutOf;
032 import static org.fest.util.Strings.concat;
033 import static org.fest.util.Strings.quote;
034
035 import java.awt.*;
036
037 import javax.accessibility.AccessibleAction;
038 import javax.swing.JMenu;
039 import javax.swing.JPopupMenu;
040
041 import org.fest.assertions.Description;
042 import org.fest.swing.annotation.RunsInCurrentThread;
043 import org.fest.swing.annotation.RunsInEDT;
044 import org.fest.swing.core.*;
045 import org.fest.swing.core.Robot;
046 import org.fest.swing.edt.GuiLazyLoadingDescription;
047 import org.fest.swing.edt.GuiTask;
048 import org.fest.swing.exception.*;
049 import org.fest.swing.format.ComponentFormatter;
050 import org.fest.swing.format.Formatting;
051 import org.fest.swing.timing.Timeout;
052 import org.fest.swing.util.TimeoutWatch;
053
054 /**
055 * Understands functional testing of <code>{@link Component}</code>s:
056 * <ul>
057 * <li>user input simulation</li>
058 * <li>state verification</li>
059 * <li>property value query</li>
060 * </ul>
061 * This class is intended for internal use only. Please use the classes in the package
062 * <code>{@link org.fest.swing.fixture}</code> in your tests.
063 *
064 * @author Alex Ruiz
065 */
066 public class ComponentDriver {
067
068 private static final String ENABLED_PROPERTY = "enabled";
069 private static final String SIZE_PROPERTY = "size";
070 private static final String VISIBLE_PROPERTY = "visible";
071
072 protected final Robot robot;
073
074 private final ComponentDragAndDrop dragAndDrop;
075
076 /**
077 * Creates a new </code>{@link ComponentDriver}</code>.
078 * @param robot the robot to use to simulate user input.
079 */
080 public ComponentDriver(Robot robot) {
081 this.robot = robot;
082 this.dragAndDrop = new ComponentDragAndDrop(robot);
083 }
084
085 /**
086 * Simulates a user clicking once the given <code>{@link Component}</code> using the left mouse button.
087 * @param c the <code>Component</code> to click on.
088 * @throws IllegalStateException if the <code>Component</code> is disabled.
089 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
090 */
091 @RunsInEDT
092 public void click(Component c) {
093 assertIsEnabledAndShowing(c);
094 robot.click(c);
095 }
096
097 /**
098 * Simulates a user clicking once the given <code>{@link Component}</code> using the given mouse button.
099 * @param c the <code>Component</code> to click on.
100 * @param button the mouse button to use.
101 * @throws NullPointerException if the given <code>MouseButton</code> is <code>null</code>.
102 * @throws IllegalStateException if the <code>Component</code> is disabled.
103 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
104 */
105 @RunsInEDT
106 public void click(Component c, MouseButton button) {
107 click(c, button, 1);
108 }
109
110 /**
111 * Simulates a user clicking the given mouse button, the given times on the given <code>{@link Component}</code>.
112 * @param c the <code>Component</code> to click on.
113 * @param mouseClickInfo specifies the button to click and the times the button should be clicked.
114 * @throws NullPointerException if the given <code>MouseClickInfo</code> is <code>null</code>.
115 * @throws IllegalStateException if the <code>Component</code> is disabled.
116 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
117 */
118 @RunsInEDT
119 public void click(Component c, MouseClickInfo mouseClickInfo) {
120 if (mouseClickInfo == null) throw new NullPointerException("The given MouseClickInfo should not be null");
121 click(c, mouseClickInfo.button(), mouseClickInfo.times());
122 }
123
124 /**
125 * Simulates a user double-clicking the given <code>{@link Component}</code>.
126 * @param c the <code>Component</code> to click on.
127 * @throws IllegalStateException if the <code>Component</code> is disabled.
128 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
129 */
130 @RunsInEDT
131 public void doubleClick(Component c) {
132 click(c, LEFT_BUTTON, 2);
133 }
134
135 /**
136 * Simulates a user right-clicking the given <code>{@link Component}</code>.
137 * @param c the <code>Component</code> to click on.
138 * @throws IllegalStateException if the <code>Component</code> is disabled.
139 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
140 */
141 @RunsInEDT
142 public void rightClick(Component c) {
143 click(c, RIGHT_BUTTON);
144 }
145
146 /**
147 * Simulates a user clicking the given mouse button, the given times on the given <code>{@link Component}</code>.
148 * @param c the <code>Component</code> to click on.
149 * @param button the mouse button to click.
150 * @param times the number of times to click the given mouse button.
151 * @throws NullPointerException if the given <code>MouseButton</code> is <code>null</code>.
152 * @throws IllegalStateException if the <code>Component</code> is disabled.
153 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
154 */
155 @RunsInEDT
156 public void click(Component c, MouseButton button, int times) {
157 if (button == null) throw new NullPointerException("The given MouseButton should not be null");
158 assertIsEnabledAndShowing(c);
159 robot.click(c, button, times);
160 }
161
162 /**
163 * Simulates a user clicking at the given position on the given <code>{@link Component}</code>.
164 * @param c the <code>Component</code> to click on.
165 * @param where the position where to click.
166 * @throws IllegalStateException if the <code>Component</code> is disabled.
167 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
168 */
169 @RunsInEDT
170 public void click(Component c, Point where) {
171 assertIsEnabledAndShowing(c);
172 robot.click(c, where);
173 }
174
175 protected Settings settings() {
176 return robot.settings();
177 }
178
179 /**
180 * Asserts that the size of the <code>{@link Component}</code> is equal to given one.
181 * @param c the target component.
182 * @param size the given size to match.
183 * @throws AssertionError if the size of the <code>Window</code> is not equal to the given size.
184 */
185 @RunsInEDT
186 public void requireSize(Component c, Dimension size) {
187 assertThat(sizeOf(c)).as(propertyName(c, SIZE_PROPERTY)).isEqualTo(size);
188 }
189
190 /**
191 * Asserts that the <code>{@link Component}</code> is visible.
192 * @param c the target component.
193 * @throws AssertionError if the <code>Component</code> is not visible.
194 */
195 @RunsInEDT
196 public void requireVisible(Component c) {
197 assertThat(isVisible(c)).as(visibleProperty(c)).isTrue();
198 }
199
200 /**
201 * Asserts that the <code>{@link Component}</code> is not visible.
202 * @param c the target component.
203 * @throws AssertionError if the <code>Component</code> is visible.
204 */
205 @RunsInEDT
206 public void requireNotVisible(Component c) {
207 assertThat(isVisible(c)).as(visibleProperty(c)).isFalse();
208 }
209
210 @RunsInEDT
211 private static Description visibleProperty(Component c) {
212 return propertyName(c, VISIBLE_PROPERTY);
213 }
214
215 /**
216 * Asserts that the <code>{@link Component}</code> has input focus.
217 * @param c the target component.
218 * @throws AssertionError if the <code>Component</code> does not have input focus.
219 */
220 @RunsInEDT
221 public void requireFocused(Component c) {
222 assertThat(hasFocus(c)).as(requiredFocusedErrorMessage(c)).isTrue();
223 }
224
225 private static Description requiredFocusedErrorMessage(final Component c) {
226 return new GuiLazyLoadingDescription() {
227 protected String loadDescription() {
228 return concat("Expected component ", format(c), " to have input focus");
229 }
230 };
231 }
232
233 /**
234 * Asserts that the <code>{@link Component}</code> is enabled.
235 * @param c the target component.
236 * @throws AssertionError if the <code>Component</code> is disabled.
237 */
238 @RunsInEDT
239 public void requireEnabled(Component c) {
240 assertThat(isEnabled(c)).as(enabledProperty(c)).isTrue();
241 }
242
243 /**
244 * Asserts that the <code>{@link Component}</code> is enabled.
245 * @param c the target component.
246 * @param timeout the time this fixture will wait for the component to be enabled.
247 * @throws WaitTimedOutError if the <code>Component</code> is never enabled.
248 */
249 @RunsInEDT
250 public void requireEnabled(Component c, Timeout timeout) {
251 pause(untilIsEnabled(c), timeout);
252 }
253
254 /**
255 * Asserts that the <code>{@link Component}</code> is disabled.
256 * @param c the target component.
257 * @throws AssertionError if the <code>Component</code> is enabled.
258 */
259 @RunsInEDT
260 public void requireDisabled(Component c) {
261 assertThat(isEnabled(c)).as(enabledProperty(c)).isFalse();
262 }
263
264 @RunsInEDT
265 private static Description enabledProperty(Component c) {
266 return propertyName(c, ENABLED_PROPERTY);
267 }
268
269 /**
270 * Simulates a user pressing and releasing the given keys on the <code>{@link Component}</code>.
271 * @param c the target component.
272 * @param keyCodes one or more codes of the keys to press.
273 * @throws NullPointerException if the given array of codes is <code>null</code>.
274 * @throws IllegalStateException if the <code>Component</code> is disabled.
275 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
276 * @throws IllegalArgumentException if the given code is not a valid key code.
277 * @see java.awt.event.KeyEvent
278 */
279 @RunsInEDT
280 public void pressAndReleaseKeys(Component c, int... keyCodes) {
281 if (keyCodes == null) throw new NullPointerException("The array of key codes should not be null");
282 assertIsEnabledAndShowing(c);
283 focusAndWaitForFocusGain(c);
284 robot.pressAndReleaseKeys(keyCodes);
285 }
286
287 /**
288 * Simulates a user pressing and releasing the given key on the <code>{@link Component}</code>. Modifiers is a
289 * mask from the available <code>{@link java.awt.event.InputEvent}</code> masks.
290 * @param c the target component.
291 * @param keyPressInfo specifies the key and modifiers to press.
292 * @throws NullPointerException if the given <code>KeyPressInfo</code> is <code>null</code>.
293 * @throws IllegalArgumentException if the given code is not a valid key code.
294 * @throws IllegalStateException if the <code>Component</code> is disabled.
295 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
296 * @see java.awt.event.KeyEvent
297 * @see java.awt.event.InputEvent
298 */
299 @RunsInEDT
300 public void pressAndReleaseKey(Component c, KeyPressInfo keyPressInfo) {
301 if (keyPressInfo == null) throw new NullPointerException("The given KeyPressInfo should not be null");
302 pressAndReleaseKey(c, keyPressInfo.keyCode(), keyPressInfo.modifiers());
303 }
304
305 /**
306 * Simulates a user pressing and releasing the given key on the <code>{@link Component}</code>. Modifiers is a
307 * mask from the available <code>{@link java.awt.event.InputEvent}</code> masks.
308 * @param c the target component.
309 * @param keyCode the code of the key to press.
310 * @param modifiers the given modifiers.
311 * @throws IllegalArgumentException if the given code is not a valid key code. *
312 * @throws IllegalStateException if the <code>Component</code> is disabled.
313 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
314 * @see java.awt.event.KeyEvent
315 * @see java.awt.event.InputEvent
316 */
317 @RunsInEDT
318 public void pressAndReleaseKey(Component c, int keyCode, int[] modifiers) {
319 focusAndWaitForFocusGain(c);
320 robot.pressAndReleaseKey(keyCode, modifiers);
321 }
322
323 /**
324 * Simulates a user pressing given key on the <code>{@link Component}</code>.
325 * @param c the target component.
326 * @param keyCode the code of the key to press.
327 * @throws IllegalArgumentException if the given code is not a valid key code.
328 * @throws IllegalStateException if the <code>Component</code> is disabled.
329 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
330 * @see java.awt.event.KeyEvent
331 */
332 @RunsInEDT
333 public void pressKey(Component c, int keyCode) {
334 focusAndWaitForFocusGain(c);
335 robot.pressKey(keyCode);
336 }
337
338 /**
339 * Simulates a user releasing the given key on the <code>{@link Component}</code>.
340 * @param c the target component.
341 * @param keyCode the code of the key to release.
342 * @throws IllegalArgumentException if the given code is not a valid key code.
343 * @throws IllegalStateException if the <code>Component</code> is disabled.
344 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
345 * @see java.awt.event.KeyEvent
346 */
347 @RunsInEDT
348 public void releaseKey(Component c, int keyCode) {
349 focusAndWaitForFocusGain(c);
350 robot.releaseKey(keyCode);
351 }
352
353 /**
354 * Gives input focus to the given <code>{@link Component}</code> and waits until the <code>{@link Component}</code>
355 * has focus.
356 * @param c the component to give focus to.
357 * @throws IllegalStateException if the <code>Component</code> is disabled.
358 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
359 */
360 @RunsInEDT
361 public void focusAndWaitForFocusGain(Component c) {
362 assertIsEnabledAndShowing(c);
363 robot.focusAndWaitForFocusGain(c);
364 }
365
366 /**
367 * Gives input focus to the given <code>{@link Component}</code>. Note that the component may not yet have focus when
368 * this method returns.
369 * @param c the component to give focus to.
370 * @throws IllegalStateException if the <code>Component</code> is disabled.
371 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
372 */
373 @RunsInEDT
374 public void focus(Component c) {
375 assertIsEnabledAndShowing(c);
376 robot.focus(c);
377 }
378
379 /**
380 * Performs a drag action at the given point.
381 * @param c the target component.
382 * @param where the point where to start the drag action.
383 */
384 @RunsInEDT
385 protected final void drag(Component c, Point where) {
386 dragAndDrop.drag(c, where);
387 }
388
389 /**
390 * Ends a drag operation, releasing the mouse button over the given target location.
391 * <p>
392 * This method is tuned for native drag/drop operations, so if you get odd behavior, you might try using a simple
393 * <code>{@link Robot#moveMouse(Component, int, int)}</code> and <code>{@link Robot#releaseMouseButtons()}</code>.
394 * @param c the target component.
395 * @param where the point where the drag operation ends.
396 * @throws ActionFailedException if there is no drag action in effect.
397 */
398 @RunsInEDT
399 protected final void drop(Component c, Point where) {
400 dragAndDrop.drop(c, where);
401 }
402
403 /**
404 * Move the mouse appropriately to get from the source to the destination. Enter/exit events will be generated where
405 * appropriate.
406 * @param c the target component.
407 * @param where the point to drag over.
408 */
409 protected final void dragOver(Component c, Point where) {
410 dragAndDrop.dragOver(c, where);
411 }
412
413 /**
414 * Performs the <code>{@link AccessibleAction}</code> in the given <code>{@link Component}</code>'s event queue.
415 * <p>
416 * <b>Note:</b> This method is <b>not</b> guaranteed to be executed in the event dispatch thread (EDT.) Clients are
417 * responsible for calling this method from the EDT.
418 * </p>
419 * @param c the given <code>Component</code>.
420 * @throws ActionFailedException if <code>action</code> is <code>null</code> or empty.
421 */
422 @RunsInCurrentThread
423 protected final void performAccessibleActionOf(Component c) {
424 performDefaultAccessibleAction(c);
425 robot.waitForIdle();
426 }
427
428 /**
429 * Wait the given number of milliseconds for the <code>{@link Component}</code> to be showing and ready. Returns
430 * <code>false</code> if the operation times out.
431 * <p>
432 * <b>Note:</b> This method is <b>not</b> guaranteed to be executed in the event dispatch thread (EDT.) Clients are
433 * responsible for calling this method from the EDT.
434 * </p>
435 * @param c the given <code>Component</code>.
436 * @param timeout the time in milliseconds to wait for the <code>Component</code> to be showing and ready.
437 * @return <code>true</code> if the <code>Component</code> is showing and ready, <code>false</code> otherwise.
438 */
439 @RunsInCurrentThread
440 protected final boolean waitForShowing(Component c, long timeout) {
441 // TODO test
442 if (robot.isReadyForInput(c)) return true;
443 TimeoutWatch watch = startWatchWithTimeoutOf(timeout);
444 while (!robot.isReadyForInput(c)) {
445 if (c instanceof JPopupMenu) {
446 // move the mouse over the parent menu item to ensure the sub-menu shows
447 Component invoker = ((JPopupMenu)c).getInvoker();
448 if (invoker instanceof JMenu) robot.jitter(invoker);
449 }
450 if (watch.isTimeOut()) return false;
451 pause();
452 }
453 return true;
454 }
455
456 /**
457 * Shows a pop-up menu using the given <code>{@link Component}</code> as the invoker of the pop-up menu.
458 * @param c the invoker of the <code>JPopupMenu</code>.
459 * @return the displayed pop-up menu.
460 * @throws IllegalStateException if the given <code>Component</code> is disabled.
461 * @throws IllegalStateException if the given <code>Component</code> is not showing on the screen.
462 * @throws ComponentLookupException if a pop-up menu cannot be found.
463 */
464 @RunsInEDT
465 public JPopupMenu invokePopupMenu(Component c) {
466 assertIsEnabledAndShowing(c);
467 return robot.showPopupMenu(c);
468 }
469
470 /**
471 * Shows a pop-up menu at the given point using the given <code>{@link Component}</code> as the invoker of the pop-up
472 * menu.
473 * @param c the invoker of the <code>JPopupMenu</code>.
474 * @param p the given point where to show the pop-up menu.
475 * @return the displayed pop-up menu.
476 * @throws NullPointerException if the given point is <code>null</code>.
477 * @throws IllegalStateException if the given <code>Component</code> is disabled.
478 * @throws IllegalStateException if the given <code>Component</code> is not showing on the screen.
479 * @throws ComponentLookupException if a pop-up menu cannot be found.
480 */
481 @RunsInEDT
482 public JPopupMenu invokePopupMenu(Component c, Point p) {
483 if (p == null) throw new NullPointerException("The given point should not be null");
484 assertIsEnabledAndShowing(c);
485 return robot.showPopupMenu(c, p);
486 }
487
488 /**
489 * Validates that the given <code>{@link Component}</code> is enabled and showing on the screen. This method is
490 * executed in the event dispatch thread.
491 * @param c the <code>Component</code> to check.
492 * @throws IllegalStateException if the <code>Component</code> is disabled.
493 * @throws IllegalStateException if the <code>Component</code> is not showing on the screen.
494 */
495 @RunsInEDT
496 protected static void assertIsEnabledAndShowing(final Component c) {
497 execute(new GuiTask() {
498 protected void executeInEDT() {
499 validateIsEnabledAndShowing(c);
500 }
501 });
502 }
503
504 /**
505 * Formats the name of a property of the given <code>{@link Component}</code> by concatenating the value obtained
506 * from <code>{@link Formatting#format(Component)}</code> with the given property name.
507 * @param c the given <code>Component</code>.
508 * @param propertyName the name of the property.
509 * @return the description of a property belonging to a <code>Component</code>.
510 * @see ComponentFormatter
511 * @see Formatting#format(Component)
512 */
513 @RunsInEDT
514 public static Description propertyName(final Component c, final String propertyName) {
515 return new GuiLazyLoadingDescription() {
516 protected String loadDescription() {
517 return concat(format(c), " - property:", quote(propertyName));
518 }
519 };
520 }
521
522 /**
523 * Simulates a user moving the mouse pointer to the given coordinates relative to the given
524 * <code>{@link Component}</code>. This method will <b>not</b> throw any exceptions if the it was not possible to
525 * move the mouse pointer.
526 * @param c the given <code>Component</code>.
527 * @param p coordinates relative to the given <code>Component</code>.
528 */
529 @RunsInEDT
530 protected final void moveMouseIgnoringAnyError(Component c, Point p) {
531 moveMouseIgnoringAnyError(c, p.x, p.y);
532 }
533
534 /**
535 * Simulates a user moving the mouse pointer to the given coordinates relative to the given
536 * <code>{@link Component}</code>. This method will <b>not</b> throw any exceptions if the it was not possible to
537 * move the mouse pointer.
538 * @param c the given <code>Component</code>.
539 * @param x horizontal coordinate relative to the given <code>Component</code>.
540 * @param y vertical coordinate relative to the given <code>Component</code>.
541 */
542 @RunsInEDT
543 protected final void moveMouseIgnoringAnyError(Component c, int x, int y) {
544 try {
545 robot.moveMouse(c, x, y);
546 } catch (RuntimeException ignored) {}
547 }
548 }