From 5dc265932e584f67ba7344809e186700e3b182a9 Mon Sep 17 00:00:00 2001 From: jonahss Date: Thu, 17 Apr 2014 14:24:34 -0700 Subject: [PATCH 1/3] performTouch --- src/io/appium/java_client/AppiumDriver.java | 9 + src/io/appium/java_client/MobileCommand.java | 2 + src/io/appium/java_client/MobileDriver.java | 2 + src/io/appium/java_client/TouchAction.java | 158 ++++++++++++++++++ .../java_client/MobileDriverGestureTest.java | 84 ++++++++++ test/io/appium/java_client/TestApp.app.zip | Bin 0 -> 22171 bytes 6 files changed, 255 insertions(+) create mode 100644 src/io/appium/java_client/TouchAction.java create mode 100644 test/io/appium/java_client/MobileDriverGestureTest.java create mode 100755 test/io/appium/java_client/TestApp.app.zip diff --git a/src/io/appium/java_client/AppiumDriver.java b/src/io/appium/java_client/AppiumDriver.java index aa37e209f..54b4ffb47 100644 --- a/src/io/appium/java_client/AppiumDriver.java +++ b/src/io/appium/java_client/AppiumDriver.java @@ -17,6 +17,7 @@ package io.appium.java_client; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import org.openqa.selenium.*; import org.openqa.selenium.remote.*; @@ -50,6 +51,7 @@ public AppiumDriver(URL remoteAddress, Capabilities desiredCapabilities){ .put(HIDE_KEYBOARD, postC("/session/:sessionId/appium/device/hide_keyboard")) .put(PUSH_FILE, postC("/session/:sessionId/appium/device/push_file")) .put(RUN_APP_IN_BACKGROUND, postC("/session/:sessionId/appium/app/background")) + .put(PERFORM_TOUCH_ACTION, postC("/session/:sessionId/touch/perform")) ; ImmutableMap mobileCommands = builder.build(); @@ -176,6 +178,13 @@ public void runAppInBackground(int seconds) { execute(RUN_APP_IN_BACKGROUND, ImmutableMap.of("seconds", seconds)); } + public TouchAction performTouchAction(TouchAction touchAction) { + ImmutableMap parameters = touchAction.getParameters(); + touchAction.clearParameters(); + execute(PERFORM_TOUCH_ACTION, parameters); + + return touchAction; + } @Override public WebDriver context(String name) { diff --git a/src/io/appium/java_client/MobileCommand.java b/src/io/appium/java_client/MobileCommand.java index 062186d22..8f999d906 100644 --- a/src/io/appium/java_client/MobileCommand.java +++ b/src/io/appium/java_client/MobileCommand.java @@ -33,6 +33,8 @@ public interface MobileCommand { String PUSH_FILE = "pushFile"; String HIDE_KEYBOARD = "hideKeyboard"; String RUN_APP_IN_BACKGROUND = "runAppInBackground"; + String PERFORM_TOUCH_ACTION = "performTouchAction"; + String PERFORM_MULTI_TOUCH = "performMultiTouch"; } diff --git a/src/io/appium/java_client/MobileDriver.java b/src/io/appium/java_client/MobileDriver.java index 80453b202..93b002d1d 100644 --- a/src/io/appium/java_client/MobileDriver.java +++ b/src/io/appium/java_client/MobileDriver.java @@ -29,4 +29,6 @@ public interface MobileDriver extends WebDriver, ContextAware { public Response execute(String driverCommand, Map parameters); + public TouchAction performTouchAction(TouchAction touchAction); + } diff --git a/src/io/appium/java_client/TouchAction.java b/src/io/appium/java_client/TouchAction.java new file mode 100644 index 000000000..2e4e35429 --- /dev/null +++ b/src/io/appium/java_client/TouchAction.java @@ -0,0 +1,158 @@ +package io.appium.java_client; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.RemoteWebElement; + +/* + +Copyright 2014 Appium contributors + +Copyright 2014 Software Freedom Conservancy + + + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + + + + http://www.apache.org/licenses/LICENSE-2.0 + + + +Unless required by applicable law or agreed to in writing, software + +distributed under the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and + +limitations under the License. + + */ +public class TouchAction { + + private MobileDriver driver; + ImmutableList.Builder parameterBuilder; + + public TouchAction(MobileDriver driver) { + this.driver = driver; + parameterBuilder = ImmutableList.builder(); + } + + public TouchAction press(WebElement el) { + ActionParameter action = new ActionParameter("press", (RemoteWebElement)el); + parameterBuilder.add(action); + return this; + } + + public TouchAction press(WebElement el, int x, int y) { + ActionParameter action = new ActionParameter("press", (RemoteWebElement)el); + action.addParameter("x", x); + action.addParameter("y", y); + parameterBuilder.add(action); + return this; + } + + public TouchAction release() { + ActionParameter action = new ActionParameter("release"); + parameterBuilder.add(action); + return this; + } + + public TouchAction moveTo(WebElement el) { + ActionParameter action = new ActionParameter("moveTo", (RemoteWebElement)el); + parameterBuilder.add(action); + return this; + } + + public TouchAction moveTo(WebElement el, int x, int y) { + ActionParameter action = new ActionParameter("moveTo", (RemoteWebElement)el); + action.addParameter("x", x); + action.addParameter("y", y); + parameterBuilder.add(action); + return this; + } + + public TouchAction tap(WebElement el) { + ActionParameter action = new ActionParameter("tap", (RemoteWebElement)el); + parameterBuilder.add(action); + return this; + } + + public TouchAction waitAction() { + ActionParameter action = new ActionParameter("wait"); + parameterBuilder.add(action); + return this; + } + + public TouchAction waitAction(int ms) { + ActionParameter action = new ActionParameter("wait"); + action.addParameter("ms", ms); + parameterBuilder.add(action); + return this; + } + + public TouchAction longPress(WebElement el) { + ActionParameter action = new ActionParameter("longPress", (RemoteWebElement)el); + parameterBuilder.add(action); + return this; + } + + public TouchAction longPress(WebElement el, int x, int y) { + ActionParameter action = new ActionParameter("longPress", (RemoteWebElement)el); + action.addParameter("x", x); + action.addParameter("y", y); + parameterBuilder.add(action); + return this; + } + + public void cancel() { + ActionParameter action = new ActionParameter("wait"); + parameterBuilder.add(action); + this.perform(); + } + + public TouchAction perform() { + driver.performTouchAction(this); + return this; + } + + + + + /** + * Get the mjsonwp parameters for this Action + * @return A map of parameters for this touch action to pass as part of mjsonwp + */ + protected ImmutableMap getParameters() { + + ImmutableList.Builder parameters = ImmutableList.builder(); + ImmutableList actionList = parameterBuilder.build(); + for (ActionParameter action : actionList){ + parameters.add(action.getParameterMap()); + } + return ImmutableMap.of("actions", parameters.build()); + } + + protected void clearParameters() { + parameterBuilder = ImmutableList.builder(); + } + + private class ActionParameter { + private String actionName; + private ImmutableMap.Builder optionsBuilder; + + public ActionParameter(String actionName) { + this.actionName = actionName; + optionsBuilder = ImmutableMap.builder(); + } + + public ActionParameter(String actionName, RemoteWebElement el) { + this.actionName = actionName; + optionsBuilder = ImmutableMap.builder(); + addParameter("element", el.getId()); + } + + public ImmutableMap getParameterMap() { + ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.put("action", actionName).put("options", optionsBuilder.build()); + return builder.build(); + } + + public void addParameter(String name, Object value) { + optionsBuilder.put(name, value); + } + } +} diff --git a/test/io/appium/java_client/MobileDriverGestureTest.java b/test/io/appium/java_client/MobileDriverGestureTest.java new file mode 100644 index 000000000..dcc16a19d --- /dev/null +++ b/test/io/appium/java_client/MobileDriverGestureTest.java @@ -0,0 +1,84 @@ +/* + +Copyright 2014 Appium contributors + +Copyright 2014 Software Freedom Conservancy + + + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + + + + http://www.apache.org/licenses/LICENSE-2.0 + + + +Unless required by applicable law or agreed to in writing, software + +distributed under the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and + +limitations under the License. + + */ + +package io.appium.java_client; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.Alert; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.CapabilityType; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.io.File; +import java.net.URL; + +/** + * Test Mobile Driver features + */ +public class MobileDriverGestureTest { + + private AppiumDriver driver; + + @Before + public void setup() throws Exception { + File appDir = new File("test/io/appium/java_client"); + File app = new File(appDir, "TestApp.app.zip"); + DesiredCapabilities capabilities = new DesiredCapabilities(); + capabilities.setCapability(CapabilityType.BROWSER_NAME, ""); + capabilities.setCapability(CapabilityType.VERSION, "7.1"); + capabilities.setCapability(CapabilityType.PLATFORM, "Mac"); + capabilities.setCapability("device", "iPhone Simulator"); + capabilities.setCapability("app", app.getAbsolutePath()); + driver = new AppiumDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities); + } + + @After + public void tearDown() throws Exception { + driver.quit(); + } + + @Test + public void TouchActionTest() throws InterruptedException { + WebElement button = driver.findElementsByTagName("button").get(3); + TouchAction action = new TouchAction(driver); + action.press(button).perform(); + Thread.sleep(2000); + } + + @Test + public void TouchActionChainTest() throws InterruptedException { + WebDriverWait wait = new WebDriverWait(driver, 2); + + WebElement button = driver.findElementsByTagName("button").get(5); + TouchAction action = new TouchAction(driver); + action.press(button).perform(); + + wait.until(ExpectedConditions.alertIsPresent()); + Alert alert = driver.switchTo().alert(); + alert.accept(); + + WebElement mapview = driver.findElementByXPath("//UIAWindow[1]/UIAMapView[1]"); + action = new TouchAction(driver); + action.press(mapview).moveTo(mapview, 0, 100).release().perform(); + Thread.sleep(2000); + } + +} diff --git a/test/io/appium/java_client/TestApp.app.zip b/test/io/appium/java_client/TestApp.app.zip new file mode 100755 index 0000000000000000000000000000000000000000..4f330c6382dae69dcd77591ec9e6d8dfa1d1d764 GIT binary patch literal 22171 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 literal 0 HcmV?d00001 From 32226fd431f1c2a7fa59213328d478275b4ad96a Mon Sep 17 00:00:00 2001 From: jonahss Date: Thu, 17 Apr 2014 15:50:33 -0700 Subject: [PATCH 2/3] multiAction --- src/io/appium/java_client/AppiumDriver.java | 23 +++ src/io/appium/java_client/MobileDriver.java | 1 + .../appium/java_client/MultiTouchAction.java | 81 +++++++++ src/io/appium/java_client/TouchAction.java | 165 ++++++++++++++++-- .../java_client/MobileDriverGestureTest.java | 20 +++ 5 files changed, 280 insertions(+), 10 deletions(-) create mode 100644 src/io/appium/java_client/MultiTouchAction.java diff --git a/src/io/appium/java_client/AppiumDriver.java b/src/io/appium/java_client/AppiumDriver.java index 54b4ffb47..974ff16c3 100644 --- a/src/io/appium/java_client/AppiumDriver.java +++ b/src/io/appium/java_client/AppiumDriver.java @@ -52,6 +52,7 @@ public AppiumDriver(URL remoteAddress, Capabilities desiredCapabilities){ .put(PUSH_FILE, postC("/session/:sessionId/appium/device/push_file")) .put(RUN_APP_IN_BACKGROUND, postC("/session/:sessionId/appium/app/background")) .put(PERFORM_TOUCH_ACTION, postC("/session/:sessionId/touch/perform")) + .put(PERFORM_MULTI_TOUCH, postC("/session/:sessionId/touch/multi/perform")) ; ImmutableMap mobileCommands = builder.build(); @@ -178,6 +179,15 @@ public void runAppInBackground(int seconds) { execute(RUN_APP_IN_BACKGROUND, ImmutableMap.of("seconds", seconds)); } + /** + * Performs a chain of touch actions, which together can be considered an entire gesture. + * See the Webriver 3 spec https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html + * + * It's more convenient to call the perform() method of the TouchAction object itself. + * + * @param touchAction A TouchAction object, which contains a list of individual touch actions to perform + * @return the same touchaction object + */ public TouchAction performTouchAction(TouchAction touchAction) { ImmutableMap parameters = touchAction.getParameters(); touchAction.clearParameters(); @@ -186,6 +196,19 @@ public TouchAction performTouchAction(TouchAction touchAction) { return touchAction; } + /** + * Performs multiple TouchAction gestures at the same time, to simulate multiple fingers/touch inputs. + * See the Webriver 3 spec https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html + * + * It's more convenient to call the perform() method of the MultiTouchAction object. + * + * @param multiAction the MultiTouchAction object to perform. + */ + public void performMultiTouchAction(MultiTouchAction multiAction) { + ImmutableMap parameters = multiAction.getParameters(); + execute(PERFORM_MULTI_TOUCH, parameters); + } + @Override public WebDriver context(String name) { if (name == null) { diff --git a/src/io/appium/java_client/MobileDriver.java b/src/io/appium/java_client/MobileDriver.java index 93b002d1d..f876b59ac 100644 --- a/src/io/appium/java_client/MobileDriver.java +++ b/src/io/appium/java_client/MobileDriver.java @@ -31,4 +31,5 @@ public interface MobileDriver extends WebDriver, ContextAware { public TouchAction performTouchAction(TouchAction touchAction); + public void performMultiTouchAction(MultiTouchAction multiAction); } diff --git a/src/io/appium/java_client/MultiTouchAction.java b/src/io/appium/java_client/MultiTouchAction.java new file mode 100644 index 000000000..f37b4abeb --- /dev/null +++ b/src/io/appium/java_client/MultiTouchAction.java @@ -0,0 +1,81 @@ +/* + +Copyright 2014 Appium contributors + +Copyright 2014 Software Freedom Conservancy + + + +Licensed under the Apache License, Version 2.0 (the "License"); + +you may not use this file except in compliance with the License. + +You may obtain a copy of the License at + + + + http://www.apache.org/licenses/LICENSE-2.0 + + + +Unless required by applicable law or agreed to in writing, software + +distributed under the License is distributed on an "AS IS" BASIS, + +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + +See the License for the specific language governing permissions and + +limitations under the License. + + */ + +package io.appium.java_client; + + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * Used for Webdriver 3 multi-touch gestures + * See the Webriver 3 spec https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html + * + * The MultiTouchAction object is a collection of TouchAction objects + * (remember that TouchAction objects are in turn, a chain of individual actions) + * + * Add multiple TouchAction objects using the add() method. + * When perform() method is called, all actions are sent to the driver. + * + * The driver performs the first step of each TouchAction object simultaneously as a multi-touch "execution group". + * Conceptually, the number of TouchAction objects added to the MultiTouchAction is equal to the number of "fingers" or + * other appendages or tools touching the screen at the same time as part of this multi-gesture. + * Then the driver performs the second step of each TouchAction object and another "execution group", and the third, and so on. + * + * Using a wait() action within a TouchAction takes up one of the slots in an "execution group", so these can be used to + * sync up complex actions. + * + * Calling perform() sends the action command to the Mobile Driver. Otherwise, more and more actions can be chained. + */ +public class MultiTouchAction { + + private MobileDriver driver; + ImmutableList.Builder actions; + + public MultiTouchAction(MobileDriver driver) { + this.driver = driver; + actions = ImmutableList.builder(); + } + + /** + * Add a TouchAction to this multi-touch gesture + * @param action TouchAction to add to this gesture + * @return This MultiTouchAction, for chaining + */ + public MultiTouchAction add(TouchAction action) { + actions.add(action); + + return this; + } + + /** + * Perform the multi-touch action on the mobile driver. + */ + public void perform() { + driver.performMultiTouchAction(this); + } + + protected ImmutableMap getParameters() { + ImmutableList.Builder listOfActionChains = ImmutableList.builder(); + ImmutableList touchActions = actions.build(); + + for (TouchAction action : touchActions) { + listOfActionChains.add(action.getParameters().get("actions")); + } + return ImmutableMap.of("actions", listOfActionChains.build()); + } +} diff --git a/src/io/appium/java_client/TouchAction.java b/src/io/appium/java_client/TouchAction.java index 2e4e35429..ca381fd29 100644 --- a/src/io/appium/java_client/TouchAction.java +++ b/src/io/appium/java_client/TouchAction.java @@ -1,10 +1,3 @@ -package io.appium.java_client; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.remote.RemoteWebElement; - /* +Copyright 2014 Appium contributors +Copyright 2014 Software Freedom Conservancy @@ -21,6 +14,25 @@ +See the License for the specific language governing permissions and +limitations under the License. + */ + +package io.appium.java_client; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.RemoteWebElement; + + +/** + * Used for Webdriver 3 touch actions + * See the Webriver 3 spec https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html + * + * The flow is to chain individual touch actions into an entire gesture. e.g. + * TouchAction action = new TouchAction(driver); + * action.press(element).wait(300).moveTo(element1).release().perform(); + * + * Calling perform() sends the action command to the Mobile Driver. Otherwise, more and more actions can be chained. + */ public class TouchAction { private MobileDriver driver; @@ -31,12 +43,38 @@ public TouchAction(MobileDriver driver) { parameterBuilder = ImmutableList.builder(); } + /** + * Press on the center of an element. + * @param el element to press on + * @return this TouchAction, for chaining + */ public TouchAction press(WebElement el) { ActionParameter action = new ActionParameter("press", (RemoteWebElement)el); parameterBuilder.add(action); return this; } + /** + * Press on an absolute position on the screen + * @param x x coordinate + * @param y y coordinate + * @return this TouchAction, for chaining + */ + public TouchAction press(int x, int y) { + ActionParameter action = new ActionParameter("press"); + action.addParameter("x", x); + action.addParameter("y", y); + parameterBuilder.add(action); + return this; + } + + /** + * Press on an element, offset from upper left corner by a number of pixels + * @param el element to press on + * @param x x offset + * @param y y offset + * @return this TouchAction, for chaining + */ public TouchAction press(WebElement el, int x, int y) { ActionParameter action = new ActionParameter("press", (RemoteWebElement)el); action.addParameter("x", x); @@ -45,18 +83,48 @@ public TouchAction press(WebElement el, int x, int y) { return this; } + /** + * Remove the current touching implement from the screen (withdraw your touch) + * @return this TouchAction, for chaining + */ public TouchAction release() { ActionParameter action = new ActionParameter("release"); parameterBuilder.add(action); return this; } + /** + * Move current touch to center of an element. + * @param el element to move to + * @return this TouchAction, for chaining + */ public TouchAction moveTo(WebElement el) { ActionParameter action = new ActionParameter("moveTo", (RemoteWebElement)el); parameterBuilder.add(action); return this; } + /** + * Move current touch to an absolute position on the screen + * @param x x coordinate + * @param y y coordinate + * @return this TouchAction, for chaining + */ + public TouchAction moveTo(int x, int y) { + ActionParameter action = new ActionParameter("moveTo"); + action.addParameter("x", x); + action.addParameter("y", y); + parameterBuilder.add(action); + return this; + } + + /** + * Move current touch to an element, offset from upper left corner + * @param el element to move current touch to + * @param x x offset + * @param y y offset + * @return this TouchAction, for chaining + */ public TouchAction moveTo(WebElement el, int x, int y) { ActionParameter action = new ActionParameter("moveTo", (RemoteWebElement)el); action.addParameter("x", x); @@ -65,18 +133,62 @@ public TouchAction moveTo(WebElement el, int x, int y) { return this; } + /** + * Tap the center of an element. + * @param el element to tap + * @return this TouchAction, for chaining + */ public TouchAction tap(WebElement el) { ActionParameter action = new ActionParameter("tap", (RemoteWebElement)el); parameterBuilder.add(action); return this; } + /** + * Tap an absolute position on the screen + * @param x x coordinate + * @param y y coordinate + * @return this TouchAction, for chaining + */ + public TouchAction tap(int x, int y) { + ActionParameter action = new ActionParameter("tap"); + action.addParameter("x", x); + action.addParameter("y", y); + parameterBuilder.add(action); + return this; + } + + /** + * Tap an element, offset from upper left corner + * @param el element to tap + * @param x x offset + * @param y y offset + * @return this TouchAction, for chaining + */ + public TouchAction tap(WebElement el, int x, int y) { + ActionParameter action = new ActionParameter("tap", (RemoteWebElement)el); + action.addParameter("x", x); + action.addParameter("y", y); + parameterBuilder.add(action); + return this; + } + + /** + * A wait action, used as a NOP in multi-chaining + * + * @return this TouchAction, for chaining + */ public TouchAction waitAction() { ActionParameter action = new ActionParameter("wait"); parameterBuilder.add(action); return this; } + /** + * Waits for specified amount of time to pass before continue to next touch action + * @param ms time in milliseconds to wait + * @return this TouchAction, for chaining + */ public TouchAction waitAction(int ms) { ActionParameter action = new ActionParameter("wait"); action.addParameter("ms", ms); @@ -84,12 +196,38 @@ public TouchAction waitAction(int ms) { return this; } + /** + * Press and hold the at the center of an element until the contextmenu event has fired. + * @param el element to long-press + * @return this TouchAction, for chaining + */ public TouchAction longPress(WebElement el) { ActionParameter action = new ActionParameter("longPress", (RemoteWebElement)el); parameterBuilder.add(action); return this; } + /** + * Press and hold the at an absolute position on the screen until the contextmenu event has fired. + * @param x x coordinate + * @param y y coordinate + * @return this TouchAction, for chaining + */ + public TouchAction longPress(int x, int y) { + ActionParameter action = new ActionParameter("longPress"); + action.addParameter("x", x); + action.addParameter("y", y); + parameterBuilder.add(action); + return this; + } + + /** + * Press and hold the at an elements upper-left corner, offset by the given amount, until the contextmenu event has fired. + * @param el element to long-press + * @param x x offset + * @param y y offset + * @return this TouchAction, for chaining + */ public TouchAction longPress(WebElement el, int x, int y) { ActionParameter action = new ActionParameter("longPress", (RemoteWebElement)el); action.addParameter("x", x); @@ -98,20 +236,24 @@ public TouchAction longPress(WebElement el, int x, int y) { return this; } + /** + * Cancel this action, if it was partially completed by the driver + */ public void cancel() { ActionParameter action = new ActionParameter("wait"); parameterBuilder.add(action); this.perform(); } + /** + * Perform this chain of actions on the driver. + * @return this TouchAction, for possible segmented-touches. + */ public TouchAction perform() { driver.performTouchAction(this); return this; } - - - /** * Get the mjsonwp parameters for this Action * @return A map of parameters for this touch action to pass as part of mjsonwp @@ -130,6 +272,9 @@ protected void clearParameters() { parameterBuilder = ImmutableList.builder(); } + /** + * Just holds values to eventually return the parameters required for the mjsonwp + */ private class ActionParameter { private String actionName; private ImmutableMap.Builder optionsBuilder; diff --git a/test/io/appium/java_client/MobileDriverGestureTest.java b/test/io/appium/java_client/MobileDriverGestureTest.java index dcc16a19d..2e809a35a 100644 --- a/test/io/appium/java_client/MobileDriverGestureTest.java +++ b/test/io/appium/java_client/MobileDriverGestureTest.java @@ -81,4 +81,24 @@ public void TouchActionChainTest() throws InterruptedException { Thread.sleep(2000); } + @Test + public void MultiGestureTest() throws InterruptedException { + WebDriverWait wait = new WebDriverWait(driver, 2); + + WebElement button = driver.findElementsByTagName("button").get(5); + TouchAction action = new TouchAction(driver); + action.press(button).perform(); + + wait.until(ExpectedConditions.alertIsPresent()); + Alert alert = driver.switchTo().alert(); + alert.accept(); + + WebElement mapview = driver.findElementByXPath("//UIAWindow[1]/UIAMapView[1]"); + + MultiTouchAction multiTouch = new MultiTouchAction(driver); + TouchAction action0 = new TouchAction(driver).press(mapview, 100, 0).moveTo(mapview, 0,-80).release(); + TouchAction action1 = new TouchAction(driver).press(mapview, 100, 50).moveTo(mapview, 0,80).release(); + multiTouch.add(action0).add(action1).perform(); + Thread.sleep(2000); + } } From 6b216815411a0d7ddf1679c1ceb0c10c5a779b3d Mon Sep 17 00:00:00 2001 From: jonahss Date: Fri, 18 Apr 2014 10:32:25 -0700 Subject: [PATCH 3/3] multiTouch convenience methods: tap, swipe, pinch, zoom --- src/io/appium/java_client/AppiumDriver.java | 156 +++++++++++++++++- .../appium/java_client/MultiTouchAction.java | 2 +- src/io/appium/java_client/TouchAction.java | 2 +- .../java_client/MobileDriverGestureTest.java | 18 ++ 4 files changed, 175 insertions(+), 3 deletions(-) diff --git a/src/io/appium/java_client/AppiumDriver.java b/src/io/appium/java_client/AppiumDriver.java index 974ff16c3..4196a0361 100644 --- a/src/io/appium/java_client/AppiumDriver.java +++ b/src/io/appium/java_client/AppiumDriver.java @@ -209,13 +209,157 @@ public void performMultiTouchAction(MultiTouchAction multiAction) { execute(PERFORM_MULTI_TOUCH, parameters); } + /** + * Convenience method for tapping the center of an element on the screen + * @param fingers number of fingers/appendages to tap with + * @param element element to tap + * @param duration how long between pressing down, and lifting fingers/appendages + */ + public void tap(int fingers, WebElement element, int duration) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + for (int i = 0; i < fingers; i++) { + multiTouch.add(createTap(element, duration)); + } + + multiTouch.perform(); + } + + /** + * Convenience method for tapping a position on the screen + * @param fingers number of fingers/appendages to tap with + * @param x x coordinate + * @param y y coordinate + * @param duration + */ + public void tap(int fingers, int x, int y, int duration) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + for (int i = 0; i < fingers; i++) { + multiTouch.add(createTap(x, y, duration)); + } + + multiTouch.perform(); + } + + /** + * Convenience method for swiping across the screen + * @param startx starting x coordinate + * @param starty starting y coordinate + * @param endx ending x coordinate + * @param endy ending y coordinate + * @param duration amount of time in milliseconds for the entire swipe action to take + */ + public void swipe(int startx, int starty, int endx, int endy, int duration) { + TouchAction touchAction = new TouchAction(this); + + //appium converts press-wait-moveto-release to a swipe action + touchAction.press(startx, starty).waitAction(duration).moveTo(endx, endy).release(); + + touchAction.perform(); + } + + /** + * Convenience method for pinching an element on the screen. + * "pinching" refers to the action of two appendages pressing the screen and sliding towards each other. + * NOTE: + * This convenience method places the initial touches around the element, if this would happen to place one of them + * off the screen, appium with return an outOfBounds error. In this case, revert to using the MultiTouchAction api + * instead of this method. + * + * @param el The element to pinch + */ + public void pinch(WebElement el) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + Dimension dimensions = el.getSize(); + Point upperLeft = el.getLocation(); + Point center = new Point(upperLeft.getX() + dimensions.getWidth() / 2, upperLeft.getY() + dimensions.getHeight() / 2); + + TouchAction action0 = new TouchAction(this).press(el, center.getX(), center.getY() - 100).moveTo(el).release(); + TouchAction action1 = new TouchAction(this).press(el, center.getX(), center.getY() + 100).moveTo(el).release(); + + multiTouch.add(action0).add(action1); + + multiTouch.perform(); + } + + /** + * Convenience method for pinching an element on the screen. + * "pinching" refers to the action of two appendages pressing the screen and sliding towards each other. + * NOTE: + * This convenience method places the initial touches around the element at a distance, if this would happen to place + * one of them off the screen, appium will return an outOfBounds error. In this case, revert to using the + * MultiTouchAction api instead of this method. + * + * @param x x coordinate to terminate the pinch on + * @param y y coordinate to terminate the pinch on + */ + public void pinch(int x, int y) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + TouchAction action0 = new TouchAction(this).press(x, y-100).moveTo(x, y).release(); + TouchAction action1 = new TouchAction(this).press(x, y+100).moveTo(x, y).release(); + + multiTouch.add(action0).add(action1); + + multiTouch.perform(); + } + + /** + * Convenience method for "zooming in" on an element on the screen. + * "zooming in" refers to the action of two appendages pressing the screen and sliding away from each other. + * NOTE: + * This convenience method slides touches away from the element, if this would happen to place one of them + * off the screen, appium will return an outOfBounds error. In this case, revert to using the MultiTouchAction api + * instead of this method. + * + * @param el The element to pinch + */ + public void zoom(WebElement el) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + Dimension dimensions = el.getSize(); + Point upperLeft = el.getLocation(); + Point center = new Point(upperLeft.getX() + dimensions.getWidth() / 2, upperLeft.getY() + dimensions.getHeight() / 2); + + TouchAction action0 = new TouchAction(this).press(el).moveTo(el, center.getX(), center.getY() - 100).release(); + TouchAction action1 = new TouchAction(this).press(el).moveTo(el, center.getX(), center.getY() + 100).release(); + + multiTouch.add(action0).add(action1); + + multiTouch.perform(); + } + + /** + * Convenience method for "zooming in" on an element on the screen. + * "zooming in" refers to the action of two appendages pressing the screen and sliding away from each other. + * NOTE: + * This convenience method slides touches away from the element, if this would happen to place one of them + * off the screen, appium will return an outOfBounds error. In this case, revert to using the MultiTouchAction api + * instead of this method. + * + * @param x x coordinate to start zoom on + * @param y y coordinate to start zoom on + */ + public void zoom(int x, int y) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + TouchAction action0 = new TouchAction(this).press(x, y).moveTo(x, y-100).release(); + TouchAction action1 = new TouchAction(this).press(x, y).moveTo(x, y+100).release(); + + multiTouch.add(action0).add(action1); + + multiTouch.perform(); + } + + @Override public WebDriver context(String name) { if (name == null) { throw new IllegalArgumentException("Must supply a context name"); } - execute(DriverCommand.SWITCH_TO_CONTEXT, ImmutableMap.of("name", name)); return AppiumDriver.this; } @@ -271,6 +415,16 @@ public List findElementsByAccessibilityId(String using) { return findElements("accessibility id", using); } + private TouchAction createTap(WebElement element, int duration) { + TouchAction tap = new TouchAction(this); + return tap.press(element).waitAction(duration).release(); + } + + private TouchAction createTap(int x, int y, int duration) { + TouchAction tap = new TouchAction(this); + return tap.press(x, y).waitAction(duration).release(); + } + private static CommandInfo getC(String url) { return new CommandInfo(url, HttpVerb.GET); } diff --git a/src/io/appium/java_client/MultiTouchAction.java b/src/io/appium/java_client/MultiTouchAction.java index f37b4abeb..8c1d061ed 100644 --- a/src/io/appium/java_client/MultiTouchAction.java +++ b/src/io/appium/java_client/MultiTouchAction.java @@ -36,7 +36,7 @@ * other appendages or tools touching the screen at the same time as part of this multi-gesture. * Then the driver performs the second step of each TouchAction object and another "execution group", and the third, and so on. * - * Using a wait() action within a TouchAction takes up one of the slots in an "execution group", so these can be used to + * Using a waitAction() action within a TouchAction takes up one of the slots in an "execution group", so these can be used to * sync up complex actions. * * Calling perform() sends the action command to the Mobile Driver. Otherwise, more and more actions can be chained. diff --git a/src/io/appium/java_client/TouchAction.java b/src/io/appium/java_client/TouchAction.java index ca381fd29..a1889abc2 100644 --- a/src/io/appium/java_client/TouchAction.java +++ b/src/io/appium/java_client/TouchAction.java @@ -29,7 +29,7 @@ * * The flow is to chain individual touch actions into an entire gesture. e.g. * TouchAction action = new TouchAction(driver); - * action.press(element).wait(300).moveTo(element1).release().perform(); + * action.press(element).waitAction(300).moveTo(element1).release().perform(); * * Calling perform() sends the action command to the Mobile Driver. Otherwise, more and more actions can be chained. */ diff --git a/test/io/appium/java_client/MobileDriverGestureTest.java b/test/io/appium/java_client/MobileDriverGestureTest.java index 2e809a35a..ae8e407dd 100644 --- a/test/io/appium/java_client/MobileDriverGestureTest.java +++ b/test/io/appium/java_client/MobileDriverGestureTest.java @@ -101,4 +101,22 @@ public void MultiGestureTest() throws InterruptedException { multiTouch.add(action0).add(action1).perform(); Thread.sleep(2000); } + + @Test + public void ZoomTest() throws InterruptedException { + WebDriverWait wait = new WebDriverWait(driver, 2); + + WebElement button = driver.findElementsByTagName("button").get(5); + TouchAction action = new TouchAction(driver); + action.press(button).perform(); + + wait.until(ExpectedConditions.alertIsPresent()); + Alert alert = driver.switchTo().alert(); + alert.accept(); + + WebElement mapview = driver.findElementByXPath("//UIAWindow[1]/UIAMapView[1]"); + + driver.zoom(mapview); + Thread.sleep(2000); + } }