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 }