Как вы объявляете веб-элементы пэдж-обжекта? (парочка примеров)

BasePage и BaseTest классы наследуются от него, в них же, собственно, и переопределяется веб-драйвер, используемый в классе Wrappers.

1 лайк

Вот пример небольшой пример:

Базовый класс:

class WebItem(WebElement):
    __metaclass__ = ABCMeta

    def __init__(self, by, value):
        self.item_by = by
        self.item_value = value
        self.name = Log.get_caller_field_name()

    def exists(self, time=0):
        try:
            self.get_element(time=time)
            return True
        except (NoSuchElementException, TimeoutException) as e:
            return False

    def get_element(self, condition='presence_of_element_located',time=JSONReader.get_data_from_config_json("global_configuration", "default_element_load_time")):
        return WebDriverWait(Browser().driver, time).until(
            getattr(EC, condition)((self.item_by, self.item_value))
        )

    def click(self):
        Browser().driver.execute_script(WebItem.js_http_requests_listener)
        self.get_element('element_to_be_clickable').click()
        Browser().wait_untill_http_requests_are_finished()

Как видите в методе click указано условие состояния элемента element_to_be_clickable - что логично.
Для других типов условие может быть более спицифическое, что легко позволяет сконфигурировать ее в нужном типе через метод get_element.
Тут же и делаеться динамическая инициализация. Т.е. поиск элемента происходит в момент действия, а не при создания страницы.
Класс кнопки:

class Button(WebItem):
    def click(self):
        Log.info("Clicking on the '%s' button" % self.name)
        super(Button, self).click()

Но кнопка это простой пример, там кроме клика нет ничего.
Вот пример спицифического метода относящегося только к DropDownList:

class DropDownList(WebItem):

    def select_item_by_value(self, value):
        Select(self.get_element()).select_by_value(value)

    def select_item_by_text(self, text):
        логика

Как видите метод select_item_by_value и select_item_by_text доступены только в DropDownList, и не доступен для кнопок и т.д. - что логично.

Далее у меня проделана очень большая работа с веб листами, где реализована работа с Грид Таблицами.

Таким образом, тесты у меня выглядят так:

 def test_create_new_customer(self):
        page = NewCustomerPage()
        page.invoke()
        page.first_name.send_keys(customer_first_name)
        page.last_name.send_keys(customer_last_name)
        page.email_1.send_keys(customer_email)
        page.save_changes.click()
        assert [condition], 'Error message'

Кусочек лога будет:

INFO:root:Sending ('QA_5638',) keys to the 'first_name' text field
INFO:root:Sending ('TESTER_5638',) keys to the 'last_name' text field
INFO:root:Sending ('oleh+5638@***.com',) keys to the 'email_1' text field
INFO:root:Clicking on the 'save_changes' button
3 лайка

@Oleg_Kuzovkov,

с синтаксисом пайтоном не очень знаком, но по коду смотрю, что вы, вроде как, сделали функцию click():

def click(self):
        Log.info("Clicking on the '%s' button" % self.name)
        super(Button, self).click()

а потом в тестах вызываете ее:

page.save_changes.click()

хотя у селениума есть метод click();, получается при чтении теста, не кажется ли, что тот клик - это клик селениума, а не ваша какая-то обертка? Не вносит ли это путаницу, или что-то не так понял по коду? self. - это this. в Java?

Разве он и так не произойдет во время действия поскольку driver.findelEment будет в самом экшене, а не как многие любят объявлять его выше в перменную?

1 лайк

зачем вообще делать подобные оберточные методы?

@Viktor_Borisov,

чтобы переиспользовать код и наполнить клик чем-то большим, чем просто клик. Скажем я хочу кликнуть, если элемент присутствует на странице. И чтобы не писать это в каждом тесте - wait.until затем click, можно сделать условие, что если элемента нет или он не кликабелен - тогда ждать пока будет доступен и кликать. А если кликабелен сразу и виден, значит ничего не ждать и кликать…

Чтобы тест читался более понятнее и можно было сделать свои обертки на вейты, клики, скролы, выпадалки и т.д. Доупстим вот так лучше в тестах писать:

    @Step("Tap with hold by element {0}")
    protected T longTap(WebElement element, int longPressTime) {
        TouchAction action = new TouchAction(driver);
        action.longPress(element, longPressTime).release().perform();
        return (T) this;
    }

чем в каждом тесте, где надо тапнуть с задержкой, писать так:

    TouchAction action = new TouchAction(driver);
    action.longPress(element, longPressTime).release().perform();

Метод получится таким:

public SomePage sendVideoMessage(int longPressTime) {
    tap(this.buttonCamera);
    longTap(this.buttonCapturePhotoVideo, longPressTime);
    tap(this.buttonSendCapture);
    return this;
}

Нежели вот таким:

public SomePage sendVideoMessage(int longPressTime) {
    this.buttonCamera.click();
    TouchAction action = new TouchAction(driver);
    action.longPress(this.buttonCapturePhotoVideo, longPressTime).release().perform();
    this.buttonSendCapture.click();
    return this;
}

Подобные обертки помогают переиспользовать элементы, и тесты делать более понятными и красивыми + помогают драйвер и вейты использовать только в абстрактном классе и не использовать в педжйах и тестах… Но эт не все плюсы… :slight_smile: Если хочешь пофиксить какой-то экшен, то фиксишь в одном месте и применяется везде. А не рефакторишь потом все пейджи, поскольку придумал как лучше взаимодействовать с тем или иным элемнтом…

2 лайка

а в каком классе этот метод longTap? в базовом пейдже?

@Viktor_Borisov,

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

Сейчас я мудрю несколько иной подход, хочу сделать так, чтобы все Ui-элементы - лежали в классе UiElements, все вейты лежали в WaitElements и так далее… Но в Java нет множественного наследования, поэтому столкнулся с трудностями проектирования. Хочу чтобы подобные классы и методы в них небыли статическими…

а что это даст? для чего выносить это из бейз пейджа?

Наглядность для людей, которые будут потом читать код. Будут точно знать, что в этих классах лежит то что им надо… В базовом классе тогда можно будет размещать не скопище методов по экшенам и что-то еще… Можно будет там разместить методы, которые обобщены на множестве пейджах… Как-то так… Я пока эксперементирую, но немного не выходит… Возможно даже оставлю как есть.

1 лайк

В Java 8 есть. Правда не совсем полноценное. Состояние все же множественно не наследуется.

1 лайк

сделали функцию click()

Клик не функция, а метод.

Не вносит ли это путаницу, или что-то не так понял по коду

Путанину для кого? Это и есть клик селениума, только дополненый тем чем я хочу. Лог например когда читаешь, то видишь для какого типа элемента делал действие. Так же можно их стека считать что за элемент, с какой страницы и т.д. Да и поиск делаешь перед экшеном.

Разве он и так не произойдет во время действия поскольку driver.findelEment будет в самом экшене, а не как многие любят объявлять его выше в перменную?

Я об этом и писал как бе :slight_smile:

зачем вообще делать подобные оберточные методы?

Тут

чтобы переиспользовать код и наполнить клик чем-то большим, чем просто клик.

Ну я имел ввиду, что click() - метод селениума, и если его дополняешь def click(self) , то немного название делаешь другим… Чтобы те кто читали тест понимали, что данный клик дополнен и это обертка, а не обычный метод селениума, к примеру:

def clickOn(self):

//или

def clickByElement(self)):

Хотя, в вашем контексте может и норм, так как ставиться в конце, у меня получается, что я в него передаю значение, по которому надо кликнуть… Хотя, когда впервые взглянул на код, то мне показалось что вы клик селениума всунули в тест, а не обертку на метод селениума :slight_smile:

На счет лога, разве не проще использовать листенер? Или такой подход, как у вас - лучше, писать в обертке?

public class EventListener implements WebDriverEventListener {

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

    @Override
    public void afterChangeValueOf(WebElement element, WebDriver driver) {
        logger.info("changed value of element with " + getLocatorFromElement(element));
    }

    @Override
    public void afterClickOn(WebElement element, WebDriver driver) {
        logger.info("clicked element with " + getLocatorFromElement(element));
    }

    @Override
    public void afterFindBy(By by, WebElement arg1, WebDriver arg2) {
        logger.info("found element " + by);
    }

    @Override
    public void afterNavigateBack(WebDriver driver) {
        logger.info("after back");
    }

    @Override
    public void afterNavigateForward(WebDriver driver) {
        logger.info("after forward");
    }

    @Override
    @Step("Navigate to {0}")
    public void afterNavigateTo(String url, WebDriver driver) {
        logger.info("navigated to " + url);
    }

    @Override
    public void afterScript(String script, WebDriver driver) {
        logger.info("ran script " + script);
    }

    @Override
    public void beforeChangeValueOf(WebElement element, WebDriver driver) {
        logger.info("change value of element with " + getLocatorFromElement(element));
    }

    @Override
    public void beforeClickOn(WebElement element, WebDriver driver) {
        logger.info("click element with " + getLocatorFromElement(element));
    }

    @Override
    public void beforeFindBy(By by, WebElement element, WebDriver arg2) {
        logger.info("find element " + by);
    }

    @Override
    public void beforeNavigateBack(WebDriver driver) {
        logger.info("before back");
    }

    @Override
    public void beforeNavigateForward(WebDriver driver) {
        logger.info("before forward");
    }

    @Override
    public void beforeNavigateTo(String url, WebDriver driver) {
        logger.info("navigate to " + url);
    }

    @Override
    public void beforeScript(String script, WebDriver driver) {
        logger.info("running script " + script);
    }

    @Override
    public void onException(Throwable thrw, WebDriver driver) {
        logger.info(thrw.getMessage());
    }

    private String getLocatorFromElement(WebElement element) {
        String str = element.toString();
        Pattern p = Pattern.compile("->\\s(.*)(?=\\])");
        Matcher m = p.matcher(str);
        return m.find() && m.groupCount() > 0 ? m.group(1) : str;
    }
}

Раньше делал, как и вы, но позже посоветовали мне - использовать листенер…

1 лайк

тоже пришла мысль на счет листенеров

В питоне у меня таких штуковин нет. Ну и разницы я особо не вижу, кроме как делать еще целый класс для того что может быть завернуто в одну обертку. А так получаеться делаем обертку, а потом еще одну для лога, ну и еще для вейтов, и еще и еще… :slight_smile:

Мне нравиться так, пока что никаких недостатков или преимущест не вижу. Люблю держать и контролировать все в как можно меньшем количестве мест.

1 лайк

Ребята, подскажите плиз, а вы в своих фреймворках делает кастомные аннотации? Что-то типо такого:

@HTML(searchBy = ID, value = "Email")
private TextInput inputEmail;

@HTML(searchBy = ID, value = "Passwd")
private TextInput inputPassword;

@HTML(searchBy = ID, value = "next")
private Button buttonNext;

@HTML(searchBy = ID, value = "signIn")
private Button buttonSignIn;

Или это уже лишнее? Выглядит здорово конечно, читаемость повышается, но стоит ли оно того?

Данный пример призван улучшить восприятие элементов. Т.к. все типизированные элементы оборачивают класс By (название которого очень слабо отражает его предназначение), а инициализация производится за кадром, то нам необходимо как-то обозначать критерии поиска. Что собственно и достигается при помощи кастомной аннотации, аля аналог FindBy + WebElement, но только для класса By. Так что удовольствие это скорее сугубо эстетическое.

1 лайк

Да, спасибо, в принципе всю суть полезности я понимаю… Так если подумать, оно клево очень. А если в приложении используются: AndroidDriver и для WebDriver, Sikulli, то можно в своем приложении наделать кастомных аннотаций, в итоге получится наглядней элементы…

Вот только вопросик больше волнует, делают ли так, тратят ли на это время? Подобная эстетика, как правило делается в свободное время перфекционистом автоматизатором, или на это выделяют время, и это не так уж и не важно? :slight_smile: @ArtOfLife, в ваших проектах вы реализовуете такие вещи, для удобства дальнейшей разработки?

Интересно понять золотую середину такой эстетической реализации с точки зрения реальных проектов… Мне понравилось такое решение, и я бы с удовольствием переписал часть своего фреймворка… Но ведь потом эти штуки поддерживать надо, где-то могут быть подводные камни :slight_smile:

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

Делаю ли я так у себя? Да, делаю. В какое время? Человеку с опытом понадобится всего несколько дней на реализацию подобной фичи. Ничего сложного там нет. К тому же, как правило, все это делается в самом начале, а не когда проект оброс со всех сторон. Хотя, при хорошо продуманной архитектуре, миграция должна пройти весьма безболезненно. По сути улучшается лишь детализация описания элементов. Такой подход может вполне нормально работать с уже существующим, когда общие действия выносятся в условную BasePage. У типизированных элементов то будет свой собственный инициализтор. А ввиду прямого наследования By класса, старый API ну никак не может сломаться, если вы изначально локаторы описывали с помощью By, а не FindBy + WebElement.

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

Касательно саппорта… Если все изначально правильно написать, то вы в эти классы вообще никогда не будете влезать. В этом то и суть фреймворка - написали 1 раз, и забыли. Разве что какие-то новые фичи по надобности прикручивать придется. В остальном, этот механизм должен быть вылизан и отлажен до последнего байта. :slight_smile: Посколько именно от его стабильности зависит стабильность ваших тестов. Я не верю в сказки о том, что основной причиной flaky тестов является приложение. Особенно когда заглядываешь в код и видишь тонны Thread.sleep, конечно же на всякий случай. :slight_smile:

1 лайк

Как по мне, то Thread.sleep - не должно быть никогда. Только в очень редких случаях…

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

Ну на самом деле универсальный вейтер можно обернуть и на уровне условной BasePage, просто переопределив логику findElement. В случае с типизированными элементами он всего лишь помещается в другое место - некую обертку над элементом. Хотя, если вообще идеализировать, можно банально отдекорировать драйвер таким образом, что вместо стандартного findElement будет использоваться его усовершенствованная версия аля explicit waits. :slight_smile: Когда дойдут руки, залью примеры на github.

1 лайк