/** * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * * Copyright 2012-2015 the original author or authors. */ package org.assertj.swing.driver; import static javax.swing.text.DefaultEditorKit.deletePrevCharAction; import static javax.swing.text.DefaultEditorKit.selectAllAction; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Fail.fail; import static org.assertj.core.util.Preconditions.checkNotNull; import static org.assertj.core.util.Strings.quote; import static org.assertj.swing.driver.ComponentPreconditions.checkEnabledAndShowing; import static org.assertj.swing.driver.JComboBoxContentQuery.contents; import static org.assertj.swing.driver.JComboBoxEditableQuery.isEditable; import static org.assertj.swing.driver.JComboBoxItemCountQuery.itemCountIn; import static org.assertj.swing.driver.JComboBoxItemIndexPreconditions.checkItemIndexInBounds; import static org.assertj.swing.driver.JComboBoxMatchingItemQuery.matchingItemIndex; import static org.assertj.swing.driver.JComboBoxSelectedIndexQuery.selectedIndexOf; import static org.assertj.swing.driver.JComboBoxSelectionValueQuery.selection; import static org.assertj.swing.driver.JComboBoxSetSelectedIndexTask.setSelectedIndex; import static org.assertj.swing.driver.TextAssert.verifyThat; import static org.assertj.swing.edt.GuiActionRunner.execute; import static org.assertj.swing.exception.ActionFailedException.actionFailure; import static org.assertj.swing.format.Formatting.format; import static org.assertj.swing.util.Arrays.format; import java.awt.Component; import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.swing.ComboBoxEditor; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JList; import org.assertj.core.description.Description; import org.assertj.core.util.VisibleForTesting; import org.assertj.swing.annotation.RunsInCurrentThread; import org.assertj.swing.annotation.RunsInEDT; import org.assertj.swing.cell.JComboBoxCellReader; import org.assertj.swing.core.Robot; import org.assertj.swing.exception.ComponentLookupException; import org.assertj.swing.exception.LocationUnavailableException; import org.assertj.swing.internal.annotation.InternalApi; import org.assertj.swing.util.Pair; import org.assertj.swing.util.PatternTextMatcher; import org.assertj.swing.util.StringTextMatcher; import org.assertj.swing.util.TextMatcher; /** * <p> * Supports functional testing of {@code JComboBox}es. * </p> * * <p> * <b>Note:</b> This class is intended for internal use only. Please use the classes in the package * {@link org.assertj.swing.fixture} in your tests. * </p> * * @author Alex Ruiz * @author Yvonne Wang */ @InternalApi public class JComboBoxDriver extends JComponentDriver { private static final String EDITABLE_PROPERTY = "editable"; private static final String SELECTED_INDEX_PROPERTY = "selectedIndex"; private final JListDriver listDriver; private final JComboBoxDropDownListFinder dropDownListFinder; private JComboBoxCellReader cellReader; /** * Creates a new {@link JComboBoxDriver}. * * @param robot the robot to use to simulate user input. */ public JComboBoxDriver(@Nonnull Robot robot) { super(robot); listDriver = new JListDriver(robot); dropDownListFinder = new JComboBoxDropDownListFinder(robot); replaceCellReader(new BasicJComboBoxCellReader()); } /** * Returns an array of {@code String}s that represents the contents of the given {@code JComboBox} list. The * {@code String} representation of each element is performed using this driver's {@link JComboBoxCellReader}. * * @param comboBox the target {@code JComboBox}. * @return an array of {@code String}s that represent the contents of the given {@code JComboBox} list. * @see #value(JComboBox, int) * @see #replaceCellReader(JComboBoxCellReader) */ @RunsInEDT public @Nonnull String[] contentsOf(@Nonnull JComboBox<?> comboBox) { return contents(comboBox, cellReader()); } /** * Selects the first item matching the given text in the {@code JComboBox}. The text of the {@code JComboBox} items is * obtained by this fixture's {@link JComboBoxCellReader}. * * @param comboBox the target {@code JComboBox}. * @param value the value to match. It can be a regular expression. * @throws LocationUnavailableException if an element matching the given value cannot be found. * @throws IllegalStateException if the {@code JComboBox} is disabled. * @throws IllegalStateException if the {@code JComboBox} is not showing on the screen. * @see #replaceCellReader(JComboBoxCellReader) */ @RunsInEDT public void selectItem(@Nonnull JComboBox<?> comboBox, @Nullable String value) { selectItem(comboBox, new StringTextMatcher(value)); } /** * Selects the first item matching the given regular expression pattern in the {@code JComboBox}. The text of the * {@code JComboBox} items is obtained by this fixture's {@link JComboBoxCellReader}. * * @param comboBox the target {@code JComboBox}. * @param pattern the regular expression pattern to match. * @throws LocationUnavailableException if an element matching the given pattern cannot be found. * @throws IllegalStateException if the {@code JComboBox} is disabled. * @throws IllegalStateException if the {@code JComboBox} is not showing on the screen. * @throws NullPointerException if the given regular expression pattern is {@code null}. * @see #replaceCellReader(JComboBoxCellReader) */ @RunsInEDT public void selectItem(@Nonnull JComboBox<?> comboBox, @Nonnull Pattern pattern) { selectItem(comboBox, new PatternTextMatcher(pattern)); } @RunsInEDT private void selectItem(@Nonnull JComboBox<?> comboBox, @Nonnull TextMatcher matcher) { int index = matchingItemIndex(comboBox, matcher, cellReader()); if (index < 0) { String format = "Unable to find item matching %s among the JComboBox contents: "; String msg = String.format(format, matcher.description(), format(contentsOf(comboBox))); throw new LocationUnavailableException(msg); } selectItem(comboBox, index); } /** * Verifies that the {@code String} representation of the selected item in the {@code JComboBox} matches the given * text. * * @param comboBox the target {@code JComboBox}. * @param value the text to match. It can be a regular expression. * @throws AssertionError if the selected item does not match the given value. * @see #replaceCellReader(JComboBoxCellReader) */ @RunsInEDT public void requireSelection(@Nonnull JComboBox<?> comboBox, @Nullable String value) { String selection = requiredSelectionOf(comboBox); verifyThat(selection).as(selectedIndexProperty(comboBox)).isEqualOrMatches(value); } /** * Verifies that the {@code String} representation of the selected item in the {@code JComboBox} matches the given * regular expression pattern. * * @param comboBox the target {@code JComboBox}. * @param pattern the regular expression pattern to match. * @throws AssertionError if the selected item does not match the given regular expression pattern. * @throws NullPointerException if the given regular expression pattern is {@code null}. * @see #replaceCellReader(JComboBoxCellReader) */ @RunsInEDT public void requireSelection(@Nonnull JComboBox<?> comboBox, @Nonnull Pattern pattern) { String selection = requiredSelectionOf(comboBox); verifyThat(selection).as(selectedIndexProperty(comboBox)).matches(pattern); } @RunsInEDT private @Nullable String requiredSelectionOf(@Nonnull JComboBox<?> comboBox) throws AssertionError { Pair<Boolean, String> selection = selection(comboBox, cellReader()); boolean hasSelection = selection.first; if (!hasSelection) { failNoSelection(comboBox); } return selection.second; } /** * Verifies that the index of the selected item in the {@code JComboBox} is equal to the given value. * * @param comboBox the target {@code JComboBox}. * @param index the expected selection index. * @throws AssertionError if the selection index is not equal to the given value. */ @RunsInEDT public void requireSelection(@Nonnull JComboBox<?> comboBox, int index) { int selectedIndex = selectedIndexOf(comboBox); if (selectedIndex == -1) { failNoSelection(comboBox); } assertThat(selectedIndex).as(selectedIndexProperty(comboBox)).isEqualTo(index); } private void failNoSelection(@Nonnull JComboBox<?> comboBox) { fail(String.format("[%s] No selection", selectedIndexProperty(comboBox).value())); } /** * Verifies that the {@code JComboBox} does not have any selection. * * @param comboBox the target {@code JComboBox}. * @throws AssertionError if the {@code JComboBox} has a selection. */ @RunsInEDT public void requireNoSelection(@Nonnull JComboBox<?> comboBox) { Pair<Boolean, String> selection = selection(comboBox, cellReader()); boolean hasSelection = selection.first; if (!hasSelection) { return; } String format = "[%s] Expecting no selection, but found:<%s>"; fail(String.format(format, selectedIndexProperty(comboBox).value(), quote(selection.second))); } /** * Returns the {@code String} representation of the element under the given index, using this driver's * {@link JComboBoxCellReader}. * * @param comboBox the target {@code JComboBox}. * @param index the given index. * @return the value of the element under the given index. * @throws IndexOutOfBoundsException if the given index is negative or greater than the index of the last item in the * {@code JComboBox}. * @see #replaceCellReader(JComboBoxCellReader) */ public @Nullable String value(@Nonnull JComboBox<?> comboBox, int index) { return valueAsText(comboBox, index, cellReader()); } @RunsInEDT private static @Nullable String valueAsText(final @Nonnull JComboBox<?> comboBox, final int index, final @Nonnull JComboBoxCellReader cellReader) { return execute(() -> { checkItemIndexInBounds(comboBox, index); return cellReader.valueAt(comboBox, index); }); } private @Nonnull Description selectedIndexProperty(@Nonnull JComboBox<?> comboBox) { return propertyName(comboBox, SELECTED_INDEX_PROPERTY); } /** * Clears the selection in the given {@code JComboBox}. Since this method does not simulate user input, it does not * verifies that the {@code JComboBox} is enabled and showing. * * @param comboBox the target {@code JComboBox}. */ public void clearSelection(@Nonnull JComboBox<?> comboBox) { setSelectedIndex(comboBox, -1); robot.waitForIdle(); } /** * Selects the item under the given index in the {@code JComboBox}. * * @param comboBox the target {@code JComboBox}. * @param index the given index. * @throws IllegalStateException if the {@code JComboBox} is disabled. * @throws IllegalStateException if the {@code JComboBox} is not showing on the screen. * @throws IndexOutOfBoundsException if the given index is negative or greater than the index of the last item in the * {@code JComboBox}. */ @RunsInEDT public void selectItem(final @Nonnull JComboBox<?> comboBox, int index) { validateCanSelectItem(comboBox, index); showDropDownList(comboBox); selectItemAtIndex(comboBox, index); hideDropDownListIfVisible(comboBox); } @RunsInEDT private static void validateCanSelectItem(final @Nonnull JComboBox<?> comboBox, final int index) { execute(() -> { checkEnabledAndShowing(comboBox); checkItemIndexInBounds(comboBox, index); }); } @VisibleForTesting @RunsInEDT void showDropDownList(@Nonnull JComboBox<?> comboBox) { // Location of pop-up button activator is LAF-dependent dropDownVisibleThroughUIDelegate(comboBox, true); } @RunsInEDT private void selectItemAtIndex(@Nonnull final JComboBox<?> comboBox, final int index) { JList<?> dropDownList = dropDownListFinder.findDropDownList(); if (dropDownList != null) { listDriver.selectItem(dropDownList, index); return; } setSelectedIndex(comboBox, index); robot.waitForIdle(); } @RunsInEDT private void hideDropDownListIfVisible(@Nonnull JComboBox<?> comboBox) { dropDownVisibleThroughUIDelegate(comboBox, false); } @RunsInEDT private void dropDownVisibleThroughUIDelegate(@Nonnull final JComboBox<?> comboBox, final boolean visible) { execute(() -> comboBox.setPopupVisible(visible)); robot.waitForIdle(); } /** * Deletes the text of the {@code JComboBox}. * * @param comboBox the target {@code JComboBox}. * @throws IllegalStateException if the {@code JComboBox} is disabled. * @throws IllegalStateException if the {@code JComboBox} is not showing on the screen. */ @RunsInEDT public void deleteText(@Nonnull JComboBox<?> comboBox) { selectAllText(comboBox); Component editor = accessibleEditorOf(comboBox); if (!(editor instanceof JComponent)) { return; } invokeAction((JComponent) editor, deletePrevCharAction); } /** * Simulates a user entering the specified text in the {@code JComboBox}, replacing any text. This action is executed * only if the {@code JComboBox} is editable. * * @param comboBox the target {@code JComboBox}. * @param text the text to enter. * @throws IllegalStateException if the {@code JComboBox} is disabled. * @throws IllegalStateException if the {@code JComboBox} is not showing on the screen. * @throws IllegalStateException if the {@code JComboBox} is not editable. */ @RunsInEDT public void replaceText(@Nonnull JComboBox<?> comboBox, @Nonnull String text) { checkNotNull(text); if (text.isEmpty()) { deleteText(comboBox); } else { selectAllText(comboBox); enterText(comboBox, text); } } /** * Simulates a user selecting the text in the {@code JComboBox}. This action is executed only if the {@code JComboBox} * is editable. * * @param comboBox the target {@code JComboBox}. * @throws IllegalStateException if the {@code JComboBox} is disabled. * @throws IllegalStateException if the {@code JComboBox} is not showing on the screen. * @throws IllegalStateException if the {@code JComboBox} is not editable. */ @RunsInEDT public void selectAllText(@Nonnull JComboBox<?> comboBox) { Component editor = accessibleEditorOf(comboBox); if (!(editor instanceof JComponent)) { return; } focus(editor); invokeAction((JComponent) editor, selectAllAction); } @RunsInEDT private static @Nullable Component accessibleEditorOf(final @Nonnull JComboBox<?> comboBox) { return execute(() -> { checkAccessibleEditor(comboBox); return editorComponentOf(comboBox); }); } /** * Simulates a user entering the specified text in the {@code JComboBox}. This action is executed only if the * {@code JComboBox} is editable. * * @param comboBox the target {@code JComboBox}. * @param text the text to enter. * @throws IllegalStateException if the {@code JComboBox} is disabled. * @throws IllegalStateException if the {@code JComboBox} is not showing on the screen. * @throws IllegalStateException if the {@code JComboBox} is not editable. * @throws org.assertj.swing.exception.ActionFailedException if the {@code JComboBox} does not have an editor. */ @RunsInEDT public void enterText(final @Nonnull JComboBox<?> comboBox, @Nonnull String text) { execute(() -> checkAccessibleEditor(comboBox)); Component editor = editorComponentOf(comboBox); // this will never happen...at least in Sun's JVM if (editor == null) { throw actionFailure("JComboBox does not have an editor"); } focusAndWaitForFocusGain(editor); robot.enterText(text); } /** * Simulates a user pressing and releasing the given keys on the {@code JComboBox}. * * @param comboBox the target {@code JComboBox}. * @param keyCodes one or more codes of the keys to press. * @throws NullPointerException if the given array of codes is {@code null}. * @throws IllegalStateException if the {@code JComboBox} is disabled. * @throws IllegalStateException if the {@code JComboBox} is not showing on the screen. * @throws IllegalArgumentException if the given code is not a valid key code. * @see java.awt.event.KeyEvent */ @RunsInEDT public void pressAndReleaseKeys(@Nonnull JComboBox<?> comboBox, @Nonnull int... keyCodes) { checkNotNull(keyCodes); checkInEdtEnabledAndShowing(comboBox); Component target = editorIfEditable(comboBox); if (target == null) { target = comboBox; } focusAndWaitForFocusGain(target); robot.pressAndReleaseKeys(keyCodes); } @RunsInEDT private static Component editorIfEditable(final JComboBox<?> comboBox) { return execute(() -> { if (!comboBox.isEditable()) { return null; } return editorComponent(comboBox); }); } @RunsInCurrentThread private static void checkAccessibleEditor(@Nonnull JComboBox<?> comboBox) { checkEnabledAndShowing(comboBox); if (!comboBox.isEditable()) { String msg = String.format("Expecting component %s to be editable", format(comboBox)); throw new IllegalStateException(msg); } } @RunsInEDT private static @Nullable Component editorComponentOf(final @Nonnull JComboBox<?> comboBox) { return execute(() -> editorComponent(comboBox)); } @RunsInCurrentThread private static @Nullable Component editorComponent(@Nonnull JComboBox<?> comboBox) { ComboBoxEditor editor = comboBox.getEditor(); if (editor == null) { return null; } return editor.getEditorComponent(); } /** * Find the {@code JList} in the pop-up raised by the {@code JComboBox}, if the LAF actually uses one. * * @return the found {@code JList}. * @throws ComponentLookupException if the {@code JList} in the pop-up could not be found. */ @RunsInEDT public @Nonnull JList<?> dropDownList() { JList<?> list = dropDownListFinder.findDropDownList(); if (list == null) { throw new ComponentLookupException("Unable to find the pop-up list for the JComboBox"); } return list; } /** * Asserts that the given {@code JComboBox} is editable. * * @param comboBox the target {@code JComboBox}. * @throws AssertionError if the {@code JComboBox} is not editable. */ @RunsInEDT public void requireEditable(final @Nonnull JComboBox<?> comboBox) { checkEditable(comboBox, true); } /** * Asserts that the given {@code JComboBox} is not editable. * * @param comboBox the given {@code JComboBox}. * @throws AssertionError if the {@code JComboBox} is editable. */ @RunsInEDT public void requireNotEditable(@Nonnull JComboBox<?> comboBox) { checkEditable(comboBox, false); } @RunsInEDT private void checkEditable(@Nonnull JComboBox<?> comboBox, boolean expected) { assertThat(isEditable(comboBox)).as(editableProperty(comboBox)).isEqualTo(expected); } @RunsInEDT private static Description editableProperty(@Nonnull JComboBox<?> comboBox) { return propertyName(comboBox, EDITABLE_PROPERTY); } /** * Updates the implementation of {@link JComboBoxCellReader} to use when comparing internal values of a * {@code JComboBox} and the values expected in a test. * * @param newCellReader the new {@code JComboBoxCellValueReader} to use. * @throws NullPointerException if {@code newCellReader} is {@code null}. */ public void replaceCellReader(@Nonnull JComboBoxCellReader newCellReader) { cellReader = checkNotNull(newCellReader); } /** * Verifies that number of items in the given {@code JComboBox} is equal to the expected one. * * @param comboBox the target {@code JComboBox}. * @param expected the expected number of items. * @throws AssertionError if the number of items in the given {@code JComboBox} is not equal to the expected one. */ @RunsInEDT public void requireItemCount(@Nonnull JComboBox<?> comboBox, int expected) { int actual = itemCountIn(comboBox); assertThat(actual).as(propertyName(comboBox, "itemCount")).isEqualTo(expected); } /** * Returns the selected value of the given {@code JComboBox} as plain text. This method returns {@code null} if the * {code JComboBox} does not have any selection. * * @param comboBox the target {@code JComboBox}. * @return the selected value of the given {code JComboBox} as plain text, or {@code null} if the {code JComboBox} * does not have any selection. */ public @Nullable String selectedItemOf(@Nonnull JComboBox<?> comboBox) { return selection(comboBox, cellReader()).second; } private @Nonnull JComboBoxCellReader cellReader() { return cellReader; } }