diff --git a/pom.xml b/pom.xml
index 29ef3469..8d00cd36 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,6 +68,7 @@
1.49.0
+ 10.1.1
4.7.6
4.8.174
2.1.3
@@ -127,6 +128,11 @@
+
+ io.appium
+ java-client
+ ${appium.version}
+
com.eatthepath
java-otp
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/Back.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/Back.java
new file mode 100644
index 00000000..fbc581ae
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/Back.java
@@ -0,0 +1,80 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import io.appium.java_client.AppiumDriver;
+import io.appium.java_client.android.AndroidDriver;
+import io.appium.java_client.android.nativekey.AndroidKey;
+import io.appium.java_client.android.nativekey.KeyEvent;
+import io.appium.java_client.ios.IOSDriver;
+import jp.co.moneyforward.autotest.framework.action.Act;
+import jp.co.moneyforward.autotest.framework.core.ExecutionEnvironment;
+import jp.co.moneyforward.autotest.framework.utils.InternalUtils;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.interactions.PointerInput;
+import org.openqa.selenium.interactions.Sequence;
+
+import java.time.Duration;
+import java.util.Collections;
+
+///
+/// An act that models a user behavior of pressing the device's back button.
+///
+/// - **Android**: presses the hardware back key (`AndroidKey.BACK`, keycode 4) via `AndroidDriver.pressKey`.
+/// - **iOS**: performs a left-edge swipe to trigger the interactive pop gesture (equivalent of the swipe-back navigation).
+///
+public class Back implements Act {
+
+ private final int count;
+
+ ///
+ /// Creates an object of this class that presses the back button once.
+ ///
+ public Back() {
+ this(1);
+ }
+
+ ///
+ /// Creates an object of this class.
+ ///
+ /// @param count The number of times to press the back button.
+ ///
+ public Back(int count) {
+ this.count = count;
+ }
+
+ @Override
+ public AppiumDriver perform(AppiumDriver driver, ExecutionEnvironment executionEnvironment) {
+ for (int i = 0; i < count; i++) {
+ if (driver instanceof AndroidDriver androidDriver) {
+ androidDriver.pressKey(new KeyEvent(AndroidKey.BACK));
+ } else if (driver instanceof IOSDriver) {
+ edgeSwipeBack(driver);
+ } else {
+ driver.navigate().back();
+ }
+ }
+ return driver;
+ }
+
+ ///
+ /// Returns a name of this object.
+ ///
+ /// @return A name of this object.
+ ///
+ @Override
+ public String name() {
+ return InternalUtils.simpleClassNameOf(this.getClass()) + "[x" + count + "]";
+ }
+
+ private static void edgeSwipeBack(AppiumDriver driver) {
+ Dimension size = driver.manage().window().getSize();
+ int endX = (int)(size.getWidth() * 0.8);
+ int centerY = size.getHeight() / 2;
+ PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
+ Sequence swipe = new Sequence(finger, 0);
+ swipe.addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), 0, centerY));
+ swipe.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()));
+ swipe.addAction(finger.createPointerMove(Duration.ofMillis(300), PointerInput.Origin.viewport(), endX, centerY));
+ swipe.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
+ driver.perform(Collections.singletonList(swipe));
+ }
+}
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/Click.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/Click.java
new file mode 100644
index 00000000..2253cc86
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/Click.java
@@ -0,0 +1,37 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import com.github.valid8j.pcond.forms.Printables;
+import io.appium.java_client.AppiumDriver;
+import jp.co.moneyforward.autotest.framework.core.ExecutionEnvironment;
+import org.openqa.selenium.By;
+
+import java.util.function.Function;
+
+///
+/// An act that models a user behavior, which clicks a specified element.
+///
+public class Click extends ClickBase {
+ ///
+ /// Creates an object of this class.
+ ///
+ /// @param by A locator to designate an element to click.
+ ///
+ public Click(By by) {
+ this(Printables.function("@" + by, (d) -> by));
+ }
+
+ ///
+ /// Creates an object of this class.
+ ///
+ /// @param locatorFunction A locator for an element to click.
+ ///
+ public Click(Function locatorFunction) {
+ super(locatorFunction);
+ }
+
+ @Override
+ public AppiumDriver perform(AppiumDriver driver, ExecutionEnvironment executionEnvironment) {
+ driver.findElement(this.locatorFunction.apply(driver)).click();
+ return driver;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ClickBase.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ClickBase.java
new file mode 100644
index 00000000..f930b681
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ClickBase.java
@@ -0,0 +1,35 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import io.appium.java_client.AppiumDriver;
+import org.openqa.selenium.By;
+import jp.co.moneyforward.autotest.framework.action.Act;
+import jp.co.moneyforward.autotest.framework.utils.InternalUtils;
+
+import java.util.function.Function;
+
+///
+/// An abstract base class for clicking acts.
+///
+public abstract class ClickBase implements Act {
+ final Function locatorFunction;
+
+ ///
+ /// Creates an object of this class.
+ ///
+ /// @param locatorFunction A function to locate an element to click.
+ ///
+ protected ClickBase(Function locatorFunction) {
+ this.locatorFunction = locatorFunction;
+ }
+
+ ///
+ /// Returns a name of this object.
+ /// The returned name is printed in action trees.
+ ///
+ /// @return A name of this object.
+ ///
+ @Override
+ public String name() {
+ return InternalUtils.simpleClassNameOf(this.getClass()) + "[" + this.locatorFunction + "]";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ClickIfPresent.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ClickIfPresent.java
new file mode 100644
index 00000000..aafeae2a
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ClickIfPresent.java
@@ -0,0 +1,42 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import com.github.valid8j.pcond.forms.Printables;
+import io.appium.java_client.AppiumDriver;
+import jp.co.moneyforward.autotest.framework.core.ExecutionEnvironment;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.List;
+import java.util.function.Function;
+
+///
+/// An act that models a user behavior, which clicks a specified element only if it is present.
+///
+public class ClickIfPresent extends ClickBase {
+ ///
+ /// Creates an object of this class.
+ ///
+ /// @param by A locator to designate an element to click if present.
+ ///
+ public ClickIfPresent(By by) {
+ this(Printables.function("@" + by, (d) -> by));
+ }
+
+ ///
+ /// Creates an object of this class.
+ ///
+ /// @param locatorFunction A function to locate an element to be clicked by this object on `perform` method's call.
+ ///
+ public ClickIfPresent(Function locatorFunction) {
+ super(locatorFunction);
+ }
+
+ @Override
+ public AppiumDriver perform(AppiumDriver driver, ExecutionEnvironment executionEnvironment) {
+ List elements = driver.findElements(this.locatorFunction.apply(driver));
+ if (!elements.isEmpty() && elements.getFirst().isDisplayed()) {
+ elements.getFirst().click();
+ }
+ return driver;
+ }
+}
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/CloseDriver.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/CloseDriver.java
new file mode 100644
index 00000000..b3b7eb92
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/CloseDriver.java
@@ -0,0 +1,14 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import io.appium.java_client.AppiumDriver;
+import jp.co.moneyforward.autotest.framework.action.Act;
+import jp.co.moneyforward.autotest.framework.core.ExecutionEnvironment;
+import org.openqa.selenium.By;
+
+public class CloseDriver implements Act {
+ @Override
+ public Void perform(AppiumDriver value, ExecutionEnvironment executionEnvironment) {
+ value.quit();
+ return null;
+ }
+}
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ElementFunctions.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ElementFunctions.java
new file mode 100644
index 00000000..a6c26f75
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ElementFunctions.java
@@ -0,0 +1,27 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import com.github.valid8j.pcond.forms.Printables;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+public enum ElementFunctions {;
+ private ElementFunctions() {
+ }
+
+ public static Function textContent() {
+ return Printables.function("textContent", WebElement::getText);
+ }
+
+ public static Function tagContent() {
+ return Printables.function("tagContent", WebElement::getTagName);
+ }
+
+ public static Function isEnabled() {
+ return Printables.function("isEnabled", WebElement::isEnabled);
+ }
+
+ public static Function isDisplayed() {
+ return Printables.function("isDisplayed", WebElement::isDisplayed);
+ }
+}
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/MobileAct.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/MobileAct.java
new file mode 100644
index 00000000..47739329
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/MobileAct.java
@@ -0,0 +1,78 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import io.appium.java_client.AppiumDriver;
+import jp.co.moneyforward.autotest.framework.action.Act;
+import jp.co.moneyforward.autotest.framework.core.ExecutionEnvironment;
+
+import java.util.function.BiConsumer;
+
+import static com.github.valid8j.classic.Requires.requireNonNull;
+
+///
+/// A general-purpose act.
+/// Convenient starting point for writing **insdog** based tests.
+///
+public abstract class MobileAct implements Act {
+ private final String description;
+
+ ///
+ /// Creates a new instance of this class.
+ ///
+ /// It is advised to give a concise and descriptive string to `description` parameter as it is printed the test report.
+ /// The `description` should be concise but informative enough for a reader to reproduce the same action that this `Act` performs.
+ ///
+ /// @param description A string that describes this object.
+ ///
+ protected MobileAct(String description) {
+ this.description = requireNonNull(description);
+ }
+
+ ///
+ /// Creates a `MobileAct with a given description and an action
+ ///
+ /// @param description A string to describe created page act.
+ /// @param action An action to be performed.
+ /// @return A page act that performs `action`.
+ ///
+ public static MobileAct mobileAct(String description,
+ BiConsumer action) {
+ return new MobileAct(description) {
+ @Override
+ protected void action(AppiumDriver driver, ExecutionEnvironment env) {
+ action.accept(driver, env);
+ }
+ };
+ }
+
+ ///
+ /// Performs an action defined for this class.
+ /// Its execution is delegated to `perform(AppiumDriver,ExecutionEnvironment)` method.
+ ///
+ /// @param value A page object on which this `act` is performed.
+ /// @param executionEnvironment An execution environment, in which this act is performed.
+ /// @return The `value` itself should be returned, usually.
+ ///
+ @Override
+ public AppiumDriver perform(AppiumDriver value, ExecutionEnvironment executionEnvironment) {
+ this.action(value, executionEnvironment);
+ return value;
+ }
+
+ ///
+ /// A method that defines the `act` to be performed by this object.
+ ///
+ /// @param driver A driver object on which this `act` is performed.
+ /// @param executionEnvironment An execution environment, in which this act is performed.
+ ///
+ protected abstract void action(AppiumDriver driver, ExecutionEnvironment executionEnvironment);
+
+ ///
+ /// Returns a name of this object.
+ ///
+ /// @return A name of this object.
+ ///
+ @Override
+ public String name() {
+ return "Driver[" + this.description + "]";
+ }
+}
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/PageFunctions.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/PageFunctions.java
new file mode 100644
index 00000000..e406eafc
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/PageFunctions.java
@@ -0,0 +1,209 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import com.github.valid8j.classic.Requires;
+import com.github.valid8j.pcond.forms.Printables;
+import io.appium.java_client.AppiumDriver;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.util.function.Function;
+
+///
+/// A utility class to handle `AppiumDriver` object.
+///
+/// Methods in this class return a function whose parameter is an `AppiumDriver` object of **Appium**.
+///
+/// It is common to see a situation, where a single method call to a driver object cannot determine a single element to be returned.
+/// In such a case, you can use functions provided by `ElementFunctions` in combination.
+///
+/// In general methods in this class are named in the following manner.
+///
+/// ```
+/// {typeName}By{SelectionMethod}
+/// ```
+///
+/// `typeName` can be, for instance, `locator`, `linkLocator`.
+/// `SelectionMethod` can be `Name`, `Text`, `Label`, `Selector`, etc.
+///
+/// Functions returned by methods in this class can be pretty printed on a call of `toString` method call.
+///
+/// @see ElementFunctions
+///
+public enum PageFunctions {;
+
+ ///
+ /// Returns a function that resolves a locator specified by `name` in a given `AppiumDriver` object.
+ ///
+ /// This is a shorthand method for `linkLocatorByName(name, false)`.
+ ///
+ /// @param name A name (accessibility id / content-desc) of a link-like element.
+ /// @return A function that resolves a locator specified by `name` in a given `AppiumDriver` object.
+ ///
+ public static Function linkLocatorByName(String name) {
+ return linkLocatorByName(name, false);
+ }
+
+ ///
+ /// Returns a function that resolves a given `name` to a locator of a link-like element whose accessibility
+ /// name matches with it.
+ ///
+ /// @param name A name to be matched against `@content-desc` (Android) or `@name` (iOS).
+ /// @param lenient `true` - partial match / `false` - exact match.
+ /// @return A function that resolves a given `name` to a locator of a link-like element whose name matches with it.
+ ///
+ public static Function linkLocatorByName(String name, boolean lenient) {
+ String lit = xpathLiteral(name);
+ String xpath = lenient
+ ? "//*[contains(@content-desc," + lit + ") or contains(@name," + lit + ")]"
+ : "//*[@content-desc=" + lit + " or @name=" + lit + "]";
+ return Printables.function("link[name" + (lenient ? "~" : "=") + name + "]",
+ d -> By.xpath(xpath));
+ }
+
+ ///
+ /// Returns a function that resolves a locator whose text contains `text` in a given driver.
+ ///
+ /// @param text A text to be contained by the matching element.
+ /// @return A function that resolves a locator whose text contains `text` in a given driver.
+ ///
+ public static Function locatorByText(String text) {
+ return locatorByText(text, false);
+ }
+
+ ///
+ /// Returns a function that resolves a locator which matches `text` in a given `AppiumDriver` object.
+ /// If `lenient` is `true`, an element whose text contains it is considered matched.
+ /// If `lenient` is `false`, an element whose text equals to `text` is considered matched.
+ ///
+ /// Matches against `@text` (Android) and `@label` / `@name` (iOS).
+ ///
+ /// @param text A text to be matched.
+ /// @param lenient `true` - lenient / `false` - strict.
+ /// @return A function that resolves a locator which matches `text` in a given `AppiumDriver` object.
+ ///
+ public static Function locatorByText(String text, boolean lenient) {
+ String lit = xpathLiteral(text);
+ String xpath = lenient
+ ? "//*[contains(@text," + lit + ") or contains(@label," + lit + ") or contains(@name," + lit + ")]"
+ : "//*[@text=" + lit + " or @label=" + lit + " or @name=" + lit + "]";
+ return Printables.function("@[text" + (lenient ? "~" : "=") + text + "]",
+ d -> By.xpath(xpath));
+ }
+
+ ///
+ /// Returns a function that resolves a locator to a button element in an `AppiumDriver`, whose name is equal to `name`.
+ ///
+ /// Matches `android.widget.Button[@text]` on Android and `XCUIElementTypeButton[@name]` on iOS.
+ ///
+ /// @param name A string to be matched with a button element's name.
+ /// @return A function that resolves a locator to a button element whose name is equal to `name`.
+ ///
+ public static Function buttonLocatorByName(String name) {
+ String lit = xpathLiteral(name);
+ return Printables.function("@[name=" + name + "]",
+ d -> By.xpath("//android.widget.Button[@text=" + lit + "] | //XCUIElementTypeButton[@name=" + lit + "]"));
+ }
+
+ ///
+ /// Returns a function that resolves a locator whose accessibility label matches with `label` in a given driver.
+ ///
+ /// @param label A string to be matched with the accessibility label of a locator.
+ /// @return A function that resolves a locator whose label matches with `label`.
+ ///
+ public static Function locatorByLabel(String label) {
+ return locatorByLabel(label, false);
+ }
+
+ ///
+ /// Returns a function that resolves a locator whose accessibility label matches with `label` in a given driver.
+ ///
+ /// Matches against `@content-desc` (Android) and `@label` (iOS).
+ ///
+ /// If `lenient` is set to `true`, an element whose label contains `label` will be considered matched.
+ /// If it is `false`, an element whose label is equal to `label` will be considered matched.
+ ///
+ /// @param label A string to be matched with an accessibility label of a locator.
+ /// @param lenient `true` - lenient / `false` - strict.
+ /// @return A function that resolves a locator whose label matches with `label` in a given driver.
+ ///
+ public static Function locatorByLabel(String label, boolean lenient) {
+ String lit = xpathLiteral(label);
+ String xpath = lenient
+ ? "//*[contains(@content-desc," + lit + ") or contains(@label," + lit + ")]"
+ : "//*[@content-desc=" + lit + " or @label=" + lit + "]";
+ return Printables.function("@[label" + (lenient ? "~" : "=") + label + "]",
+ d -> By.xpath(xpath));
+ }
+
+ ///
+ /// Returns a function that resolves a locator whose placeholder is `placeholder`.
+ ///
+ /// Matches against `@hint` (Android) and `@placeholderValue` (iOS).
+ ///
+ /// @param placeholder A string to be matched with a locator's placeholder.
+ /// @return A function that resolves a locator whose placeholder is `placeholder`.
+ ///
+ public static Function locatorByPlaceholder(String placeholder) {
+ String lit = xpathLiteral(placeholder);
+ return Printables.function("@[placeholder=" + placeholder + "]",
+ d -> By.xpath("//*[@hint=" + lit + " or @placeholderValue=" + lit + "]"));
+ }
+
+ ///
+ /// Returns a function that resolves a locator specified by `by` in a given `AppiumDriver`.
+ ///
+ /// @param by A `By` selector that specifies a locator.
+ /// @return A function that resolves a locator specified by `by` in a given `AppiumDriver`.
+ ///
+ public static Function findElementBy(By by) {
+ Requires.requireNonNull(by);
+ return Printables.function("@[" + by + "]", d -> d.findElement(by));
+ }
+
+ ///
+ /// Returns a function that resolves a locator whose text contains `text` in a given driver.
+ /// Delegates to `locatorByText(text, true)`.
+ ///
+ /// @param text A string to be contained by the matching element's text.
+ /// @return A function that resolves a locator whose text contains `text`.
+ ///
+ public static Function linkLocatorByText(String text) {
+ return locatorByText(text, true);
+ }
+
+ ///
+ /// Returns a function that resolves a locator whose text equals `text` in a given driver.
+ /// Delegates to `locatorByText(text, false)`.
+ ///
+ /// @param text A string to be matched exactly against the matching element's text.
+ /// @return A function that resolves a locator whose text equals `text`.
+ ///
+ public static Function linkLocatorByExactText(String text) {
+ return locatorByText(text, false);
+ }
+
+ ///
+ /// Returns a function that gives the title of the current screen / web view.
+ ///
+ /// @return A function that gives the title of the given driver's current context.
+ ///
+ public static Function toTitle() {
+ return Printables.function("title", AppiumDriver::getTitle);
+ }
+
+ private static String xpathLiteral(String value) {
+ if (!value.contains("'")) {
+ return "'" + value + "'";
+ }
+ StringBuilder sb = new StringBuilder("concat(");
+ String[] parts = value.split("'", -1);
+ for (int i = 0; i < parts.length; i++) {
+ if (i > 0) {
+ sb.append(",\"'\",");
+ }
+ sb.append("'").append(parts[i]).append("'");
+ }
+ sb.append(")");
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/Screenshot.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/Screenshot.java
new file mode 100644
index 00000000..d4296e36
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/Screenshot.java
@@ -0,0 +1,45 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import io.appium.java_client.AppiumDriver;
+import jp.co.moneyforward.autotest.framework.action.Act;
+import jp.co.moneyforward.autotest.framework.core.ExecutionEnvironment;
+import org.openqa.selenium.OutputType;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+///
+/// An act that does screenshot.
+/// The app screenshot is saved under `ExecutionEnvironment#testOutputFilenameFor("screenshot-{stepName}.png")`, where
+/// `{stepName}` is one of `beforeAll`, `beforeEach`, `afterEach`, or `afterAll`.
+///
+/// @see ExecutionEnvironment#testOutputFilenameFor(String)
+///
+public class Screenshot implements Act {
+ ///
+ /// Creates an instance of this class.
+ ///
+ public Screenshot() {
+ // Make default constructor findable.
+ }
+
+ ///
+ /// Performs the screenshot action.
+ ///
+ /// @param value A driver for which screenshot is executed.
+ /// @param executionEnvironment An execution environment.
+ /// @return The driver itself given as `value` parameter.
+ ///
+ @Override
+ public AppiumDriver perform(AppiumDriver value, ExecutionEnvironment executionEnvironment) {
+ try {
+ Path destination = executionEnvironment.testOutputFilenameFor(String.format("screenshot-%s.png", executionEnvironment.stepName()));
+ Files.createDirectories(destination.getParent());
+ Files.write(destination, value.getScreenshotAs(OutputType.BYTES));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return value;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ScrollToElement.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ScrollToElement.java
new file mode 100644
index 00000000..8282c1ce
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/ScrollToElement.java
@@ -0,0 +1,122 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import com.github.valid8j.pcond.forms.Printables;
+import io.appium.java_client.AppiumDriver;
+import jp.co.moneyforward.autotest.framework.action.Act;
+import jp.co.moneyforward.autotest.framework.core.ExecutionEnvironment;
+import jp.co.moneyforward.autotest.framework.utils.InternalUtils;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.PointerInput;
+import org.openqa.selenium.interactions.Sequence;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+import static com.github.valid8j.classic.Requires.requireNonNull;
+
+///
+/// An act that scrolls the screen in a given direction until a target element becomes visible.
+///
+/// Performs a swipe gesture repeatedly, up to `maxScrollAttempts` times.
+/// After each swipe the element's presence and visibility are checked.
+/// If the element is still not visible after all attempts, a `NoSuchElementException` is thrown.
+///
+public class ScrollToElement implements Act {
+
+ ///
+ /// The direction in which to scroll.
+ ///
+ public enum Direction { UP, DOWN, LEFT, RIGHT }
+
+ private static final int DEFAULT_MAX_SCROLL_ATTEMPTS = 10;
+
+ private final Function locatorFunction;
+ private final Direction direction;
+ private final int maxScrollAttempts;
+
+ ///
+ /// Creates an object of this class that scrolls downward until the element located by `by` becomes visible.
+ ///
+ /// @param by A locator for the target element.
+ ///
+ public ScrollToElement(By by) {
+ this(Printables.function("@" + by, d -> by));
+ }
+
+ ///
+ /// Creates an object of this class that scrolls downward until the element resolved by `locatorFunction` becomes visible.
+ ///
+ /// @param locatorFunction A function to locate the target element.
+ ///
+ public ScrollToElement(Function locatorFunction) {
+ this(locatorFunction, Direction.DOWN, DEFAULT_MAX_SCROLL_ATTEMPTS);
+ }
+
+ ///
+ /// Creates an object of this class.
+ ///
+ /// @param by A locator for the target element.
+ /// @param direction The direction in which to scroll.
+ /// @param maxScrollAttempts The maximum number of scroll attempts before failing.
+ ///
+ public ScrollToElement(By by, Direction direction, int maxScrollAttempts) {
+ this(Printables.function("@" + by, d -> by), direction, maxScrollAttempts);
+ }
+
+ ///
+ /// Creates an object of this class.
+ ///
+ /// @param locatorFunction A function to locate the target element.
+ /// @param direction The direction in which to scroll.
+ /// @param maxScrollAttempts The maximum number of scroll attempts before failing.
+ ///
+ public ScrollToElement(Function locatorFunction, Direction direction, int maxScrollAttempts) {
+ this.locatorFunction = requireNonNull(locatorFunction);
+ this.direction = requireNonNull(direction);
+ this.maxScrollAttempts = maxScrollAttempts;
+ }
+
+ @Override
+ public AppiumDriver perform(AppiumDriver driver, ExecutionEnvironment executionEnvironment) {
+ By by = this.locatorFunction.apply(driver);
+ for (int attempt = 0; attempt < maxScrollAttempts; attempt++) {
+ List elements = driver.findElements(by);
+ if (!elements.isEmpty() && elements.getFirst().isDisplayed()) {
+ return driver;
+ }
+ swipe(driver);
+ }
+ driver.findElement(by);
+ return driver;
+ }
+
+ @Override
+ public String name() {
+ return InternalUtils.simpleClassNameOf(this.getClass()) + "[" + this.locatorFunction + "][" + this.direction + "]";
+ }
+
+ private void swipe(AppiumDriver driver) {
+ Dimension size = driver.manage().window().getSize();
+ int centerX = size.getWidth() / 2;
+ int centerY = size.getHeight() / 2;
+ int startX, startY, endX, endY;
+ switch (this.direction) {
+ case DOWN -> { startX = centerX; startY = (int)(size.getHeight() * 0.7); endX = centerX; endY = (int)(size.getHeight() * 0.3); }
+ case UP -> { startX = centerX; startY = (int)(size.getHeight() * 0.3); endX = centerX; endY = (int)(size.getHeight() * 0.7); }
+ case RIGHT -> { startX = (int)(size.getWidth() * 0.7); startY = centerY; endX = (int)(size.getWidth() * 0.3); endY = centerY; }
+ case LEFT -> { startX = (int)(size.getWidth() * 0.3); startY = centerY; endX = (int)(size.getWidth() * 0.7); endY = centerY; }
+ default -> throw new IllegalStateException("Unexpected direction: " + this.direction);
+ }
+ PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
+ Sequence swipe = new Sequence(finger, 0);
+ swipe.addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), startX, startY));
+ swipe.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()));
+ swipe.addAction(finger.createPointerMove(Duration.ofMillis(600), PointerInput.Origin.viewport(), endX, endY));
+ swipe.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
+ driver.perform(Collections.singletonList(swipe));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/mobile/SendKey.java b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/SendKey.java
new file mode 100644
index 00000000..3f7df0a2
--- /dev/null
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/mobile/SendKey.java
@@ -0,0 +1,71 @@
+package jp.co.moneyforward.autotest.actions.mobile;
+
+import com.github.valid8j.pcond.forms.Printables;
+import io.appium.java_client.AppiumDriver;
+import jp.co.moneyforward.autotest.framework.action.Act;
+import jp.co.moneyforward.autotest.framework.core.ExecutionEnvironment;
+import org.openqa.selenium.By;
+
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static com.github.valid8j.classic.Requires.requireNonNull;
+import static jp.co.moneyforward.autotest.framework.utils.InternalUtils.MASK_PREFIX;
+
+public class SendKey implements Act {
+ private final Supplier keySequenceGenerator;
+ private final Function locatorFunction;
+
+ ///
+ /// Creates an instance of this class.
+ ///
+ /// @param by A By function used to locate elements.
+ /// @param keys Keys to be sent to a locator chosen by a `by` function.
+ /// @see SendKey#SendKey(Function, String)
+ ///
+ public SendKey(By by, String keys) {
+ this(Printables.function("@" + by, (d) -> by), keys);
+ }
+
+ ///
+ /// Creates an instance of this class.
+ ///
+ /// @param locatorFunction A function to choose a locator from a given driver.
+ /// @param keys Keys to be sent to a chosen locator.
+ ///
+ public SendKey(Function locatorFunction, String keys) {
+ this(locatorFunction, toSupplier(requireNonNull(keys)));
+ }
+
+ ///
+ /// Creates an instance of this class.
+ ///
+ /// @param locatorFunction A function to choose a locator from a given driver.
+ /// @param keySequenceGenerator A supplier that generates a key sequence to be sent to a chosen locator.
+ ///
+ public SendKey(Function locatorFunction, Supplier keySequenceGenerator) {
+ this.locatorFunction = requireNonNull(locatorFunction);
+ this.keySequenceGenerator = requireNonNull(keySequenceGenerator);
+ }
+
+ @Override
+ public AppiumDriver perform(AppiumDriver value, ExecutionEnvironment executionEnvironment) {
+ By by = this.locatorFunction.apply(value);
+ String keys = keySequenceGenerator.get();
+ value.findElement(by).sendKeys(keys.startsWith(MASK_PREFIX) ? keys.substring(MASK_PREFIX.length()) : keys);
+
+ return value;
+ }
+
+ @Override
+ public String name() {
+ String keys = keySequenceGenerator.get();
+ return Act.super.name() + "[" + locatorFunction + "][" +
+ (keys.startsWith(MASK_PREFIX) ? MASK_PREFIX
+ : keys) + "]";
+ }
+
+ private static Supplier toSupplier(String keys) {
+ return () -> keys;
+ }
+}
diff --git a/src/main/java/jp/co/moneyforward/autotest/actions/web/SendKey.java b/src/main/java/jp/co/moneyforward/autotest/actions/web/SendKey.java
index f21e39b6..10b42205 100644
--- a/src/main/java/jp/co/moneyforward/autotest/actions/web/SendKey.java
+++ b/src/main/java/jp/co/moneyforward/autotest/actions/web/SendKey.java
@@ -10,6 +10,7 @@
import java.util.function.Supplier;
import static com.github.valid8j.classic.Requires.requireNonNull;
+import static jp.co.moneyforward.autotest.framework.utils.InternalUtils.MASK_PREFIX;
///
/// A class that represents an action to send key sequence to a specified locator.
@@ -20,10 +21,6 @@
///
///
public class SendKey implements Act {
- ///
- /// A prefix to control a
- ///
- public static final String MASK_PREFIX = "MASK!";
private final Supplier keySequenceGenerator;
private final Function locatorFunction;
diff --git a/src/main/java/jp/co/moneyforward/autotest/framework/utils/InternalUtils.java b/src/main/java/jp/co/moneyforward/autotest/framework/utils/InternalUtils.java
index e221ddff..1b315f2b 100644
--- a/src/main/java/jp/co/moneyforward/autotest/framework/utils/InternalUtils.java
+++ b/src/main/java/jp/co/moneyforward/autotest/framework/utils/InternalUtils.java
@@ -31,7 +31,6 @@
import static java.lang.Thread.currentThread;
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;
-import static jp.co.moneyforward.autotest.actions.web.SendKey.MASK_PREFIX;
///
/// An internal utility class of the **insdog** framework.
@@ -39,6 +38,7 @@
public enum InternalUtils {
;
+ public static final String MASK_PREFIX = "MASK!";
public static final Logger LOGGER = LoggerFactory.getLogger(InternalUtils.class);
///
diff --git a/src/test/java/jp/co/moneyforward/autotest/ut/builtins/BuiltInActsTest.java b/src/test/java/jp/co/moneyforward/autotest/ut/builtins/BuiltInActsTest.java
index cbfff184..4d369b68 100644
--- a/src/test/java/jp/co/moneyforward/autotest/ut/builtins/BuiltInActsTest.java
+++ b/src/test/java/jp/co/moneyforward/autotest/ut/builtins/BuiltInActsTest.java
@@ -2,17 +2,25 @@
import com.github.valid8j.pcond.forms.Printables;
import com.microsoft.playwright.*;
+import io.appium.java_client.AppiumDriver;
+import jp.co.moneyforward.autotest.actions.mobile.ElementFunctions;
import jp.co.moneyforward.autotest.actions.web.*;
import jp.co.moneyforward.autotest.framework.action.Act;
import jp.co.moneyforward.autotest.framework.core.ExecutionEnvironment;
import jp.co.moneyforward.autotest.ututils.TestBase;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static com.github.valid8j.fluent.Expectations.*;
+import static jp.co.moneyforward.autotest.framework.utils.InternalUtils.MASK_PREFIX;
import static org.mockito.Mockito.*;
class BuiltInActsTest extends TestBase {
@@ -120,7 +128,7 @@ void givenMaskedString_whenSendkey_thenUnmaskedStringIsTypedIntoLocator() {
when(page.locator(any())).thenReturn(locator);
when(page.keyboard()).thenReturn(keyboard);
- Page returned = new SendKey("hello", SendKey.MASK_PREFIX + "keysToBeSentToHello").perform(page, executionEnvironment);
+ Page returned = new SendKey("hello", MASK_PREFIX + "keysToBeSentToHello").perform(page, executionEnvironment);
assertAll(value(returned).toBe().equalTo(page));
Mockito.verify(locator).focus();
@@ -130,7 +138,7 @@ void givenMaskedString_whenSendkey_thenUnmaskedStringIsTypedIntoLocator() {
@Test
void givenMaskedString_whenName_thenNameLooksOkWithoutUnmaskedString() {
- SendKey act = new SendKey("hello", SendKey.MASK_PREFIX + "keysToBeSentToHello");
+ SendKey act = new SendKey("hello", MASK_PREFIX + "keysToBeSentToHello");
String name = act.name();
@@ -138,7 +146,7 @@ void givenMaskedString_whenName_thenNameLooksOkWithoutUnmaskedString() {
assertStatement(value(name).toBe()
.containing("SendKey")
.not(v -> v.containing("keysToBeSentToHello"))
- .containing(SendKey.MASK_PREFIX));
+ .containing(MASK_PREFIX));
}
@Test
@@ -276,4 +284,308 @@ void givenSinkWithoutName_whenPerformed_thenGivenConsumerExercised() {
assertStatement(value(valueHolder).invoke("get").toBe().equalTo("XYZ"));
}
+
+ // Mobile action tests
+
+ @Test
+ void givenMobileElement_whenPerformMobileClick_thenElementIsClicked() {
+ AppiumDriver driver = Mockito.mock(AppiumDriver.class);
+ ExecutionEnvironment executionEnvironment = Mockito.mock(ExecutionEnvironment.class);
+ WebElement element = Mockito.mock(WebElement.class);
+ By by = By.id("someId");
+ when(driver.findElement(by)).thenReturn(element);
+
+ AppiumDriver returned = new jp.co.moneyforward.autotest.actions.mobile.Click(by).perform(driver, executionEnvironment);
+
+ assertAll(value(returned).toBe().equalTo(driver));
+ Mockito.verify(element).click();
+ }
+
+ @Test
+ void givenMobileClick_whenName_thenNameContainsClick() {
+ jp.co.moneyforward.autotest.actions.mobile.Click act =
+ new jp.co.moneyforward.autotest.actions.mobile.Click(By.id("someId"));
+
+ String name = act.name();
+
+ assertAll(value(name).toBe().containing("Click"));
+ }
+
+ @Test
+ void givenPresentMobileElement_whenPerformMobileClickIfPresent_thenElementIsClicked() {
+ AppiumDriver driver = Mockito.mock(AppiumDriver.class);
+ ExecutionEnvironment executionEnvironment = Mockito.mock(ExecutionEnvironment.class);
+ WebElement element = Mockito.mock(WebElement.class);
+ By by = By.id("someId");
+ when(driver.findElements(by)).thenReturn(List.of(element));
+ when(element.isDisplayed()).thenReturn(true);
+
+ AppiumDriver returned = new jp.co.moneyforward.autotest.actions.mobile.ClickIfPresent(by).perform(driver, executionEnvironment);
+
+ assertAll(value(returned).toBe().equalTo(driver));
+ Mockito.verify(element).click();
+ }
+
+ @Test
+ void givenAbsentMobileElement_whenPerformMobileClickIfPresent_thenNoClickPerformed() {
+ AppiumDriver driver = Mockito.mock(AppiumDriver.class);
+ ExecutionEnvironment executionEnvironment = Mockito.mock(ExecutionEnvironment.class);
+ By by = By.id("someId");
+ when(driver.findElements(by)).thenReturn(List.of());
+
+ AppiumDriver returned = new jp.co.moneyforward.autotest.actions.mobile.ClickIfPresent(by).perform(driver, executionEnvironment);
+
+ assertAll(value(returned).toBe().equalTo(driver));
+ Mockito.verify(driver, never()).findElement(any());
+ }
+
+ @Test
+ void givenMobileClickIfPresent_whenName_thenNameContainsClickIfPresent() {
+ jp.co.moneyforward.autotest.actions.mobile.ClickIfPresent act =
+ new jp.co.moneyforward.autotest.actions.mobile.ClickIfPresent(By.id("someId"));
+
+ String name = act.name();
+
+ assertAll(value(name).toBe().containing("ClickIfPresent"));
+ }
+
+ @Test
+ void whenElementFunctionsTextContent_thenGetTextCalledOnElement() {
+ WebElement element = Mockito.mock(WebElement.class);
+ when(element.getText()).thenReturn("Hello");
+
+ String text = ElementFunctions.textContent().apply(element);
+
+ assertAll(value(text).toBe().equalTo("Hello"));
+ Mockito.verify(element).getText();
+ }
+
+ @Test
+ void whenElementFunctionsTagContent_thenGetTagNameCalledOnElement() {
+ WebElement element = Mockito.mock(WebElement.class);
+ when(element.getTagName()).thenReturn("button");
+
+ String tag = ElementFunctions.tagContent().apply(element);
+
+ assertAll(value(tag).toBe().equalTo("button"));
+ Mockito.verify(element).getTagName();
+ }
+
+ @Test
+ void whenElementFunctionsIsEnabled_thenIsEnabledCalledOnElement() {
+ WebElement element = Mockito.mock(WebElement.class);
+ when(element.isEnabled()).thenReturn(true);
+
+ Boolean enabled = ElementFunctions.isEnabled().apply(element);
+
+ assertAll(value(enabled).toBe().equalTo(true));
+ Mockito.verify(element).isEnabled();
+ }
+
+ @Test
+ void whenPageFunctionsLinkLocatorByName_thenExactMatchXpathUsed() {
+ By by = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.linkLocatorByName("hello").apply(null);
+
+ assertStatement(value(by.toString()).toBe()
+ .containing("@content-desc='hello'")
+ .containing("@name='hello'"));
+ }
+
+ @Test
+ void whenPageFunctionsLinkLocatorByNameLenient_thenContainsMatchXpathUsed() {
+ By by = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.linkLocatorByName("hello", true).apply(null);
+
+ assertStatement(value(by.toString()).toBe()
+ .containing("contains(@content-desc,'hello')")
+ .containing("contains(@name,'hello')"));
+ }
+
+ @Test
+ void whenPageFunctionsLocatorByText_thenExactMatchXpathUsed() {
+ By by = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.locatorByText("hello").apply(null);
+
+ assertStatement(value(by.toString()).toBe()
+ .containing("@text='hello'")
+ .containing("@label='hello'")
+ .containing("@name='hello'"));
+ }
+
+ @Test
+ void whenPageFunctionsLocatorByTextLenient_thenContainsMatchXpathUsed() {
+ By by = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.locatorByText("hello", true).apply(null);
+
+ assertStatement(value(by.toString()).toBe()
+ .containing("contains(@text,'hello')")
+ .containing("contains(@label,'hello')")
+ .containing("contains(@name,'hello')"));
+ }
+
+ @Test
+ void whenPageFunctionsButtonLocatorByName_thenCorrectXpathUsed() {
+ By by = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.buttonLocatorByName("Submit").apply(null);
+
+ assertStatement(value(by.toString()).toBe()
+ .containing("android.widget.Button[@text='Submit']")
+ .containing("XCUIElementTypeButton[@name='Submit']"));
+ }
+
+ @Test
+ void whenPageFunctionsLocatorByLabel_thenExactMatchXpathUsed() {
+ By by = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.locatorByLabel("myLabel").apply(null);
+
+ assertStatement(value(by.toString()).toBe()
+ .containing("@content-desc='myLabel'")
+ .containing("@label='myLabel'"));
+ }
+
+ @Test
+ void whenPageFunctionsLocatorByLabelLenient_thenContainsMatchXpathUsed() {
+ By by = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.locatorByLabel("myLabel", true).apply(null);
+
+ assertStatement(value(by.toString()).toBe()
+ .containing("contains(@content-desc,'myLabel')")
+ .containing("contains(@label,'myLabel')"));
+ }
+
+ @Test
+ void whenPageFunctionsLocatorByPlaceholder_thenCorrectXpathUsed() {
+ By by = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.locatorByPlaceholder("Enter name").apply(null);
+
+ assertStatement(value(by.toString()).toBe()
+ .containing("@hint='Enter name'")
+ .containing("@placeholderValue='Enter name'"));
+ }
+
+ @Test
+ void whenPageFunctionsLocatorBySelector_thenByIsReturned() {
+ AppiumDriver driver = Mockito.mock(AppiumDriver.class);
+ By by = By.id("targetId");
+ WebElement expected = Mockito.mock(WebElement.class);
+ when(driver.findElement(by)).thenReturn(expected);
+
+ WebElement result = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.findElementBy(by).apply(driver);
+
+ assertAll(value(result).toBe().equalTo(expected));
+ }
+
+ @Test
+ void whenPageFunctionsLinkLocatorByText_thenLenientXpathUsed() {
+ By by = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.linkLocatorByText("hello").apply(null);
+
+ assertStatement(value(by.toString()).toBe()
+ .containing("contains(@text,'hello')")
+ .containing("contains(@label,'hello')")
+ .containing("contains(@name,'hello')"));
+ }
+
+ @Test
+ void whenPageFunctionsLinkLocatorByExactText_thenExactXpathUsed() {
+ By by = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.linkLocatorByExactText("hello").apply(null);
+
+ assertStatement(value(by.toString()).toBe()
+ .containing("@text='hello'")
+ .containing("@label='hello'")
+ .containing("@name='hello'"));
+ }
+
+ @Test
+ void whenPageFunctionsToTitle_thenDriverGetTitleIsCalled() {
+ AppiumDriver driver = Mockito.mock(AppiumDriver.class);
+ when(driver.getTitle()).thenReturn("My Title");
+
+ String title = jp.co.moneyforward.autotest.actions.mobile.PageFunctions.toTitle().apply(driver);
+
+ assertAll(value(title).toBe().equalTo("My Title"));
+ Mockito.verify(driver).getTitle();
+ }
+
+ @Test
+ void whenPerformMobileScreenshot_thenScreenshotCopiedAndDriverReturned() throws IOException {
+ AppiumDriver driver = Mockito.mock(AppiumDriver.class);
+ ExecutionEnvironment executionEnvironment = Mockito.mock(ExecutionEnvironment.class);
+ when(executionEnvironment.stepName()).thenReturn("TEST_STEP");
+ Path destPath = Path.of(System.getProperty("java.io.tmpdir"), "screenshot-test-" + System.nanoTime(), "screenshot-dest.png");
+ when(driver.getScreenshotAs(any())).thenReturn(new byte[]{1, 2, 3});
+ when(executionEnvironment.testOutputFilenameFor(any(String.class))).thenReturn(destPath);
+
+ AppiumDriver returned = new jp.co.moneyforward.autotest.actions.mobile.Screenshot().perform(driver, executionEnvironment);
+
+ assertAll(
+ value(returned).toBe().equalTo(driver),
+ value(destPath.toFile().exists()).toBe().equalTo(true));
+ Mockito.verify(driver).getScreenshotAs(any());
+ }
+
+ @Test
+ void givenUnmaskedKey_whenPerformMobileSendKey_thenKeysSentToElement() {
+ AppiumDriver driver = Mockito.mock(AppiumDriver.class);
+ ExecutionEnvironment executionEnvironment = Mockito.mock(ExecutionEnvironment.class);
+ WebElement element = Mockito.mock(WebElement.class);
+ By by = By.id("inputField");
+ when(driver.findElement(by)).thenReturn(element);
+
+ AppiumDriver returned = new jp.co.moneyforward.autotest.actions.mobile.SendKey(by, "myPassword").perform(driver, executionEnvironment);
+
+ assertAll(value(returned).toBe().equalTo(driver));
+ Mockito.verify(element).sendKeys("myPassword");
+ }
+
+ @Test
+ void givenMaskedKey_whenPerformMobileSendKey_thenUnmaskedKeysSentToElement() {
+ AppiumDriver driver = Mockito.mock(AppiumDriver.class);
+ ExecutionEnvironment executionEnvironment = Mockito.mock(ExecutionEnvironment.class);
+ WebElement element = Mockito.mock(WebElement.class);
+ By by = By.id("inputField");
+ when(driver.findElement(by)).thenReturn(element);
+
+ AppiumDriver returned = new jp.co.moneyforward.autotest.actions.mobile.SendKey(
+ by, MASK_PREFIX + "myPassword"
+ ).perform(driver, executionEnvironment);
+
+ assertAll(value(returned).toBe().equalTo(driver));
+ Mockito.verify(element).sendKeys("myPassword");
+ }
+
+ @Test
+ void givenMobileSendKey_whenName_thenNameContainsSendKeyAndLocatorAndKeys() {
+ jp.co.moneyforward.autotest.actions.mobile.SendKey act =
+ new jp.co.moneyforward.autotest.actions.mobile.SendKey(By.id("field"), "keys");
+
+ String name = act.name();
+
+ assertStatement(value(name).toBe()
+ .containing("SendKey")
+ .containing("field")
+ .containing("keys"));
+ }
+
+ @Test
+ void givenMobileSendKeyWithMaskedKey_whenName_thenNameContainsMaskPrefixNotSecret() {
+ jp.co.moneyforward.autotest.actions.mobile.SendKey act =
+ new jp.co.moneyforward.autotest.actions.mobile.SendKey(By.id("field"), MASK_PREFIX + "secret");
+
+ String name = act.name();
+
+ assertStatement(value(name).toBe()
+ .containing("SendKey")
+ .containing(MASK_PREFIX)
+ .not(v -> v.containing("secret")));
+ }
+
+ @Test
+ void givenMobilePageFunctionsLocator_whenConstructingActs_thenComposable() {
+ AppiumDriver driver = Mockito.mock(AppiumDriver.class);
+ ExecutionEnvironment env = Mockito.mock(ExecutionEnvironment.class);
+ WebElement element = Mockito.mock(WebElement.class);
+ when(driver.findElement(any(By.class))).thenReturn(element);
+ when(driver.findElements(any(By.class))).thenReturn(List.of(element));
+ when(element.isDisplayed()).thenReturn(true);
+
+ new jp.co.moneyforward.autotest.actions.mobile.Click(jp.co.moneyforward.autotest.actions.mobile.PageFunctions.buttonLocatorByName("Submit")).perform(driver, env);
+ new jp.co.moneyforward.autotest.actions.mobile.ClickIfPresent(jp.co.moneyforward.autotest.actions.mobile.PageFunctions.locatorByText("hello")).perform(driver, env);
+ new jp.co.moneyforward.autotest.actions.mobile.SendKey(jp.co.moneyforward.autotest.actions.mobile.PageFunctions.locatorByPlaceholder("Enter name"), "text").perform(driver, env);
+
+ Mockito.verify(element, Mockito.atLeast(2)).click();
+ Mockito.verify(element).sendKeys("text");
+ }
}