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 }