Данный пример призван улучшить восприятие элементов. Т.к. все типизированные элементы оборачивают класс By
(название которого очень слабо отражает его предназначение), а инициализация производится за кадром, то нам необходимо как-то обозначать критерии поиска. Что собственно и достигается при помощи кастомной аннотации, аля аналог FindBy + WebElement
, но только для класса By
. Так что удовольствие это скорее сугубо эстетическое.
Да, спасибо, в принципе всю суть полезности я понимаю… Так если подумать, оно клево очень. А если в приложении используются: AndroidDriver и для WebDriver, Sikulli, то можно в своем приложении наделать кастомных аннотаций, в итоге получится наглядней элементы…
Вот только вопросик больше волнует, делают ли так, тратят ли на это время? Подобная эстетика, как правило делается в свободное время перфекционистом автоматизатором, или на это выделяют время, и это не так уж и не важно? @ArtOfLife, в ваших проектах вы реализовуете такие вещи, для удобства дальнейшей разработки?
Интересно понять золотую середину такой эстетической реализации с точки зрения реальных проектов… Мне понравилось такое решение, и я бы с удовольствием переписал часть своего фреймворка… Но ведь потом эти штуки поддерживать надо, где-то могут быть подводные камни
Я бы не ручался за других, но могу предположить, что так делают условные единицы. Не потому что не умеют, а потому что им это не надо.
Делаю ли я так у себя? Да, делаю. В какое время? Человеку с опытом понадобится всего несколько дней на реализацию подобной фичи. Ничего сложного там нет. К тому же, как правило, все это делается в самом начале, а не когда проект оброс со всех сторон. Хотя, при хорошо продуманной архитектуре, миграция должна пройти весьма безболезненно. По сути улучшается лишь детализация описания элементов. Такой подход может вполне нормально работать с уже существующим, когда общие действия выносятся в условную BasePage
. У типизированных элементов то будет свой собственный инициализтор. А ввиду прямого наследования By
класса, старый API ну никак не может сломаться, если вы изначально локаторы описывали с помощью By
, а не FindBy + WebElement
.
И нет никакой золотой середины. У каждого свое видение идеальных фреймворков / процессов. В любом случае придется подстраивать некоторые вещи под себя.
Касательно саппорта… Если все изначально правильно написать, то вы в эти классы вообще никогда не будете влезать. В этом то и суть фреймворка - написали 1 раз, и забыли. Разве что какие-то новые фичи по надобности прикручивать придется. В остальном, этот механизм должен быть вылизан и отлажен до последнего байта. Посколько именно от его стабильности зависит стабильность ваших тестов. Я не верю в сказки о том, что основной причиной flaky тестов является приложение. Особенно когда заглядываешь в код и видишь тонны Thread.sleep
, конечно же на всякий случай.
Как по мне, то Thread.sleep
- не должно быть никогда. Только в очень редких случаях…
Я вот что еще подметил, как плюс в подобной реализации… С помощью нее можно очень грамотно спрятать обертки по работе с элементами, и прикрутить разумное ожидание… И не складировать все в абстрактном классе и не изощряться со всякими там утилами для ожиданий и по работе с элементами…
Ну на самом деле универсальный вейтер можно обернуть и на уровне условной BasePage
, просто переопределив логику findElement
. В случае с типизированными элементами он всего лишь помещается в другое место - некую обертку над элементом. Хотя, если вообще идеализировать, можно банально отдекорировать драйвер таким образом, что вместо стандартного findElement
будет использоваться его усовершенствованная версия аля explicit waits. Когда дойдут руки, залью примеры на github.
Спасибо огромное, было бы здорово взглянуть И вот это еще, если будет время, пожалуйста - “С рефлексией все будет гораздо компактней. Может на днях залью новую версию.” - Какой должна быть идеальная структура проекта автотестов? - #33 от пользователя adv
Ну у меня из-за специфики проекта, как плюс это считаю, повторяются многие элементы и локаторы на страницах, но может быть такое что в разных блоках страницы то уже структурирую по компонентам и степам к ним и объединяю в страницы это всё чудо.
Т.к. есть кастомные решения для выпадаек и для кнопок, то есть объекты со специфическими методами для их манипуляций.
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;
}
аа ну для этого делаю сайта автоматизацию: gsmserver.com, можете увидеть какая кнопка “Купить” и “Поиск” о которых говорил
И метод getStepsFrom в тестовых класса вызываю в основном в методах с анотациями @BeforeClass или @Before
А конфигурации уже в таске gradle
я вот сейчас именно эту штуку активно использую, если речь идет о дефолтных интерфейсах, в которых можно имплементировать только реализацию, без состояния
Речь шла о дефолтных и статических методах в интерфейсах. В девятке еще и приватные методы добавят.
ага, я эт и имел ввиду. Сейчас стабильно в своих автотестах юзаю эти вещи. Очень сильно улучшает фреймворк…
В восьмой джаве очень много фишек, значительно улучшающих дизайн архитектуры.
Это точно
Интересно: а как у Вас описаны классы страниц?
В частности интерес представляет расположение веб-айтемов в классе (атрибуты класса или атрибуты инстанса)
Столкнулся на днях с некоторыми проблемами при определении веб-элементов как атрибутов класса, интересно как подобные проблемы решаются другими людьми.
И реализовывали ли веб-айтемы вида “контейнер”? Т.е. группа элементов связанная логически (input, div с эррор-меседжем, etc), для которой длеается отдельный реюзабельный класс, дабы каждый раз не описывать по 3-5 веб-айтемов на странице.
Примерно так:
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()
Спасибо, полезно. Жаль что я выбрал более заковыристый путь, в угоду красоте и юзабельности “не программистами”
как по мне - самый удобный вариант ,
не лучший пример, но SelenideElement emailInputField= $("#email"); выглядит вполне щикарно и просто
Только работать не будет так как по дефолту идет CSS, а у Вас написан xPath.
вот так будет правильней.
SelenideElement loginField = $(By.xpath("//*[@id='email']"));
Та да, то я затупил, когда писал, у меня так и написано Просто когда копировал с первого примера, то копипастил все что в кавычках к сожалению позже уже поправить не смог, поскольку первое сообщение из темы, сейчас тоже поправить не могу