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    }