Appium (Android) + Selenide, инициализация драйвера

Подскажите пожалуйста, сталкивался кто-то с подобной связкой? В Appium немного сложно работать с элементами и ожиданиями, решил попробовать, может с Selenide будет проще. Реально ли это вообще? К примеру, хочу открыть приложение на главном скрине по имени (пример: SomeApps, допустим приложение надо открыть на скрине не через активити, а просто тапнуть по иконке с таким именем), а оно мне фаерфокс открывает :slight_smile:

Скорее всего надо какуе-то обертку над драйвером сделать… Немного моего кода:

DriverSetup

package autoTests.apps.components;

import autoTests.apps.properties.PropertyLoader;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.remote.MobileCapabilityType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Properties;
import java.util.concurrent.TimeUnit;

public class DriverSetup {

    private static final Logger logger = LogManager.getLogger(DriverSetup.class);

    private static DriverSetup _instance = null;

    private AndroidDriver driver;
    private WebDriverWait wait;
    private URL           serverUrl;

    public int EXPLICIT_WAIT_TIME;
    public int DEFAULT_WAIT_TIME;
    public int IMPLICIT_WAIT_TIME;

    public String WAIT_ACTIVITY;
    public String APPIUM_PORT;

    public String NEW_COMMAND_TIMEOUT;
    public String DEVICE_READY_TIMEOUT;

    public String DEVICE_NAME;
    public String BROWSER_NAME;
    public String PLATFORM_VERSION;
    public String PLATFORM_NAME;
    public String APP_PKG;
    public String APP_ACTIVITY;
    public String AUTOMATION_INSTRUMENTATION;
    public String APPLICATION_NAME;
    public String PERFORMANCE_LOGGING;

    private DesiredCapabilities capabilities = new DesiredCapabilities();
    private Properties prop = new Properties();

    public DriverSetup() {

        this.loadConfigProp();
        this.setCapabilities();

        try {
            serverUrl = new URL("http://127.0.0.1:" + APPIUM_PORT + "/wd/hub");
        } catch (MalformedURLException e) {
            e.printStackTrace();
            logger.error(e);
        }
        driver = new AndroidDriver(serverUrl, capabilities);
        wait = new WebDriverWait(driver, EXPLICIT_WAIT_TIME);
        driver.manage().timeouts().implicitlyWait(DEFAULT_WAIT_TIME, TimeUnit.SECONDS);
    }

    public static DriverSetup getInstance() {
        if (_instance == null) {
            _instance = new DriverSetup();
        }
        return _instance;
    }

    public AndroidDriver getDriver() {
        return driver;
    }

    public WebDriverWait getWait() {
        return wait;
    }

    public void loadConfigProp() {
        EXPLICIT_WAIT_TIME = Integer.parseInt(PropertyLoader.loadProperty("explicitWait"));
        DEFAULT_WAIT_TIME = Integer.parseInt(PropertyLoader.loadProperty("defaultWait"));
        IMPLICIT_WAIT_TIME = Integer.parseInt(PropertyLoader.loadProperty("implicitWait"));
        WAIT_ACTIVITY = PropertyLoader.loadProperty("waitActivity");
        APPIUM_PORT = PropertyLoader.loadProperty("appiumServerPort");
        NEW_COMMAND_TIMEOUT = PropertyLoader.loadProperty("newCommandTimeout");
        DEVICE_READY_TIMEOUT = PropertyLoader.loadProperty("deviceReadyTimeout");

        DEVICE_NAME = PropertyLoader.loadProperty("deviceName");
        BROWSER_NAME = PropertyLoader.loadProperty("browserName");
        PLATFORM_VERSION = PropertyLoader.loadProperty("platformVersion");
        PLATFORM_NAME = PropertyLoader.loadProperty("platformName");
        APP_PKG = PropertyLoader.loadProperty("applicationPackage");
        APP_ACTIVITY = PropertyLoader.loadProperty("applicationActivity");
        AUTOMATION_INSTRUMENTATION = PropertyLoader.loadProperty("automationInstumentation");
        APPLICATION_NAME = PropertyLoader.loadProperty("applicationPath");
        //PERFORMANCE_LOGGING = PropertyLoader.loadProperty("enablePerformanceLogging");
    }

    public void setCapabilities() {
        capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, DEVICE_NAME);
        capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, BROWSER_NAME);
        capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, PLATFORM_VERSION);
        capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, PLATFORM_NAME);
        capabilities.setCapability(MobileCapabilityType.APP_PACKAGE, APP_PKG);

        if (prop.containsKey("waitActivity")) {
            capabilities.setCapability(MobileCapabilityType.APP_WAIT_ACTIVITY, WAIT_ACTIVITY);
        } else {
            capabilities.setCapability(MobileCapabilityType.APP_ACTIVITY, APP_ACTIVITY);
        }
        //if (!APPLICATION_NAME.isEmpty()) {
        //    capabilities.setCapability(MobileCapabilityType.AUTOMATION_NAME, AUTOMATION_INSTRUMENTATION);
        //    capabilities.setCapability(MobileCapabilityType.APP, new File(
        //            ClassLoader.getSystemResource(APPLICATION_NAME).getFile()).getAbsolutePath());
        //}
        capabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, NEW_COMMAND_TIMEOUT);
        capabilities.setCapability(MobileCapabilityType.DEVICE_READY_TIMEOUT, DEVICE_READY_TIMEOUT);
        //capabilities.setCapability("unicodeKeyboard", true);
    }

часть Application Manager

public class ApplicationManager {

    public AndroidDriver driver;
    public WebDriverWait wait;

    public ApplicationManager() {
        driver = DriverSetup.getInstance().getDriver();
        wait = DriverSetup.getInstance().getWait();
    }

Получилось, нашел решение, работает :slight_smile:

public class ApplicationManager {

    public AndroidDriver driver;
    //public WebDriverWait wait;

    public ApplicationManager() {
        driver = DriverSetup.getInstance().getDriver();
        WebDriverRunner.setWebDriver(driver);
        //wait = DriverSetup.getInstance().getWait();
    } 

Добавил: WebDriverRunner.setWebDriver(driver); в него передал свой драйвер и заработало…

Подскажите пожалуйста, это правильно так делать?

2 лайка

Не сложнее чем с любым другим драйвером, Appium то тут ни при чем :slight_smile:

Для передачи вашего аппиум драйвера в селенид используйте

WebDriverRunner.setWebDriver(driver);

и будет вам щасьте )

3 лайка

Да, именно так и сделал выше, нашел на гите где-то примерчик и там было что-то подобное. Если честно после внедрения Selenide все скрины начали шустрее работать.

Есть у кого-то примеры небольшие, как юзать appium и selenide?

2 лайка

Я бы тоже посмотрел. Давно хочу добавить в Selenide поддержку Appium, только не знаю, что именно добавлять. :slight_smile:

2 лайка

На одном из проектов, с самого начала решил попробовать использовать appium и selenide вместе. Долго и упорно совмещал, но по итогу selenide выпилил. Как бы ни был он хорош, их ветки разошлись в разные стороны. Мой совет - не мучайтесь! У последних версий appium java-client появилось много фишек, в том числе связанных с чисто мобильными действиями (swipe, tap, pinch, zoom). Ничего не мешает вам создать и хранить драйвер в отдельном контейнере, как это делает selenide. Посмотрите как ребята это реализовали. Я подглядел )) удобно. Но совместить пытаться не стоит. Это конечно мое мнение. Я так и не нашел тех, кто совместил. Да и попыток было не много.

1 лайк

Спасибо за информацию.
Я всё-таки хочу совместить когда-нибудь.
Хотя бы потому, что появляется всё больше лаптопов с тачскрином, а стало быть, классические браузеры (Chrome, IE, FF) тоже начнут поддерживать все эти события (swipe, tap, pinch, zoom). А может, уже поддерживают.

Алексей Баранцев рассказывал в девклубе, что разработчики Selenium тоже поглядывают в эту сторону и думают о том, как расширить стандарт Selenium Webdriver так, чтобы он поддерживал все новые девайсы, какими бы сумашедшими они ни были.

Так что будем двигаться в сторону совмещения.

3 лайка

@asolntsev, было бы здорово, если бы совместили что-то подобное :slight_smile: В будущем это многим понадобится…

@sergvkom, до последнего бился чтобы оставить, но на многие локаторы скорее всего что-то было с js, как-то оно падало на локаторах, которые реально присутствовали на скринах. Поэтому тоже выпилил… Но сама идея была конечно хорошая, чтобы быстро на мобилке тесты писать. Так приходится где-то шото с вейтами шаманить или ожидать :slight_smile:

Хм… Наверное не подскажу в силу моего скудного опыта с Appium и Selenide. Но он даже с SelenideElement работает как-то странно, то есть элемент реально присутствует на скрине но в логах вылетают ошибки, что якобы его нет, при этом если менять на AndroidFintBy, то все без проблем находить, тапает, заполняет и так далее. Также очень странно, но я взял SelenideElement, к примеру инпут поле, в которое хотел сделать setValue.

selenideElementInputField.setValue("johny");

выбрасывало эксепшены, а вот такой вариант, вводил в поле текст:

selenideElementInputField.sendKeys("johny");

Сейчас уже эксепшены не скину, так как выпилил, но если когда будет еще возможность попробовать, сделаю файлик и туда все случаи с эксепшенами накидаю, скину в личку, может пригодится…

1 лайк

Это нереально. По умолчанию setValue() как раз и вызывает sendKeys(). Правда, перед ним ещё clear(). Может, на мобильнике clear() не работает?

P.S. А аннотации FindBy вообще зло, незачем их использовать.

1 лайк

@asolntsev, Вот это вечная тема :slight_smile:

Кто-то пишет что FindBy - прикольная штука, а кто-то категорично пишет, что зло :slight_smile:

Что посоветуете вместо @FindBy? В таком виде писать?

WebElement foobar = driver.findElement(By.id(""));
SelenideElement foobar = $("");

На мобилке clear() - работает, я проверял, тоже думал именно на это…

Про “костыль” забыли:

1 лайк

Я советую не объвлять веб-элементы как поля пэдж-обжекта, а объявлять методы в пэдж-обжекты, которые умеют делать логические действия с этой страницей. Внутри методов можно находить элементы через $(selector).

3 лайка

Блин. Вы думаете, именно events.fireChangeEvent(element); на андроиде грохается? А почему? Разве там нет событий?

Но вообще этот events.fireChangeEvent(element); был когда-то сделан ради IE, потому что в нём часто не срабатывал change event при изменении значения через sendKeys(). Может, в наше время он больше и не нужен…

А можно вопрос, какой от этого плюс, если не считать, что в файле меньше текста? Ведь в пейдже так наглядней получается, и более понятней… Да и каждый элемент получается отдельной переменной со смыслом…

Наглядней? Да нет, не наглядней. Все просто к этому привыкли, поэтому кажется, что наглядней.

Плюс фундаментальный.
Вся суть пэдж обжекта в том, что он должен инкапсулировать (т.е. скрывать) детали взаимодействия со страницей, с её элементами.

  1. Золотое правило Мартина Фаулера: Если селектор поменяется - должен поменяться только пэдж обжект.
    Этот принцип все и привыкли решать с помощью полей и аннотаций.
    Но есть ещё второе правило:

  2. Моё золотое правило: если поменяется алгоритм работы с элементом - должен поменяться только пэдж обжект. Например, чтобы ввести день рождения, раньше нужно было просто ввести значение в input, а теперь нужно сначала кликнуть “edit”, потом в открывшемся календаре выбрать нужный год, месяц и день и нажать “submit”. Я считаю, что и в этом случае должен поменяться только пэдж обжект. Точнее, даже только один метод пэдж обжекта - “enterBirthday()”.

Если взглянуть на вещи с этой позиции, становится понятно, что поля лишние - это детали реализации, они могут меняться.

  1. Тут надо смотреть по конкретной реализации, иногда один селектор, используется больше чем в одном мтоде, да и в рамках метода может дергаться пару раз, тогда проще его обьявить как поле и менять один раз в одном месте. Если же селектор юзается исключительно один раз, эпизодически, то нет смысла его заводить в @FindBy.

  2. При таком значительном изменении функциональности, как в вашем примере, что в том, что в другом случае будет меняться пейдж обджект, единственное, что с @FindBy прейдеться немного попрыгать между полями и методами, но это мелочи имхо.

В любом случае, я считаю, что легко можно миксовать оба подхода в зависимости от конкретной реализации.

1 лайк

Да, я примерно это и имел в виду.
Главное, чтобы у пэдж обжекта были публичные методы, позволяющие оперировать со страницей как с объектом. А как эти методы внутри реализованы - это ваше дело. Если вам для этого удобно использовать поля - ради бога. Главное, чтобы эти поля не были публичными, иначе нарушается инкапсуляция.

3 лайка

Как тогда в этом случае, я смогу пользоваться проверками именно в тесте, если поля будут private ? Например, в тесте я делаю проверку: placesPage.placesList.shouldHaveSize(1); где placesList это поле которое я дергаю из пейджи. Мне тогда придется писать проверки в самой пейдже (делать методы для проверок).

Да, придётся, и в этом и состоит суть пэдж обжектов: скрывать знание о том, КАК именно проводить проверки (т.е. взаимедействовать с веб-элементами). А знание о ом, КОГДА дёргать тут или иную проверку (т.е. вызвать соответствующий метод ПО), должно быть в тесте.

1 лайк