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 in compliance with
005     * 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 is distributed on
010     * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
011     * specific language governing permissions and limitations under the License.
012     *
013     * Copyright @2008-2010 the original author or authors.
014     */
015    package org.fest.swing.driver;
016    
017    import static java.awt.event.KeyEvent.VK_UNDEFINED;
018    import static org.fest.swing.driver.Actions.findActionKey;
019    import static org.fest.swing.driver.JComponentToolTipQuery.toolTipOf;
020    import static org.fest.swing.driver.KeyStrokes.findKeyStrokesForAction;
021    import static org.fest.swing.driver.TextAssert.verifyThat;
022    import static org.fest.swing.edt.GuiActionRunner.execute;
023    import static org.fest.swing.exception.ActionFailedException.actionFailure;
024    import static org.fest.util.Strings.concat;
025    import static org.fest.util.Strings.quote;
026    
027    import java.awt.Point;
028    import java.awt.Rectangle;
029    import java.util.regex.Pattern;
030    
031    import javax.swing.JComponent;
032    import javax.swing.KeyStroke;
033    
034    import org.fest.swing.annotation.RunsInCurrentThread;
035    import org.fest.swing.annotation.RunsInEDT;
036    import org.fest.swing.core.Robot;
037    import org.fest.swing.edt.GuiQuery;
038    import org.fest.swing.exception.ActionFailedException;
039    
040    /**
041     * Understands functional testing of <code>{@link JComponent}</code>s:
042     * <ul>
043     * <li>user input simulation</li>
044     * <li>state verification</li>
045     * <li>property value query</li>
046     * </ul>
047     * This class is intended for internal use only. Please use the classes in the package
048     * <code>{@link org.fest.swing.fixture}</code> in your tests.
049     *
050     * @author Alex Ruiz
051     * @author Yvonne Wang
052     */
053    public class JComponentDriver extends ContainerDriver {
054    
055      private static final String TOOL_TIP_TEXT_PROPERTY = "toolTipText";
056    
057      /**
058       * Creates a new </code>{@link JComponentDriver}</code>.
059       * @param robot the robot the robot to use to simulate user input.
060       */
061      public JComponentDriver(Robot robot) {
062        super(robot);
063      }
064    
065      /**
066       * Invoke <code>{@link JComponent#scrollRectToVisible(Rectangle)}</code> on the given <code>{@link JComponent}</code>.
067       * <p>
068       * <b>Note:</b> This method is <b>not</b> guaranteed to be executed in the event dispatch thread (EDT.) Clients are
069       * responsible for calling this method from the EDT.
070       * </p>
071       * @param c the given <code>JComponent</code>.
072       * @param r the visible <code>Rectangle</code>.
073       */
074      @RunsInCurrentThread
075      protected final void scrollToVisible(JComponent c, Rectangle r) {
076        // From Abbot:
077        // Ideally, we'd use scrollBar commands to effect the scrolling, but that gets really complicated for no real gain
078        // in function. Fortunately, Swing's Scrollable makes for a simple solution.
079        // NOTE: absolutely MUST wait for idle in order for the scroll to finish, and the UI to update so that the next
080        // action goes to the proper location within the scrolled component.
081        c.scrollRectToVisible(r);
082      }
083    
084      /**
085       * Indicates whether the given <code>{@link JComponent}</code>'s visible <code>{@link Rectangle}</code> contains the
086       * given one.
087       * <p>
088       * <b>Note:</b> This method is <b>not</b> guaranteed to be executed in the event dispatch thread (EDT.) Clients are
089       * responsible for calling this method from the EDT.
090       * </p>
091       * @param c the given <code>JComponent</code>.
092       * @param r the <code>Rectangle</code> to verify.
093       * @return <code>true</code> if the given <code>Rectangle</code> is contained in the given <code>JComponent</code>'s
094       *         visible <code>Rectangle</code>.
095       */
096      @RunsInCurrentThread
097      protected static boolean isVisible(JComponent c, Rectangle r) {
098        return c.getVisibleRect().contains(r);
099      }
100    
101      /**
102       * Indicates whether the given <code>{@link JComponent}</code>'s visible <code>{@link Rectangle}</code> contains
103       * the given <code>{@link Point}</code>.
104       * <p>
105       * <b>Note:</b> This method is <b>not</b> guaranteed to be executed in the event dispatch thread (EDT.) Clients are
106       * responsible for calling this method from the EDT.
107       * </p>
108       * @param c the given <code>JComponent</code>.
109       * @param p the <code>Point</code> to verify.
110       * @return <code>true</code> if the given <code>Point</code> is contained in the given <code>JComponent</code>'s
111       * visible <code>Rectangle</code>.
112       */
113      @RunsInCurrentThread
114      protected final boolean isVisible(JComponent c, Point p) {
115        return c.getVisibleRect().contains(p);
116      }
117    
118      /**
119       * Invoke an <code>{@link javax.swing.Action}</code> from the <code>{@link JComponent}</code>'s
120       * <code>{@link javax.swing.ActionMap}</code>.
121       * @param c the given <code>JComponent</code>.
122       * @param name the name of the <code>Action</code> to invoke.
123       * @throws ActionFailedException if an <code>Action</code> cannot be found under the given name.
124       * @throws ActionFailedException if a <code>KeyStroke</code> cannot be found for the <code>Action</code> under the
125       * given name.
126       * @throws ActionFailedException if it is not possible to type any of the found <code>KeyStroke</code>s.
127       */
128      @RunsInEDT
129      protected final void invokeAction(JComponent c, String name) {
130        robot.focusAndWaitForFocusGain(c);
131        for (KeyStroke keyStroke : keyStrokesForAction(c, name)) {
132          try {
133            type(keyStroke);
134            robot.waitForIdle();
135            return;
136          } catch (IllegalArgumentException e) { /* try the next one, if any */ }
137        }
138        throw actionFailure(concat("Unable to type any key for the action with key ", quote(name)));
139      }
140    
141      @RunsInCurrentThread
142      private static KeyStroke[] keyStrokesForAction(JComponent component, String actionName) {
143        Object key = findActionKey(actionName, component.getActionMap());
144        return findKeyStrokesForAction(actionName, key, component.getInputMap());
145      }
146    
147      private void type(KeyStroke keyStroke) {
148        if (keyStroke.getKeyCode() == VK_UNDEFINED) {
149          robot.type(keyStroke.getKeyChar());
150          return;
151        }
152        robot.pressAndReleaseKey(keyStroke.getKeyCode(), keyStroke.getModifiers());
153      }
154    
155      /**
156       * Asserts that the toolTip in the given <code>{@link JComponent}</code> matches the given value.
157       * @param c the given <code>JComponent</code>.
158       * @param expected the expected toolTip. It can be a regular expression.
159       * @throws AssertionError if the toolTip of the given <code>JComponent</code> does not match the given value.
160       * @since 1.2
161       */
162      @RunsInEDT
163      public void requireToolTip(JComponent c, String expected) {
164        verifyThat(toolTipOf(c)).as(propertyName(c, TOOL_TIP_TEXT_PROPERTY)).isEqualOrMatches(expected);
165      }
166    
167      /**
168       * Asserts that the toolTip in the given <code>{@link JComponent}</code> matches the given regular expression pattern.
169       * @param c the given <code>JComponent</code>.
170       * @param pattern the regular expression pattern to match.
171       * @throws NullPointerException if the given regular expression pattern is <code>null</code>.
172       * @throws AssertionError if the toolTip of the given <code>JComponent</code> does not match the given value.
173       * @since 1.2
174       */
175      @RunsInEDT
176      public void requireToolTip(JComponent c, Pattern pattern) {
177        verifyThat(toolTipOf(c)).as(propertyName(c, TOOL_TIP_TEXT_PROPERTY)).matches(pattern);
178      }
179    
180      /**
181       * Returns the client property stored in the given <code>{@link JComponent}</code>, under the given key.
182       * @param c the given <code>JComponent</code>.
183       * @param key the key to use to retrieve the client property.
184       * @return the value of the client property stored under the given key, or <code>null</code> if the property was
185       * not found.
186       * @throws NullPointerException if the given key is <code>null</code>.
187       * @since 1.2
188       */
189      @RunsInEDT
190      public Object clientProperty(JComponent c, Object key) {
191        if (key == null) throw new NullPointerException("The key of the client property to return should not be null");
192        return clientPropertyIn(c, key);
193      }
194    
195      private static Object clientPropertyIn(final JComponent c, final Object key) {
196        return execute(new GuiQuery<Object>() {
197          protected Object executeInEDT() {
198            return c.getClientProperty(key);
199          }
200        });
201      }
202    }