Java tutorial
// [Twin] Copyright eBay Inc., Twin authors, and other contributors. // This file is provided to you under the terms of the Apache License, Version 2.0. // See LICENSE.txt and NOTICE.txt for license and copyright information. package org.ebayopensource.twin; import java.lang.reflect.*; import java.util.*; import java.awt.Point; import java.awt.Dimension; import java.awt.Rectangle; import org.apache.commons.codec.binary.Base64; import org.ebayopensource.twin.ScrollBar.Orientation; import org.ebayopensource.twin.element.*; import org.ebayopensource.twin.pattern.*; /** * This class actually implements the behaviour of Element and its subinterfaces. * All Elements returned from Twin are generated by Element.create(). This returns proxy objects * that reflectively delegate to an instance of ElementImpl. Note that these are not themselves ElementImpl instances. * The only reason ElementImpl implements Element is so we get a static check that all methods will be present at runtime. */ class ElementImpl extends RemoteResource implements Element { /** Win32 window class */ private String className; /** UIAutomation AutomationId */ private String id; /** Name property, cached from last fetch, for use in toString() */ private String cachedName; /** * For internal use only. Creates an Element wrapping the given RemoteObject * This should be used instead of new Element(), as it will instantiate the correct subclass. */ public static Element create(RemoteObject o) { if (o == null) return null; List<Class<?>> interfaces = new ArrayList<Class<?>>(); interfaces.add(Element.class); interfaces.add(RemoteResourceInterface.class); final Class<? extends ControlType> controlTypeInterface = NameMappings .getTypeInterface((String) o.properties.get("controlType")); if (controlTypeInterface.equals(Desktop.class)) return new DesktopImpl(o.session); if (controlTypeInterface != null) interfaces.add(controlTypeInterface); List<Class<? extends ControlPattern>> controlPatternInterfaces = getControlPatternInterfaces(o); interfaces.addAll(controlPatternInterfaces); final ElementImpl impl = new ElementImpl(o, controlTypeInterface, controlPatternInterfaces); if (interfaces.isEmpty()) return impl; final HashSet<Class<?>> implementedPatterns = new HashSet<Class<?>>(); for (Class<?> iface : interfaces) if (isInterfaceExtending(iface, ControlPattern.class)) implementedPatterns.add(iface); return (Element) Proxy.newProxyInstance(ElementImpl.class.getClassLoader(), interfaces.toArray(new Class[interfaces.size()]), new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Method implMethod = null; try { implMethod = impl.getClass().getMethod(method.getName(), method.getParameterTypes()); } catch (NoSuchMethodException e) { implMethod = impl.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes()); } Require requirement = implMethod.getAnnotation(Require.class); if (requirement != null) { for (Class<?> pattern : requirement.pattern()) if (!implementedPatterns.contains(pattern)) throw new TwinException("This " + (impl.getControlType() == null ? "Unknown" : impl.getControlType()) + " does not implement the control pattern " + pattern.getSimpleName()); if (requirement.type() != Void.class) if (controlTypeInterface != requirement.type()) throw new TwinException("This " + (impl.getControlType() == null ? "Unknown" : impl.getControlType()) + " is not of ControlType " + requirement.type().getSimpleName()); } try { return implMethod.invoke(impl, args); } catch (InvocationTargetException e) { throw e.getCause(); } } }); } private static List<Class<? extends ControlPattern>> getControlPatternInterfaces(RemoteObject o) { List<Class<? extends ControlPattern>> result = new ArrayList<Class<? extends ControlPattern>>(); if (o.properties.containsKey("controlPatterns")) { for (Object patternObj : (List<?>) o.properties.get("controlPatterns")) { String pattern = String.valueOf(patternObj); Class<? extends ControlPattern> matchingIface = NameMappings.getPatternInterface(pattern); if (matchingIface != null) result.add(matchingIface); } } return result; } /** Is c an interface that directly extends i? */ static final boolean isInterfaceExtending(Class<?> c, Class<?> i) { if (!c.isInterface()) return false; for (Class<?> d : c.getInterfaces()) if (i.equals(d)) return true; return false; } /** * For internal use only. Creates an Element wrapping the given RemoteObject * Element.create() should be used instead, as it will instantiate the correct subclass. */ protected ElementImpl(RemoteObject o, Class<? extends ControlType> controlType, List<Class<? extends ControlPattern>> controlPatterns) { super(o); this.controlType = controlType; this.controlPatterns = controlPatterns; this.className = (String) o.properties.get("className"); this.id = (String) o.properties.get("id"); this.cachedName = (String) o.properties.get("name"); } /** * For internal use only. Creates an element without an associated RemoteObject. * Use with caution! */ protected ElementImpl(Application session, Class<? extends ControlType> controlType, List<Class<? extends ControlPattern>> controlPatterns) { super(session); this.controlType = controlType; this.controlPatterns = controlPatterns; } public String getCachedName() { return cachedName; } public String getName() throws TwinException { return cachedName = (String) session.request("GET", getPath() + "/name", null); } public String getId() throws TwinException { return id; } public String getClassName() throws TwinException { return className; } // /** Get the ControlType of this element. This is an abstract description of the function performed by the element */ // public ControlTypeEnum getControlType() { // return controlType; // } private Class<? extends ControlType> controlType; private List<Class<? extends ControlPattern>> controlPatterns; public Class<? extends ControlType> getControlType() { return controlType; } public List<Class<? extends ControlPattern>> getControlPatterns() { return controlPatterns; } public boolean is(Class<? extends Element> pattern) { if (pattern.isInterface()) for (Class<?> iface : pattern.getInterfaces()) { if (iface == ControlPattern.class) return controlPatterns.contains(pattern); else if (iface == ControlType.class) return controlType == pattern; } // if it's not a control type or control pattern, return instanceof return pattern.isInstance(this); } public <T extends Element> T as(Class<T> pattern) { T t = pattern.cast(this); // generate exception if regular type doesn't match if (!is(pattern)) throw new ClassCastException("Object does not support " + pattern.getSimpleName() + ": " + this); return t; } @Require(pattern = Editable.class) public String getValue() throws TwinException { return (String) session.request("GET", getPath() + "/value", null); } @Require(pattern = Editable.class) public void setValue(String s) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("value", s); session.request("POST", getPath() + "/value", data); } @Require(pattern = Editable.class) public boolean isReadOnly() throws TwinException { return session.options(getPath() + "/value").contains("POST"); } public boolean isEnabled() throws TwinException { return (boolean) (Boolean) session.request("GET", getPath() + "/enabled", null); } @Require(pattern = Expandable.class) public boolean isExpanded() throws TwinException { return (boolean) (Boolean) session.request("GET", getPath() + "/expanded", null); } @Require(pattern = Expandable.class) public void setExpanded(boolean expanded) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("expanded", expanded); session.request("POST", getPath() + "/expanded", data); } @Require(pattern = Selectable.class) public boolean isSelected() throws TwinException { return (boolean) (Boolean) session.request("GET", getPath() + "/selected", null); } @Require(pattern = Selectable.class) public void setSelected(boolean selected) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("selected", selected); session.request("POST", getPath() + "/selected", data); } @Require(pattern = Selectable.class) public SelectionContainer getContainer() throws TwinException { RemoteObject container = (RemoteObject) session.request("GET", getPath() + "/selection-container", null); return (SelectionContainer) ElementImpl.create(container); } @Require(pattern = SelectionContainer.class) public boolean isMultipleSelectionAllowed() throws TwinException { Map<String, Object> result = session.requestObject("GET", getPath() + "/selection", null); return (boolean) (Boolean) result.get("multiple"); } @Require(pattern = SelectionContainer.class) public boolean isSelectionRequired() throws TwinException { Map<String, Object> result = session.requestObject("GET", getPath() + "/selection", null); return (boolean) (Boolean) result.get("required"); } @Require(pattern = SelectionContainer.class) public List<Selectable> getSelection() throws TwinException { Map<String, Object> result = session.requestObject("GET", getPath() + "/selection-info", null); List<?> selectionObjects = (List<?>) result.get("values"); List<Selectable> ret = new ArrayList<Selectable>(); for (Object obj : selectionObjects) { if (!(obj instanceof Selectable)) throw new IllegalStateException( "getSelection() /selection-info contained " + obj + " which is not selectable"); ret.add((Selectable) obj); } return ret; } @Require(pattern = SelectionContainer.class) public Selectable getSelectedItem() throws TwinException { List<Selectable> selection = getSelection(); if (selection.size() == 0) return null; if (selection.size() == 1) return selection.get(0); throw new TwinException("Expected 0 or 1 result, got " + selection); } public Dimension getSize() throws TwinException { Map<String, Object> results = session.requestObject("GET", getPath() + "/bounds", null); int width = ((Number) results.get("width")).intValue(); int height = ((Number) results.get("height")).intValue(); return new Dimension(width, height); } public Point getLocation() throws TwinException { Map<String, Object> results = session.requestObject("GET", getPath() + "/bounds", null); int x = ((Number) results.get("x")).intValue(); int y = ((Number) results.get("y")).intValue(); return new Point(x, y); } public Rectangle getBounds() throws TwinException { Map<String, Object> results = session.requestObject("GET", getPath() + "/bounds", null); int width = ((Number) results.get("width")).intValue(); int height = ((Number) results.get("height")).intValue(); int x = ((Number) results.get("x")).intValue(); int y = ((Number) results.get("y")).intValue(); return new Rectangle(x, y, width, height); } @Require(pattern = Transformable.class) public void setSize(int width, int height) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("width", width); data.put("height", height); session.request("POST", getPath() + "/size", data); } @Require(pattern = Transformable.class) public void setLocation(int x, int y) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("x", x); data.put("y", y); session.request("POST", getPath() + "/location", data); } @Require(pattern = Transformable.class) public void setBounds(int x, int y, int width, int height) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("x", x); data.put("y", y); data.put("width", width); data.put("height", height); session.request("POST", getPath() + "/bounds", data); } @Require(pattern = Toggle.class) public boolean getState() throws TwinException { return (boolean) (Boolean) session.request("GET", getPath() + "/toggle", null); } @Require(pattern = Toggle.class) public void setState(boolean b) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("state", b); session.request("POST", getPath() + "/toggle", data); } @Require(pattern = Toggle.class) public boolean toggle() throws TwinException { return (boolean) (Boolean) session.request("POST", getPath() + "/toggle", null); } public void click() throws TwinException { session.request("POST", getPath() + "/click", null); } public void click(MouseButton button) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("button", button.toString().toLowerCase()); session.request("POST", getPath() + "/click", data); } public void click(int x, int y) throws TwinException { click(x, y, MouseButton.Left); } public void click(int x, int y, MouseButton button) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("button", button.toString().toLowerCase()); data.put("x", x); data.put("y", y); session.request("POST", getPath() + "/click", data); } public Menu contextMenu() throws TwinException { click(MouseButton.Right); return (Menu) session.getDesktop().waitForDescendant(Criteria.type(Menu.class), 1); } public Menu contextMenu(int x, int y) throws TwinException { click(x, y, MouseButton.Right); return (Menu) session.getDesktop().waitForDescendant(Criteria.type(Menu.class), 1); } public String getStructure(boolean verbose) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("verbose", verbose); return (String) session.request("GET", getPath() + "/structure", data); } public String getStructure() throws TwinException { return getStructure(false); } public Screenshot getScreenshot() throws TwinException { return decodeScreenshot(session.requestObject("GET", getPath() + "/screenshot", null)); } public Screenshot getScreenshot(Rectangle bounds) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("x", bounds.x); data.put("y", bounds.y); data.put("width", bounds.width); data.put("height", bounds.height); return decodeScreenshot(session.requestObject("GET", getPath() + "/screenshot", data)); } public Screenshot getBoundsScreenshot() throws TwinException { return getApplication().getDesktop().getScreenshot(getBounds()); } public void sendKeys(String text) throws TwinException { Map<String, Object> keys = new HashMap<String, Object>(); keys.put("keys", text); session.request("POST", getPath() + "/keyboard", keys); } public void type(String text) throws TwinException { text = text.replaceAll("[\\+\\^\\%\\(\\)\\{\\}\\[\\]]", "{$0}"); text = text.replace("\n", "~"); text = text.replace("\b", "{BS}"); text = text.replace("\t", "{TAB}"); sendKeys(text); } private Element cachedParent; public Element getCachedParent() { if (cachedParent == null) return getParent(); return cachedParent; } public Element getParent() throws TwinException { return cachedParent = ElementImpl .create((RemoteObject) session.request("GET", getPath() + "/parent", null)); } public List<Element> getChildren() throws TwinException { return getChildren(null); } public <T extends Element> List<T> getChildren(Criteria criteria) throws TwinException { return getElements("children", criteria, 0, 0, false); } public List<Element> getDescendants(Criteria criteria) throws TwinException { return getElements("descendants", criteria, 0, 0, false); } public <T extends Element> T getChild(Criteria criteria) throws TwinException { return single(this.<T>getElements("children", criteria, 0, 0, true)); } public <T extends Element> T waitForChild(Criteria criteria) throws TwinException { return this.<T>waitForChild(criteria, getApplication().getTimeout()); } public <T extends Element> T waitForChild(Criteria criteria, double timeout) throws TwinException { return single(this.<T>getElements("children", criteria, 0, timeout, true)); } public <T extends Element> T getDescendant(Criteria criteria) throws TwinException { return single(this.<T>getElements("descendants", criteria, 0, 0, true)); } public <T extends Element> T waitForDescendant(Criteria criteria) throws TwinException { return this.<T>waitForDescendant(criteria, getApplication().getTimeout()); } public <T extends Element> T waitForDescendant(Criteria criteria, double timeout) throws TwinException { return single(this.<T>getElements("descendants", criteria, 0, timeout, true)); } public <T extends Element> List<T> getClosestDescendants(Criteria criteria) throws TwinException { return getElements("descendants", criteria, 1, 0, false); } public <T extends Element> List<T> waitForClosestDescendants(Criteria criteria) throws TwinException { return waitForClosestDescendants(criteria, getApplication().getTimeout()); } public <T extends Element> List<T> waitForClosestDescendants(Criteria criteria, double timeout) throws TwinException { return getElements("descendants", criteria, 1, timeout, false); } /** * Internal impl behind {get,waitFor}{Closest,}{Child,Children,Descendant,Descendants} methods * @param path what to append to getPath(), e.g. "/children" or "/descendants" * @param criteria the criteria to apply (process-ID matching is added by the server) * @param count if 0, return all results. Else BFS layer-by-layer until we have at least count * @param timeout if 0, return results immediately. Else don't return an empty result set until this timeout has elapsed * @param shouldThrow should throw a TwinNoSuchElementException on empty list */ @SuppressWarnings("unchecked") private <T extends Element> List<T> getElements(String subpath, Criteria criteria, int count, double timeout, boolean shouldThrow) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); if (criteria != null) data.put("criteria", criteria); if (count > 0) data.put("count", count); if (timeout > 0) { if (Double.isInfinite(timeout)) data.put("waitForResults", true); else data.put("waitForResults", timeout); } List<Object> searchResults = session.requestArray("GET", getPath() + "/" + subpath, data); List<T> result = new ArrayList<T>(); for (Object remote : searchResults) result.add((T) ElementImpl.create((RemoteObject) remote)); if (shouldThrow && result.isEmpty()) { String message = "Found no " + subpath + " of " + this; if (criteria != null) message += " matching " + criteria; if (timeout > 0) message += " after waiting " + timeout; throw TwinError.NoSuchElement.create(message); } return result; } /** Return the single element in a single list, null for an empty list, and throw for a list with multiple entries */ private <T> T single(List<T> list) throws TwinException { if (list.size() == 1) return list.get(0); throw new TwinException("Expected 1 result, found " + list.size()); } public void focus() throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("focusedElement", remote); session.request("POST", "/element/active", data); } /** Decode a screenshot from the result object */ private Screenshot decodeScreenshot(Map<String, Object> results) { String contentType = (String) results.get("contentType"); String base64data = (String) results.get("data"); byte[] data = new Base64().decode(base64data); return new Screenshot(data, contentType); } /** Get the server path for this element */ public String getPath() { return "/element/" + remote.uuid; } /** Return a string including the controltype, last knownname, className, id, and server UUID of this element */ public String toString() { StringBuffer sb = new StringBuffer(NameMappings.getTypeName(controlType)); sb.append("("); if (cachedName != null) sb.append("name=").append(cachedName).append(' '); if (className != null) sb.append("class=").append(className).append(' '); sb.append("id=").append(id).append(")"); sb.append("@").append(remote); return sb.toString(); } public ScrollBar getHorizontalScrollBar() throws TwinException { return getScrollBar(ScrollBar.Orientation.Horizontal); } public ScrollBar getVerticalScrollBar() throws TwinException { return getScrollBar(ScrollBar.Orientation.Vertical); } public ScrollBar getScrollBar(ScrollBar.Orientation orientation) throws TwinException { String resource = (orientation == ScrollBar.Orientation.Horizontal) ? "axisX" : "axisY"; RemoteObject o = (RemoteObject) session.request("GET", getPath() + "/" + resource, null); if (o == null) throw TwinError.NoSuchElement.create("Couldn't get " + orientation + " scrollbar for element " + this); return new ScrollBarImpl(o, this, orientation); } public Element button(String name) throws TwinException { return single(this.getClosestDescendants(Criteria.type(Button.class).and(Criteria.name(name)))); } public boolean exists() throws TwinException { return (Boolean) session.request("GET", getPath() + "/exists", null); } public void waitForNotExists() throws TwinException { waitForNotExists(getApplication().getTimeout()); } public void waitForNotExists(double timeout) throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("timeout", timeout); data.put("value", false); session.request("POST", getPath() + "/exists", data); } public void scrollVisible(Element child) throws TwinException { Rectangle bounds = getBounds(); Rectangle childBounds = child.getBounds(); scrollVisible(child, Orientation.Horizontal, bounds, childBounds); scrollVisible(child, Orientation.Vertical, bounds, childBounds); } public void scrollVisible(Element child, Orientation orientation) { scrollVisible(child, orientation, getBounds(), child.getBounds()); } private void scrollVisible(Element child, Orientation orientation, Rectangle bounds, Rectangle childBounds) { // if we're in bounds, we're done and don't need to check for a bar if (orientation.contains(bounds, childBounds)) return; // sanity check: child may be too big if (orientation.getSize(childBounds) > orientation.getSize(bounds)) throw new TwinException("Cannot scroll element " + child + " visible inside " + this + " for orientation " + orientation + ": child is bigger than parent"); ScrollBar bar = getScrollBar(orientation); if (bar == null) throw new TwinException("Cannot scroll element " + child + " visible inside " + this + " for orientation " + orientation + ": no scrollbar"); // test positions at 1.0 and 0.0, if neither match then we can use the results to interpolate bar.setPosition(1); Rectangle maxBounds = child.getBounds(); if (orientation.contains(bounds, maxBounds)) // cheap client side check, maybe we're done return; bar.setPosition(0); Rectangle minBounds = child.getBounds(); if (orientation.contains(bounds, minBounds)) // cheap client side check, maybe we're done return; // the pixel difference between max-scroll and min-scroll double scrollHeight = orientation.getMid(maxBounds) - orientation.getMid(minBounds); if (scrollHeight == 0) // scroll bar does nothing throw new TwinException("Cannot scroll element " + child + " visible inside " + this + " for orientation " + orientation + ": it does not move in response to scrollbar action"); // the difference between centre-of-child-at-minscroll and centre-of-this double offsetFromMinInPixels = orientation.getMid(bounds) - orientation.getMid(minBounds); // same thing, but scaled to scroll height double offsetFromMinAsFraction = offsetFromMinInPixels / scrollHeight; // clamp to [0,1] range if (offsetFromMinAsFraction < 0) offsetFromMinAsFraction = 0; if (offsetFromMinAsFraction > 1) offsetFromMinAsFraction = 1; bar.setPosition(offsetFromMinAsFraction); // now we should be done... maybe we should do some sanity check here } @Require(type = Menu.class) public MenuItem item(int index) throws TwinException { List<Element> elements = getDescendants(Criteria.type(MenuItem.class)); return (MenuItem) elements.get(index); } @Require(type = Menu.class) public MenuItem item(String name) throws TwinException { return (MenuItem) getDescendant(Criteria.type(MenuItem.class).and(Criteria.name(name))); } @Require(type = MenuItem.class) public Menu openMenu() throws TwinException { click(); try { return (Menu) waitForChild(Criteria.type(Menu.class), 1); } catch (TwinNoSuchElementException e) { Menu open = session.getOpenMenu(); if (open == null) throw TwinError.NoSuchElement.create("Couldn't find child menu"); return open; } } @Require(type = Window.class) public MenuItem menu(String name) throws TwinException { return (MenuItem) getDescendant(Criteria.type(MenuItem.class).and(Criteria.name(name))); } @Require(type = Window.class) public void close() throws TwinException { remote.session.request("DELETE", getPath(), null); } @Require(type = Window.class) public boolean isMaximized() throws TwinException { return "maximized" .equalsIgnoreCase((String) remote.session.request("GET", getPath() + "/window-state", null)); } @Require(type = Window.class) public boolean isMinimized() throws TwinException { return "minimized" .equalsIgnoreCase((String) remote.session.request("GET", getPath() + "/window-state", null)); } @Require(type = Window.class) public void maximize() throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("state", "maximized"); remote.session.request("POST", getPath() + "/window-state", data); } @Require(type = Window.class) public void minimize() throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("state", "minimized"); remote.session.request("POST", getPath() + "/window-state", data); } @Require(type = Window.class) public void restore() throws TwinException { Map<String, Object> data = new HashMap<String, Object>(); data.put("state", "normal"); remote.session.request("POST", getPath() + "/window-state", data); } }