/*******************************************************************************
* 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.swing;
import java.awt.Button;
import java.awt.Checkbox;
import java.awt.Choice;
import java.awt.Component;
import java.awt.Dialog;
import java.awt.Frame;
import java.awt.Label;
import java.awt.Point;
import java.awt.Window;
import java.util.Iterator;
import javax.swing.AbstractButton;
import javax.swing.JLabel;
import abbot.finder.AWTHierarchy;
import abbot.finder.Hierarchy;
import com.windowtester.internal.runtime.ClassReference;
import com.windowtester.internal.runtime.IDiagnostic;
import com.windowtester.internal.runtime.IDiagnosticParticipant;
import com.windowtester.internal.runtime.locator.IUISelector;
import com.windowtester.internal.runtime.matcher.CompoundMatcher;
import com.windowtester.internal.runtime.matcher.ExactClassMatcher;
import com.windowtester.internal.swing.UIContextSwing;
import com.windowtester.internal.swing.condition.HasFocusConditionHandler;
import com.windowtester.internal.swing.matcher.ClassByNameMatcher;
import com.windowtester.internal.swing.matcher.ClassMatcher;
import com.windowtester.internal.swing.matcher.HierarchyMatcher;
import com.windowtester.internal.swing.matcher.IndexMatcher;
import com.windowtester.internal.swing.matcher.NameOrTextMatcher;
import com.windowtester.runtime.IClickDescription;
import com.windowtester.runtime.IUIContext;
import com.windowtester.runtime.InaccessableWidgetException;
import com.windowtester.runtime.WidgetSearchException;
import com.windowtester.runtime.condition.HasFocus;
import com.windowtester.runtime.condition.HasText;
import com.windowtester.runtime.condition.IUICondition;
import com.windowtester.runtime.condition.IUIConditionHandler;
import com.windowtester.runtime.condition.IsEnabled;
import com.windowtester.runtime.condition.IsVisible;
import com.windowtester.runtime.condition.IsVisibleCondition;
import com.windowtester.runtime.locator.IWidgetLocator;
import com.windowtester.runtime.locator.IWidgetMatcher;
import com.windowtester.runtime.locator.IWidgetReference;
import com.windowtester.runtime.locator.WidgetReference;
import com.windowtester.runtime.swing.locator.JButtonLocator;
import com.windowtester.runtime.swing.locator.JCheckBoxLocator;
import com.windowtester.runtime.swing.locator.JRadioButtonLocator;
import com.windowtester.runtime.swing.locator.JTableItemLocator;
import com.windowtester.runtime.swing.locator.JToggleButtonLocator;
/**
* A class that captures Swing hierarchy (containment) relationships between widgets for use
* in widget identification.
*/
public class SwingWidgetLocator extends com.windowtester.runtime.WidgetLocator
implements IUISelector, IDiagnosticParticipant, IsVisible {
/*
* NOTE: this class is serializable and uses the default serialization scheme.
* This should _not_ be a problem (hierarchies are not too deep); still, we
* could consider a custom serialization scheme.
*
* ADDENDUM [11/04/06]: the matcher is transient. As a consequence, it does not
* get serialized. In general this should be OK, since serialization
* is intended for the case where locators are passed over the wire
* for use in codegen. That said, we might remedy this in the future by,
* for instance, making the matcher lazily initialized.
*/
private static final long serialVersionUID = -6896731161658482507L;
/** Delegate matcher
* NOTICE: the matcher is transient.
*/
protected transient IWidgetMatcher _matcher; //TODO: build up in constructor
/**
* Create an instance that identifies an SWT widget by its class name.
* @param className the target widget's fully qualified class name
* @since 3.8.1
*/
public SwingWidgetLocator(String className) {
this(className, null);
}
/**
* Create an instance that identifies an SWT widget by its class name.
* @param className the target widget's fully qualified class name
* @param parent the target's parent
* @since 3.8.1
*/
public SwingWidgetLocator(String className, SwingWidgetLocator parent) {
this(className, UNASSIGNED, parent);
}
/**
* Create an instance that identifies an SWT widget by its class name.
* @param className the target widget's fully qualified class name
* @param index the target's index relative to its parent
* @param parent the target's parent
* @since 3.8.1
*/
public SwingWidgetLocator(String className, int index, SwingWidgetLocator parent) {
super(ClassReference.forName(className), null, index, parent);
_matcher = ClassByNameMatcher.create(className);
if (index != UNASSIGNED)
_matcher = IndexMatcher.create(_matcher, index);
if (parent != null){
if (index != UNASSIGNED)
_matcher = HierarchyMatcher.create(_matcher, parent.getMatcher(), index);
else
_matcher = HierarchyMatcher.create(_matcher,parent.getMatcher());
}
}
/**
* Create an instance.
* @param cls - the target class
*/
public SwingWidgetLocator(Class cls) {
this(cls,(SwingWidgetLocator)null);
}
/**
* Create an instance.
* @param cls - the target class
* @param index - the target's index relative to its parent
*/
public SwingWidgetLocator(Class cls, int index) {
this(cls, null, index);
}
/**
* Create an instance.
* @param cls - the target class
* @param nameOrLabel - the target's name or label
*/
public SwingWidgetLocator(Class cls, String nameOrLabel) {
this(cls, nameOrLabel,(SwingWidgetLocator)null);
}
/**
* Create an instance.
* @param cls - the target class
* @param parentInfo - the target's parent info
*/
public SwingWidgetLocator(Class cls, SwingWidgetLocator parentInfo) {
this(cls, null,parentInfo);
}
/**
* Create an instance.
* @param cls - the target class
* @param index - the target's index relative to its parent
* @param parentInfo - the target's parent info
*/
public SwingWidgetLocator(Class cls, int index, SwingWidgetLocator parentInfo) {
this(cls, null,index, parentInfo);
}
/**
* Create an instance.
* @param cls - the target class
* @param nameOrLabel - the target's name or label
* @param index - the target's index relative to its parent
*
*/
public SwingWidgetLocator(Class cls, String nameOrLabel, int index) {
this(cls, nameOrLabel, index,null);
}
/**
* Create an instance.
* @param cls - the target class
* @param nameOrLabel - the target's name or label
* @param parentInfo - the target's parent info
*/
public SwingWidgetLocator(Class cls, String nameOrLabel, SwingWidgetLocator parentInfo) {
this(cls, nameOrLabel,UNASSIGNED,parentInfo);
}
/**
* Create an instance.
* @param cls - the target class
* @param nameOrLabel - the target's name or label
* @param index - the target's index relative to its parent
* @param parentInfo - the target's parent info
*/
public SwingWidgetLocator(Class cls, String nameOrLabel, int index, SwingWidgetLocator parentInfo) {
super(cls, nameOrLabel, index, parentInfo);
// create the matcher
// components such as buttons, lists and tables are matched by whether they are
// an instance of their respective swing classes, and not by exact class matching
if (cls != null){
if (this instanceof JButtonLocator || this instanceof JRadioButtonLocator )
_matcher = ClassMatcher.create(cls);
else if (this instanceof JCheckBoxLocator || this instanceof JToggleButtonLocator
|| this instanceof JTableItemLocator )
_matcher = ClassMatcher.create(cls);
else _matcher = new ExactClassMatcher(cls);
}
if ((nameOrLabel != null) && (nameOrLabel.length() > 0))
_matcher = new CompoundMatcher(_matcher, NameOrTextMatcher.create(nameOrLabel));
if (index != UNASSIGNED)
_matcher = IndexMatcher.create(_matcher, index);
if (parentInfo != null){
if (index != UNASSIGNED)
_matcher = HierarchyMatcher.create(_matcher, parentInfo.getMatcher(), index);
else
_matcher = HierarchyMatcher.create(_matcher,parentInfo.getMatcher());
}
}
/* (non-Javadoc)
* @see com.windowtester.runtime.WidgetLocator#matches(java.lang.Object)
*/
public boolean matches(Object widget) {
//delegates matching to component matcher
//will be overriden in subclasses
//commonly, subclasses will want to define their match to call super as well:
// return doSubclassMatching(w) && super.matches(w);
return _matcher.matches(widget);
}
public IWidgetMatcher getMatcher(){
return _matcher;
}
///////////////////////////////////////////////////////////////////////////////
//
// Widget finding convenience methods
//
///////////////////////////////////////////////////////////////////////////////
protected Component findComponent(IUIContext ui) throws WidgetSearchException {
//we know this is a widget reference
WidgetReference widgetReference = (WidgetReference) ui.find(this);
//get the widget
return (Component)widgetReference.getWidget();
}
///////////////////////////////////////////////////////////////////////////////
//
// Basic click functionality
//
///////////////////////////////////////////////////////////////////////////////
public IWidgetLocator click(IUIContext ui, IWidgetReference widget, IClickDescription click) throws WidgetSearchException {
Component component = (Component)widget.getWidget();
Point offset = getXYOffset(component, click);
Component clicked = doClick(ui, click.clicks(), component, offset, click.modifierMask());
return WidgetReference.create(clicked, this);
}
/**
* Perform the click. This is intended to be overridden in subclasses
* @param clicks - the number of clicks
* @param w - the widget to click
* @param offset - the x,y offset (from top left corner)
* @param modifierMask - the mouse modifier mask
* @return the clicked widget
*/
protected Component doClick(IUIContext ui, int clicks, Component c, Point offset, int modifierMask) {
return ((UIContextSwing)ui).getDriver().click(clicks,c, offset.x, offset.y, modifierMask);
}
public IWidgetLocator contextClick(IUIContext ui, IWidgetReference widget, IClickDescription click, String menuItemPath) throws WidgetSearchException {
Component component = (Component)widget.getWidget();
//TODO: hook up xys
Component clicked = ((UIContextSwing)ui).getDriver().contextClick(component, menuItemPath);
return WidgetReference.create(clicked, this);
}
/**
* Get the x,y offset for the click.
* @param click
*/
public Point getXYOffset(Component c, IClickDescription click) {
if (unspecifiedXY(click)) {
return new Point(c.getWidth()/2, c.getHeight()/2);
}
return new Point(click.x(), click.y());
}
/**
* Test this click to see if an offset is specified.
*/
protected boolean unspecifiedXY(IClickDescription click) {
//dummy sentinel for now
return click.relative() == -1;
}
/////////////////////////////////////////////////////////////////////////
// Diagnostics
//
/////////////////////////////////////////////////////////////////////////
/**
* Provide additional diagnostic information
*/
public void diagnose(IDiagnostic diagnostic) {
Hierarchy h = AWTHierarchy.getDefault();
Iterator iter = h.getRoots().iterator();
while (iter.hasNext()) {
Component c = (Component)iter.next();
if (((Window)c).isActive()){
getAllChildren(c,h,diagnostic);
}
else {
if (c.getClass().getName().equals("javax.swing.SwingUtilities$SharedOwnerFrame")){
Window[] windows = ((Window)c).getOwnedWindows();
for (int i = 0; i< windows.length; i++){
if (windows[i].isActive())
getAllChildren(c,h,diagnostic);
}
}
}
}
}
private void getAllChildren(Component c, Hierarchy h,IDiagnostic diagnostic){
Iterator iter = h.getComponents(c).iterator();
while (iter.hasNext()) {
Component component = (Component)iter.next();
getAllChildren(component,h, diagnostic);
}
if (c instanceof Frame || c instanceof Dialog ){
diagnostic.attribute("class", c.getClass().toString());
if (c.getName()!= null)
diagnostic.attribute("name", c.getName());
diagnostic.attribute("title", getComponentText(c));
}
else if (c.getClass().equals(getTargetClass())){
diagnostic.attribute("class", c.getClass().toString());
if (c.getName()!= null)
diagnostic.attribute("name", c.getName());
diagnostic.attribute("text", getComponentText(c));
}
}
private String getComponentText(Component w){
if (w instanceof Button)
return ((Button)w).getLabel();
if (w instanceof Checkbox)
return ((Checkbox)w).getLabel();
if (w instanceof Choice)
return ((Choice)w).getSelectedItem();
if (w instanceof Label)
return ((Label)w).getText();
if (w instanceof AbstractButton)
return ((AbstractButton)w).getText();
if (w instanceof JLabel)
return ((JLabel)w).getText();
if (w instanceof Dialog)
return ((Dialog)w).getTitle();
if (w instanceof Frame)
return ((Frame)w).getTitle();
return "";
}
////////////////////////////////////////////////////////////////////////////
//
// HasText
//
////////////////////////////////////////////////////////////////////////////
/**
* Resolve the locator to a single object and answer the text associated with it.
* This method is ONLY supported for those subclasses that implement the
* {@link HasText} interface. This method finds the widget then calls the
* {@link #getWidgetText(Component)} to obtain the widget text.
*
* @param ui the UI context in which to find the widgets
* @return the text associated with that object (may be null)
*/
public String getText(IUIContext ui) throws WidgetSearchException {
IWidgetLocator found = ui.find(this);
if (found instanceof IWidgetReference) {
Object widget = ((IWidgetReference) found).getWidget();
String result = getWidgetText((Component)widget);
return result;
}
return null;
}
/**
* This is called by {@link #getText(IUIContext)} to obtain the
* widget's text. Subclasses that implement {@link HasText} should override
* {@link #getText(IUIContext)} or this method to return text for the widget, because
* this method always throws a Runtime "not implemented" exception. This is only
* intended to be called by the {@link #getText(IUIContext)} method and not by
* clients.
*
* @param widget the widget from which text is to be obtained (not <code>null</code>)
* @return the widget's text or <code>null</code> if none
* @throws RuntimeException if this is not supported by this type of locator
*/
protected String getWidgetText(Component widget) throws WidgetSearchException {
throw new InaccessableWidgetException("HasText not implemented by this locator: " + getClass().getName());
}
////////////////////////////////////////////////////////////////////////////
//
// IsEnabledLocator
//
////////////////////////////////////////////////////////////////////////////
/**
* Resolve the locator to a single object and determine if that object is enabled.
* This method is ONLY supported for those subclasses that implement the
* {@link IsEnabled} interface. This method finds the widget then calls the
* {@link #isWidgetEnabled(Component)} to determine if the widget is
* enabled.
*
* @param ui the UI context in which to find the widgets
* @return <code>true</code> if the object is enabled, else false
* @see IsEnabled#isEnabled()
*/
public boolean isEnabled(IUIContext ui) throws WidgetSearchException {
IWidgetLocator found = ui.find(this);
if (found instanceof IWidgetReference) {
final Object widget = ((IWidgetReference) found).getWidget();
boolean result = isWidgetEnabled((Component)widget);
return result;
}
return false;
}
/**
* This is called by {@link #isEnabled(IUIContext)} on the UI thread to determine if
* the widget is enabled. Subclasses may override to provide additional or alternate
* behavior. This is only intended to be called by the {@link #isEnabled(IUIContext)}
* method and not by clients.
*
* @param widget the widget to be tested (not <code>null</code>)
* @return <code>true</code> if enabled, else <code>false</code>
*/
protected boolean isWidgetEnabled(Component widget) throws WidgetSearchException {
return widget.isEnabled();
}
////////////////////////////////////////////////////////////////////////////
//
// HasFocusLocator
//
////////////////////////////////////////////////////////////////////////////
/**
* Resolve the locator to a single object and determine if that object has focus.
* This method is ONLY supported for those subclasses that implement the
* {@link HasFocus} interface.
*
* @param ui the UI context in which to find the widgets
* @return <code>true</code> if the object has focus, else false
* @see IsVisible#isVisible()
*/
public boolean hasFocus(IUIContext ui) throws WidgetSearchException {
return new HasFocusConditionHandler(this).hasFocus(ui);
}
/**
* Resolve the locator to a single object and determine if that object has focus.<p/>
* Used in an {@link IUIContext#ensureThat(com.windowtester.runtime.condition.IConditionHandler)} clause, the resulting condition
* can be used to ensure that the associated widget has focus. For example:
* <p>
* <code>
* ui.ensureThat(new ButtonLocator("OK").hasFocus());
* </code>
* </p>
* tests if the "OK" button has focus and if it does not, gives it focus.
*
*/
public IUIConditionHandler hasFocus() {
return new HasFocusConditionHandler(this);
}
////////////////////////////////////////////////////////////////////////////
//
// IsVisibleLocator
//
////////////////////////////////////////////////////////////////////////////
public boolean isVisible(IUIContext ui) throws WidgetSearchException {
return ui.findAll(this).length > 0;
}
/**
* Create a condition that tests if the widget is visible.
* Note that this is a convenience method, equivalent to:
* <code>isSelected(true)</code>
*/
public IUICondition isVisible() {
return isVisible(true);
}
/**
* Create a condition that tests if the given the widget is visible.
* @param selected
* @param expected <code>true</code> if the widget is expected to be selected, else
* <code>false</code>
*/
public IUICondition isVisible(boolean expected) {
return new IsVisibleCondition(this, expected);
}
}