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 }