Удаленка для jenkins+selenide+selenoid+allure+docker спецов на 2-3 часа в день. 100% remote! Присоединиться к проекту

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

page-object
selenium
webdriver
Теги: #<Tag:0x00007fedbc62f3c0> #<Tag:0x00007fedbc62f208> #<Tag:0x00007fedbc62f000>

(Sergey QA) #21

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


(Oleg Kuzovkov) #22

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

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

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

(Eugene Moskalenko) #23

@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 будет в самом экшене, а не как многие любят объявлять его выше в перменную?


#24

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


(Eugene Moskalenko) #25

@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: Если хочешь пофиксить какой-то экшен, то фиксишь в одном месте и применяется везде. А не рефакторишь потом все пейджи, поскольку придумал как лучше взаимодействовать с тем или иным элемнтом…


#26

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


(Eugene Moskalenko) #27

@Viktor_Borisov,

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

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


#28

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


(Eugene Moskalenko) #29

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


(Sergey Korol) #30

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


(Oleg Kuzovkov) #31

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

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

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

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

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

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

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

Тут

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


(Eugene Moskalenko) #32

Ну я имел ввиду, что 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;
    }
}

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


#33

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


(Oleg Kuzovkov) #34

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

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


(Eugene Moskalenko) #35

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

@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;

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


(Sergey Korol) #36

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


(Eugene Moskalenko) #37

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

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

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


(Sergey Korol) #38

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

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

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

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


(Eugene Moskalenko) #39

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

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


(Sergey Korol) #40

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