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    }