001    /*
002     * Created on Jan 21, 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.lang.Math.max;
018    import static java.lang.Math.min;
019    import static java.lang.String.valueOf;
020    import static javax.swing.text.DefaultEditorKit.deletePrevCharAction;
021    import static org.fest.assertions.Assertions.assertThat;
022    import static org.fest.swing.driver.ComponentStateValidator.validateIsEnabledAndShowing;
023    import static org.fest.swing.driver.JTextComponentEditableQuery.isEditable;
024    import static org.fest.swing.driver.JTextComponentSelectAllTask.selectAllText;
025    import static org.fest.swing.driver.JTextComponentSelectTextTask.selectTextInRange;
026    import static org.fest.swing.driver.JTextComponentSetTextTask.setTextIn;
027    import static org.fest.swing.driver.PointAndParentForScrollingJTextFieldQuery.pointAndParentForScrolling;
028    import static org.fest.swing.driver.TextAssert.verifyThat;
029    import static org.fest.swing.edt.GuiActionRunner.execute;
030    import static org.fest.swing.exception.ActionFailedException.actionFailure;
031    import static org.fest.swing.format.Formatting.format;
032    import static org.fest.util.Strings.*;
033    
034    import java.awt.*;
035    import java.util.regex.Pattern;
036    
037    import javax.swing.*;
038    import javax.swing.text.BadLocationException;
039    import javax.swing.text.JTextComponent;
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.Robot;
045    import org.fest.swing.edt.GuiQuery;
046    import org.fest.swing.edt.GuiTask;
047    import org.fest.swing.exception.ActionFailedException;
048    import org.fest.swing.util.Pair;
049    
050    /**
051     * Understands functional testing of <code>{@link JTextComponent}</code>s:
052     * <ul>
053     * <li>user input simulation</li>
054     * <li>state verification</li>
055     * <li>property value query</li>
056     * </ul>
057     * This class is intended for internal use only. Please use the classes in the package
058     * <code>{@link org.fest.swing.fixture}</code> in your tests.
059     *
060     * @author Alex Ruiz
061     */
062    public class JTextComponentDriver extends JComponentDriver implements TextDisplayDriver<JTextComponent> {
063    
064      private static final String EDITABLE_PROPERTY = "editable";
065      private static final String TEXT_PROPERTY = "text";
066    
067      /**
068       * Creates a new </code>{@link JTextComponentDriver}</code>.
069       * @param robot the robot to use to simulate user input.
070       */
071      public JTextComponentDriver(Robot robot) {
072        super(robot);
073      }
074    
075      /**
076       * Deletes the text of the <code>{@link JTextComponent}</code>.
077       * @param textBox the target <code>JTextComponent</code>.
078       * @throws IllegalStateException if the <code>JTextComponent</code> is disabled.
079       * @throws IllegalStateException if the <code>JTextComponent</code> is not showing on the screen.
080       */
081      @RunsInEDT
082      public void deleteText(JTextComponent textBox) {
083        selectAll(textBox);
084        invokeAction(textBox, deletePrevCharAction);
085      }
086    
087      /**
088       * Types the given text into the <code>{@link JTextComponent}</code>, replacing any existing text already there.
089       * @param textBox the target <code>JTextComponent</code>.
090       * @param text the text to enter.
091       * @throws NullPointerException if the text to enter is <code>null</code>.
092       * @throws IllegalArgumentException if the text to enter is empty.
093       * @throws IllegalStateException if the <code>JTextComponent</code> is disabled.
094       * @throws IllegalStateException if the <code>JTextComponent</code> is not showing on the screen.
095       */
096      @RunsInEDT
097      public void replaceText(JTextComponent textBox, String text) {
098        if (text == null) throw new NullPointerException("The text to enter should not be null");
099        if (isEmpty(text)) throw new IllegalArgumentException("The text to enter should not be empty");
100        selectAll(textBox);
101        enterText(textBox, text);
102      }
103    
104      /**
105       * Selects the text in the <code>{@link JTextComponent}</code>.
106       * @param textBox the target <code>JTextComponent</code>.
107       * @throws IllegalStateException if the <code>JTextComponent</code> is disabled.
108       * @throws IllegalStateException if the <code>JTextComponent</code> is not showing on the screen.
109       */
110      @RunsInEDT
111      public void selectAll(JTextComponent textBox) {
112        validateAndScrollToPosition(textBox, 0);
113        selectAllText(textBox);
114      }
115    
116      /**
117       * Types the given text into the <code>{@link JTextComponent}</code>.
118       * @param textBox the target <code>JTextComponent</code>.
119       * @param text the text to enter.
120       * @throws IllegalStateException if the <code>JTextComponent</code> is disabled.
121       * @throws IllegalStateException if the <code>JTextComponent</code> is not showing on the screen.
122       */
123      @RunsInEDT
124      public void enterText(JTextComponent textBox, String text) {
125        focusAndWaitForFocusGain(textBox);
126        robot.enterText(text);
127      }
128    
129      /**
130       * Sets the given text into the <code>{@link JTextComponent}</code>. Unlike
131       * <code>{@link #enterText(JTextComponent, String)}</code>, this method bypasses the event system and allows immediate
132       * updating on the underlying document model.
133       * <p>
134       * Primarily desired for speeding up tests when precise user event fidelity isn't necessary.
135       * </p>
136       * @param textBox the target <code>JTextComponent</code>.
137       * @param text the text to enter.
138       * @throws IllegalStateException if the <code>JTextComponent</code> is disabled.
139       * @throws IllegalStateException if the <code>JTextComponent</code> is not showing on the screen.
140       */
141      @RunsInEDT
142      public void setText(JTextComponent textBox, String text) {
143        focusAndWaitForFocusGain(textBox);
144        setTextIn(textBox, text);
145        robot.waitForIdle();
146      }
147    
148      /**
149       * Select the given text range.
150       * @param textBox the target <code>JTextComponent</code>.
151       * @param text the text to select.
152       * @throws IllegalStateException if the <code>JTextComponent</code> is disabled.
153       * @throws IllegalStateException if the <code>JTextComponent</code> is not showing on the screen.
154       * @throws IllegalArgumentException if the <code>JTextComponent</code> does not contain the given text to select.
155       * @throws ActionFailedException if selecting the text fails.
156       */
157      @RunsInEDT
158      public void selectText(JTextComponent textBox, String text) {
159        int indexFound = indexOfText(textBox, text);
160        if (indexFound == -1) throw new IllegalArgumentException(concat("The text ", quote(text), " was not found"));
161        selectText(textBox, indexFound, indexFound + text.length());
162      }
163    
164      @RunsInEDT
165      private static int indexOfText(final JTextComponent textBox, final String text) {
166        return execute(new GuiQuery<Integer>() {
167          protected Integer executeInEDT() {
168            validateIsEnabledAndShowing(textBox);
169            String actualText = textBox.getText();
170            if (isEmpty(actualText)) return -1;
171            return actualText.indexOf(text);
172          }
173        });
174      }
175    
176      /**
177       * Select the given text range.
178       * @param textBox the target <code>JTextComponent</code>.
179       * @param start the starting index of the selection.
180       * @param end the ending index of the selection.
181       * @throws IllegalStateException if the <code>JTextComponent</code> is disabled.
182       * @throws IllegalStateException if the <code>JTextComponent</code> is not showing on the screen.
183       * @throws ActionFailedException if selecting the text in the given range fails.
184       */
185      @RunsInEDT
186      public void selectText(JTextComponent textBox, int start, int end) {
187        robot.moveMouse(textBox, validateAndScrollToPosition(textBox, start));
188        robot.moveMouse(textBox, scrollToPosition(textBox, end));
189        performAndValidateTextSelection(textBox, start, end);
190      }
191    
192      @RunsInEDT
193      private static Point validateAndScrollToPosition(final JTextComponent textBox, final int index) {
194        return execute(new GuiQuery<Point>() {
195          protected Point executeInEDT() {
196            validateIsEnabledAndShowing(textBox);
197            return scrollToVisible(textBox, index);
198          }
199        });
200      }
201    
202      @RunsInEDT
203      private static Point scrollToPosition(final JTextComponent textBox, final int index) {
204        return execute(new GuiQuery<Point>() {
205          protected Point executeInEDT() {
206            return scrollToVisible(textBox, index);
207          }
208        });
209      }
210    
211      /**
212       * Move the pointer to the location of the given index. Takes care of auto-scrolling through text.
213       * @param textBox the target <code>JTextComponent</code>.
214       * @param index the given location.
215       * @return the position of the pointer after being moved.
216       * @throws ActionFailedException if it was not possible to scroll to the location of the given index.
217       */
218      @RunsInCurrentThread
219      private static Point scrollToVisible(JTextComponent textBox, int index) {
220        Rectangle indexLocation = locationOf(textBox, index);
221        if (isRectangleVisible(textBox, indexLocation)) return centerOf(indexLocation);
222        scrollToVisible(textBox, indexLocation);
223        indexLocation = locationOf(textBox, index);
224        if (isRectangleVisible(textBox, indexLocation)) return centerOf(indexLocation);
225        throw actionFailure(concat(
226            "Unable to make visible the location of the index '", valueOf(index),
227            "' by scrolling the point (", formatOriginOf(indexLocation), ") on ", format(textBox)));
228      }
229    
230      @RunsInCurrentThread
231      private static Rectangle locationOf(JTextComponent textBox, int index) {
232        Rectangle r = null;
233        try {
234          r = textBox.modelToView(index);
235        } catch (BadLocationException e) {
236          throw cannotGetLocation(textBox, index);
237        }
238        if (r != null) return r;
239        throw cannotGetLocation(textBox, index);
240      }
241    
242      private static ActionFailedException cannotGetLocation(JTextComponent textBox, int index) {
243        throw actionFailure(concat("Unable to get location for index '", valueOf(index), "' in ", format(textBox)));
244      }
245    
246      @RunsInCurrentThread
247      private static boolean isRectangleVisible(JTextComponent textBox, Rectangle r) {
248        Rectangle visible = textBox.getVisibleRect();
249        return visible.contains(r.x, r.y);
250      }
251    
252      private static String formatOriginOf(Rectangle r) {
253        return concat(valueOf(r.x), ",", valueOf(r.y));
254      }
255    
256      @RunsInCurrentThread
257      private static void scrollToVisible(JTextComponent textBox, Rectangle r) {
258        textBox.scrollRectToVisible(r);
259        if (isVisible(textBox, r)) return;
260        scrollToVisibleIfIsTextField(textBox, r);
261      }
262    
263      @RunsInCurrentThread
264      private static void scrollToVisibleIfIsTextField(JTextComponent textBox, Rectangle r) {
265        if (!(textBox instanceof JTextField)) return;
266        Pair<Point, Container> pointAndParent = pointAndParentForScrolling((JTextField)textBox);
267        Container parent = pointAndParent.ii;
268        if (parent == null || parent instanceof CellRendererPane || !(parent instanceof JComponent)) return;
269        ((JComponent)parent).scrollRectToVisible(addPointToRectangle(pointAndParent.i, r));
270      }
271    
272      private static Rectangle addPointToRectangle(Point p, Rectangle r) {
273        Rectangle destination = new Rectangle(r);
274        destination.x += p.x;
275        destination.y += p.y;
276        return destination;
277      }
278    
279      private static Point centerOf(Rectangle r) {
280        return new Point(r.x + r.width / 2, r.y + r.height / 2);
281      }
282    
283      @RunsInEDT
284      private static void performAndValidateTextSelection(final JTextComponent textBox, final int start, final int end) {
285        execute(new GuiTask() {
286          protected void executeInEDT() {
287            selectTextInRange(textBox, start, end);
288            verifyTextWasSelected(textBox, start, end);
289          }
290        });
291      }
292    
293      @RunsInCurrentThread
294      private static void verifyTextWasSelected(JTextComponent textBox, int start, int end) {
295        int actualStart = textBox.getSelectionStart();
296        int actualEnd = textBox.getSelectionEnd();
297        if (actualStart == min(start, end) && actualEnd == max(start, end)) return;
298        throw actionFailure(concat(
299            "Unable to select text using indices '", valueOf(start), "' and '", valueOf(end),
300            ", current selection starts at '", valueOf(actualStart), "' and ends at '", valueOf(actualEnd), "'"));
301      }
302    
303      /**
304       * Asserts that the text in the given <code>{@link JTextComponent}</code> is equal to the specified value.
305       * @param textBox the given <code>JTextComponent</code>.
306       * @param expected the text to match. It can be a regular expression pattern.
307       * @throws AssertionError if the text of the <code>JTextComponent</code> is not equal to the given one.
308       */
309      @RunsInEDT
310      public void requireText(JTextComponent textBox, String expected) {
311        verifyThat(textOf(textBox)).as(textProperty(textBox)).isEqualOrMatches(expected);
312      }
313    
314      /**
315       * Asserts that the text in the given <code>{@link JTextComponent}</code> matches the given regular expression
316       * pattern.
317       * @param textBox the given <code>JTextComponent</code>.
318       * @param pattern the regular expression pattern to match.
319       * @throws NullPointerException if the given regular expression pattern is <code>null</code>.
320       * @throws AssertionError if the text of the <code>JTextComponent</code> is not equal to the given one.
321       * @since 1.2
322       */
323      @RunsInEDT
324      public void requireText(JTextComponent textBox, Pattern pattern) {
325        verifyThat(textOf(textBox)).as(textProperty(textBox)).matches(pattern);
326      }
327    
328      /**
329       * Asserts that the given <code>{@link JTextComponent}</code> is empty.
330       * @param textBox the given <code>JTextComponent</code>.
331       * @throws AssertionError if the <code>JTextComponent</code> is not empty.
332       */
333      @RunsInEDT
334      public void requireEmpty(JTextComponent textBox) {
335        assertThat(textOf(textBox)).as(textProperty(textBox)).isEmpty();
336      }
337    
338      @RunsInEDT
339      private static Description textProperty(JTextComponent textBox) {
340        return propertyName(textBox, TEXT_PROPERTY);
341      }
342    
343      /**
344       * Asserts that the given <code>{@link JTextComponent}</code> is editable.
345       * @param textBox the given <code>JTextComponent</code>.
346       * @throws AssertionError if the <code>JTextComponent</code> is not editable.
347       */
348      @RunsInEDT
349      public void requireEditable(JTextComponent textBox) {
350        assertEditable(textBox, true);
351      }
352    
353      /**
354       * Asserts that the given <code>{@link JTextComponent}</code> is not editable.
355       * @param textBox the given <code>JTextComponent</code>.
356       * @throws AssertionError if the <code>JTextComponent</code> is editable.
357       */
358      @RunsInEDT
359      public void requireNotEditable(JTextComponent textBox) {
360        assertEditable(textBox, false);
361      }
362    
363      @RunsInEDT
364      private void assertEditable(JTextComponent textBox, boolean editable) {
365        assertThat(isEditable(textBox)).as(editableProperty(textBox)).isEqualTo(editable);
366      }
367    
368      @RunsInEDT
369      private static Description editableProperty(JTextComponent textBox) {
370        return propertyName(textBox, EDITABLE_PROPERTY);
371      }
372    
373      /**
374       * Returns the text of the given <code>{@link JTextComponent}</code>.
375       * @param textBox the given <code>JTextComponent</code>.
376       * @return the text of the given <code>JTextComponent</code>.
377       * @since 1.2
378       */
379      @RunsInEDT
380      public String textOf(JTextComponent textBox) {
381        return JTextComponentTextQuery.textOf(textBox);
382      }
383    
384    }