├── .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 --------------------------------------------------------------------------------