/******************************************************************************* * Copyright (c) 2012 Google, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Google, Inc. - initial API and implementation *******************************************************************************/ package com.windowtester.runtime.swt.internal.selector; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.swt.widgets.Widget; import abbot.Platform; import abbot.finder.swt.TestHierarchy; import abbot.tester.swt.Robot; import abbot.tester.swt.RunnableWithResult; import abbot.tester.swt.WidgetLocator; import com.windowtester.internal.debug.IRuntimePluginTraceOptions; import com.windowtester.internal.debug.TraceHandler; import com.windowtester.internal.runtime.provisional.WTInternal; import com.windowtester.runtime.MultipleWidgetsFoundException; import com.windowtester.runtime.WidgetNotFoundException; import com.windowtester.runtime.swt.condition.SWTIdleCondition; import com.windowtester.runtime.swt.internal.finder.FinderUtil; import com.windowtester.runtime.swt.internal.operation.SWTDisplayLocation; import com.windowtester.runtime.swt.internal.operation.SWTMouseOperation; import com.windowtester.runtime.swt.internal.operation.SWTWidgetLocation; import com.windowtester.runtime.swt.internal.selector.PopupMenuSelector.PopupFailedException; import com.windowtester.runtime.swt.internal.widgets.ISWTWidgetReference; import com.windowtester.runtime.swt.internal.widgets.SWTWidgetReference; /** * A base widget selector that provides basic widget selection functions. */ public class BasicWidgetSelector implements ISWTWidgetSelectorDelegate { //TODO: parcel up these constants elsewhere? /** Suitable delay for most cases; tests have been run safely at this value. Should definitely be less than the double-click threshold. (The default value, zero, causes half the tests to fail on linux). FIXME need to find a value between 0 and 100 (100 is kinda slow). 30 works (almost) for w32/linux, but OSX 10.1.5 text input lags (50 is minimum). <p> As platforms are tested at 0 delay, adjust this value.<p> OSX test run time was reduced from 130s to 96s.<p> Not sure it's worth tracking down all the robot bugs and working around them. */ private static final int DEFAULT_DELAY = Platform.isOSX() || Platform.isLinux() || Platform.isWindows() ? 0 : 50; //constant used for closing menus in case of failure private static final int MAX_MENU_DEPTH = 5; /** Normal click delay: between mouseDown and mouseUp */ private int clickDelay = DEFAULT_DELAY; /** Double click delay: between first and the second click */ private int doubleClickDelay = DEFAULT_DELAY; protected int getClickDelay() { return clickDelay; } protected void setClickDelay(int ms) { clickDelay = ms; } protected int getDoubleClickDelay() { return doubleClickDelay; } protected void setDoubleClickDelay(int ms) { doubleClickDelay = ms; } //a dispatcher for raw SWT events private final DisplayEventDispatcher _dispatcher = new DisplayEventDispatcher(); /////////////////////////////////////////////////////////////////////////// // // Click actions // /////////////////////////////////////////////////////////////////////////// /** Click in the center of the given component. This is not static b/c it * sometimes needs to be redefined (i.e. JComponent to scroll before * clicking). */ public synchronized Widget click(final Widget w) { return click(w, SWT.BUTTON1); } public Widget click(final Widget w, final int mask) { Rectangle rect = getBounds(w); return click(w, rect.width/2, rect.height/2, mask); } private Rectangle getBounds(Widget w) { return SWTWidgetReference.forWidget(w).getDisplayBounds(); } public Widget click(Widget w, int x, int y, int mask) { return click(w, x, y, mask, 1); } // public Widget doubleClick(Widget w, int mask) { // Rectangle rect = getBounds(w); // return click(w, rect.width/2, rect.height/2, mask, 2); // } public Widget doubleClick(Widget w, int x, int y, int mask) { return click(w, x, y, mask, 2); } /** * @throws MultipleWidgetsFoundException * @throws WidgetNotFoundException * @see com.windowtester.event.swt.ISWTWidgetSelectorDelegate#click(org.eclipse.swt.widgets.Widget, java.lang.String) */ public Widget click(Widget w, String itemLabel) throws WidgetNotFoundException, MultipleWidgetsFoundException { return click(w, itemLabel, SWT.BUTTON1); } /** * @throws MultipleWidgetsFoundException * @throws WidgetNotFoundException * @see com.windowtester.event.swt.ISWTWidgetSelectorDelegate#click(org.eclipse.swt.widgets.Widget, java.lang.String, int) */ public Widget click(Widget w, String itemLabel, int mask) throws WidgetNotFoundException, MultipleWidgetsFoundException { throw new UnsupportedOperationException(); //subclass responsibility } /** * @see com.windowtester.event.swt.ISWTWidgetSelectorDelegate#doubleClickItem(org.eclipse.swt.widgets.Widget, java.lang.String) */ public Widget doubleClickItem(Widget w, String itemLabel) { return doubleClick(w, itemLabel, SWT.BUTTON1); } /** * Must be overriden in subclasses. * @see com.windowtester.event.swt.ISWTWidgetSelectorDelegate#doubleClick(org.eclipse.swt.widgets.Widget, java.lang.String, int) */ public Widget doubleClick(Widget w, String itemLabel, int mask) { throw new UnsupportedOperationException(); //subclass responsibility } /** * @see com.windowtester.event.swt.ISWTWidgetSelectorDelegate#contextClick(org.eclipse.swt.widgets.Widget, java.lang.String) */ // public Widget contextClick(Widget w, String menuItemPath) throws MultipleWidgetsFoundException, WidgetNotFoundException { // // /** // * Disabling old approach in favor of BEA's PopupMenuSelector. // */ // //// //bring up the context menu //// click(w, SWT.BUTTON3); //// //find the menu and click the item //// return findChildMenuAndClick(w, path); // // Control control = FinderUtil.getControl(w); // if (control == null) // throw new UnsupportedOperationException("Context menus unsupported for widgets of type: " + w.getClass()); // Widget clicked = null; // // //clicked = new PopupMenuSelector2().contextClick(w, path); // // try { // clicked = new PopupMenuSelector2().runPopup(control, w, menuItemPath); // } catch (PopupFailedException e) { // throw new WidgetNotFoundException("Context Menu item: " + menuItemPath + " not found in widget " + SWTWidgetReference.forWidget(w).toString()); // } // // return clicked; // } public Widget contextClick(Widget w, int x, int y, String menuItemPath) throws WidgetNotFoundException, MultipleWidgetsFoundException { Control control = FinderUtil.getControl(w); if (control == null) throw new UnsupportedOperationException("Context menus unsupported for widgets of type: " + w.getClass()); Widget clicked = null; try { // TODO[pq]: this should return a reference and not a widget clicked = new PopupMenuSelector2().runPopup(control, w, x, y, menuItemPath); } catch (PopupFailedException e) { throw new WidgetNotFoundException("menu item: " + menuItemPath + " not found in widget " + w); } return clicked; } /** * @throws WidgetNotFoundException * @throws MultipleWidgetsFoundException * @see com.windowtester.event.swt.ISWTWidgetSelectorDelegate#contextClick(org.eclipse.swt.widgets.Widget, java.lang.String, java.lang.String) */ // public Widget contextClick(Widget w, String itemPath, String menuPath) throws WidgetNotFoundException, MultipleWidgetsFoundException { // throw new UnsupportedOperationException(); //subclass responsibility // } // protected Widget findChildMenuAndClick(Widget w, String path) throws MultipleWidgetsFoundException, WidgetNotFoundException { // Exception caught = null; // // try { // //TODO: finish cases --- and consider pushing into specific selectors (this forward dependency is bad) // if (w instanceof TreeItem) { // w = UIProxy.getParent((TreeItem)w); // } // if (w instanceof MenuItem) { // w = UIProxy.getParent((MenuItem)w); // } // if (w instanceof TableItem) { // w = new TableItemTester().getParent((TableItem)w); // } // Menu menu = (Menu)BasicFinder2.getDefault().find(new HierarchyMatcher(Menu.class, new InstanceMatcher(w))); // return new MenuItemSelector().click(menu, path); // // } catch (WidgetNotFoundException e) { // caught = e; // } catch (MultipleWidgetsFoundException e) { // caught = e; // } catch (abbot.finder.swt.WidgetNotFoundException e) { // caught = new WidgetNotFoundException(e.getMessage()); // } catch (abbot.finder.swt.MultipleWidgetsFoundException e) { // caught = new MultipleWidgetsFoundException(e.getMessage()); // } finally { // if (caught != null) { // LogHandler.log(caught); // //rethrow // if (caught instanceof MultipleWidgetsFoundException) // throw (MultipleWidgetsFoundException)caught; // if (caught instanceof WidgetNotFoundException) // throw (WidgetNotFoundException)caught; // } // } // return null; // } /* * TODO rewrite this to query everything it needs from the widget *before* * it starts posting mouse clicks (in click2). The widget can, in theory, * be disposed any time after the first click. */ public Widget click(final Widget w, final int x, final int y, final int mask, final int count) { if (w == null) return null; // boolean shift = (mask & SWT.SHIFT) ==SWT.SHIFT; // boolean ctrl = (mask & SWT.CTRL) == SWT.CTRL; // boolean check = (mask & SWT.CHECK) == SWT.CHECK; // boolean alt = (mask & SWT.ALT) == SWT.ALT; // Mac testing // boolean command = (mask & SWT.COMMAND) == SWT.COMMAND; // Mac testing // // int type = SWT.MouseUp; // // If we're simulating a modifier key then our thread must be made to wait until // // click2() generates the key-up event. Note that this is extremely dependent // // on the implementation of click2(). // if (ctrl||shift||check|alt|command) { // type = SWT.KeyUp; // } // Widget listenToWidget = w; // if(w instanceof ToolItem) // listenToWidget = UIProxy.getParent((ToolItem)w); // if(w instanceof CTabItem) // listenToWidget = UIProxy.getParent((CTabItem)w); // if(w instanceof TabItem) // listenToWidget = UIProxy.getParent((TabItem)w); // if(w instanceof TreeItem) // listenToWidget = UIProxy.getParent((TreeItem)w); if(w instanceof MenuItem){ click2(w, x, y, mask, count); pauseCurrentThread(300); if (Platform.isOSX()) // Mac testing wiggleMouseAt(w, x, y); return w; } // new SystemEventMonitor(listenToWidget, type){ // public void syncExecEvents() { click2(w, x, y, mask, count); // } // }.run(); return w; } private void wiggleMouseAt(Widget widget, int x, int y) { try { _dispatcher.mouseMove(widget, x+1, y+1); pauseCurrentThread(50); _dispatcher.mouseMove(widget, x, y); } catch (SWTException ex) { // ignore disposed widget problems } } /** * Click in the given part of the component. All other click methods * must eventually invoke this one. Except the cases that call the old * click(int,int,int,int) method, which does not handle checks. (Unless * those are bugs waiting to be found, which is a possibility.) * TODO rewrite this to query everything it needs from the widget *before* * it starts posting mouse clicks. The widget can, in theory, be disposed * any time after the first click. (Then check the sender tree.) */ protected Widget click2(final Widget w, final int x, final int y, int mask, int count) { // TODO[pq]: this mapping should be pushed up (the ref should be passed into this method) ISWTWidgetReference<?> ref = SWTWidgetReference.forWidget(w); new SWTMouseOperation(mask).at(new SWTWidgetLocation(ref, WTInternal.TOPLEFT).offset(x, y)).count(count).execute(); // printTraceMessage(w, x, y); // // boolean shift = (mask & SWT.SHIFT) == SWT.SHIFT; // boolean ctrl = (mask & SWT.CTRL) == SWT.CTRL; // boolean check = (mask & SWT.CHECK) == SWT.CHECK; // boolean alt = (mask & SWT.ALT) == SWT.ALT; // Mac testing // boolean command = (mask & SWT.COMMAND) == SWT.COMMAND; // Mac testing // // if (shift) // trace("got shift!"); // if (ctrl) // trace("got ctrl!"); // if (check) // trace("got check!"); // if (alt) // trace("got alt!"); // if (command) // trace("got command!"); // // // FIXME handle other modifiers // mask &= (SWT.BUTTON1 // |SWT.BUTTON2 // |SWT.BUTTON3); // // if (shift) // _dispatcher.keyDown(SWT.SHIFT); // if (ctrl) // _dispatcher.keyDown(SWT.CTRL); // if (alt) // _dispatcher.keyDown(SWT.ALT); // if (command) // _dispatcher.keyDown(SWT.COMMAND); // // // if(!Platform.isLinux() ){ // mousePress(w, x, y, mask); // }else{ // mouseMove(w, x, y); // new abbot.swt.Robot().mousePress(mask); // } // // [author=Dan] No pause on Linux between mouse down and mouse up // // because some controls such as CTabFolder may receive the mouse down // // and call OS.lock, thus preventing us from ever posting the mouse up // // until the user wiggles the mouse. // if (!Platform.isLinux()) { // menu item check doesn't work // pauseCurrentThread(getClickDelay()); // } // if (Platform.isOSX() || (Platform.isLinux()&& w instanceof MenuItem)) { // Mac testing // wiggleMouseAt(w, x, y); // } // // while (count-- > 1) { // if(!Platform.isLinux() ){ // _dispatcher.mouseUp(mask); // }else{ // new abbot.swt.Robot().mousePress(mask); // } // pauseCurrentThread(DEFAULT_DELAY); // if(!Platform.isLinux() ){ // _dispatcher.mouseDown(mask); // }else{ // new abbot.swt.Robot().mouseRelease(mask); // } // if (!Platform.isLinux()) // pauseCurrentThread(getClickDelay()); // if (Platform.isOSX()) // Mac testing // wiggleMouseAt(w, x, y); // } // if( !Platform.isLinux()){ // _dispatcher.mouseUp(mask); // }else{ // new abbot.swt.Robot().mouseRelease(mask); // } // // /** // * Handle checks here // */ // if (check) { // pauseCurrentThread(100); // setChecked(w); // } // // if (shift) // _dispatcher.keyUp(SWT.SHIFT); // if (ctrl) // _dispatcher.keyUp(SWT.CTRL); // if (alt) // _dispatcher.keyUp(SWT.ALT); // if (command) // _dispatcher.keyUp(SWT.COMMAND); if ((w != null) && (!w.isDisposed())) { waitForIdle(getDisplay(w)); } return w; } //guard to catch widget disposal timing issue private Display getDisplay(Widget w) { try { return w.getDisplay(); } catch (SWTException e) { return Display.getDefault(); } } /** Click in the given part of the component. All other click methods * must eventually invoke this one. * @deprecated */ protected void click(final int x, final int y, int mask, int count) { new SWTMouseOperation(mask).at(new SWTDisplayLocation().offset(x, y)).count(count).execute(); // boolean shift = (mask & SWT.SHIFT) == SWT.SHIFT; // boolean ctrl = (mask & SWT.CTRL) == SWT.CTRL; // boolean alt = (mask & SWT.ALT) == SWT.ALT; // Mac testing // boolean command = (mask & SWT.COMMAND) == SWT.COMMAND; // Mac testing // // if (shift) // trace("got shift!"); // if (ctrl) // trace("got ctrl!"); // if (alt) // trace("got alt!"); // if (command) // trace("got command!"); // // // FIXME handle other modifiers // mask &= (SWT.BUTTON1 // |SWT.BUTTON2 // |SWT.BUTTON3); // // if (shift) // _dispatcher.keyDown(SWT.SHIFT); // if (ctrl) // _dispatcher.keyDown(SWT.CTRL); // if (alt) // _dispatcher.keyDown(SWT.ALT); // if (command) // _dispatcher.keyDown(SWT.COMMAND); // // mouseMove(x, y); // // _dispatcher.mouseDown(mask); // pauseCurrentThread(getClickDelay()); // // while (count-- > 1) { // _dispatcher.mouseUp(mask); // pauseCurrentThread(DEFAULT_DELAY); // _dispatcher.mouseDown(mask); // pauseCurrentThread(getClickDelay()); // } // _dispatcher.mouseUp(mask); // // if (shift) // _dispatcher.keyUp(SWT.SHIFT); // if (ctrl) // _dispatcher.keyUp(SWT.CTRL); // if (alt) // _dispatcher.keyUp(SWT.ALT); // if (command) // _dispatcher.keyUp(SWT.COMMAND); } /* (non-Javadoc) * @see com.windowtester.event.swt.ISWTWidgetSelectorDelegate#getClickOffset(org.eclipse.swt.widgets.Widget) */ public Point getClickOffset(Widget w, int mask) { /* * Default is CENTER. */ Rectangle bounds = getBounds(w); return new Point(bounds.width/2, bounds.height/2); } /** * Set this widget to be checked (NOTE: the widget is guaranteed to have just been clicked). * Note: This method MUST cause a KeyUp event for click/click2 to work properly. (That is, * unless we need to model a modifier key being pressed during setChecked(). We might * not get the key-up event for the modifier key with this implementation.) */ protected void setChecked(Widget w) { //removing tentatively (default selection below should suffice) // if (Platform.isOSX()) { // Mac testing // Rectangle rect = UIProxy.getBounds(w); // Rectangle outer = rect; // if (w instanceof Item) { // SWTHierarchy h = new SWTHierarchy(getDisplay(w)); // Widget parent = h.getParent(w); // outer = UIProxy.getBounds(parent); // } // int left = rect.x - outer.x - 15; // magic number, offset from left to checkbox // mousePress(w, -left, rect.height/2, SWT.BUTTON1); // mouseRelease(SWT.BUTTON1); // } // fix for GTK must be safe on other platforms if (SWT.getPlatform().equals("gtk")){ // tree item special case // fix for: case 14412 // Note: logic taken from BEA TreeItemTester special case logic // Arrow left 3 times b/c linux native widget has 3 widgets, with most // left being the checkbox. if (w instanceof TreeItem || w instanceof Tree) { _dispatcher.keyClick(SWT.ARROW_LEFT); } _dispatcher.keyClick(SWT.ARROW_LEFT); _dispatcher.keyClick(SWT.ARROW_LEFT); } /** * The default check action is emit a ' ' keystroke (TODO: confirm on non Windows OSes) * Can be overrriden in subclasses -- to, for instance, use setChecked(..) where provided */ _dispatcher.keyClick(' '); } /** * Must be provided by subclasses. * @see com.windowtester.event.swt.ISWTWidgetSelectorDelegate#clickExpand(org.eclipse.swt.widgets.Widget) */ public Widget clickExpand(Widget w) { throw new UnsupportedOperationException(); //subclass responsibility } /////////////////////////////////////////////////////////////////////////// // // Selection actions // /////////////////////////////////////////////////////////////////////////// /** * @see com.windowtester.event.swt.ISWTWidgetSelectorDelegate#select(org.eclipse.swt.widgets.Widget, int, int) */ public void select(Widget w, int start, int stop) { throw new UnsupportedOperationException(); //subclass responsibility } /** * @see com.windowtester.event.swt.ISWTWidgetSelectorDelegate#selectAll(org.eclipse.swt.widgets.Widget) */ public void selectAll(Widget w) { throw new UnsupportedOperationException(); //subclass responsibility } /////////////////////////////////////////////////////////////////////////// // // Internal // /////////////////////////////////////////////////////////////////////////// /** * Get the bounding rectangle for the given Widget in global * screen coordinates. */ protected Rectangle getGlobalBounds(Widget w){ return getGlobalBounds(w,true); } /** * Get the bounding rectangle for the given Widget in global * screen coordinates, optionally ignoring the 'trimmings'. */ protected Rectangle getGlobalBounds(final Widget w, final boolean ignoreBorder){ Rectangle result = (Rectangle) Robot.syncExec(getDisplay(w), new RunnableWithResult() { public Object runWithResult() { return WidgetLocator.getBounds(w,ignoreBorder); } }); return result; } /** Set the focus on to the given component. */ /* TODO MAY NEED TO CHECK THAT THE CONTROL DOES INDEED HAVE FOCUS */ protected void setFocus(Widget widget) { TestHierarchy hierarchy = new TestHierarchy(Display.getDefault()); while(!(widget instanceof Control)) widget = hierarchy.getParent(widget); focus((Control)widget); waitForIdle(getDisplay(widget)); } /** Move keyboard focus to the given component. */ protected void focus(final Control c) { final Display display = c.getDisplay(); display.syncExec(new Runnable(){ public void run(){ if (!c.forceFocus()) trace("unable to give " + c + " focus"); } }); } protected void pauseDisplayThread(final Display d, final int ms){ d.syncExec(new Runnable() { public void run() { d.timerExec(ms, new Runnable() { public void run() { // do nothing } }); } }); } protected void pauseCurrentThread(int ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { } } protected /*synchronized*/ void waitForIdle(final Display display){ /* * Slow integ of new waitForIdle fixes * To start, only for GTK (to guard against win32 regressions) */ if (SWT.getPlatform().equals("gtk") || Platform.isOSX()) { new SWTIdleCondition(display).waitForIdle(); } else { /* * The OLD way to wait (found not safe in Linux) */ // display.syncExec(new Runnable() { // public void run() { // while(display.readAndDispatch()); // } // }); //provisional fix for Dialogs Opened During Window Tester Widget Selector Actions Cause Hangs new SWTIdleCondition(display).waitForIdle(); } } /////////////////////////////////////////////////////////////////////////// // // Debugging // /////////////////////////////////////////////////////////////////////////// protected void trace(String msg) { TraceHandler.trace(IRuntimePluginTraceOptions.WIDGET_SELECTION, msg); } protected void trace(Object msg) { trace(msg.toString()); } // private void printTraceMessage(final Widget w, final int x, final int y) { // Object traceMsg = UIProxy.syncExec(getDisplay(w), new RunnableWithResult() { // public Object runWithResult() { // return "Click at (" + x + "," + y + ") on " + w; // } // }); // trace(traceMsg); // } /////////////////////////////////////////////////////////////////////////// // // SWT event posting proxies // /////////////////////////////////////////////////////////////////////////// protected void keyClick(int key) { _dispatcher.keyClick(key); } protected void keyUp(int key) { _dispatcher.keyUp(key); } protected void keyDown(int key) { _dispatcher.keyDown(key); } protected void mousePress(int accelerator) { _dispatcher.mouseDown(accelerator); } protected void mousePress(final Widget w, int x, int y, int mask) { mouseMove(w, x, y); _dispatcher.mouseDown(mask); } protected void mouseRelease(int accelerator) { _dispatcher.mouseUp(accelerator); } /** * Move the mouse to hover over the center of this widget * @param w - the widget to hover over */ public void mouseMove(Widget w) { Rectangle rect = getBounds(w); mouseMove(w, rect.width/2, rect.height/2); } public void mouseMove(int x, int y) { // double mouse move needed to trigger highlighting in menus on Linux _dispatcher.mouseMove(x, y); _dispatcher.mouseMove(x, y); } public void mouseMove(Widget w, int x, int y) { // double mouse move needed to trigger highlighting in menus on Linux _dispatcher.mouseMove(w, x, y); _dispatcher.mouseMove(w, x, y); } /////////////////////////////////////////////////////////////////////////////// // // Error Handling // /////////////////////////////////////////////////////////////////////////////// /** * Close open menus (called on error cases). */ protected void handleMenuClose() { //TODO: this may be OS-specific... //TODO: this number should reflect the number of actual menu levels; for now we're just picking a constant MAX for (int i= 0; i <= MAX_MENU_DEPTH; ++i) { //System.err.println("ESC"); keyClick(SWT.ESC); //close menu by hitting ESCAPE } } }