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
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 javax.swing.text.DefaultEditorKit.selectAllAction;
019 import static org.fest.assertions.Assertions.assertThat;
020 import static org.fest.assertions.Fail.fail;
021 import static org.fest.swing.driver.CommonValidations.validateCellReader;
022 import static org.fest.swing.driver.ComponentStateValidator.validateIsEnabledAndShowing;
023 import static org.fest.swing.driver.JComboBoxAccessibleEditorValidator.validateEditorIsAccessible;
024 import static org.fest.swing.driver.JComboBoxContentQuery.contents;
025 import static org.fest.swing.driver.JComboBoxEditableQuery.isEditable;
026 import static org.fest.swing.driver.JComboBoxItemCountQuery.itemCountIn;
027 import static org.fest.swing.driver.JComboBoxItemIndexValidator.validateIndex;
028 import static org.fest.swing.driver.JComboBoxMatchingItemQuery.matchingItemIndex;
029 import static org.fest.swing.driver.JComboBoxSelectedIndexQuery.selectedIndexOf;
030 import static org.fest.swing.driver.JComboBoxSelectionValueQuery.NO_SELECTION_VALUE;
031 import static org.fest.swing.driver.JComboBoxSelectionValueQuery.selection;
032 import static org.fest.swing.driver.JComboBoxSetPopupVisibleTask.setPopupVisible;
033 import static org.fest.swing.driver.JComboBoxSetSelectedIndexTask.setSelectedIndex;
034 import static org.fest.swing.driver.TextAssert.verifyThat;
035 import static org.fest.swing.edt.GuiActionRunner.execute;
036 import static org.fest.swing.exception.ActionFailedException.actionFailure;
037 import static org.fest.util.Arrays.format;
038 import static org.fest.util.Strings.concat;
039 import static org.fest.util.Strings.quote;
040
041 import java.awt.Component;
042 import java.util.regex.Pattern;
043
044 import javax.swing.*;
045
046 import org.fest.assertions.Description;
047 import org.fest.swing.annotation.RunsInCurrentThread;
048 import org.fest.swing.annotation.RunsInEDT;
049 import org.fest.swing.cell.JComboBoxCellReader;
050 import org.fest.swing.core.Robot;
051 import org.fest.swing.edt.GuiQuery;
052 import org.fest.swing.edt.GuiTask;
053 import org.fest.swing.exception.*;
054 import org.fest.swing.util.*;
055 import org.fest.util.VisibleForTesting;
056
057 /**
058 * Understands functional testing of <code>{@link JComboBox}</code>es:
059 * <ul>
060 * <li>user input simulation</li>
061 * <li>state verification</li>
062 * <li>property value query</li>
063 * </ul>
064 * This class is intended for internal use only. Please use the classes in the package
065 * <code>{@link org.fest.swing.fixture}</code> in your tests.
066 *
067 * @author Alex Ruiz
068 * @author Yvonne Wang
069 */
070 public class JComboBoxDriver extends JComponentDriver {
071
072 private static final String EDITABLE_PROPERTY = "editable";
073 private static final String SELECTED_INDEX_PROPERTY = "selectedIndex";
074
075 private final JListDriver listDriver;
076 private final JComboBoxDropDownListFinder dropDownListFinder;
077
078 private JComboBoxCellReader cellReader;
079
080 /**
081 * Creates a new </code>{@link JComboBoxDriver}</code>.
082 * @param robot the robot to use to simulate user input.
083 */
084 public JComboBoxDriver(Robot robot) {
085 super(robot);
086 listDriver = new JListDriver(robot);
087 dropDownListFinder = new JComboBoxDropDownListFinder(robot);
088 cellReader(new BasicJComboBoxCellReader());
089 }
090
091 /**
092 * Returns an array of <code>String</code>s that represents the contents of the given <code>{@link JComboBox}</code>
093 * list. The <code>String</code> representation of each element is performed using this driver's
094 * <code>{@link JComboBoxCellReader}</code>.
095 * @param comboBox the target <code>JComboBox</code>.
096 * @return an array of <code>String</code>s that represent the contents of the given <code>JComboBox</code> list.
097 * @see #value(JComboBox, int)
098 * @see #cellReader(JComboBoxCellReader)
099 */
100 @RunsInEDT
101 public String[] contentsOf(JComboBox comboBox) {
102 return contents(comboBox, cellReader);
103 }
104
105 /**
106 * Selects the first item matching the given text in the <code>{@link JComboBox}</code>. The text of the
107 * <code>JComboBox</code> items is obtained by this fixture's <code>{@link JComboBoxCellReader}</code>.
108 * @param comboBox the target <code>JComboBox</code>.
109 * @param value the value to match. It can be a regular expression.
110 * @throws LocationUnavailableException if an element matching the given value cannot be found.
111 * @throws IllegalStateException if the <code>JComboBox</code> is disabled.
112 * @throws IllegalStateException if the <code>JComboBox</code> is not showing on the screen.
113 * @see #cellReader(JComboBoxCellReader)
114 */
115 @RunsInEDT
116 public void selectItem(JComboBox comboBox, String value) {
117 selectItem(comboBox, new StringTextMatcher(value));
118 }
119
120 /**
121 * Selects the first item matching the given regular expression pattern in the <code>{@link JComboBox}</code>. The
122 * text of the <code>JComboBox</code> items is obtained by this fixture's <code>{@link JComboBoxCellReader}</code>.
123 * @param comboBox the target <code>JComboBox</code>.
124 * @param pattern the regular expression pattern to match.
125 * @throws LocationUnavailableException if an element matching the given pattern cannot be found.
126 * @throws IllegalStateException if the <code>JComboBox</code> is disabled.
127 * @throws IllegalStateException if the <code>JComboBox</code> is not showing on the screen.
128 * @throws NullPointerException if the given regular expression pattern is <code>null</code>.
129 * @see #cellReader(JComboBoxCellReader)
130 * @since 1.2
131 */
132 @RunsInEDT
133 public void selectItem(JComboBox comboBox, Pattern pattern) {
134 selectItem(comboBox, new PatternTextMatcher(pattern));
135 }
136
137 @RunsInEDT
138 private void selectItem(JComboBox comboBox, TextMatcher matcher) {
139 int index = matchingItemIndex(comboBox, matcher, cellReader);
140 if (index < 0) throw failMatchingNotFound(comboBox, matcher);
141 selectItem(comboBox, index);
142 }
143
144 private LocationUnavailableException failMatchingNotFound(JComboBox comboBox, TextMatcher matcher) {
145 throw new LocationUnavailableException(concat(
146 "Unable to find item matching the ", matcher.description(), " ", matcher.formattedValues(),
147 " among the JComboBox contents (", format(contentsOf(comboBox)), ")"));
148 }
149
150 /**
151 * Verifies that the <code>String</code> representation of the selected item in the <code>{@link JComboBox}</code>
152 * matches the given text.
153 * @param comboBox the target <code>JComboBox</code>.
154 * @param value the text to match. It can be a regular expression.
155 * @throws AssertionError if the selected item does not match the given value.
156 * @see #cellReader(JComboBoxCellReader)
157 */
158 @RunsInEDT
159 public void requireSelection(JComboBox comboBox, String value) {
160 String selection = requiredSelectionOf(comboBox);
161 verifyThat(selection).as(selectedIndexProperty(comboBox)).isEqualOrMatches(value);
162 }
163
164 /**
165 * Verifies that the <code>String</code> representation of the selected item in the <code>{@link JComboBox}</code>
166 * matches the given regular expression pattern.
167 * @param comboBox the target <code>JComboBox</code>.
168 * @param pattern the regular expression pattern to match.
169 * @throws AssertionError if the selected item does not match the given regular expression pattern.
170 * @throws NullPointerException if the given regular expression pattern is <code>null</code>.
171 * @see #cellReader(JComboBoxCellReader)
172 * @since 1.2
173 */
174 @RunsInEDT
175 public void requireSelection(JComboBox comboBox, Pattern pattern) {
176 String selection = requiredSelectionOf(comboBox);
177 verifyThat(selection).as(selectedIndexProperty(comboBox)).matches(pattern);
178 }
179
180 private String requiredSelectionOf(JComboBox comboBox) throws AssertionError {
181 Object selection = selection(comboBox, cellReader);
182 if (NO_SELECTION_VALUE == selection) throw failNoSelection(comboBox);
183 return (String)selection;
184 }
185
186 /**
187 * Verifies that the index of the selected item in the <code>{@link JComboBox}</code> is equal to the given value.
188 * @param comboBox the target <code>JComboBox</code>.
189 * @param index the expected selection index.
190 * @throws AssertionError if the selection index is not equal to the given value.
191 * @since 1.2
192 */
193 @RunsInEDT
194 public void requireSelection(JComboBox comboBox, int index) {
195 int selectedIndex = selectedIndexOf(comboBox);
196 if (selectedIndex == -1) failNoSelection(comboBox);
197 assertThat(selectedIndex).as(selectedIndexProperty(comboBox)).isEqualTo(index);
198 }
199
200 private AssertionError failNoSelection(JComboBox comboBox) {
201 throw fail(concat("[", selectedIndexProperty(comboBox).value(), "] No selection"));
202 }
203
204 /**
205 * Verifies that the <code>{@link JComboBox}</code> does not have any selection.
206 * @param comboBox the target <code>JComboBox</code>.
207 * @throws AssertionError if the <code>JComboBox</code> has a selection.
208 */
209 @RunsInEDT
210 public void requireNoSelection(JComboBox comboBox) {
211 Object selection = selection(comboBox, cellReader);
212 if (NO_SELECTION_VALUE == selection) return;
213 fail(concat(
214 "[", selectedIndexProperty(comboBox).value(), "] Expecting no selection, but found:<", quote(selection), ">"));
215 }
216
217 /**
218 * Returns the <code>String</code> representation of the element under the given index, using this driver's
219 * <code>{@link JComboBoxCellReader}</code>.
220 * @param comboBox the target <code>JComboBox</code>.
221 * @param index the given index.
222 * @return the value of the element under the given index.
223 * @throws IndexOutOfBoundsException if the given index is negative or greater than the index of the last item in the
224 * <code>JComboBox</code>.
225 * @see #cellReader(JComboBoxCellReader)
226 */
227 public String value(JComboBox comboBox, int index) {
228 return valueAsText(comboBox, index, cellReader);
229 }
230
231 @RunsInEDT
232 private static String valueAsText(final JComboBox comboBox, final int index, final JComboBoxCellReader cellReader) {
233 return execute(new GuiQuery<String>() {
234 protected String executeInEDT() {
235 validateIndex(comboBox, index);
236 return cellReader.valueAt(comboBox, index);
237 }
238 });
239 }
240
241 private Description selectedIndexProperty(JComboBox comboBox) {
242 return propertyName(comboBox, SELECTED_INDEX_PROPERTY);
243 }
244
245
246 /**
247 * Clears the selection in the given <code>{@link JComboBox}</code>. Since this method does not simulate user input,
248 * it does not verifies that the <code>JComboBox</code> is enabled and showing.
249 * @param comboBox the target <code>JComboBox</code>.
250 * @since 1.2
251 */
252 public void clearSelection(JComboBox comboBox) {
253 setSelectedIndex(comboBox, -1);
254 robot.waitForIdle();
255 }
256
257 /**
258 * Selects the item under the given index in the <code>{@link JComboBox}</code>.
259 * @param comboBox the target <code>JComboBox</code>.
260 * @param index the given index.
261 * @throws IllegalStateException if the <code>JComboBox</code> is disabled.
262 * @throws IllegalStateException if the <code>JComboBox</code> is not showing on the screen.
263 * @throws IndexOutOfBoundsException if the given index is negative or greater than the index of the last item in the
264 * <code>JComboBox</code>.
265 */
266 @RunsInEDT
267 public void selectItem(final JComboBox comboBox, int index) {
268 validateCanSelectItem(comboBox, index);
269 showDropDownList(comboBox);
270 selectItemAtIndex(comboBox, index);
271 hideDropDownListIfVisible(comboBox);
272 }
273
274 @RunsInEDT
275 private static void validateCanSelectItem(final JComboBox comboBox, final int index) {
276 execute(new GuiTask() {
277 protected void executeInEDT() {
278 validateIsEnabledAndShowing(comboBox);
279 validateIndex(comboBox, index);
280 }
281 });
282 }
283
284 @VisibleForTesting
285 @RunsInEDT
286 void showDropDownList(JComboBox comboBox) {
287 // Location of pop-up button activator is LAF-dependent
288 dropDownVisibleThroughUIDelegate(comboBox, true);
289 }
290
291 @RunsInEDT
292 private void selectItemAtIndex(final JComboBox comboBox, final int index) {
293 JList dropDownList = dropDownListFinder.findDropDownList();
294 if (dropDownList != null) {
295 listDriver.selectItem(dropDownList, index);
296 return;
297 }
298 setSelectedIndex(comboBox, index);
299 robot.waitForIdle();
300 }
301
302 @RunsInEDT
303 private void hideDropDownListIfVisible(JComboBox comboBox) {
304 dropDownVisibleThroughUIDelegate(comboBox, false);
305 }
306
307 @RunsInEDT
308 private void dropDownVisibleThroughUIDelegate(JComboBox comboBox, final boolean visible) {
309 setPopupVisible(comboBox, visible);
310 robot.waitForIdle();
311 }
312
313 /**
314 * Simulates a user entering the specified text in the <code>{@link JComboBox}</code>, replacing any text. This action
315 * is executed only if the <code>{@link JComboBox}</code> is editable.
316 * @param comboBox the target <code>JComboBox</code>.
317 * @param text the text to enter.
318 * @throws IllegalStateException if the <code>JComboBox</code> is disabled.
319 * @throws IllegalStateException if the <code>JComboBox</code> is not showing on the screen.
320 * @throws IllegalStateException if the <code>JComboBox</code> is not editable.
321 */
322 @RunsInEDT
323 public void replaceText(JComboBox comboBox, String text) {
324 selectAllText(comboBox);
325 enterText(comboBox, text);
326 }
327
328 /**
329 * Simulates a user selecting the text in the <code>{@link JComboBox}</code>. This action is executed only if the
330 * <code>{@link JComboBox}</code> is editable.
331 * @param comboBox the target <code>JComboBox</code>.
332 * @throws IllegalStateException if the <code>JComboBox</code> is disabled.
333 * @throws IllegalStateException if the <code>JComboBox</code> is not showing on the screen.
334 * @throws IllegalStateException if the <code>JComboBox</code> is not editable.
335 */
336 @RunsInEDT
337 public void selectAllText(JComboBox comboBox) {
338 Component editor = accessibleEditorOf(comboBox);
339 if (!(editor instanceof JComponent)) return;
340 focus(editor);
341 invokeAction((JComponent) editor, selectAllAction);
342 }
343
344 @RunsInEDT
345 private static Component accessibleEditorOf(final JComboBox comboBox) {
346 return execute(new GuiQuery<Component>() {
347 protected Component executeInEDT() {
348 validateEditorIsAccessible(comboBox);
349 return editorComponentOf(comboBox);
350 }
351 });
352 }
353
354 /**
355 * Simulates a user entering the specified text in the <code>{@link JComboBox}</code>. This action is executed only
356 * if the <code>{@link JComboBox}</code> is editable.
357 * @param comboBox the target <code>JComboBox</code>.
358 * @param text the text to enter.
359 * @throws IllegalStateException if the <code>JComboBox</code> is disabled.
360 * @throws IllegalStateException if the <code>JComboBox</code> is not showing on the screen.
361 * @throws IllegalStateException if the <code>JComboBox</code> is not editable.
362 * @throws ActionFailedException if the <code>JComboBox</code> does not have an editor.
363 */
364 @RunsInEDT
365 public void enterText(JComboBox comboBox, String text) {
366 inEdtValidateEditorIsAccessible(comboBox);
367 Component editor = editorComponentOf(comboBox);
368 // this will never happen...at least in Sun's JVM
369 if (editor == null) throw actionFailure("JComboBox does not have an editor");
370 focusAndWaitForFocusGain(editor);
371 robot.enterText(text);
372 }
373
374 /**
375 * Simulates a user pressing and releasing the given keys on the <code>{@link JComboBox}</code>.
376 * @param comboBox the target <code>JComboBox</code>.
377 * @param keyCodes one or more codes of the keys to press.
378 * @throws NullPointerException if the given array of codes is <code>null</code>.
379 * @throws IllegalStateException if the <code>JComboBox</code> is disabled.
380 * @throws IllegalStateException if the <code>JComboBox</code> is not showing on the screen.
381 * @throws IllegalArgumentException if the given code is not a valid key code.
382 * @see java.awt.event.KeyEvent
383 */
384 @RunsInEDT
385 public void pressAndReleaseKeys(JComboBox comboBox, int... keyCodes) {
386 if (keyCodes == null) throw new NullPointerException("The array of key codes should not be null");
387 assertIsEnabledAndShowing(comboBox);
388 Component target = editorIfEditable(comboBox);
389 if (target == null) target = comboBox;
390 focusAndWaitForFocusGain(target);
391 robot.pressAndReleaseKeys(keyCodes);
392 }
393
394 @RunsInEDT
395 private static Component editorIfEditable(final JComboBox comboBox) {
396 return execute(new GuiQuery<Component>() {
397 protected Component executeInEDT() {
398 if (!comboBox.isEditable()) return null;
399 return editorComponent(comboBox);
400 }
401 });
402 }
403
404 @RunsInEDT
405 private static void inEdtValidateEditorIsAccessible(final JComboBox comboBox) {
406 execute(new GuiTask() {
407 protected void executeInEDT() {
408 validateEditorIsAccessible(comboBox);
409 }
410 });
411 }
412
413 @RunsInEDT
414 private static Component editorComponentOf(final JComboBox comboBox) {
415 return execute(new GuiQuery<Component>() {
416 protected Component executeInEDT() {
417 return editorComponent(comboBox);
418 }
419 });
420 }
421
422 @RunsInCurrentThread
423 private static Component editorComponent(JComboBox comboBox) {
424 ComboBoxEditor editor = comboBox.getEditor();
425 if (editor == null) return null;
426 return editor.getEditorComponent();
427 }
428
429 /**
430 * Find the <code>{@link JList}</code> in the pop-up raised by the <code>{@link JComboBox}</code>, if the LAF actually
431 * uses one.
432 * @return the found <code>JList</code>.
433 * @throws ComponentLookupException if the <code>JList</code> in the pop-up could not be found.
434 */
435 @RunsInEDT
436 public JList dropDownList() {
437 JList list = dropDownListFinder.findDropDownList();
438 if (list == null) throw listNotFound();
439 return list;
440 }
441
442 private ComponentLookupException listNotFound() {
443 throw new ComponentLookupException("Unable to find the pop-up list for the JComboBox");
444 }
445
446 /**
447 * Asserts that the given <code>{@link JComboBox}</code> is editable.
448 * @param comboBox the target <code>JComboBox</code>.
449 * @throws AssertionError if the <code>JComboBox</code> is not editable.
450 */
451 @RunsInEDT
452 public void requireEditable(final JComboBox comboBox) {
453 assertEditable(comboBox, true);
454 }
455
456 /**
457 * Asserts that the given <code>{@link JComboBox}</code> is not editable.
458 * @param comboBox the given <code>JComboBox</code>.
459 * @throws AssertionError if the <code>JComboBox</code> is editable.
460 */
461 @RunsInEDT
462 public void requireNotEditable(JComboBox comboBox) {
463 assertEditable(comboBox, false);
464 }
465
466 @RunsInEDT
467 private void assertEditable(JComboBox comboBox, boolean expected) {
468 assertThat(isEditable(comboBox)).as(editableProperty(comboBox)).isEqualTo(expected);
469 }
470
471 @RunsInEDT
472 private static Description editableProperty(JComboBox comboBox) {
473 return propertyName(comboBox, EDITABLE_PROPERTY);
474 }
475
476 /**
477 * Updates the implementation of <code>{@link JComboBoxCellReader}</code> to use when comparing internal values
478 * of a <code>{@link JComboBox}</code> and the values expected in a test.
479 * @param newCellReader the new <code>JComboBoxCellValueReader</code> to use.
480 * @throws NullPointerException if <code>newCellReader</code> is <code>null</code>.
481 */
482 public void cellReader(JComboBoxCellReader newCellReader) {
483 validateCellReader(newCellReader);
484 cellReader = newCellReader;
485 }
486
487 /**
488 * Verifies that number of items in the given <code>{@link JComboBox}</code> is equal to the expected one.
489 * @param comboBox the target <code>JComboBox</code>.
490 * @param expected the expected number of items.
491 * @throws AssertionError if the number of items in the given <code>{@link JComboBox}</code> is not equal to the
492 * expected one.
493 * @since 1.2
494 */
495 @RunsInEDT
496 public void requireItemCount(JComboBox comboBox, int expected) {
497 int actual = itemCountIn(comboBox);
498 assertThat(actual).as(propertyName(comboBox, "itemCount")).isEqualTo(expected);
499 }
500 }