├── .github
├── dependabot.yml
└── workflows
│ ├── 3-automatically-merge-dependabot-pr.yml
│ ├── android-test.yml
│ └── ios-test.yml
├── .gitignore
├── apps
├── Android-MyDemoAppRN.1.3.0.build-244.apk
└── iOS-Simulator-MyRNDemoApp.1.3.0-162.zip
├── pom.xml
├── readme.md
└── src
├── main
└── java
│ └── com
│ ├── commands
│ ├── ScrollToElement.java
│ └── SwipeToScreenEnd.java
│ ├── conditions
│ └── CustomCondition.java
│ ├── locator
│ └── LocatorIdentifier.java
│ ├── provider
│ ├── SauceLabAndroidDriverProvider.java
│ └── SauceLabIosDriverProvider.java
│ └── screens
│ ├── ProductDetailsScreen.java
│ └── ProductsListingScreen.java
└── test
├── java
└── com
│ └── tests
│ ├── AddToCartTest.java
│ ├── CustomCommandTest.java
│ ├── CustomConditionTest.java
│ ├── DeepLinkTest.java
│ └── base
│ └── TestSetup.java
└── resources
└── selenide.properties
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: maven
4 | directory: "/"
5 | schedule:
6 | interval: monthly
7 | time: "07:38"
8 | open-pull-requests-limit: 10
9 | commit-message:
10 | prefix: "mvn"
--------------------------------------------------------------------------------
/.github/workflows/3-automatically-merge-dependabot-pr.yml:
--------------------------------------------------------------------------------
1 | name: automatically merge dependabot PR's
2 | on:
3 | pull_request:
4 | branches: [ master ]
5 |
6 | jobs:
7 | run-unit-test:
8 | name: run unit test
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | with:
13 | fetch-depth: 0
14 |
15 | - name: Set up JDK 11
16 | uses: actions/setup-java@v1
17 | with:
18 | java-version: 11
19 |
20 | - name: run unit tests
21 | run: echo "unit test passed"
22 |
23 | auto-merge-dependabot:
24 | name: 🤖 Auto merge dependabot PR
25 | timeout-minutes: 10
26 | needs: [ run-unit-test ]
27 | if: ${{ github.actor == 'dependabot[bot]' }}
28 | runs-on: ubuntu-latest
29 | permissions:
30 | pull-requests: write
31 | contents: write
32 | steps:
33 | - name: 🤖 Merge PR from dependabot
34 | uses: fastify/github-action-merge-dependabot@v3.6.4
35 | with:
36 | github-token: ${{ secrets.GITHUB_TOKEN }}
37 | target: minor
38 | merge-method: rebase
39 |
--------------------------------------------------------------------------------
/.github/workflows/android-test.yml:
--------------------------------------------------------------------------------
1 | name: Run android tests in github runner
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | test:
8 | runs-on: macos-latest
9 | strategy:
10 | matrix:
11 | api-level: [25]
12 | steps:
13 | - name: checkout
14 | uses: actions/checkout@v2
15 |
16 | - name: Set up JDK 1.11
17 | uses: actions/setup-java@v1
18 | with:
19 | java-version: 11
20 |
21 | - uses: actions/setup-node@v2
22 | with:
23 | node-version: '12'
24 | - run: |
25 | npm install -g appium@v1.22
26 | appium -v
27 | appium &>/dev/null &
28 |
29 | - name: AVD cache
30 | uses: actions/cache@v2
31 | id: avd-cache
32 | with:
33 | path: |
34 | ~/.android/avd/*
35 | ~/.android/adb*
36 | key: avd-${{ matrix.api-level }}
37 |
38 | - name: create AVD and generate snapshot for caching
39 | if: steps.avd-cache.outputs.cache-hit != 'true'
40 | uses: reactivecircus/android-emulator-runner@v2
41 | with:
42 | api-level: ${{ matrix.api-level }}
43 | force-avd-creation: false
44 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
45 | disable-animations: false
46 | script: echo "Generated AVD snapshot for caching."
47 |
48 | - name: run android tests
49 | uses: reactivecircus/android-emulator-runner@v2
50 | with:
51 | api-level: ${{ matrix.api-level }}
52 | force-avd-creation: false
53 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
54 | disable-animations: true
55 | script: mvn clean test -Dselenide.browser=com.provider.SauceLabAndroidDriverProvider
56 |
--------------------------------------------------------------------------------
/.github/workflows/ios-test.yml:
--------------------------------------------------------------------------------
1 | name: Run appium iOS test in Github Runner
2 |
3 | on:
4 | workflow_dispatch:
5 | jobs:
6 | build:
7 | runs-on: macos-10.15
8 | steps:
9 | - uses: actions/checkout@v2
10 |
11 | - name: Set up JDK 1.11
12 | uses: actions/setup-java@v1
13 | with:
14 | java-version: 11
15 |
16 | - uses: actions/setup-node@v2
17 | with:
18 | node-version: '12'
19 | - run: |
20 | npm install -g appium@v1.22
21 | appium -v
22 | appium &>/dev/null &
23 | mvn clean test -Dselenide.browser=com.provider.SauceLabIosDriverProvider
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | build
3 | .idea
4 | SelenideAppiumFramework.iml
5 |
--------------------------------------------------------------------------------
/apps/Android-MyDemoAppRN.1.3.0.build-244.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amuthansakthivel/SelenideAppiumFramework/a0c3f9bd0c8ddddfe24485dd138da85bf2b62368/apps/Android-MyDemoAppRN.1.3.0.build-244.apk
--------------------------------------------------------------------------------
/apps/iOS-Simulator-MyRNDemoApp.1.3.0-162.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amuthansakthivel/SelenideAppiumFramework/a0c3f9bd0c8ddddfe24485dd138da85bf2b62368/apps/iOS-Simulator-MyRNDemoApp.1.3.0-162.zip
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.example
8 | SelenideAppiumFramework
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 11
13 | 11
14 | UTF-8
15 |
16 |
17 |
18 |
19 | com.codeborne
20 | selenide-appium
21 | 2.7.1
22 |
23 |
24 |
25 | org.junit.jupiter
26 | junit-jupiter
27 | 5.12.0
28 | test
29 |
30 |
31 |
32 | org.projectlombok
33 | lombok
34 | 1.18.34
35 |
36 |
37 |
38 | org.slf4j
39 | slf4j-simple
40 | 2.0.17
41 | test
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | org.apache.maven.plugins
51 | maven-surefire-plugin
52 | 3.5.2
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | ## Selenide-Appium Example
2 |
3 | This project will give an example of how we can automate native mobile application using Selenide-Appium Library.
4 |
5 | To know more - [Selenide-Appium](https://github.com/selenide/selenide-appium)
6 |
7 | ### How to choose mobile OS at runtime?
8 |
9 | App under test is having same flow in both android and ios. So we need to tell Selenide-Appium which implementation to choose. We have two implementation classes, each for opening android and ios apps.
10 |
11 | We can choose which app we want to open by setting.
12 |
13 | 1. Setting Configuration.browser value via code.
14 |
15 | `Configuration.browser = SauceLabAndroidDriverProvider.class.getName();` //For opening android session
16 | `Configuration.browser = SauceLabIosDriverProvider.class.getName();` //For opening ios session
17 |
18 | Typical test may look like this
19 |
20 | ```java
21 | class SauceLabsTest {
22 |
23 | @Test
24 | void testSauceLabsApp() {
25 | Configuration.browser = SauceLabAndroidDriverProvider.class.getName();
26 | SelenideAppium.launchApp();
27 | //test
28 | }
29 | }
30 | ```
31 |
32 | The problem with this approach is to change the value manually or write some logic to find which class implementation to use based on needs.
33 |
34 | 2. Pass `Configuration.browser` value as System property. It is also recommended to have default implementation set in the selenide.properties file
35 |
36 | `mvn clean test -Dselenide.browser=com.provider.SauceLabAndroidDriverProvider` to run test on android
37 | `mvn clean test -Dselenide.browser=com.provider.SauceLabIosDriverProvider` to run test on ios
38 |
39 | ### Custom Commands
40 |
41 | We can create our own custom command by implementing Command Interface.
42 |
43 | For usage purpose, I have implemented custom command to scroll to an element.
44 |
45 | ```java
46 | public class ProductsListingScreen {
47 |
48 | @AndroidFindBy(xpath = "//android.widget.TextView[@text='$7.99']/preceding-sibling::android.view.ViewGroup/android.widget.ImageView")
49 | @iOSXCUITFindBy(accessibility = "Sauce Labs Onesie")
50 | private WebElement oneSieProduct;
51 |
52 | public ProductDetailsScreen selectOneSieProduct() {
53 | $(oneSieProduct).execute(new ScrollToElement()).shouldBe(visible).click();
54 | return page(ProductDetailsScreen.class);
55 | }
56 | }
57 | ```
58 |
59 | ### Custom Condition
60 |
61 | We can create custom conditions to match our requirements.
62 | In my case, for Android - I want to use `text` attribute and for iOS - I want to use `label` attribute for assertion.
63 |
64 | ```java
65 | public final class CustomCondition {
66 |
67 | private CustomCondition() {
68 | }
69 |
70 | public static Condition attributeMatching(String androidAttribute, String iosAttribute, String expectedValue) {
71 | boolean androidDriver = AppiumDriverRunner.isAndroidDriver();
72 | String attributeInContext = androidDriver ? androidAttribute : iosAttribute;
73 |
74 | return new Condition("element should have " + attributeInContext + " with value " + expectedValue) {
75 | @Nonnull
76 | @Override
77 | public CheckResult check(Driver driver, WebElement element) {
78 | String actualValue = element.getAttribute(attributeInContext);
79 | return new CheckResult(actualValue.contains(expectedValue), actualValue);
80 | }
81 | };
82 | }
83 | }
84 | ```
85 | ```java
86 | public class ProductsListingScreen {
87 |
88 | private static final By FOOTER_ANDROID = AppiumSelectors.withText("All Rights Reserved");
89 | private static final By FOOTER_IOS = AppiumSelectors.withName("All Rights Reserved");
90 |
91 | public void checkWhetherFooterIsPresent() {
92 | $(getLocator(FOOTER_ANDROID, FOOTER_IOS))
93 | .execute(new ScrollToElement())
94 | .shouldHave(CustomCondition.attributeMatching("text", "label", "Sauce Labs"));
95 | }
96 | }
97 | ```
98 |
99 |
--------------------------------------------------------------------------------
/src/main/java/com/commands/ScrollToElement.java:
--------------------------------------------------------------------------------
1 | package com.commands;
2 |
3 | import com.codeborne.selenide.Command;
4 | import com.codeborne.selenide.SelenideElement;
5 | import com.codeborne.selenide.appium.AppiumDriverRunner;
6 | import com.codeborne.selenide.impl.WebElementSource;
7 | import io.appium.java_client.AppiumDriver;
8 | import org.openqa.selenium.Dimension;
9 | import org.openqa.selenium.NoSuchElementException;
10 | import org.openqa.selenium.interactions.PointerInput;
11 | import org.openqa.selenium.interactions.Sequence;
12 |
13 | import javax.annotation.Nullable;
14 |
15 | import static com.codeborne.selenide.appium.WebdriverUnwrapper.cast;
16 | import static java.time.Duration.ofMillis;
17 | import static java.util.Collections.singletonList;
18 |
19 | public class ScrollToElement implements Command {
20 |
21 | @Nullable
22 | @Override
23 | public SelenideElement execute(
24 | SelenideElement proxy, WebElementSource locator, @Nullable Object[] objects) {
25 | AppiumDriver appiumDriver = cast(locator.driver(), AppiumDriver.class).orElseThrow();
26 |
27 | int currentSwipeCount = 0;
28 | String previousPageSource = "";
29 |
30 | while (isElementNotDisplayed(locator)
31 | && isNotEndOfPage(appiumDriver, previousPageSource)
32 | && isLessThanMaxSwipeCount(currentSwipeCount)) {
33 | previousPageSource = appiumDriver.getPageSource();
34 | performScroll(appiumDriver);
35 | currentSwipeCount++;
36 | }
37 | return proxy;
38 | }
39 |
40 | private boolean isLessThanMaxSwipeCount(int currentSwipeCount) {
41 | final int maxSwipeCount = 30;
42 | return currentSwipeCount < maxSwipeCount;
43 | }
44 |
45 | private boolean isElementNotDisplayed(WebElementSource locator) {
46 | try {
47 | if (AppiumDriverRunner.isAndroidDriver()) {
48 | return !locator.getWebElement().isDisplayed();
49 | } else {
50 | return !locator.getWebElement().getAttribute("visible").equals("true");
51 | }
52 | } catch (NoSuchElementException noSuchElementException) {
53 | return true;
54 | }
55 | }
56 |
57 | private boolean isNotEndOfPage(AppiumDriver appiumDriver, String initialPageSource) {
58 | return !initialPageSource.equals(appiumDriver.getPageSource());
59 | }
60 |
61 | private Dimension getMobileDeviceSize(AppiumDriver appiumDriver) {
62 | return appiumDriver.manage().window().getSize();
63 | }
64 |
65 | private void performScroll(AppiumDriver appiumDriver) {
66 | Dimension size = getMobileDeviceSize(appiumDriver);
67 | PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
68 | Sequence sequenceToPerformScroll = getSequenceToPerformScroll(finger, size);
69 | appiumDriver.perform(singletonList(sequenceToPerformScroll));
70 | }
71 |
72 | private Sequence getSequenceToPerformScroll(PointerInput finger, Dimension size) {
73 | int oneFourthHeightOfDevice = (int) (size.getHeight() * 0.25);
74 | return new Sequence(finger, 1)
75 | .addAction(
76 | finger.createPointerMove(
77 | ofMillis(0),
78 | PointerInput.Origin.viewport(),
79 | size.getWidth() / 2,
80 | size.getHeight() / 2))
81 | .addAction(finger.createPointerDown(PointerInput.MouseButton.MIDDLE.asArg()))
82 | .addAction(
83 | finger.createPointerMove(
84 | ofMillis(200),
85 | PointerInput.Origin.viewport(),
86 | size.getWidth() / 2,
87 | oneFourthHeightOfDevice))
88 | .addAction(finger.createPointerUp(PointerInput.MouseButton.MIDDLE.asArg()));
89 | }
90 | }
--------------------------------------------------------------------------------
/src/main/java/com/commands/SwipeToScreenEnd.java:
--------------------------------------------------------------------------------
1 | package com.commands;
2 |
3 | import com.codeborne.selenide.Command;
4 | import com.codeborne.selenide.SelenideElement;
5 | import com.codeborne.selenide.impl.WebElementSource;
6 | import io.appium.java_client.AppiumDriver;
7 | import org.openqa.selenium.Dimension;
8 | import org.openqa.selenium.interactions.PointerInput;
9 | import org.openqa.selenium.interactions.Sequence;
10 |
11 | import javax.annotation.Nullable;
12 |
13 | import static com.codeborne.selenide.appium.WebdriverUnwrapper.cast;
14 | import static java.time.Duration.ofMillis;
15 | import static java.util.Collections.singletonList;
16 |
17 | public class SwipeToScreenEnd implements Command {
18 |
19 | @Nullable
20 | @Override
21 | public SelenideElement execute(
22 | SelenideElement proxy, WebElementSource locator, @Nullable Object[] objects) {
23 | AppiumDriver appiumDriver = cast(locator.driver(), AppiumDriver.class).orElseThrow();
24 |
25 | int currentSwipeCount = 0;
26 | String previousPageSource = "";
27 |
28 | while (isNotEndOfPage(appiumDriver, previousPageSource)
29 | && isLessThanMaxSwipeCount(currentSwipeCount)) {
30 | previousPageSource = appiumDriver.getPageSource();
31 | performScroll(appiumDriver);
32 | currentSwipeCount++;
33 | }
34 | return proxy;
35 | }
36 |
37 | private boolean isLessThanMaxSwipeCount(int currentSwipeCount) {
38 | final int maxSwipeCount = 30;
39 | return currentSwipeCount < maxSwipeCount;
40 | }
41 |
42 | private boolean isNotEndOfPage(AppiumDriver appiumDriver, String initialPageSource) {
43 | return !initialPageSource.equals(appiumDriver.getPageSource());
44 | }
45 |
46 | private Dimension getMobileDeviceSize(AppiumDriver appiumDriver) {
47 | return appiumDriver.manage().window().getSize();
48 | }
49 |
50 | private void performScroll(AppiumDriver appiumDriver) {
51 | Dimension size = getMobileDeviceSize(appiumDriver);
52 | PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
53 | Sequence sequenceToPerformScroll = getSequenceToPerformScroll(finger, size);
54 | appiumDriver.perform(singletonList(sequenceToPerformScroll));
55 | }
56 |
57 | private Sequence getSequenceToPerformScroll(PointerInput finger, Dimension size) {
58 | int oneFourthHeightOfDevice = (int) (size.getHeight() * 0.25);
59 | return new Sequence(finger, 1)
60 | .addAction(
61 | finger.createPointerMove(
62 | ofMillis(0),
63 | PointerInput.Origin.viewport(),
64 | size.getWidth() / 2,
65 | size.getHeight() / 2))
66 | .addAction(finger.createPointerDown(PointerInput.MouseButton.MIDDLE.asArg()))
67 | .addAction(
68 | finger.createPointerMove(
69 | ofMillis(200),
70 | PointerInput.Origin.viewport(),
71 | size.getWidth() / 2,
72 | oneFourthHeightOfDevice))
73 | .addAction(finger.createPointerUp(PointerInput.MouseButton.MIDDLE.asArg()));
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/java/com/conditions/CustomCondition.java:
--------------------------------------------------------------------------------
1 | package com.conditions;
2 |
3 | import com.codeborne.selenide.CheckResult;
4 | import com.codeborne.selenide.Condition;
5 | import com.codeborne.selenide.Driver;
6 | import com.codeborne.selenide.appium.AppiumDriverRunner;
7 | import org.openqa.selenium.WebElement;
8 |
9 | import javax.annotation.Nonnull;
10 |
11 | public final class CustomCondition {
12 |
13 | private CustomCondition() {
14 | }
15 |
16 | public static Condition attributeMatching(String androidAttribute, String iosAttribute, String expectedValue) {
17 | boolean androidDriver = AppiumDriverRunner.isAndroidDriver();
18 | String attributeInContext = androidDriver ? androidAttribute : iosAttribute;
19 |
20 | return new Condition("element should have " + attributeInContext + " with value " + expectedValue) {
21 | @Nonnull
22 | @Override
23 | public CheckResult check(Driver driver, WebElement element) {
24 | String actualValue = element.getAttribute(attributeInContext);
25 | return new CheckResult(actualValue.contains(expectedValue), actualValue);
26 | }
27 | };
28 | }
29 | }
--------------------------------------------------------------------------------
/src/main/java/com/locator/LocatorIdentifier.java:
--------------------------------------------------------------------------------
1 | package com.locator;
2 |
3 | import com.codeborne.selenide.appium.AppiumDriverRunner;
4 | import org.openqa.selenium.By;
5 |
6 | public final class LocatorIdentifier {
7 |
8 | private LocatorIdentifier() {
9 | }
10 |
11 | public static By getLocator(By androidBy, By iosBy) {
12 | return AppiumDriverRunner.isAndroidDriver()
13 | ? androidBy
14 | : iosBy;
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/src/main/java/com/provider/SauceLabAndroidDriverProvider.java:
--------------------------------------------------------------------------------
1 | package com.provider;
2 |
3 | import com.codeborne.selenide.WebDriverProvider;
4 | import io.appium.java_client.android.AndroidDriver;
5 | import io.appium.java_client.android.options.UiAutomator2Options;
6 | import io.appium.java_client.remote.AutomationName;
7 | import lombok.SneakyThrows;
8 | import org.openqa.selenium.Capabilities;
9 | import org.openqa.selenium.WebDriver;
10 |
11 | import javax.annotation.Nonnull;
12 | import java.net.URL;
13 |
14 | public class SauceLabAndroidDriverProvider implements WebDriverProvider {
15 |
16 | @SneakyThrows
17 | @Nonnull
18 | @Override
19 | public WebDriver createDriver(@Nonnull Capabilities capabilities) {
20 | UiAutomator2Options options = new UiAutomator2Options();
21 | options.setAutomationName(AutomationName.ANDROID_UIAUTOMATOR2);
22 | options.setPlatformName("Android");
23 | options.setDeviceName("Android Emulator");
24 | //options.setUiautomator2ServerInstallTimeout(Duration.ofSeconds(2)); //Seems to be not working
25 | options.setCapability("uiautomator2ServerInstallTimeout", 60000);
26 | options.setApp(System.getProperty("user.dir") + "/apps/Android-MyDemoAppRN.1.3.0.build-244.apk");
27 | return new AndroidDriver(new URL("http://127.0.0.1:4723"), options);
28 | }
29 | }
--------------------------------------------------------------------------------
/src/main/java/com/provider/SauceLabIosDriverProvider.java:
--------------------------------------------------------------------------------
1 | package com.provider;
2 |
3 | import com.codeborne.selenide.WebDriverProvider;
4 | import io.appium.java_client.android.AndroidDriver;
5 | import io.appium.java_client.ios.IOSDriver;
6 | import io.appium.java_client.ios.options.XCUITestOptions;
7 | import io.appium.java_client.remote.AutomationName;
8 | import lombok.SneakyThrows;
9 | import org.openqa.selenium.Capabilities;
10 | import org.openqa.selenium.WebDriver;
11 |
12 | import javax.annotation.Nonnull;
13 | import java.net.URL;
14 | import java.time.Duration;
15 |
16 | public class SauceLabIosDriverProvider implements WebDriverProvider {
17 |
18 | @SneakyThrows
19 | @Nonnull
20 | @Override
21 | public WebDriver createDriver(@Nonnull Capabilities capabilities) {
22 | XCUITestOptions options = new XCUITestOptions();
23 | options.setAutomationName(AutomationName.IOS_XCUI_TEST);
24 | options.setWdaLaunchTimeout(Duration.ofMinutes(10));
25 | options.setDeviceName("iPhone 13");
26 | options.setFullReset(false);
27 | options.setApp(System.getProperty("user.dir") + "/apps/iOS-Simulator-MyRNDemoApp.1.3.0-162.zip");
28 | return new IOSDriver(new URL("http://127.0.0.1:4723"), options);
29 | }
30 | }
--------------------------------------------------------------------------------
/src/main/java/com/screens/ProductDetailsScreen.java:
--------------------------------------------------------------------------------
1 | package com.screens;
2 |
3 | import com.codeborne.selenide.appium.AppiumSelectors;
4 | import org.openqa.selenium.By;
5 |
6 | import static com.codeborne.selenide.Condition.visible;
7 | import static com.codeborne.selenide.Selenide.$;
8 | import static com.locator.LocatorIdentifier.getLocator;
9 |
10 | public class ProductDetailsScreen {
11 | /*
12 | We can also use conventional @AndroidFindBy @iOSXCUITFindBy
13 | But it might not be suitable for dynamic locators
14 | Also AppiumSelectors class contains rich locating strategies we could leverage.
15 | */
16 | private static final By ADD_TO_CART_BUTTON_ANDROID = AppiumSelectors.byContentDescription("Add To Cart button");
17 | private static final By ADD_TO_CART_BUTTON_IOS = AppiumSelectors.byName("Add To Cart button");
18 |
19 | public void checkWhetherAddToCartButtonPresent() {
20 | $(getLocator(ADD_TO_CART_BUTTON_ANDROID, ADD_TO_CART_BUTTON_IOS)).shouldHave(visible);
21 | }
22 | }
--------------------------------------------------------------------------------
/src/main/java/com/screens/ProductsListingScreen.java:
--------------------------------------------------------------------------------
1 | package com.screens;
2 |
3 | import com.codeborne.selenide.As;
4 | import com.codeborne.selenide.appium.AppiumSelectors;
5 | import com.commands.ScrollToElement;
6 | import com.conditions.CustomCondition;
7 | import io.appium.java_client.pagefactory.AndroidFindBy;
8 | import io.appium.java_client.pagefactory.iOSXCUITFindBy;
9 | import org.openqa.selenium.By;
10 | import org.openqa.selenium.WebElement;
11 |
12 | import static com.codeborne.selenide.Condition.visible;
13 | import static com.codeborne.selenide.Selenide.$;
14 | import static com.codeborne.selenide.appium.AppiumClickOptions.tap;
15 | import static com.codeborne.selenide.appium.ScreenObject.screen;
16 | import static com.locator.LocatorIdentifier.getLocator;
17 |
18 | public class ProductsListingScreen {
19 |
20 | @As("bike light product")
21 | @AndroidFindBy(xpath = "//android.widget.TextView[@text='$9.99']/preceding-sibling::android.view.ViewGroup/android.widget.ImageView")
22 | @iOSXCUITFindBy(accessibility = "Sauce Labs Bike Light")
23 | private WebElement bikeLightProduct;
24 |
25 | @As("one sie product")
26 | @AndroidFindBy(xpath = "//android.widget.TextView[@text='$7.99']/preceding-sibling::android.view.ViewGroup/android.widget.ImageView")
27 | @iOSXCUITFindBy(accessibility = "Sauce Labs Onesie")
28 | private WebElement oneSieProduct;
29 |
30 | @As("Footer text")
31 | private static final By FOOTER_ANDROID = AppiumSelectors.withText("All Rights Reserved");
32 |
33 | @As("Footer text")
34 | private static final By FOOTER_IOS = AppiumSelectors.withName("All Rights Reserved");
35 |
36 | public ProductDetailsScreen selectBikeLightProduct() {
37 | $(bikeLightProduct).shouldBe(visible).click(tap()); //native event tap
38 | return screen(ProductDetailsScreen.class);
39 | }
40 |
41 | public ProductDetailsScreen selectOneSieProduct() {
42 | $(oneSieProduct).scrollTo().shouldBe(visible).click();
43 | return screen(ProductDetailsScreen.class);
44 | }
45 |
46 | public void checkWhetherFooterIsPresent() {
47 | $(getLocator(FOOTER_ANDROID, FOOTER_IOS))
48 | .execute(new ScrollToElement())
49 | .shouldHave(CustomCondition.attributeMatching("text", "label", "Sauce Labs"));
50 | }
51 | }
--------------------------------------------------------------------------------
/src/test/java/com/tests/AddToCartTest.java:
--------------------------------------------------------------------------------
1 | package com.tests;
2 |
3 | import com.codeborne.selenide.appium.ScreenObject;
4 | import com.screens.ProductsListingScreen;
5 | import com.tests.base.TestSetup;
6 | import org.junit.jupiter.api.Test;
7 |
8 | class AddToCartTest extends TestSetup {
9 |
10 | private ProductsListingScreen productsListingScreen;
11 |
12 | @Test
13 | void testSauceLabsApp() {
14 | productsListingScreen = ScreenObject.screen(ProductsListingScreen.class);
15 |
16 | productsListingScreen
17 | .selectBikeLightProduct()
18 | .checkWhetherAddToCartButtonPresent();
19 | }
20 | }
--------------------------------------------------------------------------------
/src/test/java/com/tests/CustomCommandTest.java:
--------------------------------------------------------------------------------
1 | package com.tests;
2 |
3 | import com.screens.ProductsListingScreen;
4 | import com.tests.base.TestSetup;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import static com.codeborne.selenide.appium.ScreenObject.screen;
8 |
9 | class CustomCommandTest extends TestSetup {
10 |
11 | private ProductsListingScreen productsListingScreen;
12 |
13 | @Test
14 | void testScrollToElement() {
15 | productsListingScreen = screen(ProductsListingScreen.class);
16 |
17 | productsListingScreen
18 | .selectOneSieProduct()
19 | .checkWhetherAddToCartButtonPresent();
20 | }
21 | }
--------------------------------------------------------------------------------
/src/test/java/com/tests/CustomConditionTest.java:
--------------------------------------------------------------------------------
1 | package com.tests;
2 |
3 | import com.screens.ProductsListingScreen;
4 | import com.tests.base.TestSetup;
5 | import org.junit.jupiter.api.Tag;
6 | import org.junit.jupiter.api.Test;
7 |
8 | import static com.codeborne.selenide.appium.ScreenObject.screen;
9 |
10 | class CustomConditionTest extends TestSetup {
11 |
12 | private ProductsListingScreen productsListingScreen;
13 |
14 | @Tag("scroll-test")
15 | @Test
16 | void testCustomCondition() {
17 | productsListingScreen = screen(ProductsListingScreen.class);
18 |
19 | productsListingScreen
20 | .checkWhetherFooterIsPresent();
21 | }
22 | }
--------------------------------------------------------------------------------
/src/test/java/com/tests/DeepLinkTest.java:
--------------------------------------------------------------------------------
1 | package com.tests;
2 |
3 | import com.codeborne.selenide.appium.AppiumDriverRunner;
4 | import com.codeborne.selenide.appium.SelenideAppium;
5 | import com.screens.ProductDetailsScreen;
6 | import com.tests.base.TestSetup;
7 | import org.junit.jupiter.api.Test;
8 |
9 | import static com.codeborne.selenide.appium.ScreenObject.screen;
10 |
11 | class DeepLinkTest extends TestSetup {
12 |
13 | @Test
14 | void testDeepLinks() {
15 | openDeepLink();
16 |
17 | screen(ProductDetailsScreen.class)
18 | .checkWhetherAddToCartButtonPresent();
19 | }
20 |
21 | private static void openDeepLink() {
22 | if (AppiumDriverRunner.isAndroidDriver()) {
23 | SelenideAppium.openAndroidDeepLink("mydemoapprn://product-details/4",
24 | "com.saucelabs.mydemoapp.rn");
25 | } else {
26 | SelenideAppium.openIOSDeepLink("mydemoapprn://product-details/4");
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/src/test/java/com/tests/base/TestSetup.java:
--------------------------------------------------------------------------------
1 | package com.tests.base;
2 |
3 | import com.codeborne.selenide.Selenide;
4 | import com.codeborne.selenide.appium.SelenideAppium;
5 | import com.codeborne.selenide.junit5.TextReportExtension;
6 | import org.junit.jupiter.api.AfterEach;
7 | import org.junit.jupiter.api.BeforeEach;
8 | import org.junit.jupiter.api.extension.ExtendWith;
9 |
10 | @ExtendWith(TextReportExtension.class)
11 | public class TestSetup {
12 |
13 | @BeforeEach
14 | protected void openApp() {
15 | SelenideAppium.launchApp();
16 | }
17 |
18 | @AfterEach
19 | protected void closeApp() {
20 | Selenide.closeWebDriver();
21 | }
22 | }
--------------------------------------------------------------------------------
/src/test/resources/selenide.properties:
--------------------------------------------------------------------------------
1 | selenide.browser=com.provider.SauceLabAndroidDriverProvider
2 | selenide.timeout=10000
--------------------------------------------------------------------------------