001    /*
002     * Created on Jan 26, 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.swing.driver.ComponentStateValidator.validateIsEnabledAndShowing;
021    import static org.fest.swing.driver.JSpinnerSetValueTask.setValue;
022    import static org.fest.swing.driver.JSpinnerValueQuery.valueOf;
023    import static org.fest.swing.edt.GuiActionRunner.execute;
024    import static org.fest.swing.exception.ActionFailedException.actionFailure;
025    import static org.fest.swing.format.Formatting.format;
026    import static org.fest.util.Strings.concat;
027    import static org.fest.util.Strings.quote;
028    
029    import java.awt.Component;
030    import java.text.ParseException;
031    import java.util.ArrayList;
032    import java.util.List;
033    
034    import javax.swing.JSpinner;
035    import javax.swing.text.JTextComponent;
036    
037    import org.fest.swing.annotation.RunsInCurrentThread;
038    import org.fest.swing.annotation.RunsInEDT;
039    import org.fest.swing.core.Robot;
040    import org.fest.swing.core.TypeMatcher;
041    import org.fest.swing.edt.GuiTask;
042    import org.fest.swing.exception.*;
043    
044    /**
045     * Understands functional testing of <code>{@link JSpinner}</code>s:
046     * <ul>
047     * <li>user input simulation</li>
048     * <li>state verification</li>
049     * <li>property value query</li>
050     * </ul>
051     * This class is intended for internal use only. Please use the classes in the package
052     * <code>{@link org.fest.swing.fixture}</code> in your tests.
053     *
054     * @author Alex Ruiz
055     * @author Yvonne Wang
056     */
057    public class JSpinnerDriver extends JComponentDriver {
058    
059      private static final TypeMatcher EDITOR_MATCHER = new TypeMatcher(JTextComponent.class, true);
060      private static final String VALUE_PROPERTY = "value";
061    
062      /**
063       * Creates a new </code>{@link JSpinnerDriver}</code>.
064       * @param robot the robot to use to simulate user input.
065       */
066      public JSpinnerDriver(Robot robot) {
067        super(robot);
068      }
069    
070      /**
071       * Increments the value of the <code>{@link JSpinner}</code> the given number of times.
072       * @param spinner the target <code>JSpinner</code>.
073       * @param times how many times the value of this fixture's <code>JSpinner</code> should be incremented.
074       * @throws IllegalArgumentException if <code>times</code> is less than or equal to zero.
075       * @throws IllegalStateException if the <code>JSpinner</code> is disabled.
076       * @throws IllegalStateException if the <code>JSpinner</code> is not showing on the screen.
077       */
078      @RunsInEDT
079      public void increment(JSpinner spinner, int times) {
080        validate(times, "increment the value");
081        validateAndIncrementValue(spinner, times);
082        robot.waitForIdle();
083      }
084    
085      @RunsInEDT
086      private static void validateAndIncrementValue(final JSpinner spinner, final int times) {
087        execute(new GuiTask() {
088          protected void executeInEDT() {
089            validateIsEnabledAndShowing(spinner);
090            incrementValue(spinner, times);
091          }
092        });
093      }
094    
095      @RunsInCurrentThread
096      private static void incrementValue(JSpinner spinner, int times) {
097        for (int i = 0; i < times; i++) {
098          Object newValue = spinner.getNextValue();
099          if (newValue == null) return;
100          spinner.setValue(newValue);
101        }
102      }
103    
104      /**
105       * Increments the value of the <code>{@link JSpinner}</code>.
106       * @param spinner the target <code>JSpinner</code>.
107       * @throws IllegalStateException if the <code>JSpinner</code> is disabled.
108       * @throws IllegalStateException if the <code>JSpinner</code> is not showing on the screen.
109       */
110      @RunsInEDT
111      public void increment(JSpinner spinner) {
112        validateAndIncrementValue(spinner);
113        robot.waitForIdle();
114      }
115    
116      @RunsInEDT
117      private static void validateAndIncrementValue(final JSpinner spinner) {
118        execute(new GuiTask() {
119          protected void executeInEDT() {
120            validateIsEnabledAndShowing(spinner);
121            Object newValue = spinner.getNextValue();
122            if (newValue != null) spinner.setValue(newValue);
123          }
124        });
125      }
126    
127      /**
128       * Decrements the value of the <code>{@link JSpinner}</code> the given number of times.
129       * @param spinner the target <code>JSpinner</code>.
130       * @param times how many times the value of this fixture's <code>JSpinner</code> should be decremented.
131       * @throws IllegalArgumentException if <code>times</code> is less than or equal to zero.
132       * @throws IllegalStateException if the <code>JSpinner</code> is disabled.
133       * @throws IllegalStateException if the <code>JSpinner</code> is not showing on the screen.
134       */
135      @RunsInEDT
136      public void decrement(JSpinner spinner, int times) {
137        validate(times, "decrement the value");
138        validateAndDecrementValue(spinner, times);
139        robot.waitForIdle();
140      }
141    
142      private void validate(int times, String action) {
143        if (times > 0) return;
144        throw new IllegalArgumentException(concat(
145            "The number of times to ", action, " should be greater than zero, but was <", times, ">"));
146      }
147    
148      @RunsInEDT
149      private static void validateAndDecrementValue(final JSpinner spinner, final int times) {
150        execute(new GuiTask() {
151          protected void executeInEDT() {
152            validateIsEnabledAndShowing(spinner);
153            decrementValue(spinner, times);
154          }
155        });
156      }
157    
158      @RunsInCurrentThread
159      private static void decrementValue(JSpinner spinner, int times) {
160        for (int i = 0; i < times; i++) {
161          Object newValue = spinner.getPreviousValue();
162          if (newValue == null) return;
163          spinner.setValue(newValue);
164        }
165      }
166    
167      /**
168       * Decrements the value of the <code>{@link JSpinner}</code>.
169       * @param spinner the target <code>JSpinner</code>.
170       * @throws IllegalStateException if the <code>JSpinner</code> is disabled.
171       * @throws IllegalStateException if the <code>JSpinner</code> is not showing on the screen.
172       */
173      @RunsInEDT
174      public void decrement(JSpinner spinner) {
175        validateAndDecrementValue(spinner);
176        robot.waitForIdle();
177      }
178    
179      @RunsInEDT
180      private static void validateAndDecrementValue(final JSpinner spinner) {
181        execute(new GuiTask() {
182          protected void executeInEDT() {
183            validateIsEnabledAndShowing(spinner);
184            Object newValue = spinner.getPreviousValue();
185            if (newValue != null) spinner.setValue(newValue);
186          }
187        });
188      }
189    
190      /**
191       * Returns the text displayed in the given <code>{@link JSpinner}</code>. This method first tries to get the text
192       * displayed in the <code>JSpinner</code>'s editor, assuming it is a <code>{@link JTextComponent}</code>. If the
193       * text from the editor cannot be retrieved, it will return the <code>String</code> representation of the value
194       * in the <code>JSpinner</code>'s model.
195       * @param spinner the target <code>JSpinner</code>.
196       * @return the text displayed in the given <code>JSpinner</code>.
197       * @since 1.2
198       */
199      @RunsInEDT
200      public String textOf(JSpinner spinner) {
201        JTextComponent editor = findEditor(spinner);
202        if (editor != null) return JTextComponentTextQuery.textOf(editor);
203        Object value = valueOf(spinner);
204        return value != null ? value.toString() : null;
205      }
206    
207      /**
208       * Enters and commits the given text in the <code>{@link JSpinner}</code>, assuming its editor has a
209       * <code>{@link JTextComponent}</code> under it.
210       * @param spinner the target <code>JSpinner</code>.
211       * @param text the text to enter.
212       * @throws IllegalStateException if the <code>JSpinner</code> is disabled.
213       * @throws IllegalStateException if the <code>JSpinner</code> is not showing on the screen.
214       * @throws ActionFailedException if the editor of the <code>JSpinner</code> is not a <code>JTextComponent</code> or
215       * cannot be found.
216       * @throws UnexpectedException if entering the text in the <code>JSpinner</code>'s editor fails.
217       */
218      @RunsInEDT
219      public void enterTextAndCommit(JSpinner spinner, String text) {
220        enterText(spinner, text);
221        commit(spinner);
222        robot.waitForIdle();
223      }
224    
225      @RunsInEDT
226      private static void commit(final JSpinner spinner) {
227        execute(new GuiTask() {
228          protected void executeInEDT() throws ParseException {
229            spinner.commitEdit();
230          }
231        });
232      }
233    
234      /**
235       * Enters the given text in the <code>{@link JSpinner}</code>, assuming its editor has a
236       * <code>{@link JTextComponent}</code> under it. This method does not commit the value to the <code>JSpinner</code>.
237       * @param spinner the target <code>JSpinner</code>.
238       * @param text the text to enter.
239       * @throws IllegalStateException if the <code>JSpinner</code> is disabled.
240       * @throws IllegalStateException if the <code>JSpinner</code> is not showing on the screen.
241       * @throws ActionFailedException if the editor of the <code>JSpinner</code> is not a <code>JTextComponent</code> or
242       * cannot be found.
243       * @throws UnexpectedException if entering the text in the <code>JSpinner</code>'s editor fails.
244       * @see #enterTextAndCommit(JSpinner, String)
245       */
246      @RunsInEDT
247      public void enterText(JSpinner spinner, String text) {
248        assertIsEnabledAndShowing(spinner);
249        JTextComponent editor = findEditor(spinner);
250        validate(spinner, editor);
251        robot.waitForIdle();
252        robot.focusAndWaitForFocusGain(editor);
253        invokeAction(editor, selectAllAction);
254        robot.enterText(text);
255      }
256    
257      @RunsInEDT
258      private JTextComponent findEditor(JSpinner spinner) {
259        List<Component> found = new ArrayList<Component>(robot.finder().findAll(spinner, EDITOR_MATCHER));
260        if (found.size() != 1) return null;
261        Component c = found.get(0);
262        if (c instanceof JTextComponent) return (JTextComponent)c;
263        return null;
264      }
265    
266      @RunsInEDT
267      private static void validate(final JSpinner spinner, final JTextComponent editor) {
268        execute(new GuiTask() {
269          protected void executeInEDT() {
270            if (editor == null) throw actionFailure(concat("Unable to find editor for ", format(spinner)));
271          }
272        });
273      }
274    
275      /**
276       * Selects the given value in the given <code>{@link JSpinner}</code>.
277       * @param spinner the target <code>JSpinner</code>.
278       * @param value the value to select.
279       * @throws IllegalStateException if the <code>JSpinner</code> is disabled.
280       * @throws IllegalStateException if the <code>JSpinner</code> is not showing on the screen.
281       * @throws IllegalArgumentException if the given <code>JSpinner</code> does not support the given value.
282       */
283      @RunsInEDT
284      public void selectValue(JSpinner spinner, Object value) {
285        try {
286          setValue(spinner, value);
287        } catch (IllegalArgumentException e) {
288          // message from original exception is useless
289          throw new IllegalArgumentException(concat("Value ", quote(value), " is not valid"));
290        }
291        robot.waitForIdle();
292      }
293    
294      /**
295       * Returns the <code>{@link JTextComponent}</code> used as editor in the given <code>{@link JSpinner}</code>.
296       * @param spinner the target <code>JSpinner</code>.
297       * @return the <code>JTextComponent</code> used as editor in the given <code>JSpinner</code>.
298       * @throws ComponentLookupException if the given <code>JSpinner</code> does not have a <code>JTextComponent</code> as
299       * editor.
300       */
301      @RunsInEDT
302      public JTextComponent editor(JSpinner spinner) {
303        return (JTextComponent)robot.finder().find(spinner, EDITOR_MATCHER);
304      }
305    
306      /**
307       * Verifies that the value of the <code>{@link JSpinner}</code> is equal to the given one.
308       * @param spinner the target <code>JSpinner</code>.
309       * @param value the expected value.
310       * @throws AssertionError if the value of the <code>JSpinner</code> is not equal to the given one.
311       */
312      @RunsInEDT
313      public void requireValue(JSpinner spinner, Object value) {
314        assertThat(valueOf(spinner)).as(propertyName(spinner, VALUE_PROPERTY)).isEqualTo(value);
315      }
316    }