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

page-object
webdriver
selenium
Теги: #<Tag:0x00007fedc4161818> #<Tag:0x00007fedc41616b0> #<Tag:0x00007fedc4161548>

(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.


(Eugene Moskalenko) #41

Спасибо огромное, было бы здорово взглянуть :slight_smile: И вот это еще, если будет время, пожалуйста - "С рефлексией все будет гораздо компактней. Может на днях залью новую версию." - http://automated-testing.info/t/kakoj-dolzhna-byt-idealnaya-struktura-proekta-avtotestov/9903/33

:slight_smile:


(Roma Marinsky) #42

Ну у меня из-за специфики проекта, как плюс это считаю, повторяются многие элементы и локаторы на страницах, но может быть такое что в разных блоках страницы то уже структурирую по компонентам и степам к ним и объединяю в страницы это всё чудо.
Т.к. есть кастомные решения для выпадаек и для кнопок, то есть объекты со специфическими методами для их манипуляций.

public class ProductButton extends AbstractElement{
    @FindBy(css = "a.ga-add-to-cart")
    public SelenideElement buttonAddToCart;
    @FindBy(css = "a.button-preorder")
    public SelenideElement buttonPreOrder;
    @FindBy(css = "span.in-cart > a")
    public SelenideElement buttonInCart;
    @FindBy(name = "btn-minus")
    public SelenideElement buttonMinus;
    @FindBy(name = "quantity")
    public SelenideElement fieldQuantity;
    @FindBy(name = "btn-plus")
    public SelenideElement buttonPlus;
    @FindBy(css = "span.icon")
    public SelenideElement labelStockWithIcon;}

Это всё может находится на разных страницах и может в разных блоках, поэтому есть такая штука в объекте какого-то блока
@FindBy(css = "div.product-button-wrapper")
public ProductButton productButton;

Далее, эту кнопку мне нужно найти для определённого товара на странице по id, к примеру на странице категорий. А страница категорий состоит из "строк/блоков" каждой единицы товара и чтобы взять определённого товара блок и только в нём работать есть акая штука:

public class ProductList extends AbstractElement {
    public ProductsLineBlock findProductByIDInTags(String id, String tagName) {
        return initContainer($(String.format("%s[data-id='%s']", tagName, id)),
                ProductsLineBlock.class);
    }
}

Именно так потому что вид страницы может быть в виде таблицы, строк с расширенным описанием или строк без описание. И таким способом я дальше могу работать с локаторами которые содержатся в ProductsLineBlock. В котором есть особенная кнопка "Купить"

Вот что содержится в AbstractElement
public abstract class AbstractElement extends ElementsContainer{

protected <T extends AbstractElement> T initContainer(SelenideElement element, Class<? extends AbstractElement> type) {
    T result;
    try {
        result = (T) type.newInstance();
    } catch (Exception e) {
        throw new RuntimeException("Failed to create elements container for element", e);
    }
    PageFactory.initElements(new SelenideFieldDecorator(element), result);
    result.setSelf(element);
    return result;
}

Вот к примеру кастомная выпадайка и метод для неё
public class CustomDropdown extends AbstractElement {

public void selectItem(String nameOfItem){
    getSelf().click();
    $(byXpath(String.format("//span[.='%s']", nameOfItem))).scrollTo().click();
}}

Таких кастомных 3 штуки есть, + есть аналог для номера телефона и выбора кода страны, составной элемент с инпутом и выпадайкой

Также есть кастомные элементы это поиск, у него есть выпадайка с перечнем категорий в которых искать, + составной из двух блоков быстрый поиск.

Это всё классы компоненты страниц и из них я уже леплю степы для них, которые объединяю в пакеты страниц.

Пример теста: public class TestTest extends BaseTest {

@Test
public void olo(){
    Configuration.baseUrl = TestData.urlMainPageGSMCom;
    addProductViaAjaxUrl(simpleProduct);
    open(urlCheckout);
    CheckoutContactInformationSteps checkoutContactInformationSteps = getStepsFrom(CheckoutContactInformationSteps.class);
    checkoutContactInformationSteps.testMethod();
}

}

Где getSteps это:

protected static <T> T getStepsFrom(Class<? extends AbstractSteps> pageObjectClass) {
    return (T) page(pageObjectClass);
}

ну и testMethod

public CheckoutContactInformationSteps testMethod(){
    this.checkoutContactInformation.customCountry.selectItem(TURKEY);
    this.checkoutContactInformation.customPhone.setPhoneByCountry(TURKEY, TestData.CONTACT_NUMBER);
    return this;
}

(Roma Marinsky) #43

аа ну для этого делаю сайта автоматизацию: gsmserver.com, можете увидеть какая кнопка "Купить" и "Поиск" о которых говорил


(Roma Marinsky) #44

И метод getStepsFrom в тестовых класса вызываю в основном в методах с анотациями @BeforeClass или @Before
А конфигурации уже в таске gradle


(Eugene Moskalenko) #45

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


(Sergey Korol) #46

Речь шла о дефолтных и статических методах в интерфейсах. В девятке еще и приватные методы добавят.


(Eugene Moskalenko) #47

ага, я эт и имел ввиду. Сейчас стабильно в своих автотестах юзаю эти вещи. :slight_smile: Очень сильно улучшает фреймворк..


(Sergey Korol) #48

В восьмой джаве очень много фишек, значительно улучшающих дизайн архитектуры.


(Eugene Moskalenko) #49

Это точно :slight_smile:


(ex3me0) #50

Интересно: а как у Вас описаны классы страниц?
В частности интерес представляет расположение веб-айтемов в классе (атрибуты класса или атрибуты инстанса)

Столкнулся на днях с некоторыми проблемами при определении веб-элементов как атрибутов класса, интересно как подобные проблемы решаются другими людьми.

И реализовывали ли веб-айтемы вида "контейнер"? Т.е. группа элементов связанная логически (input, div с эррор-меседжем, etc), для которой длеается отдельный реюзабельный класс, дабы каждый раз не описывать по 3-5 веб-айтемов на странице.


(Oleg Kuzovkov) #51

Примерно так:

class TagLoginPage(WebPage):
    web_page_id = (By.ID, 'login')

    def __init__(self):
        super().__init__()
        # Page items
        self.email = Input(By.XPATH, '//input[@type="email"]', self)
        self.password = Input(By.XPATH, '//input[@type="password"]', self)
        self.submit = Button(By.XPATH, '//input[@type="submit"]', self)

    def invoke_actions(self):
        self.browser_session.open(JSONReader.get_data_from_string("environment_links", "tag", json_string=Vars().env_config))

    @allure.step
    def login(self):
        self.invoke().email.send_keys(Vars().retailer_email)
        self.password.send_keys(Vars().retailer_password)
        self.submit.click()

(ex3me0) #52

Спасибо, полезно. Жаль что я выбрал более заковыристый путь, в угоду красоте и юзабельности "не программистами"


(Руслан) #53

как по мне - самый удобный вариант ,
не лучший пример, но SelenideElement emailInputField= $("#email"); выглядит вполне щикарно и просто


(Crazyk2) #54

Только работать не будет так как по дефолту идет CSS, а у Вас написан xPath.
вот так будет правильней.
SelenideElement loginField = $(By.xpath("//*[@id='email']"));


(Eugene Moskalenko) #55

Та да, то я затупил, когда писал, у меня так и написано :slight_smile: Просто когда копировал с первого примера, то копипастил все что в кавычках :slight_smile: к сожалению позже уже поправить не смог, поскольку первое сообщение из темы, сейчас тоже поправить не могу