/******************************************************************************* * 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 java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; 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.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.TreeItem; import org.eclipse.swt.widgets.Widget; import abbot.WaitTimedOutError; import abbot.finder.swt.TestHierarchy; import abbot.finder.swt.WidgetSearchException; import abbot.script.Condition; import abbot.tester.swt.ControlTester; import abbot.tester.swt.MenuItemTester; import abbot.tester.swt.MenuTester; import abbot.tester.swt.Robot; import abbot.tester.swt.WidgetTester; import com.windowtester.internal.debug.IRuntimePluginTraceOptions; import com.windowtester.internal.debug.TraceHandler; import com.windowtester.runtime.WT; import com.windowtester.runtime.swt.internal.operation.SWTDisplayLocation; import com.windowtester.runtime.swt.internal.operation.SWTLocation; import com.windowtester.runtime.swt.internal.operation.SWTMouseOperation; import com.windowtester.runtime.util.ScreenCapture; import com.windowtester.runtime.util.StringComparator; import com.windowtester.runtime.util.TestMonitor; import com.windowtester.runtime.swt.internal.settings.TestSettings; import com.windowtester.swt.util.PathStringTokenizer; /** * Selector helper for context menus. * * @author Phil Quitslund */ public class PopupMenuSelector extends BasicWidgetSelector { public static final String DEFAULT_CASCADING_MENUITEM_SEPARATOR = "/"; /* * Now in TestSettings */ //private static final long DEFAULT_WAIT_TIMEOUT_SECONDS = 10; //wait for single Menu to be visible, or MenuItem to be enabled. private static final int DEFAULT_WAIT_INTERVAL_MILLISECONDS = 500; private static final int DEFAULT_USER_DELAY = 500; private static final int LINUX_DELAY = 1000; private static final int SECOND = 1000; private static final String MESSAGE_PREFIX = "PopupMenuSelector2: "; // private final Display _display; public PopupMenuSelector() { this(Display.getDefault()); } public PopupMenuSelector(Display display) { // _display = display; } private void click(MenuItem item) { new MenuItemTester().actionClick(item); } private void dismissMenus(int numberOfPoppedMenus) { for(int i = 0; i < numberOfPoppedMenus; i++) { log(MESSAGE_PREFIX + "Dismissing menu..."); keyClick(SWT.ESC); } } private void takeScreenShot() { String testcaseID = TestMonitor.getInstance().getCurrentTestCaseID(); ScreenCapture.createScreenCapture(testcaseID); } /** * Provide some logic to determine a click location, if one was * not provided by the caller of runPopup. * * @param widget the widget used to determine the ClickLocation * @return The ClickLocation for the widget */ private ClickLocation getDefaultClickLocation(Widget widget) { ClickLocation location = null; if (widget instanceof TreeItem) { location = new ClickTopLeft(widget); //TreeItem are clicked top left, b/c they might have very long text, that is scrolled. } else { location = new ClickCenter(widget); //The center seems ok for most widgets. } return location; } private void invokePopup(ClickLocation clickLocation) { //assert clickLocation != null : "clickLocation was null."; log(MESSAGE_PREFIX + "Invoking at ClickLocation: " + clickLocation); Point p = clickLocation.getClickLocation(); // /* // * We tried using abbot's mousePress functionality, but it failed to work // * consistently with on linux. The Event approach was taken from // * eclipse'st swt tests. // */ // // Event event = new Event(); // // /* // * Provisioning for possibly remapped keys here: // */ // event.button = MouseConfig.SECONDARY_BUTTON; // // event.type = SWT.MouseMove; // event.x = p.x; // event.y = p.y; // new SWTPushEventOperation(event).execute(); // pauseCurrentThread(200); // // event = new Event(); // event.button = MouseConfig.SECONDARY_BUTTON; // event.type = SWT.MouseDown; // event.x = p.x; // event.y = p.y; // new SWTPushEventOperation(event).execute(); // pauseCurrentThread(200); // // event = new Event(); // event.button = MouseConfig.SECONDARY_BUTTON; // event.type = SWT.MouseUp; // event.x = p.x; // event.y = p.y; // new SWTPushEventOperation(event).execute(); new SWTMouseOperation(WT.BUTTON3).at(new SWTDisplayLocation().offset(p)).execute(); pauseCurrentThread(200); } private void log(String msg) { //System.out.println(msg); } /** * Right click a Widget to invoke a popup/context menu, and then * click the menu item. * * @param menuOwner * @param widgetToRightClick * @param menuItemPath - this may be the text for a single menu item, or a path (delimited by "/") * of menu items for cascading menus. */ public Widget runPopup(Control menuOwner, Widget widgetToRightClick, String menuItemPath) { return runPopup(menuOwner, widgetToRightClick, menuItemPath, DEFAULT_CASCADING_MENUITEM_SEPARATOR); } public Widget runPopup(Control menuOwner, Widget widgetToRightClick, int x, int y, String menuItemPath) { return runPopup(menuOwner, widgetToRightClick, new ClickOffset(widgetToRightClick, x, y), menuItemPath, DEFAULT_CASCADING_MENUITEM_SEPARATOR, null); } public Widget runPopup(Control menuOwner, Widget widgetToRightClick, String menuItemPath, String pathDelimiter) { return runPopup(menuOwner, widgetToRightClick, null, menuItemPath, pathDelimiter, null); } public Widget runPopup(Control menuOwner, ClickLocation clickLocation, String menuItemPath) { return runPopup(menuOwner, clickLocation, menuItemPath, DEFAULT_CASCADING_MENUITEM_SEPARATOR, null); } private Widget runPopup(Control menuOwner, Widget widgetToRightClick, ClickLocation clickLocation, String menuItemPath, String pathDelimiter, List/*<PopupItemInfo>*/ verificationList) { if (clickLocation == null) { clickLocation = getDefaultClickLocation(widgetToRightClick); } return runPopup(menuOwner, clickLocation, menuItemPath, pathDelimiter, verificationList); } private Widget runPopup(final Control menuOwner, final ClickLocation clickLocation, final String menuItemPath, String pathDelimiter, final List/*<PopupItemInfo>*/ verificationList) { //assert menuItemPath != null : "menuItemPath was null."; //assert !menuItemPath.equals("") : "menuItemPath was empty."; //assert clickLocation != null : "clickLocation was null."; int popUpPause = TestSettings.getInstance().getPreContextClickDelay(); pauseCurrentThread(popUpPause); //FIXME: CR264634 - we need a condition to tell us when we are ready. final StringTokenizer menuItemTexts = new PathStringTokenizer(menuItemPath /*, pathDelimiter */); //final StringTokenizer menuItemTexts = new StringTokenizer(menuItemPath, pathDelimiter ); final int numberOfMenus = menuItemTexts.countTokens(); final MenuHandler initialMenuHandler = new MenuHandler(menuOwner); final String userThreadId = MESSAGE_PREFIX + menuItemPath; final UserThread userThread = new UserThread(userThreadId, initialMenuHandler, menuItemPath, menuItemTexts, clickLocation, verificationList); try { userThread.start(); long waitPerMenu = TestSettings.getInstance().getWaitForContextMenuTimeOut(); long maxWait = numberOfMenus * (2 * waitPerMenu/*DEFAULT_WAIT_TIMEOUT_SECONDS * SECOND*/ + DEFAULT_USER_DELAY) + SECOND + LINUX_DELAY; if (verificationList != null) { long verificationWait = verificationList.size() * waitPerMenu /*DEFAULT_WAIT_TIMEOUT_SECONDS * SECOND*/; maxWait = maxWait + verificationWait; } log("Waiting for a maximum number of seconds: " + maxWait / SECOND); Robot.wait(new Condition() { public boolean test() { return userThread.isFinished(); } //@Override public String toString() { return MESSAGE_PREFIX + "User Thread to finish for " + userThreadId ; } }, maxWait, DEFAULT_WAIT_INTERVAL_MILLISECONDS); //This wait must ensure that all handlers have enough time to finish; } finally { Throwable throwable = userThread.getThrowable(); if(throwable != null) { throw new PopupFailedException(throwable); } } return userThread.getClicked(); } // public void verifyPopupContents(Composite menuAndRectangleOwner, org.eclipse.draw2d.geometry.Rectangle rectangleToRightClick, String menuItemPath, String pathDelimiter, List<PopupItemInfo> verificationList) { // runPopup(menuAndRectangleOwner, rectangleToRightClick, null, menuItemPath, pathDelimiter, verificationList); // } public void verfiyPopupContents(Control menuOwner, ClickLocation clickLocation, String menuItemPath, List/*<PopupItemInfo>*/ verificationList) { runPopup(menuOwner, clickLocation, menuItemPath, DEFAULT_CASCADING_MENUITEM_SEPARATOR, verificationList); } public void verifyPopupContents(Control menuOwner, Widget widgetToRightClick, List/*<PopupItemInfo>*/ verificationList) { runPopup(menuOwner, widgetToRightClick, null, "", DEFAULT_CASCADING_MENUITEM_SEPARATOR, verificationList); } public void verifyPopupContents(Control menuOwner, Widget widgetToRightClick, String menuItemPath, List/*<PopupItemInfo>*/ verificationList) { runPopup(menuOwner, widgetToRightClick, null, menuItemPath, DEFAULT_CASCADING_MENUITEM_SEPARATOR, verificationList); } public void verifyPopupContents(Control menuOwner, Widget widgetToRightClick, String menuItemPath, String pathDelimiter, List/*<PopupItemInfo>*/ verificationList) { runPopup(menuOwner, widgetToRightClick, null, menuItemPath, pathDelimiter, verificationList); } private void verifyPopupContents(Menu popup, List /*<PopupItemInfo>*/ verificationList) { log(MESSAGE_PREFIX + "Verifying PopupItemInfo list..."); // for(PopupItemInfo itemInfo : verificationList) // { for (Iterator iter = verificationList.iterator(); iter.hasNext(); ) { PopupItemInfo itemInfo = (PopupItemInfo)iter.next(); MenuItemHandler itemHandler = new MenuItemHandler(popup); itemHandler.waitForMenuItem(itemInfo.text, itemInfo.enabled); log(MESSAGE_PREFIX + itemInfo); } } public static class ClickCenter extends WidgetClickLocation { public ClickCenter(Widget widget) { super(widget); } //@Override public Point getClickLocation() { WidgetTester widgetTester = new WidgetTester(); Rectangle bounds = widgetTester.getGlobalBounds(_widget); Point point = new Point(bounds.x + bounds.width/2, bounds.y + bounds.height/2); return point; } public SWTLocation asSWTLocation() { // return new SWTWidgetLocation(_widget, WTInternal.CENTER); throw new RuntimeException("Reimplement caller to use widget reference"); } } //!pq: public static class ClickOffset extends WidgetClickLocation { private final int _x; private final int _y; public ClickOffset(Widget widget, int x, int y) { super(widget); _x = x; _y = y; } //@Override public Point getClickLocation() { WidgetTester widgetTester = new WidgetTester(); Rectangle bounds = widgetTester.getGlobalBounds(_widget); Point point = new Point(bounds.x + _x, bounds.y +_y); return point; } public SWTLocation asSWTLocation() { // return new SWTWidgetLocation(_widget, WTInternal.TOPLEFT).offset(_x, _y); throw new RuntimeException("Reimplement caller to use widget reference"); } } public static abstract class ClickLocation { abstract public Point getClickLocation(); abstract public SWTLocation asSWTLocation(); //@Override public String toString() { String msg = getClass().getName() + ": "; msg = msg + (getClickLocation() != null ? getClickLocation().toString() : "null click location"); return msg; } } // public static class ClickRectangle extends ClickLocation // { // protected org.eclipse.draw2d.geometry.Rectangle _bounds; // protected Composite _parent; // private CompositeTester _compositeTester; // // public ClickRectangle(Composite parent, org.eclipse.draw2d.geometry.Rectangle bounds) // { // _bounds = bounds; // _parent = parent; // _compositeTester = TesterFactory.newCompositeTester(); // } // // //@Override // public Point getClickLocation() // { // final org.eclipse.draw2d.geometry.Point center = _bounds.getCenter(); // Display display = _compositeTester.getDisplay(_parent); // Point p = (Point) Robot.syncExec(display, new RunnableWithResult() { // public Object runWithResult() { // return _parent.toDisplay(center.x, center.y); // } // }); // return p; // } // } public static class ClickTopLeft extends WidgetClickLocation { public ClickTopLeft(Widget widget) { super(widget); } //@Override public Point getClickLocation() { WidgetTester widgetTester = new WidgetTester(); Rectangle bounds = widgetTester.getGlobalBounds(_widget); Point point = new Point(bounds.x + 1, bounds.y + 1); return point; } public SWTLocation asSWTLocation() { // return new SWTWidgetLocation(_widget, WTInternal.TOPLEFT).offset(1, 1); throw new RuntimeException("Reimplement caller to use widget reference"); } } // public static class ClickTopRight extends WidgetClickLocation // { // final private int _offset; // // public ClickTopRight(Widget widget) // { // this(widget, 1); // } // // public ClickTopRight(Widget widget, int offset) // { // super(widget); // _offset = offset; // } // // //@Override // public Point getClickLocation() // { // WidgetTester widgetTester = new WidgetTester(); // Rectangle bounds = widgetTester.getGlobalBounds(_widget); // int x = bounds.width - _offset; // //if there's a vertical scrollbar, click right next to it. // if (_widget instanceof Scrollable) // { // Scrollable scrollable = (Scrollable)_widget; // ScrollBarTester scrollBarTester = new ScrollBarTester(); // ScrollableTester scrollableTester = new ScrollableTester(); // if (scrollableTester.getVerticalBar(scrollable) != null && // scrollBarTester.getVisible(scrollableTester.getVerticalBar(scrollable))) // { // int scrollBarWidth = scrollBarTester.getSize(scrollableTester.getVerticalBar((Scrollable)_widget)).x; // x = x - scrollBarWidth; // } // } // Point point = new Point(bounds.x + x, bounds.y + 0); // return point; // } // } private class MenuHandler implements Condition { private String LISTENER_MESSAGE_PREFIX = MESSAGE_PREFIX; private Widget _menuOwner; private Menu _menu; private Throwable _throwable; private final ControlTester _controlTester; private final MenuTester _menuTester; private final MenuItemTester _menuItemTester; public MenuHandler(Widget menuOwner) { _menuOwner = menuOwner; if (menuOwner instanceof MenuItem) { LISTENER_MESSAGE_PREFIX = LISTENER_MESSAGE_PREFIX + "CascadedMenuHandler: "; } else { LISTENER_MESSAGE_PREFIX = LISTENER_MESSAGE_PREFIX + "MenuHandler: "; } _controlTester = new ControlTester(); _menuTester = new MenuTester(); _menuItemTester = new MenuItemTester(); } /** * Return true, if menu not null, and is visible. */ public boolean test() { boolean visible = false; try { if(_menu != null) { visible = _menuTester.isVisible(_menu); if(!visible) { _throwable = new WidgetSearchException("Menu NOT VISIBLE.[" + _menuTester.toString(_menu) + "]"); } } else { _menu = findMenu(_menuOwner); } } catch(SWTException e) { _throwable = e; //A widget *could* be disposed. return false; } return visible; } /** * Provide debug output suitable for a Robot.wait(Condition) debug * message. */ //@Override public String toString() { StringBuffer message = new StringBuffer(); message.append("Menu to be visible. "); if(_throwable != null) { message.append(_throwable.getLocalizedMessage()); } else { message.append("Menu NOT FOUND."); } return message.toString(); } public Menu waitForVisibleMenu() { _throwable = null; //DEFAULT_WAIT_TIMEOUT_SECONDS * SECOND int maxWait = TestSettings.getInstance().getWaitForContextMenuTimeOut(); Robot.wait(this, maxWait, DEFAULT_WAIT_INTERVAL_MILLISECONDS); log(LISTENER_MESSAGE_PREFIX + "Found visible menu."); return this._menu; } /** * Return the menu with owner.getMenu, if available, * otherwise, search the hiearchy for descendants of the owner, which * are Controls, and return the first Menu found. * * Returns null if menu not found. */ private Menu findMenu(Widget owner) { log(LISTENER_MESSAGE_PREFIX + "Looking for menu for: " + WidgetTester.toString(owner)); Menu menu = null; if(owner instanceof Control) { menu = _controlTester.getMenu((Control)owner); } else if(owner instanceof MenuItem) { menu = _menuItemTester.getMenu((MenuItem)owner); } if(menu == null) { menu = findMenuRecursively(owner); } if(menu != null) { log(LISTENER_MESSAGE_PREFIX + "Found menu: " + WidgetTester.toString(menu)); } return menu; } /** * Search direct children for menu, if not found, look recursively through children. * Only return the menu from a Control. */ private Menu findMenuRecursively(Widget parent) { log(LISTENER_MESSAGE_PREFIX + "Looking for menu recursively for: " + WidgetTester.toString(parent)); Display display = _menuTester.getDisplay(parent); TestHierarchy hierarchy = new TestHierarchy(display); //@SuppressWarnings("unchecked") Collection/*<Widget>*/ children = hierarchy.getWidgets(parent); //This returns other widgets, in addition, to getChildren() // for(Widget control : children) // { for(Iterator iter = children.iterator(); iter.hasNext(); ) { Widget control = (Widget)iter.next(); if(control instanceof Control) { Menu menu = _controlTester.getMenu((Control)control); if (menu != null) { return menu; } } } // for(Widget control : children) // { for(Iterator iter = children.iterator(); iter.hasNext(); ) { Widget control = (Widget)iter.next(); if(control instanceof Composite) { Menu menu = findMenuRecursively((Composite)control); if(menu != null) { return menu; } } } return null; } } private class MenuItemHandler implements Condition { private final Menu _parent; private String _itemText; private MenuItem _item; private boolean _expectedState = true; private Throwable _throwable; private final MenuTester _menuTester; private final MenuItemTester _menuItemTester; public MenuItemHandler(Menu menu) { //assert menu != null : "menu was null."; _parent = menu; _menuTester = new MenuTester(); _menuItemTester = new MenuItemTester(); } /** * Return true if MenuItem found and is in correct enabled state. */ public boolean test() { boolean enabled = !_expectedState; try { if(_itemText == null) { _throwable = new WidgetSearchException("itemText was null for MenuItemHandler."); return false; } _item = getNextMenuItem(_parent, _itemText); enabled = _menuItemTester.isEnabled(_item); if(!(enabled == _expectedState)) { _throwable = new WidgetSearchException("MenuItem not in correct enabled state. "); return false; } } catch(WidgetSearchException e) { _throwable = e; return false; } return enabled == _expectedState; } //@Override public String toString() { StringBuffer message = new StringBuffer(); message.append("MenuItem [" + _itemText + "], with enabled == [" + _expectedState + "] "); if(_throwable != null) { message.append(_throwable.getLocalizedMessage()); } return message.toString(); } public MenuItem waitForMenuItem(String itemText, boolean expectedState) { _throwable = null; _itemText = itemText; // @todo: we might just want to restrict a _expectedState = expectedState; int maxWait = TestSettings.getInstance().getWaitForContextMenuTimeOut(); Robot.wait(this, maxWait /*DEFAULT_WAIT_TIMEOUT_SECONDS * SECOND*/, DEFAULT_WAIT_INTERVAL_MILLISECONDS); return _item; } private MenuItem getNextMenuItem(Menu parent, String itemText) throws WidgetSearchException { //assert parent != null : "parent Menu was null."; //assert itemText != null : "itemText was null."; MenuItem[] items = _menuTester.getItems(parent); StringBuffer buffer = new StringBuffer(); buffer.append("Menu Item ["); buffer.append(itemText); buffer.append("] NOT FOUND in item list:\n"); for(int i = 0; i < items.length; i++) { String text = _menuItemTester.getText(items[i]); if(stringsMatch(itemText, text)) { return items[i]; } buffer.append("\n"); buffer.append(text); } throw new WidgetSearchException(buffer.toString()); } private boolean stringsMatch(String expected, String actual) { if(expected == null || actual == null) { return false; } else { return StringComparator.matches(actual, expected); } } } public static class PopupFailedException extends RuntimeException { private static final long serialVersionUID = 2699160308329690072L; private PopupFailedException(String msg) { super(msg); } private PopupFailedException(Throwable t) { super(t); } } /** Data class for verifying existence and state of a MenuItem in a popup. */ public static class PopupItemInfo { public final String text; public final boolean enabled; public PopupItemInfo(String text, boolean enabled) { this.text = text; this.enabled = enabled; } //@Override public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("Text="); buffer.append(text); buffer.append("\n"); buffer.append("ExpectedState="); buffer.append(enabled); buffer.append("\n"); return buffer.toString(); } } private class UserThread extends Thread { private Throwable _throwable; private boolean _finished; private int _numberOfPoppedMenus; private final MenuHandler _initialMenuHandler; private final String _menuItemPath; private final StringTokenizer _menuItemTexts; private final ClickLocation _clickLocation; private final List/*<PopupItemInfo>*/ _verificationList; //the clicked item private MenuItem _clicked; private UserThread(String id, MenuHandler initialMenuHandler, String menuItemPath, StringTokenizer menuItemTexts, ClickLocation clickLocation, List/*<PopupItemInfo>*/ verificationList) { super(id); _initialMenuHandler = initialMenuHandler; _menuItemPath = menuItemPath; _menuItemTexts = menuItemTexts; _clickLocation = clickLocation; _verificationList = verificationList; } public Throwable getThrowable() { return _throwable; } public boolean isFinished() { return _finished; } public MenuItem getClicked() { return _clicked; } //@Override public void run() { _finished = false; _throwable = null; _numberOfPoppedMenus = 0; try { /* Invoke the popup Menu, and click MenuItem(s) */ log(MESSAGE_PREFIX + "Invoking popup Menu with path: " + _menuItemPath); invokePopup(_clickLocation); // Wait for popup Menu Menu menu = _initialMenuHandler.waitForVisibleMenu(); _numberOfPoppedMenus++; MenuHandler cascadedMenuHandler = null; // Loop for clicking and verifying menu items. while(_menuItemTexts.hasMoreTokens()) { String itemText = _menuItemTexts.nextToken(); // Otherwise continue with either clicking, or cascading MenuItemHandler itemHandler = new MenuItemHandler(menu); MenuItem item = itemHandler.waitForMenuItem(itemText, true); pauseCurrentThread(DEFAULT_USER_DELAY); // This delay more closely simulates user interaction. log(MESSAGE_PREFIX + "Clicking Menu Item: " + itemText); click(item); _clicked = item; cascadedMenuHandler = new MenuHandler(item); if(_menuItemTexts.hasMoreTokens()) { log(MESSAGE_PREFIX + "Cascading..."); menu = cascadedMenuHandler.waitForVisibleMenu(); _numberOfPoppedMenus++; } } // Verify popup menu items if required if(_verificationList != null) { if(cascadedMenuHandler != null) { menu = cascadedMenuHandler.waitForVisibleMenu(); _numberOfPoppedMenus++; } try { verifyPopupContents(menu, _verificationList); } catch(WaitTimedOutError e) { //capture screen BEFORE dismissing menus TraceHandler.trace(IRuntimePluginTraceOptions.BASIC, "creating screenshot of pop-up timeout failure"); takeScreenShot(); //rethrow exception throw e; } finally { dismissMenus(_numberOfPoppedMenus); } } } catch(Throwable e) { //capture screen BEFORE dismissing menus TraceHandler.trace(IRuntimePluginTraceOptions.BASIC, "creating screenshot of pop-up timeout failure"); takeScreenShot(); _throwable = e; dismissMenus(_numberOfPoppedMenus); } finally { log(MESSAGE_PREFIX + "Finished."); _finished = true; } } } public static abstract class WidgetClickLocation extends ClickLocation { protected Widget _widget; public WidgetClickLocation(Widget widget) { _widget = widget; } } }