001    /*
002     * Created on Jan 12, 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 org.fest.assertions.Assertions.assertThat;
019    import static org.fest.swing.core.MouseButton.LEFT_BUTTON;
020    import static org.fest.swing.core.MouseButton.RIGHT_BUTTON;
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.JTreeChildrenShowUpCondition.untilChildrenShowUp;
024    import static org.fest.swing.driver.JTreeClearSelectionTask.clearSelectionOf;
025    import static org.fest.swing.driver.JTreeEditableQuery.isEditable;
026    import static org.fest.swing.driver.JTreeExpandPathTask.expandTreePath;
027    import static org.fest.swing.driver.JTreeMatchingPathQuery.*;
028    import static org.fest.swing.driver.JTreeNodeTextQuery.nodeText;
029    import static org.fest.swing.driver.JTreeToggleExpandStateTask.toggleExpandState;
030    import static org.fest.swing.driver.JTreeVerifySelectionTask.verifyNoSelection;
031    import static org.fest.swing.driver.JTreeVerifySelectionTask.verifySelection;
032    import static org.fest.swing.edt.GuiActionRunner.execute;
033    import static org.fest.swing.exception.ActionFailedException.actionFailure;
034    import static org.fest.swing.timing.Pause.pause;
035    import static org.fest.swing.util.Arrays.isEmptyIntArray;
036    import static org.fest.util.Arrays.isEmpty;
037    import static org.fest.util.Strings.concat;
038    
039    import java.awt.Point;
040    import java.awt.Rectangle;
041    import javax.swing.JPopupMenu;
042    import javax.swing.JTree;
043    import javax.swing.plaf.TreeUI;
044    import javax.swing.plaf.basic.BasicTreeUI;
045    import javax.swing.tree.TreePath;
046    
047    import org.fest.assertions.Description;
048    import org.fest.swing.annotation.RunsInCurrentThread;
049    import org.fest.swing.annotation.RunsInEDT;
050    import org.fest.swing.cell.JTreeCellReader;
051    import org.fest.swing.core.*;
052    import org.fest.swing.edt.*;
053    import org.fest.swing.exception.*;
054    import org.fest.swing.util.Pair;
055    import org.fest.swing.util.Triple;
056    import org.fest.util.VisibleForTesting;
057    
058    /**
059     * Understands functional testing of <code>{@link JTree}</code>s:
060     * <ul>
061     * <li>user input simulation</li>
062     * <li>state verification</li>
063     * <li>property value query</li>
064     * </ul>
065     * This class is intended for internal use only. Please use the classes in the package
066     * <code>{@link org.fest.swing.fixture}</code> in your tests.
067     *
068     * @author Alex Ruiz
069     */
070    public class JTreeDriver extends JComponentDriver {
071    
072      private static final String EDITABLE_PROPERTY = "editable";
073      private static final String SELECTION_PROPERTY = "selection";
074    
075      private final JTreeLocation location;
076      private final JTreePathFinder pathFinder;
077    
078      /**
079       * Creates a new </code>{@link JTreeDriver}</code>.
080       * @param robot the robot to use to simulate user input.
081       */
082      public JTreeDriver(Robot robot) {
083        super(robot);
084        location = new JTreeLocation();
085        pathFinder = new JTreePathFinder();
086      }
087    
088      /**
089       * Clicks the given row.
090       * @param tree the target <code>JTree</code>.
091       * @param row the given row.
092       * @throws IllegalStateException if the <code>JTree</code> is disabled.
093       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
094       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
095       * visible rows in the <code>JTree</code>.
096       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
097       * @since 1.2
098       */
099      @RunsInEDT
100      public void clickRow(JTree tree, int row) {
101        Point p = scrollToRow(tree, row);
102        robot.click(tree, p);
103      }
104    
105      /**
106       * Clicks the given row.
107       * @param tree the target <code>JTree</code>.
108       * @param row the given row.
109       * @param button the mouse button to use.
110       * @throws NullPointerException if the given button is <code>null</code>.
111       * @throws IllegalStateException if the <code>JTree</code> is disabled.
112       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
113       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
114       * visible rows in the <code>JTree</code>.
115       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
116       * @since 1.2
117       */
118      @RunsInEDT
119      public void clickRow(JTree tree, int row, MouseButton button) {
120        validateIsNotNull(button);
121        clickRow(tree, row, button, 1);
122      }
123    
124      /**
125       * Clicks the given row.
126       * @param tree the target <code>JTree</code>.
127       * @param row the given row.
128       * @param mouseClickInfo specifies the mouse button to use and how many times to click.
129       * @throws NullPointerException if the given <code>MouseClickInfo</code> is <code>null</code>.
130       * @throws IllegalStateException if the <code>JTree</code> is disabled.
131       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
132       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
133       * visible rows in the <code>JTree</code>.
134       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
135       * @since 1.2
136       */
137      @RunsInEDT
138      public void clickRow(JTree tree, int row, MouseClickInfo mouseClickInfo) {
139        validateIsNotNull(mouseClickInfo);
140        clickRow(tree, row, mouseClickInfo.button(), mouseClickInfo.times());
141      }
142    
143      @RunsInEDT
144      private void clickRow(JTree tree, int row, MouseButton button, int times) {
145        Point p = scrollToRow(tree, row);
146        robot.click(tree, p, button, times);
147      }
148    
149      /**
150       * Double-clicks the given row.
151       * @param tree the target <code>JTree</code>.
152       * @param row the given row.
153       * @throws IllegalStateException if the <code>JTree</code> is disabled.
154       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
155       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
156       * visible rows in the <code>JTree</code>.
157       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
158       * @since 1.2
159       */
160      @RunsInEDT
161      public void doubleClickRow(JTree tree, int row) {
162        Point p = scrollToRow(tree, row);
163        doubleClick(tree, p);
164      }
165    
166      /**
167       * Right-clicks the given row.
168       * @param tree the target <code>JTree</code>.
169       * @param row the given row.
170       * @throws IllegalStateException if the <code>JTree</code> is disabled.
171       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
172       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
173       * visible rows in the <code>JTree</code>.
174       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
175       * @since 1.2
176       */
177      @RunsInEDT
178      public void rightClickRow(JTree tree, int row) {
179        Point p = scrollToRow(tree, row);
180        rightClick(tree, p);
181      }
182    
183      @RunsInEDT
184      private Point scrollToRow(JTree tree, int row) {
185        Point p = scrollToRow(tree, row, location).ii;
186        robot.waitForIdle();
187        return p;
188      }
189    
190      /**
191       * Clicks the given path, expanding parent nodes if necessary.
192       * @param tree the target <code>JTree</code>.
193       * @param path the path to path.
194       * @throws IllegalStateException if the <code>JTree</code> is disabled.
195       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
196       * @throws LocationUnavailableException if the given path cannot be found.
197       */
198      @RunsInEDT
199      public void clickPath(JTree tree, String path) {
200        Point p = scrollToPath(tree, path);
201        robot.click(tree, p);
202      }
203    
204      /**
205       * Clicks the given path, expanding parent nodes if necessary.
206       * @param tree the target <code>JTree</code>.
207       * @param path the path to path.
208       * @param button the mouse button to use.
209       * @throws NullPointerException if the given button is <code>null</code>.
210       * @throws IllegalStateException if the <code>JTree</code> is disabled.
211       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
212       * @throws LocationUnavailableException if the given path cannot be found.
213       * @since 1.2
214       */
215      @RunsInEDT
216      public void clickPath(JTree tree, String path, MouseButton button) {
217        validateIsNotNull(button);
218        clickPath(tree, path, button, 1);
219      }
220    
221      private void validateIsNotNull(MouseButton button) {
222        if (button == null) throw new NullPointerException("The given MouseButton should not be null");
223      }
224    
225      /**
226       * Clicks the given path, expanding parent nodes if necessary.
227       * @param tree the target <code>JTree</code>.
228       * @param path the path to path.
229       * @param mouseClickInfo specifies the mouse button to use and how many times to click.
230       * @throws NullPointerException if the given <code>MouseClickInfo</code> is <code>null</code>.
231       * @throws IllegalStateException if the <code>JTree</code> is disabled.
232       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
233       * @throws LocationUnavailableException if the given path cannot be found.
234       * @since 1.2
235       */
236      @RunsInEDT
237      public void clickPath(JTree tree, String path, MouseClickInfo mouseClickInfo) {
238        validateIsNotNull(mouseClickInfo);
239        clickPath(tree, path, mouseClickInfo.button(), mouseClickInfo.times());
240      }
241    
242      private void validateIsNotNull(MouseClickInfo mouseClickInfo) {
243        if (mouseClickInfo == null) throw new NullPointerException("The given MouseClickInfo should not be null");
244      }
245    
246      private void clickPath(JTree tree, String path, MouseButton button, int times) {
247        Point p = scrollToPath(tree, path);
248        robot.click(tree, p, button, times);
249      }
250    
251      /**
252       * Double-clicks the given path.
253       * @param tree the target <code>JTree</code>.
254       * @param path the path to double-click.
255       * @throws IllegalStateException if the <code>JTree</code> is disabled.
256       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
257       * @throws LocationUnavailableException if the given path cannot be found.
258       * @since 1.2
259       */
260      @RunsInEDT
261      public void doubleClickPath(JTree tree, String path) {
262        Point p = scrollToPath(tree, path);
263        doubleClick(tree, p);
264      }
265    
266      private Point scrollToPath(JTree tree, String path) {
267        Point p = scrollToMatchingPath(tree, path).iii;
268        robot.waitForIdle();
269        return p;
270      }
271    
272      private void doubleClick(JTree tree, Point p) {
273        robot.click(tree, p, LEFT_BUTTON, 2);
274      }
275    
276      /**
277       * Right-clicks the given path, expanding parent nodes if necessary.
278       * @param tree the target <code>JTree</code>.
279       * @param path the path to path.
280       * @throws IllegalStateException if the <code>JTree</code> is disabled.
281       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
282       * @throws LocationUnavailableException if the given path cannot be found.
283       * @since 1.2
284       */
285      @RunsInEDT
286      public void rightClickPath(JTree tree, String path) {
287        Point p = scrollToPath(tree, path);
288        rightClick(tree, p);
289      }
290    
291      private void rightClick(JTree tree, Point p) {
292        robot.click(tree, p, RIGHT_BUTTON, 1);
293      }
294    
295      /**
296       * Expands the given row, is possible. If the row is already expanded, this method will not do anything.
297       * <p>
298       * NOTE: a reasonable assumption is that the toggle control is just to the left of the row bounds and is roughly a
299       * square the dimensions of the row height. Clicking in the center of that square should work.
300       * </p>
301       * @param tree the target <code>JTree</code>.
302       * @param row the given row.
303       * @throws IllegalStateException if the <code>JTree</code> is disabled.
304       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
305       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
306       * visible rows in the <code>JTree</code>.
307       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
308       * @throws ActionFailedException if this method fails to expand the row.
309       * @since 1.2
310       */
311      @RunsInEDT
312      public void expandRow(JTree tree, int row) {
313        Triple<Boolean, Point, Integer> info = scrollToRowAndGetToggleInfo(tree, row, location);
314        robot.waitForIdle();
315        if (info.i) return; // already expanded
316        toggleCell(tree, info.ii, info.iii);
317      }
318    
319      /**
320       * Collapses the given row, is possible. If the row is already collapsed, this method will not do anything.
321       * <p>
322       * NOTE: a reasonable assumption is that the toggle control is just to the left of the row bounds and is roughly a
323       * square the dimensions of the row height. Clicking in the center of that square should work.
324       * </p>
325       * @param tree the target <code>JTree</code>.
326       * @param row the given row.
327       * @throws IllegalStateException if the <code>JTree</code> is disabled.
328       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
329       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
330       * visible rows in the <code>JTree</code>.
331       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
332       * @throws ActionFailedException if this method fails to collapse the row.
333       * @since 1.2
334       */
335      @RunsInEDT
336      public void collapseRow(JTree tree, int row) {
337        Triple<Boolean, Point, Integer> info = scrollToRowAndGetToggleInfo(tree, row, location);
338        robot.waitForIdle();
339        if (!info.i) return; // already collapsed
340        toggleCell(tree, info.ii, info.iii);
341      }
342    
343      /**
344       * Change the open/closed state of the given row, if possible.
345       * <p>
346       * NOTE: a reasonable assumption is that the toggle control is just to the left of the row bounds and is roughly a
347       * square the dimensions of the row height. Clicking in the center of that square should work.
348       * </p>
349       * @param tree the target <code>JTree</code>.
350       * @param row the given row.
351       * @throws IllegalStateException if the <code>JTree</code> is disabled.
352       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
353       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
354       * visible rows in the <code>JTree</code>.
355       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
356       * @throws ActionFailedException if this method fails to toggle the row.
357       */
358      @RunsInEDT
359      public void toggleRow(JTree tree, int row) {
360        Triple<Boolean, Point, Integer> info = scrollToRowAndGetToggleInfo(tree, row, location);
361        robot.waitForIdle();
362        toggleCell(tree, info.ii, info.iii);
363      }
364    
365      /*
366       * Returns:
367       * 1. if the row is expanded
368       * 2. the location of the row
369       * 3. the number of mouse clicks to toggle a row
370       */
371      @RunsInEDT
372      private static Triple<Boolean, Point, Integer> scrollToRowAndGetToggleInfo(final JTree tree, final int row,
373          final JTreeLocation location) {
374        return execute(new GuiQuery<Triple<Boolean, Point, Integer>>() {
375          protected Triple<Boolean, Point, Integer> executeInEDT() {
376            validateIsEnabledAndShowing(tree);
377            Point p = scrollToVisible(tree, row, location);
378            return new Triple<Boolean, Point, Integer>(tree.isExpanded(row), p, tree.getToggleClickCount());
379          }
380        });
381      }
382    
383      /**
384       * Expands the given path, is possible. If the path is already expanded, this method will not do anything.
385       * <p>
386       * NOTE: a reasonable assumption is that the toggle control is just to the left of the row bounds and is roughly a
387       * square the dimensions of the row height. Clicking in the center of that square should work.
388       * </p>
389       * @param tree the target <code>JTree</code>.
390       * @param path the path to expand.
391       * @throws IllegalStateException if the <code>JTree</code> is disabled.
392       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
393       * @throws LocationUnavailableException if the given path cannot be found.
394       * @throws ActionFailedException if this method fails to expand the path.
395       * @since 1.2
396       */
397      @RunsInEDT
398      public void expandPath(JTree tree, String path) {
399        Triple<Boolean, Point, Integer> info = scrollToMatchingPathAndGetToggleInfo(tree, path, pathFinder, location);
400        if (info.i) return; // already expanded
401        toggleCell(tree, info.ii, info.iii);
402      }
403    
404      /**
405       * Collapses the given path, is possible. If the path is already expanded, this method will not do anything.
406       * <p>
407       * NOTE: a reasonable assumption is that the toggle control is just to the left of the row bounds and is roughly a
408       * square the dimensions of the row height. Clicking in the center of that square should work.
409       * </p>
410       * @param tree the target <code>JTree</code>.
411       * @param path the path to collapse.
412       * @throws IllegalStateException if the <code>JTree</code> is disabled.
413       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
414       * @throws LocationUnavailableException if the given path cannot be found.
415       * @throws ActionFailedException if this method fails to collapse the path.
416       * @since 1.2
417       */
418      @RunsInEDT
419      public void collapsePath(JTree tree, String path) {
420        Triple<Boolean, Point, Integer> info = scrollToMatchingPathAndGetToggleInfo(tree, path, pathFinder, location);
421        if (!info.i) return; // already collapsed
422        toggleCell(tree, info.ii, info.iii);
423      }
424    
425      /*
426       * Returns:
427       * 1. if the node is expanded
428       * 2. the location of the node
429       * 3. the number of mouse clicks to toggle a node
430       */
431      @RunsInEDT
432      private static Triple<Boolean, Point, Integer> scrollToMatchingPathAndGetToggleInfo(final JTree tree,
433          final String path, final JTreePathFinder pathFinder, final JTreeLocation location) {
434        return execute(new GuiQuery<Triple<Boolean, Point, Integer>>() {
435          protected Triple<Boolean, Point, Integer> executeInEDT() {
436            validateIsEnabledAndShowing(tree);
437            TreePath matchingPath = matchingPathFor(tree, path, pathFinder);
438            Point p = scrollToTreePath(tree, matchingPath, location);
439            return new Triple<Boolean, Point, Integer>(tree.isExpanded(matchingPath), p, tree.getToggleClickCount());
440          }
441        });
442      }
443    
444      @RunsInEDT
445      private void toggleCell(JTree tree, Point p, int toggleClickCount) {
446        if (toggleClickCount == 0) {
447          toggleRowThroughTreeUI(tree, p);
448          robot.waitForIdle();
449          return;
450        }
451        robot.click(tree, p, LEFT_BUTTON, toggleClickCount);
452      }
453    
454      @RunsInEDT
455      private static void toggleRowThroughTreeUI(final JTree tree, final Point p) {
456        execute(new GuiTask() {
457          protected void executeInEDT() {
458            TreeUI treeUI = tree.getUI();
459            if (!(treeUI instanceof BasicTreeUI)) throw actionFailure(concat("Can't toggle row for ", treeUI));
460            toggleExpandState(tree, p);
461          }
462        });
463      }
464    
465      /**
466       * Selects the given rows.
467       * @param tree the target <code>JTree</code>.
468       * @param rows the rows to select.
469       * @throws NullPointerException if the array of rows is <code>null</code>.
470       * @throws IllegalArgumentException if the array of rows is empty.
471       * @throws IllegalStateException if the <code>JTree</code> is disabled.
472       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
473       * @throws IndexOutOfBoundsException if any of the given rows is less than zero or equal than or greater than the
474       * number of visible rows in the <code>JTree</code>.
475       * @throws LocationUnavailableException if a tree path for any of the given rows cannot be found.
476       */
477      @RunsInEDT
478      public void selectRows(final JTree tree, final int[] rows) {
479        validateRows(rows);
480        clearSelection(tree);
481        new MultipleSelectionTemplate(robot) {
482          int elementCount() {
483            return rows.length;
484          }
485          void selectElement(int index) {
486            selectRow(tree, rows[index]);
487          }
488        }.multiSelect();
489      }
490    
491      private void validateRows(final int[] rows) {
492        if (rows == null) throw new NullPointerException("The array of rows should not be null");
493        if (isEmptyIntArray(rows)) throw new IllegalArgumentException("The array of rows should not be empty");
494      }
495    
496      @RunsInEDT
497      private void clearSelection(final JTree tree) {
498        clearSelectionOf(tree);
499        robot.waitForIdle();
500      }
501    
502      /**
503       * Selects the given row.
504       * @param tree the target <code>JTree</code>.
505       * @param row the row to select.
506       * @throws IllegalStateException if the <code>JTree</code> is disabled.
507       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
508       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
509       * visible rows in the <code>JTree</code>.
510       */
511      @RunsInEDT
512      public void selectRow(JTree tree, int row) {
513        scrollAndSelectRow(tree, row);
514      }
515    
516      /**
517       * Selects the given paths, expanding parent nodes if necessary.
518       * @param tree the target <code>JTree</code>.
519       * @param paths the paths to select.
520       * @throws NullPointerException if the array of rows is <code>null</code>.
521       * @throws IllegalArgumentException if the array of rows is empty.
522       * @throws IllegalStateException if the <code>JTree</code> is disabled.
523       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
524       * @throws LocationUnavailableException if any the given path cannot be found.
525       */
526      @RunsInEDT
527      public void selectPaths(final JTree tree, final String[] paths) {
528        validatePaths(paths);
529        clearSelection(tree);
530        new MultipleSelectionTemplate(robot) {
531          int elementCount() {
532            return paths.length;
533          }
534          void selectElement(int index) {
535            selectPath(tree, paths[index]);
536          }
537        }.multiSelect();
538      }
539    
540      private void validatePaths(final String[] paths) {
541        if (paths == null) throw new NullPointerException("The array of paths should not be null");
542        if (isEmpty(paths)) throw new IllegalArgumentException("The array of paths should not be empty");
543      }
544    
545      /**
546       * Selects the given path, expanding parent nodes if necessary. Unlike <code>{@link #clickPath(JTree, String)}</code>,
547       * this method will not click the path if it is already selected
548       * @param tree the target <code>JTree</code>.
549       * @param path the path to select.
550       * @throws IllegalStateException if the <code>JTree</code> is disabled.
551       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
552       * @throws LocationUnavailableException if the given path cannot be found.
553       */
554      @RunsInEDT
555      public void selectPath(JTree tree, String path) {
556        selectMatchingPath(tree, path);
557      }
558    
559      /**
560       * Shows a pop-up menu at the position of the node in the given row.
561       * @param tree the target <code>JTree</code>.
562       * @param row the given row.
563       * @return a driver that manages the displayed pop-up menu.
564       * @throws IllegalStateException if the <code>JTree</code> is disabled.
565       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
566       * @throws ComponentLookupException if a pop-up menu cannot be found.
567       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
568       * visible rows in the <code>JTree</code>.
569       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
570       */
571      @RunsInEDT
572      public JPopupMenu showPopupMenu(JTree tree, int row) {
573        Pair<Boolean, Point> info = scrollToRow(tree, row, location);
574        Point p = info.ii;
575        return robot.showPopupMenu(tree, p);
576      }
577    
578      /**
579       * Shows a pop-up menu at the position of the last node in the given path. The last node in the given path will be
580       * made visible (by expanding the parent node(s)) if it is not visible.
581       * @param tree the target <code>JTree</code>.
582       * @param path the given path.
583       * @return a driver that manages the displayed pop-up menu.
584       * @throws IllegalStateException if the <code>JTree</code> is disabled.
585       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
586       * @throws ComponentLookupException if a pop-up menu cannot be found.
587       * @throws LocationUnavailableException if the given path cannot be found.
588       * @see #separator(String)
589       */
590      @RunsInEDT
591      public JPopupMenu showPopupMenu(JTree tree, String path) {
592        Triple<TreePath, Boolean, Point> info = scrollToMatchingPath(tree, path);
593        robot.waitForIdle();
594        Point where = info.iii;
595        return robot.showPopupMenu(tree, where);
596      }
597    
598      /**
599       * Starts a drag operation at the location of the given row.
600       * @param tree the target <code>JTree</code>.
601       * @param row the given row.
602       * @throws IllegalStateException if the <code>JTree</code> is disabled.
603       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
604       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
605       * visible rows in the <code>JTree</code>.
606       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
607       */
608      @RunsInEDT
609      public void drag(JTree tree, int row) {
610        Point p = scrollAndSelectRow(tree, row);
611        drag(tree, p);
612      }
613    
614      @RunsInEDT
615      private Point scrollAndSelectRow(JTree tree, int row) {
616        Pair<Boolean, Point> info = scrollToRow(tree, row, location);
617        Point p = info.ii;
618        if (!info.i) robot.click(tree, p); // path not selected, click to select
619        return p;
620      }
621    
622      /**
623       * Ends a drag operation at the location of the given row.
624       * @param tree the target <code>JTree</code>.
625       * @param row the given row.
626       * @throws IllegalStateException if the <code>JTree</code> is disabled.
627       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
628       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
629       * visible rows in the <code>JTree</code>.
630       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
631       * @throws ActionFailedException if there is no drag action in effect.
632       */
633      @RunsInEDT
634      public void drop(JTree tree, int row) {
635        drop(tree, scrollToRow(tree, row, location).ii);
636      }
637    
638      /*
639       * Returns:
640       * 1. if the node is expanded
641       * 2. the location of the node
642       */
643      @RunsInEDT
644      private static Pair<Boolean, Point> scrollToRow(final JTree tree, final int row, final JTreeLocation location) {
645        return execute(new GuiQuery<Pair<Boolean, Point>>() {
646          protected Pair<Boolean, Point> executeInEDT() {
647            validateIsEnabledAndShowing(tree);
648            Point p = scrollToVisible(tree, row, location);
649            boolean selected = tree.getSelectionCount() == 1 && tree.isRowSelected(row);
650            return new Pair<Boolean, Point>(selected, p);
651          }
652        });
653      }
654    
655      @RunsInCurrentThread
656      private static Point scrollToVisible(JTree tree, int row, JTreeLocation location) {
657        Pair<Rectangle, Point> boundsAndCoordinates = location.rowBoundsAndCoordinates(tree, row);
658        tree.scrollRectToVisible(boundsAndCoordinates.i);
659        return boundsAndCoordinates.ii;
660      }
661    
662      /**
663       * Starts a drag operation at the location of the given <code>{@link TreePath}</code>.
664       * @param tree the target <code>JTree</code>.
665       * @param path the given path.
666       * @throws IllegalStateException if the <code>JTree</code> is disabled.
667       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
668       * @throws LocationUnavailableException if the given path cannot be found.
669       * @see #separator(String)
670       */
671      @RunsInEDT
672      public void drag(JTree tree, String path) {
673        Point p = selectMatchingPath(tree, path);
674        drag(tree, p);
675      }
676    
677      @RunsInEDT
678      private Point selectMatchingPath(JTree tree, String path) {
679        Triple<TreePath, Boolean, Point> info = scrollToMatchingPath(tree, path);
680        robot.waitForIdle();
681        Point where = info.iii;
682        if (!info.ii) robot.click(tree, where); // path not selected, click to select
683        return where;
684      }
685    
686      /**
687       * Ends a drag operation at the location of the given <code>{@link TreePath}</code>.
688       * @param tree the target <code>JTree</code>.
689       * @param path the given path.
690       * @throws IllegalStateException if the <code>JTree</code> is disabled.
691       * @throws IllegalStateException if the <code>JTree</code> is not showing on the screen.
692       * @throws LocationUnavailableException if the given path cannot be found.
693       * @throws ActionFailedException if there is no drag action in effect.
694       * @see #separator(String)
695       */
696      @RunsInEDT
697      public void drop(JTree tree, String path) {
698        Point p = scrollToMatchingPath(tree, path).iii;
699        drop(tree, p);
700      }
701    
702      /*
703       * returns:
704       * 1. the found matching path
705       * 2. whether the path is already selected
706       * 3. the location where the path is in the JTree
707       */
708      @RunsInEDT
709      private Triple<TreePath, Boolean, Point> scrollToMatchingPath(JTree tree, String path) {
710        TreePath matchingPath = verifyJTreeIsReadyAndFindMatchingPath(tree, path, pathFinder);
711        makeVisible(tree, matchingPath, false);
712        Pair<Boolean, Point> info = scrollToPathToSelect(tree, matchingPath, location);
713        return new Triple<TreePath, Boolean, Point>(matchingPath, info.i, info.ii);
714      }
715    
716      /*
717       * returns:
718       * 1. whether the path is already selected
719       * 2. the location where the path is in the JTree
720       */
721      @RunsInEDT
722      private static Pair<Boolean, Point> scrollToPathToSelect(final JTree tree, final TreePath path, final JTreeLocation location) {
723        return execute(new GuiQuery<Pair<Boolean, Point>>() {
724          protected Pair<Boolean, Point> executeInEDT() {
725            boolean isSelected = tree.getSelectionCount() == 1 && tree.isPathSelected(path);
726            return new Pair<Boolean, Point>(isSelected, scrollToTreePath(tree, path, location));
727          }
728        });
729      }
730    
731      @RunsInCurrentThread
732      private static Point scrollToTreePath(JTree tree, TreePath path, JTreeLocation location) {
733        Pair<Rectangle, Point> boundsAndCoordinates = location.pathBoundsAndCoordinates(tree, path);
734        tree.scrollRectToVisible(boundsAndCoordinates.i);
735        return boundsAndCoordinates.ii;
736      }
737    
738      @RunsInEDT
739      private boolean makeParentVisible(JTree tree, TreePath path) {
740        boolean changed = makeVisible(tree, path.getParentPath(), true);
741        if (changed) robot.waitForIdle();
742        return changed;
743      }
744    
745      /**
746       * Matches, makes visible, and expands the path one component at a time, from uppermost ancestor on down, since
747       * children may be lazily loaded/created.
748       * @param tree the target <code>JTree</code>.
749       * @param path the tree path to make visible.
750       * @param expandWhenFound indicates if nodes should be expanded or not when found.
751       * @return if it was necessary to make visible and/or expand a node in the path.
752       */
753      @RunsInEDT
754      private boolean makeVisible(JTree tree, TreePath path, boolean expandWhenFound) {
755        boolean changed = false;
756        if (path.getPathCount() > 1) changed = makeParentVisible(tree, path);
757        if (!expandWhenFound) return changed;
758        expandTreePath(tree, path);
759        waitForChildrenToShowUp(tree, path);
760        return true;
761      }
762    
763      @RunsInEDT
764      private void waitForChildrenToShowUp(JTree tree, TreePath path) {
765        int timeout = robot.settings().timeoutToBeVisible();
766        try {
767          pause(untilChildrenShowUp(tree, path), timeout);
768        } catch (WaitTimedOutError e) {
769          throw new LocationUnavailableException(e.getMessage());
770        }
771      }
772    
773      /**
774       * Asserts that the given <code>{@link JTree}</code>'s selected rows are equal to the given one.
775       * @param tree the target <code>JTree</code>.
776       * @param rows the indices of the rows, expected to be selected.
777       * @throws NullPointerException if the array of row indices is <code>null</code>.
778       * @throws AssertionError if the given <code>JTree</code> selection is not equal to the given rows.
779       */
780      @RunsInEDT
781      public void requireSelection(JTree tree, int[] rows) {
782        if (rows == null) throw new NullPointerException("The array of row indices should not be null");
783        verifySelection(tree, rows, selectionProperty(tree));
784      }
785    
786      /**
787       * Asserts that the given <code>{@link JTree}</code>'s selected paths are equal to the given one.
788       * @param tree the target <code>JTree</code>.
789       * @param paths the given paths, expected to be selected.
790       * @throws NullPointerException if the array of paths is <code>null</code>.
791       * @throws LocationUnavailableException if any of the given paths cannot be found.
792       * @throws AssertionError if the given <code>JTree</code> selection is not equal to the given paths.
793       * @see #separator(String)
794       */
795      @RunsInEDT
796      public void requireSelection(JTree tree, String[] paths) {
797        if (paths == null) throw new NullPointerException("The array of paths should not be null");
798        verifySelection(tree, paths, pathFinder, selectionProperty(tree));
799      }
800    
801      /**
802       * Asserts that the given <code>{@link JTree}</code> does not have any selection.
803       * @param tree the given <code>JTree</code>.
804       * @throws AssertionError if the <code>JTree</code> has a selection.
805       */
806      @RunsInEDT
807      public void requireNoSelection(JTree tree) {
808        verifyNoSelection(tree, selectionProperty(tree));
809      }
810    
811      @RunsInEDT
812      private Description selectionProperty(JTree tree) {
813        return propertyName(tree, SELECTION_PROPERTY);
814      }
815    
816      /**
817       * Asserts that the given <code>{@link JTree}</code> is editable.
818       * @param tree the given <code>JTree</code>.
819       * @throws AssertionError if the <code>JTree</code> is not editable.
820       */
821      @RunsInEDT
822      public void requireEditable(JTree tree) {
823        assertEditable(tree, true);
824      }
825    
826      /**
827       * Asserts that the given <code>{@link JTree}</code> is not editable.
828       * @param tree the given <code>JTree</code>.
829       * @throws AssertionError if the <code>JTree</code> is editable.
830       */
831      @RunsInEDT
832      public void requireNotEditable(JTree tree) {
833        assertEditable(tree, false);
834      }
835    
836      @RunsInEDT
837      private void assertEditable(JTree tree, boolean editable) {
838        assertThat(isEditable(tree)).as(editableProperty(tree)).isEqualTo(editable);
839      }
840    
841      @RunsInEDT
842      private static Description editableProperty(JTree tree) {
843        return propertyName(tree, EDITABLE_PROPERTY);
844      }
845    
846      /**
847       * Returns the separator to use when converting <code>{@link TreePath}</code>s to <code>String</code>s.
848       * @return the separator to use when converting <code>{@link TreePath}</code>s to <code>String</code>s.
849       */
850      public String separator() {
851        return pathFinder.separator();
852      }
853    
854      /**
855       * Updates the separator to use when converting <code>{@link TreePath}</code>s to <code>String</code>s.
856       * @param newSeparator the new separator.
857       * @throws NullPointerException if the given separator is <code>null</code>.
858       */
859      public void separator(String newSeparator) {
860        if (newSeparator == null) throw new NullPointerException("The path separator should not be null");
861        pathFinder.separator(newSeparator);
862      }
863    
864      /**
865       * Updates the implementation of <code>{@link JTreeCellReader}</code> to use when comparing internal values of a
866       * <code>{@link JTree}</code> and the values expected in a test.
867       * @param newCellReader the new <code>JTreeCellValueReader</code> to use.
868       * @throws NullPointerException if <code>newCellReader</code> is <code>null</code>.
869       */
870      public void cellReader(JTreeCellReader newCellReader) {
871        validateCellReader(newCellReader);
872        pathFinder.cellReader(newCellReader);
873      }
874    
875      /**
876       * Verifies that the given row index is valid.
877       * @param tree the given <code>JTree</code>.
878       * @param row the given index.
879       * @throws IndexOutOfBoundsException if the given index is less than zero or equal than or greater than the number of
880       * visible rows in the <code>JTree</code>.
881       * @since 1.2
882       */
883      @RunsInEDT
884      public void validateRow(JTree tree, int row) {
885        location.validIndex(tree, row);
886      }
887    
888      /**
889       * Verifies that the given node path exists.
890       * @param tree the given <code>JTree</code>.
891       * @param path the given path.
892       * @throws LocationUnavailableException if the given path cannot be found.
893       * @since 1.2
894       */
895      @RunsInEDT
896      public void validatePath(JTree tree, String path) {
897        matchingPathFor(tree, path, pathFinder);
898      }
899    
900      /**
901       * Returns the <code>String</code> representation of the node at the given path.
902       * @param tree the given <code>JTree</code>.
903       * @param path the given path.
904       * @return the <code>String</code> representation of the node at the given path.
905       * @throws LocationUnavailableException if the given path cannot be found.
906       * @since 1.2
907       */
908      @RunsInEDT
909      public String nodeValue(JTree tree, String path) {
910        return nodeText(tree, path, pathFinder);
911      }
912    
913      /**
914       * Returns the <code>String</code> representation of the node at the given row index.
915       * @param tree the given <code>JTree</code>.
916       * @param row the given row.
917       * @return the <code>String</code> representation of the node at the given row index.
918       * @throws IndexOutOfBoundsException if the given row is less than zero or equal than or greater than the number of
919       * visible rows in the <code>JTree</code>.
920       * @throws LocationUnavailableException if a tree path for the given row cannot be found.
921       * @since 1.2
922       */
923      public String nodeValue(JTree tree, int row) {
924        return nodeText(tree, row, location, pathFinder);
925      }
926    
927      @VisibleForTesting
928      JTreeCellReader cellReader() { return pathFinder.cellReader(); }
929    }