-
Notifications
You must be signed in to change notification settings - Fork 5
feat: Develop actions for mobile #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
398ae96
a1d192c
0b0a10b
0856cfa
5a29f52
90f7cdc
0a75fa4
096e8cf
070081a
834b969
52d656a
f4d5ead
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<AppiumDriver, By> locatorFunction) { | ||
| super(locatorFunction); | ||
| } | ||
|
|
||
| @Override | ||
| public AppiumDriver perform(AppiumDriver driver, ExecutionEnvironment executionEnvironment) { | ||
| driver.findElement(this.locatorFunction.apply(driver)).click(); | ||
| return driver; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<AppiumDriver, AppiumDriver> { | ||
| final Function<AppiumDriver, By> locatorFunction; | ||
|
|
||
| /// | ||
| /// Creates an object of this class. | ||
| /// | ||
| /// @param locatorFunction A function to locate an element to click. | ||
| /// | ||
| protected ClickBase(Function<AppiumDriver, By> 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 + "]"; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<AppiumDriver, By> locatorFunction) { | ||
| super(locatorFunction); | ||
| } | ||
|
|
||
| @Override | ||
| public AppiumDriver perform(AppiumDriver driver, ExecutionEnvironment executionEnvironment) { | ||
| List<WebElement> elements = driver.findElements(this.locatorFunction.apply(driver)); | ||
| if (!elements.isEmpty()) { | ||
| elements.getFirst().click(); | ||
| } | ||
| return driver; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| 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<WebElement, String> textContent() { | ||
| return Printables.function("textContent", WebElement::getText); | ||
| } | ||
|
|
||
| public static Function<WebElement, String> tagContent() { | ||
| return Printables.function("tagContent", WebElement::getTagName); | ||
| } | ||
|
|
||
| public static Function<WebElement, Boolean> isEnabled() { | ||
| return Printables.function("isEnabled", WebElement::isEnabled); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| 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<AppiumDriver, WebElement> linkLocatorByName(String name) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [1] Type incompatibility: Every method here returns Related: these functions call Suggestion — fix both at once: return
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Follow-up with a concrete reproduction, verified against this PR's head ( The branch itself compiles because nothing in the PR ever passes a package jp.co.moneyforward.autotest.ut.builtins;
import jp.co.moneyforward.autotest.actions.mobile.Click;
import jp.co.moneyforward.autotest.actions.mobile.ClickIfPresent;
import jp.co.moneyforward.autotest.actions.mobile.PageFunctions;
import jp.co.moneyforward.autotest.actions.mobile.SendKey;
/// Mobile transplant of compositions the web package supports and tests.
class MobileCompositionRepro {
void composeActsWithPageFunctions() {
new Click(PageFunctions.buttonLocatorByName("Submit"));
new ClickIfPresent(PageFunctions.locatorByText("hello"));
new SendKey(PageFunctions.locatorByPlaceholder("Enter name"), "text");
}
}and running So every Suggested guard once the signatures are aligned (mirrors the existing web test, and would have turned this into a red build): @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));
// The point of this test is that these constructor calls COMPILE,
// i.e. PageFunctions' return type is the acts' input type.
new jp.co.moneyforward.autotest.actions.mobile.Click(PageFunctions.buttonLocatorByName("Submit")).perform(driver, env);
new jp.co.moneyforward.autotest.actions.mobile.ClickIfPresent(PageFunctions.locatorByText("hello")).perform(driver, env);
new jp.co.moneyforward.autotest.actions.mobile.SendKey(PageFunctions.locatorByPlaceholder("Enter name"), "text").perform(driver, env);
Mockito.verify(element, Mockito.atLeast(2)).click();
Mockito.verify(element).sendKeys("text");
}🤖 Generated with Claude Code |
||
| 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<AppiumDriver, WebElement> linkLocatorByName(String name, boolean lenient) { | ||
| String xpath = lenient | ||
| ? "//*[contains(@content-desc,'" + name + "') or contains(@name,'" + name + "')]" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [2] XPath built by raw string concatenation breaks on any argument containing a single quote.
Suggestion: prefer
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. これ、一回踏むと、デバッグつらそうなので、直した方がいいかも。(Fableとも相談した) |
||
| : "//*[@content-desc='" + name + "' or @name='" + name + "']"; | ||
| return Printables.function("link[name" + (lenient ? "~" : "=") + name + "]", | ||
| d -> d.findElement(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<AppiumDriver, WebElement> 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<AppiumDriver, WebElement> locatorByText(String text, boolean lenient) { | ||
| String xpath = lenient | ||
| ? "//*[contains(@text,'" + text + "') or contains(@label,'" + text + "') or contains(@name,'" + text + "')]" | ||
| : "//*[@text='" + text + "' or @label='" + text + "' or @name='" + text + "']"; | ||
| return Printables.function("@[text" + (lenient ? "~" : "=") + text + "]", | ||
| d -> d.findElement(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<AppiumDriver, WebElement> buttonLocatorByName(String name) { | ||
| return Printables.function("@[name=" + name + "]", | ||
| d -> d.findElement(By.xpath( | ||
| "//android.widget.Button[@text='" + name + "'] | //XCUIElementTypeButton[@name='" + name + "']"))); | ||
| } | ||
|
|
||
| /// | ||
| /// 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<AppiumDriver, WebElement> 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<AppiumDriver, WebElement> locatorByLabel(String label, boolean lenient) { | ||
| String xpath = lenient | ||
| ? "//*[contains(@content-desc,'" + label + "') or contains(@label,'" + label + "')]" | ||
| : "//*[@content-desc='" + label + "' or @label='" + label + "']"; | ||
| return Printables.function("@[label" + (lenient ? "~" : "=") + label + "]", | ||
| d -> d.findElement(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<AppiumDriver, WebElement> locatorByPlaceholder(String placeholder) { | ||
| return Printables.function("@[placeholder=" + placeholder + "]", | ||
| d -> d.findElement(By.xpath( | ||
| "//*[@hint='" + placeholder + "' or @placeholderValue='" + placeholder + "']"))); | ||
| } | ||
|
|
||
| /// | ||
| /// 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<AppiumDriver, WebElement> locatorBySelector(By by) { | ||
| Requires.requireNonNull(by); | ||
| return Printables.function("@[" + by + "]", d -> d.findElement(by)); | ||
| } | ||
|
|
||
| /// | ||
| /// Returns a function that gives a locator of a link-like element whose text contains a given `text`. | ||
| /// | ||
| /// @param text A string to be contained in the text of a link-like element. | ||
| /// @return A function that gives a locator of a link-like element whose text contains a given `text`. | ||
| /// | ||
| public static Function<AppiumDriver, WebElement> linkLocatorByText(String text) { | ||
| return linkLocatorByText(text, true); | ||
| } | ||
|
|
||
| /// | ||
| /// Returns a function that gives a locator of a link-like element whose text equals to a given `text`. | ||
| /// | ||
| /// @param text A string to be matched exactly with the text of a link-like element. | ||
| /// @return A function that gives a locator of a link-like element whose text equals to a given `text`. | ||
| /// | ||
| public static Function<AppiumDriver, WebElement> linkLocatorByExactText(String text) { | ||
| return linkLocatorByText(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<AppiumDriver, String> toTitle() { | ||
| return Printables.function("title", AppiumDriver::getTitle); | ||
| } | ||
|
|
||
| public static Function<AppiumDriver, WebElement> linkLocatorByText(String text, boolean lenient) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [7] Only the Suggestion: either delegate to |
||
| String xpath = lenient | ||
| ? "//*[contains(@text,'" + text + "') or contains(@label,'" + text + "') or contains(@name,'" + text + "')]" | ||
| : "//*[@text='" + text + "' or @label='" + text + "' or @name='" + text + "']"; | ||
| return Printables.function("link:@[text" + (lenient ? "~" : "=") + text + "]", | ||
| d -> d.findElement(By.xpath(xpath))); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[3] Visibility guard dropped relative to the web
ClickIfPresent.The web version clicks only if
targetElement.isVisible()(its Javadoc: "Check for presence is done byLocator#isVisible"). This copy clicks wheneverfindElements()returns a non-empty list, so an element that is present in the tree but not visible (off-screen, hidden view) gets clicked — or throwsElementNotInteractableException— which is exactly the "skip safely" case this class exists for.Suggestion: