commit f280b8c44c0d59ee5cba9009c232adb1c7772070 Author: valerpan Date: Fri Aug 14 16:47:31 2026 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e27cba5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*/target/* +*/allure-results/* +.idea/* +*.iml +*.class +*.log +*.ctxt +.mtj.tmp/ +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +hs_err_pid* +*.allure/* +.build/ +idea/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..50ef1d2 --- /dev/null +++ b/README.md @@ -0,0 +1,275 @@ +## Шаблон Фреймворка для запуска автотестов +BDD фреймворк для автотестов на Java, использующий: +- [Selenide](https://ru.selenide.org) для тестирования Web UI +- [Cucumber](https://cucumber.io) для написания сценариев в стиле BDD +- [REST assured](https://rest-assured.io) для тестирования REST API + +## Как начать писать WEB автотесты +## 1. Page Objects +В модуле ***autotest-web*** в директории ***src/main/java/pages*** находятся классы ***PageObjects***
+**1.1** Каждый ***PageObject*** должен наследоваться от класса ***WebPage***
+**1.2** Над классом необходимо проставить аннотацию *@Name(**value** = "<имя страницы>")*
+Пример:
+```java +@Name(value = "Главная страница приложения") +public class MainPage extends WebPage { + + @Name("Слайдер") + private SelenideElement uuid = $(".slick-track"); +} +``` +## 2. Степы +**2.1** В классе со степами необходимо наследовать от класса AbstractWebSteps и конструктор класса следующим образом:
+```java +public class WebActionSteps extends AbstractWebSteps { + + public WebActionSteps(PageManager pageManager) { + super(pageManager); + } + // steps +} +``` +* ссылка ***pageManager*** хранит в себе инициализированный контекст текущей страницы, с помощью которой можно достать элемент через ***value*** аннотации ***@Name*** элемента
+* При компиляции ссылка ***pageManager*** проинициализируется автоматически путем Dependency Injection через PicoContainer
+* Более подробно о подходе можно ознакомиться по ссылке [Cucumber PicoContainer](https://cucumber.io/docs/cucumber/state/)
+ + + +**2.2** Пример инициализации страницы:
+Для того, чтобы получить доступ к элементу, нам необходимо перед этим проинициализировать ***PageObject***
+**pageName** - это value аннотации **Name** класса ***PageObject*** - в нашем примере *"Google"* +```java +public void setPage(String pageName) { + WebPage page = getPage(pageName); + pageManager.setCurrentPage(page); +} +``` +**2.3** Теперь страница проинициализирована и получить доступ к элементам можно по его имени ***(value)***
+```java +@Если("кликнуть на элемент {string}") +public void clickOnElement(String elementName) { + SelenideElement element = pageManager + .getCurrentPage() + .getElement(elementName); + element.shouldBe(visible).click(); +} +``` + +**2.4** Осуществление мягких проверок (SoftAssert)
+В конфигурации Selenide есть параметр Configuration.assertionMode, который не отрабатывает должным образом в связке с Cucumber и потому его использование в проекте не допускается. +Для осуществления мягких проверок следует использовать класс ru.lanit.at.assertion.AssertsManager. +Пример использования (Проверка утверждений у SelenideElement) + +```java +public static void elementContainsText(SelenideElement element, String text) { + element.execute(Commands.checkSoft(Condition.text(text), Duration.ofSeconds(10))); +} +``` + +**2.5** Расширение и изменения методов SelenideElement с помощью механизма SPI
+Если нужно изменить поведение стандартных методов SelenideElement, это можно сделать след образом. +В классе ru.lanit.at.utils.selenide.extensions.CustomCommands, необходимо релизовать свою логику, например код ниже, будет перед осуществлением клика, прикладывать скриншот элемента, на который будет происходить нажатие. +```java +public T execute(Object proxy, WebElementSource webElementSource, String methodName, @Nullable Object[] args) throws IOException { + if(methodName.equals("click")){ + addAllureScreenshootElement((SelenideElement)proxy); + } + return super.execute(proxy, webElementSource, methodName, args); + } + + private void addAllureScreenshootElement(SelenideElement selenideElement){ + AllureHelper.attachScreenShot("Клик на элементе ", selenideElement.getScreenshotAs(OutputType.BYTES)); + } +``` +Изменения логики формирования Page объектов, осуществляется в классе ru.lanit.at.utils.selenide.extensions.CustomSelenidePageFactory. С помощью этого класса можно расширить/изменить логику инициализации элементов. + +## 3. Тесты + +**3.1** Написание сценариев +```gherkin +#language:ru +Функционал: Поиск гугл + Сценарий: Открытие страницы google.com, ввод значения в поиск + + * открыть браузер + * инициализация страницы "Google" + * ввести в поле "поле поиска" значение "Погода в Москве" + * на странице имеется элемент "результаты поиска" + * кликнуть на элемент "кнопка поиска" + * инициализация страницы "страница результатов поиска" + * на странице присутствует текст "Погода в Москве" +``` +* Шаг 1 - открытие веб страницы +* Шаг 2 - инициализация ***PageObject*** через его ***value*** аннотации ***@Name*** +* Шаг 3 - как в примере **2.3** получаем текущий элемент по его ***value*** аннотации ***@Name*** и производим действия/проверки + +**3.2** Сценарии из фрагментов +Начиная с версии 1.2, фреймворк поддержиивает напиисание сценариев из фрагментов. Фрагменты должны храниться в +директории "fragments" в корне проекта в файлах с расширением *.feature. Каждый фрагмент должен быть выделен в отдельный +сценарий и аннотирован @fragment +```gherkin +@fragment +Сценарий: открытие виджета + + * на странице имеется элемент "кнопка поиска" + * кликнуть на элемент "кнопка поиска" + * переход на страницу "Google страница результатов" + * на странице имеется элемент "виджет погоды" +``` +Чтобы использовать фрагмент, в сценарии следует добавить шаг вида + +```gherkin + * вызвать фрагмент "открытие виджета" +``` +Передаваемый текстовый параметр шага должен соответвовать имени сценария, содержащего фрагмент. Перед запуском тестов, +фреймворк анализирует содержимое всех фича-файлов и заменяет шаг * вызвать фрагмент"{string}" на группу шагов из +соотвествующего фрагмента. +!IMPORTANT! +Чтобы функционал работал корректно, компиляцию проекта необходимо выполнять при помощи команды mvn compile. +Для запуска фич по отдельности через функционал или отладчик IDEA внесите измение в шаблон конфигураци Cucumber: +Run/Debug Configurations/ Edit configurations template / Cucumber Java / Build Добавить действие Maven "compile" +![feature](images/cucumber_compile.png) +Для запуска фрагментов через Maven дополнительных настроек не требуется. + +## 4. Настройки +В директории ***autotest-web/src/test/resources/config*** имеются примеры *config-файлов* для разных браузеров и общих настроек запуска. + +configuration.properties +```properties +stand= названия стенда для тестирования в классе (ru.lanit.at.utils.Stand) содержатся адреса стендов +screen_after_step=false - необходимость прикреплять скриншот к каждому шагу +baseUrl=https://petstore.swagger.io/v2/ - базовый url для апи запросов + +remoteUrl=127.0.0.1:4444 - адрес удаленного хаба/selenoid. +enableVNC=true - возможность отображения работы теста в браузере, переменная применима только selenoid. +enableVideo=true - возможность записывать видео, переменная применима только selenoid. +enableLog=true - флаг для сохранения логов selenoid контейнера, переменная применима только selenoid. + +``` + +chrome.properties +```properties +webdriver.browser.size=1920x1080 - разрешение браузера +webdriver.browser.name=chrome - название браузера +webdriver.timeoutSeconds=4 - таймаут ожидания состояния веб-элементов +polling.timeoutMs=200 - периодичность опроса веб-элемента +webdriver.version=91.0 - версия веб-драйвера + +``` +Так же с помощью одноименных переменных окружения можно переопределить данные настройки
+Драйвера скачиваются с помощью **WebDriverManager**, учитывайте это если находитесь в закрытом контуре + +## Как начать писать API тесты +### Принцип написания тестов похож на подход создания и отправки запроса в Postman +*Шаг 1. Конфигурируем запрос с помощью шага* +```gherkin +* создать запрос + | method | path | body | url | +``` +Если какой то из столбцов не указан в данном шаге, то он не учитывается в запросе +Например: +```gherkin +* создать запрос + | method | path | body | + | POST | /user | createUser.json | +ИЛИ +* создать запрос + | method | path | body | + | POST | /user | {<тело запроса>} | +ИЛИ +* создать запрос + | method | url | + | GET | https://petstore.swagger.io/v2/user/ | +``` +* Можно указать ***basePath*** через одноименную системную переменную или в файле конфигурации ***configuration.properties***. Тогда вместо столбца url можно указывать просто path. И наоборот, если указать столбец url с полным url хоста и path то basePath не учитывается, даже если указан с системных переменных. +* Тело запроса - в качестве тела можно передать в таблицу, как просто текст, так и название файла ***json***, которое будет лежать по пути ***autotest-rest/src/test/resources/json*** + +*Шаг 2. Добавление Headers и Query* +```gherkin +* добавить header + | Content-Type | application/json | +* добавить query параметры + | city | Moscow | +``` +*Шаг 3. Отправка запроса* +```gherkin +* отправить запрос +``` +*Шаг 4. Проверка ответа* +```gherkin +* статус код 200 +``` +Если необходимо проверить тело ответа, то данные можно вытащить с помощью jsonpath. Значение сохранится в переменную из столбца 1
+```gherkin +* извлечь данные + | user_id | $.message | +``` +Проверить извлеченные данные можно с помощью шага: +```gherkin +* сравнить значения +| ${user_id} | != | null | +ИЛИ +| ${user_id} | == | 1234567890 | +ИЛИ +| ${user_id} | > | 0 | +ИЛИ +| ${user_id} | < | 100 | +ИЛИ +| ${user_id} | содержит | qwerty123 | +``` +### Иная информация. +С помощью следующего шага можно сгенерить переменные для последующего использования в тесте +```gherkin +* сгенерировать переменные + | id | 0 | + | username | EEEEEEEE | + | firstName | EEEEEEEE | + | lastName | EEEEEEEE | + | email | EEEEEEE@EEEDDD.EE | + | password | DDDEEEDDDEEE | +``` +**R** - случайная русская буква
+**E** - случайная английская буква
+**D** - случайное число
+Другие символы в строке игнорируются и остаются неизменяемыми +Сгенерированные значения хранятся в контексте теста. Их можно подставлять в запросы, тела запросов. Достать их можно используя синтаксис ***${username}***
+ +### Запуск тестов через консоль + +```java +mvn clean test -Ddataproviderthreadcount=4 -Dscreen_after_step=false -Dtags="@authentication" + ``` +* ***-Ddataproviderthreadcount=4*** - кол-во поток выполнения, значение по умолчанию 1 поток (Изменение дефолтного кол-ва поток производится в файле src\test\resources\suite.xml) + +* ***-Dscreen_after_step=false*** - Автоматическое снятие скриншотов после каждого шага. Дефолтное значение =false (Изменение дефолтного значения производится в файле src\test\resources\config\configuration.properties) + +* ***-Dtags="@authentication"*** - Выбор тестов с определенным тегом. Дефолтное значение отсутствует, т.е. если в запуске не указывать данный параметр то, будет запущены все фичи файлы. + +>Тажке в командную строку можно передать любой параметр из файлов *.properties +>Для добавление нового параметра, его нужно будет прописать в соответствующем .properties файле и добавить соответствущий полю getter в java классе. + +#### Для запуска с дефолтными параметрами используется команда. +```java +mvn clean test + ``` + + +Так же запустить тесты можно через плагин Cucumber (предварительно установив его в Idea), для этого необходимо открыть любой feature-файл, и кликнуть по зеленой стрелке рядом со стройкой **Функционал** или **Сценарий**
+![feature](images/run-feature.png) + +### Генерация отчета +По итогу прогонов можно сгенерить _Allure отчет_, для этого необходимо в Intellij Idea кликнуть на строку **Maven** в правом верхнем углу IDE и следовать инструкции по пунктам ниже:
+В **п.1** необходимо выбрать тот модуль, в котором запускались тесты.
+![allure](images/allure.png)
+**По итогу сформируется Html страница с отчетом.**
+![allure-report](images/allure_report.png)
+**В отчете можно провалиться в каждый шаг и посмотреть информацию по нему**
+![allure-step](images/allure_report_steps.png)
+ +где +* Зеленым цветом отмечены - успешно прошедшие тесты +* Желтым цветом отмечены - тесты в которых есть неблокирующие дефекты +* Серым цветом отмечены - тесты который были пропущены +* Красным цветом отмечены - тесты с блокирующим дефектом + +**Шаги с неблокирующими дефекты** помечаются в отчете желтым цветом. По окончании теста отображается информация о всех подобных дефектах. diff --git a/fragments/DynamicFragmentExample.feature b/fragments/DynamicFragmentExample.feature new file mode 100644 index 0000000..9c1ceb0 --- /dev/null +++ b/fragments/DynamicFragmentExample.feature @@ -0,0 +1,8 @@ +#language:ru +Функционал: динамический фрагмент + + @fragment + Сценарий: открытие страницы и ввод текста из параметра + * открыть url "https://www.google.ru/" + * инициализация страницы "Google" + * ввести в поле "поле поиска" значение "<текст_для_ввода>" \ No newline at end of file diff --git a/fragments/Example.feature b/fragments/Example.feature new file mode 100644 index 0000000..f93cef8 --- /dev/null +++ b/fragments/Example.feature @@ -0,0 +1,17 @@ +#language:ru +Функционал: фрагмент + + @fragment + Сценарий: открытие виджета + + * на странице имеется элемент "кнопка поиска" + * кликнуть на элемент "кнопка поиска" + * переход на страницу "Google страница результатов" + * на странице имеется элемент "виджет погоды" + + @fragment + Сценарий: открытие страницы google + + * открыть url "https://www.google.ru/" + * инициализация страницы "Google" + * ввести в поле "поле поиска" значение "Погода в Москве" \ No newline at end of file diff --git a/images/allure.png b/images/allure.png new file mode 100644 index 0000000..e3f3f14 Binary files /dev/null and b/images/allure.png differ diff --git a/images/allure_report.png b/images/allure_report.png new file mode 100644 index 0000000..2daf289 Binary files /dev/null and b/images/allure_report.png differ diff --git a/images/allure_report_steps.png b/images/allure_report_steps.png new file mode 100644 index 0000000..4d03fb3 Binary files /dev/null and b/images/allure_report_steps.png differ diff --git a/images/cucumber_compile.png b/images/cucumber_compile.png new file mode 100644 index 0000000..5d1e50c Binary files /dev/null and b/images/cucumber_compile.png differ diff --git a/images/run-feature.png b/images/run-feature.png new file mode 100644 index 0000000..e9e6126 Binary files /dev/null and b/images/run-feature.png differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..11a796c --- /dev/null +++ b/pom.xml @@ -0,0 +1,217 @@ + + + 4.0.0 + + ru.lanit.at + autotest-template-with-cucumber + 1.2-SNAPSHOT + + UTF-8 + 8 + 8 + 1.0.12 + 7.2.3 + 1.9.6 + 1.14.0 + 2.22.2 + 6.2.0 + 2.15.0 + 1.7.30 + 3.8.1 + 2.17.2 + 2.10.0 + 3.12.0 + 4.4.0 + 2.12.3 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.source} + ${java.target} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + src/test/resources/suite.xml + + false + false + + -Dcucumber.filter.tags="${tags}" + -Dfile.encoding=UTF-8 + -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" + + + ${project.basedir}/ + ${project.build.directory}/allure-results + ${project.build.directory}/report + ${project.build.directory}/download + + + + + org.aspectj + aspectjweaver + ${aspectj.version} + + + + + io.qameta.allure + allure-maven + ${allure.maven.version} + + ${project.build.directory}/allure-report + + + + org.codehaus.mojo + aspectj-maven-plugin + ${aspectj-maven-plugin.version} + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + ${java.source} + ${java.source} + ${java.source} + ${project.build.sourceEncoding} + + + io.cucumber + cucumber-testng + + + io.cucumber + cucumber-core + + + true + + + + + compile + test-compile + + + + + + + + + + + + io.cucumber + cucumber-java + ${cucumber.version} + + + io.cucumber + cucumber-testng + ${cucumber.version} + + + io.cucumber + cucumber-picocontainer + ${cucumber.version} + + + + + io.qameta.allure + allure-cucumber7-jvm + 2.17.2 + + + io.qameta.allure + allure-selenide + ${allure.version} + + + + + org.aspectj + aspectjrt + ${aspectj.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + com.jayway.jsonpath + json-path + 2.5.0 + + + org.apache.commons + commons-lang3 + ${common-lang3.version} + + + com.codeborne + selenide + ${selenide.version} + + + org.reflections + reflections + 0.9.10 + + + org.aeonbits.owner + owner-java8 + ${owner.version} + + + + + io.rest-assured + rest-assured + ${restassured.version} + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/actions/WebActions.java b/src/main/java/ru/lanit/at/actions/WebActions.java new file mode 100644 index 0000000..102f2ad --- /dev/null +++ b/src/main/java/ru/lanit/at/actions/WebActions.java @@ -0,0 +1,84 @@ +package ru.lanit.at.actions; + +import com.codeborne.selenide.Selenide; +import com.codeborne.selenide.SelenideElement; +import com.codeborne.selenide.WebDriverRunner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import ru.lanit.at.utils.ErrorMessage; +import ru.lanit.at.utils.Sleep; + +import java.util.ArrayList; +import java.util.List; + + +public class WebActions { + + private static final Logger LOGGER = LoggerFactory.getLogger(WebActions.class); + + /** + * Открывает ссылку по переданному url и переводит контекст драйвера на новое окно + */ + public static void openUrlOnNewTab(String url) { + String command = String.format("window.open('%s')", url); + Selenide.executeJavaScript(command); + List handles = new ArrayList<>(WebDriverRunner.getWebDriver().getWindowHandles()); + Selenide.switchTo().window(handles.get(handles.size() - 1)); + LOGGER.info("Отрытие новой вкладки с урл {}", url); + } + + /** + * Переключается на следующую вкладку или вкладку по порядковому номеру (1, 2, ...) + */ + public static void switchToNextTab(Integer tabNumber) { + List handles = new ArrayList<>(WebDriverRunner.getWebDriver().getWindowHandles()); + if (tabNumber != null) { + Assert.assertTrue(handles.size() >= tabNumber, String.format(ErrorMessage.BROWSER_TAB_NUMBER_MORE_THAN_TABS, tabNumber)); + } + tabNumber = tabNumber == null ? handles.size() - 1 : tabNumber - 1; + Selenide.switchTo().window(handles.get(tabNumber)); + LOGGER.info("Переключение на вкладку {}", handles.size()); + } + + /** + * Закрывает текущую вкладку и переключается на предыдущую + */ + public static void closeCurrentTabAndSwitchToPrevious() { + Selenide.closeWindow(); + List handles = new ArrayList<>(WebDriverRunner.getWebDriver().getWindowHandles()); + Selenide.switchTo().window(handles.get(handles.size() - 1)); + } + + /** + * Посимвольное заполнение поля + * + * @param element - элемент + * @param text - значение + */ + public static void fillInputByCharacter(SelenideElement element, String text) { + for (char character : text.toCharArray()) { + element.sendKeys(String.valueOf(character)); + Sleep.pauseSec(1); + } + } + + /** + * Установка чекбокса на элементе + * + * @param element - элемент + * @param flag - значение + */ + public static void setCheckBox(SelenideElement element, boolean flag) { + element.click(); + if (flag) { + if (!element.isSelected()) { + element.click(); + } + } else { + if (element.isSelected()) { + element.click(); + } + } + } +} diff --git a/src/main/java/ru/lanit/at/actions/WebChecks.java b/src/main/java/ru/lanit/at/actions/WebChecks.java new file mode 100644 index 0000000..35c7e8b --- /dev/null +++ b/src/main/java/ru/lanit/at/actions/WebChecks.java @@ -0,0 +1,216 @@ +package ru.lanit.at.actions; + + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.Selectors; +import com.codeborne.selenide.SelenideElement; +import com.codeborne.selenide.WebDriverRunner; +import com.codeborne.selenide.ex.ElementNotFound; +import com.codeborne.selenide.ex.ElementShould; +import org.aeonbits.owner.ConfigFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import ru.lanit.at.utils.ErrorMessage; +import ru.lanit.at.utils.selenide.command.Commands; +import ru.lanit.at.utils.web.properties.WebConfigurations; + +import java.time.Duration; +import java.util.List; + +import static com.codeborne.selenide.Selenide.$; +import static java.lang.String.format; + +public class WebChecks { + + private static final Logger LOGGER = LoggerFactory.getLogger(WebChecks.class); + + + private static Integer getTimeoutSeconds(Integer timeout) { + WebConfigurations cfg = ConfigFactory.create(WebConfigurations.class); + return timeout == null ? cfg.webDriverTimeoutSeconds() : timeout; + } + + /** + * Проверяет что текущий url равен переданному + * + * @param url ожидаемый url + */ + public static void urlEquals(String url) { + Assert.assertEquals(WebDriverRunner.getWebDriver().getCurrentUrl(), url, + format(ErrorMessage.URL_NOT_EQUAL_ACTUAL, url, WebDriverRunner.getWebDriver().getCurrentUrl())); + LOGGER.info("url '{}' равен текущему '{}'", url, WebDriverRunner.getWebDriver().getCurrentUrl()); + } + + /** + * Проверяет что текущий url содержит текст + * + * @param text ожидаемый текст + */ + public static void urlContains(String text) { + Assert.assertTrue(WebDriverRunner.getWebDriver().getCurrentUrl().contains(text), + format(ErrorMessage.URL_NOT_CONTAINS_TEXT, WebDriverRunner.getWebDriver().getCurrentUrl(), text)); + LOGGER.info("Текущий url '{}' содержит текст '{}'", WebDriverRunner.getWebDriver().getCurrentUrl(), text); + } + + /** + * Проверяет, что атрибут элемента равен ожидаемому + * + * @param element Элемент + * @param attrName Атрибут + * @param expectValue Ожидаемое значение атрибута + */ + public static void checkAttribute(SelenideElement element, String attrName, String expectValue, Integer timeoutSeconds) { + int timeout = getTimeoutSeconds(timeoutSeconds); + element + .shouldBe(Condition.exist, Duration.ofSeconds(timeout)) + .shouldBe(Condition.attribute(attrName, expectValue), Duration.ofSeconds(timeout)); + } + + /** + * Проверяет, что на странице доступен элемент + */ + public static void elementEnablesOnPage(SelenideElement element, Integer timeoutSeconds) { + int timeout = getTimeoutSeconds(timeoutSeconds); + element.shouldBe(Condition.enabled, Duration.ofSeconds(timeout)); + } + + + /** + * Проверяет, что на странице имеется элемент + */ + public static void elementVisibleOnPage(SelenideElement element, Integer timeoutSeconds) { + int timeout = getTimeoutSeconds(timeoutSeconds); + element.shouldBe(Condition.visible, Duration.ofSeconds(timeout)); + } + + + /** + * Проверяет, что элемент имеет текст + */ + public static void elementContainsText(SelenideElement element, String text) { + element.execute(Commands.checkSoft(Condition.text(text), null)); + } + + /** + * Проверяет, что элемент содержит текст + */ + public static void elementContainsText(SelenideElement element, List texts) { + for (String s : texts) { + element.execute(Commands.checkSoft(Condition.text(s), Duration.ofSeconds(10))); + } + } + + + /** + * Проверяет, что на странице имеется текст + */ + public static void textVisibleOnPage(String text, Integer timeoutSeconds) { + int timeout = getTimeoutSeconds(timeoutSeconds); + $(Selectors.byText(text)) + .shouldBe(Condition.visible, Duration.ofSeconds(timeout)); + } + + /** + * Проверяет, что на странице отсутствует текст + */ + public static void textAbsentOnPage(String text, Integer timeoutSeconds) { + int timeout = getTimeoutSeconds(timeoutSeconds); + $(Selectors.byText(text)) + .shouldBe(Condition.not(Condition.visible), Duration.ofSeconds(timeout)); + } + + /** + * Проверяет, что на странице отсутствует элемент + */ + public static void elementAbsentOnPage(SelenideElement element, Integer timeoutSeconds) { + int timeout = getTimeoutSeconds(timeoutSeconds); + element + .shouldBe(Condition.not(Condition.visible), Duration.ofSeconds(timeout)); + } + + /** + * Проверяет, что текст элемента соответствует ожидаемому тексту + * + * @param element элемент + * @param expectedText ожидаемый текст + * @param timeoutSeconds количество секунд, в течении этого времени ожидается текст + */ + public static void elementTextEqualsExpectedText(SelenideElement element, String expectedText, Integer timeoutSeconds) { + int timeout = getTimeoutSeconds(timeoutSeconds); + element.shouldBe(Condition.exactTextCaseSensitive(expectedText), Duration.ofSeconds(timeout)); + } + + /** + * проверяет что текст элемента не соответствует ожидаемому тексту + * + * @param element элемент + * @param expectedText текст + * @param timeoutSeconds количество секунд + */ + public static void elementTextNotEqualsExpectedText(SelenideElement element, String expectedText, Integer timeoutSeconds) { + int timeout = getTimeoutSeconds(timeoutSeconds); + element.shouldNotBe(Condition.exactTextCaseSensitive(expectedText), Duration.ofSeconds(timeout)); + } + + /** + * Проверяет, что текст элемента содержит ожидаемый текст + * + * @param element элемент + * @param expectedText ожидаемый текст или регулярное выражение + * @param timeoutSeconds количество секунд, в течении этого времени ожидается текст + */ + public static void elementTextContainsExpectedText(SelenideElement element, String expectedText, Integer timeoutSeconds) { + int timeout = getTimeoutSeconds(timeoutSeconds); + element.shouldBe(Condition.matchText(expectedText), Duration.ofSeconds(timeout)); + } + + /** + * Проверяет, появился ли элемент за переданное время + * + * @param element - веб-элемент + * @param timeoutSeconds - таймаут ожидания (может быть null, в таком случае будет использоваться параметр из конфига webdriver.timeoutSeconds ) + * @return - появился ли элемент + */ + public static boolean isElementWillAppear(SelenideElement element, Integer timeoutSeconds) { + try { + elementVisibleOnPage(element, timeoutSeconds); + return true; + } catch (ElementNotFound elementNotFound) { + return false; + } + } + + /** + * Проверяет, исчез ли элемент за переданное время + * + * @param element веб-элемент + * @param timeoutSeconds - таймаут ожидания (может быть null, в таком случае будет использоваться параметр из конфига webdriver.timeoutSeconds ) + * @return - исчез ли элемент + */ + public static boolean isElementWillDisappear(SelenideElement element, Integer timeoutSeconds) { + try { + elementAbsentOnPage(element, timeoutSeconds); + return true; + } catch (ElementShould elementShould) { + return false; + } + } + + /** + * Проверяет, исчез ли элемент за переданное время + * + * @param xpath - xpath веб-элемента + * @param timeoutSeconds - таймаут ожидания (может быть null, в таком случае будет использоваться параметр из конфига webdriver.timeoutSeconds ) + * @return - исчез ли элемент + */ + public static boolean isElementWillDisappear(String xpath, Integer timeoutSeconds) { + try { + SelenideElement element = $(Selectors.byXpath(xpath)); + elementAbsentOnPage(element, timeoutSeconds); + return true; + } catch (ElementShould elementShould) { + return false; + } + } +} diff --git a/src/main/java/ru/lanit/at/api/ApiRequest.java b/src/main/java/ru/lanit/at/api/ApiRequest.java new file mode 100644 index 0000000..3e3e748 --- /dev/null +++ b/src/main/java/ru/lanit/at/api/ApiRequest.java @@ -0,0 +1,131 @@ +package ru.lanit.at.api; + +import io.qameta.allure.Allure; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.http.Method; +import io.restassured.response.Response; +import io.restassured.specification.RequestSpecification; +import org.aeonbits.owner.ConfigFactory; +import ru.lanit.at.api.listeners.RestAssuredCustomLogger; +import ru.lanit.at.api.models.RequestModel; +import ru.lanit.at.api.properties.RestConfigurations; +import ru.lanit.at.utils.FileUtil; +import ru.lanit.at.utils.JsonUtil; +import ru.lanit.at.utils.RegexUtil; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static io.restassured.RestAssured.given; +import static ru.lanit.at.utils.ContextHolder.replaceVarsIfPresent; + +/** + * Created by Sorokin Boris on 21.01.2022. + */ +public class ApiRequest { + private final static RestConfigurations CONFIGURATIONS = ConfigFactory.create(RestConfigurations.class, + System.getProperties(), + System.getenv()); + + private String baseUrl; + private String path; + private Method method; + private String body; + private String fullUrl; + private Response response; + + private RequestSpecBuilder builder; + + public ApiRequest(RequestModel requestModel) { + this.builder = new RequestSpecBuilder(); + + this.baseUrl = CONFIGURATIONS.getBaseUrl(); + this.path = replaceVarsIfPresent(requestModel.getPath()); + this.method = Method.valueOf(requestModel.getMethod()); + this.body = requestModel.getBody(); + this.fullUrl = replaceVarsIfPresent(requestModel.getUrl()); + + URI uri; + + if (!fullUrl.isEmpty()) { + uri = URI.create(fullUrl.replace(" ", "+")); + } else { + uri = URI.create(baseUrl); + builder.setBasePath(path); + } + + this.builder.setBaseUri(uri); + setBodyFromFile(); + addLoggingListener(); + } + + public Response getResponse() { + return response; + } + + /** + * Сеттит заголовки + */ + public void setHeaders(Map headers) { + headers.forEach((k, v) -> { + builder.addHeader(k, v); + }); + } + + /** + * Сеттит query-параметры + */ + public void setQuery(Map query) { + query.forEach((k, v) -> { + builder.addQueryParam(k, v); + }); + } + + /** + * Отправляет сформированный запрос + */ + public void sendRequest() { + RequestSpecification requestSpecification = builder.build(); + + Response response = given() + .spec(requestSpecification) + .request(method); + + attachRequestResponseToAllure(response, body); + this.response = response; + } + + /** + * Сессит тело запроса из файла + */ + private void setBodyFromFile() { + if (body != null && RegexUtil.getMatch(body, ".*\\.json")) { + body = replaceVarsIfPresent(FileUtil.readBodyFromJsonDir(body)); + builder.setBody(body); + } + } + + /** + * Аттачит тело запроса и тело ответа в шаг отправки запроса + */ + private void attachRequestResponseToAllure(Response response, String requestBody) { + if (requestBody != null) { + Allure.addAttachment( + "Request", + "application/json", + new ByteArrayInputStream(requestBody.getBytes(StandardCharsets.UTF_8)), + ".txt"); + } + String responseBody = JsonUtil.jsonToUtf(response.body().asPrettyString()); + Allure.addAttachment("Response", "application/json", responseBody, ".txt"); + } + + /** + * Добавляет логгер, печатающий в консоль данные запросов и ответов + */ + private void addLoggingListener() { + builder.addFilter(new RestAssuredCustomLogger()); + } +} diff --git a/src/main/java/ru/lanit/at/api/listeners/RestAssuredCustomLogger.java b/src/main/java/ru/lanit/at/api/listeners/RestAssuredCustomLogger.java new file mode 100644 index 0000000..9276d8f --- /dev/null +++ b/src/main/java/ru/lanit/at/api/listeners/RestAssuredCustomLogger.java @@ -0,0 +1,63 @@ +package ru.lanit.at.api.listeners; + +import io.restassured.filter.Filter; +import io.restassured.filter.FilterContext; +import io.restassured.filter.log.UrlDecoder; +import io.restassured.response.Response; +import io.restassured.specification.FilterableRequestSpecification; +import io.restassured.specification.FilterableResponseSpecification; + +import java.nio.charset.Charset; + +import static ru.lanit.at.utils.ContextHolder.replaceVarsIfPresent; + + +/** + * Created by Sorokin Boris on 21.01.2022. + */ +public class RestAssuredCustomLogger implements Filter { + + @Override + public Response filter(FilterableRequestSpecification requestSpec + , FilterableResponseSpecification responseSpec + , FilterContext context) { + Response response = context.next(requestSpec, responseSpec); + + String uri = UrlDecoder.urlDecode(requestSpec.getURI(), + Charset.forName(requestSpec + .getConfig() + .getEncoderConfig() + .defaultQueryParameterCharset()), + true); + + StringBuilder stringBuilder = new StringBuilder(); + + stringBuilder.append("------------- REQUEST -------------\n"); + stringBuilder.append("URL: ") + .append(uri).append("\n") + .append("Method: ").append(requestSpec.getMethod()).append("\n"); + requestSpec.getHeaders().asList().forEach(header -> stringBuilder + .append("Header: ") + .append(replaceVarsIfPresent(header.getName())) + .append("=") + .append(replaceVarsIfPresent(header.getValue())) + .append("\n")); + requestSpec.getQueryParams().forEach((k, v) -> stringBuilder + .append("Query: ") + .append(replaceVarsIfPresent(k)) + .append("=") + .append(replaceVarsIfPresent(v)) + .append("\n")); + stringBuilder.append("Request Body: \n") + .append(replaceVarsIfPresent(requestSpec.getBody())) + .append("\n"); + stringBuilder.append("------------- RESPONSE -------------\n"); + stringBuilder.append("Status code: ") + .append(response.statusCode()) + .append("\n"); + stringBuilder.append("Response Body: \n") + .append(response.getBody().asPrettyString()); + System.out.println(stringBuilder); + return response; + } +} diff --git a/src/main/java/ru/lanit/at/api/models/RequestModel.java b/src/main/java/ru/lanit/at/api/models/RequestModel.java new file mode 100644 index 0000000..4ae131d --- /dev/null +++ b/src/main/java/ru/lanit/at/api/models/RequestModel.java @@ -0,0 +1,41 @@ +package ru.lanit.at.api.models; + +import com.google.gson.GsonBuilder; + +public class RequestModel { + private String method; + private String body; + private String path; + private String url; + + public RequestModel(String method, String body, String path, String url) { + this.method = method; + this.body = body; + this.path = path; + this.url = url; + } + + public String getMethod() { + return method; + } + + public String getBody() { + return body; + } + + public String getPath() { + return path; + } + + public String getUrl() { + return url; + } + + @Override + public String toString() { + return new GsonBuilder() + .setPrettyPrinting() + .create() + .toJson(this); + } +} diff --git a/src/main/java/ru/lanit/at/api/properties/RestConfigurations.java b/src/main/java/ru/lanit/at/api/properties/RestConfigurations.java new file mode 100644 index 0000000..5e86dc6 --- /dev/null +++ b/src/main/java/ru/lanit/at/api/properties/RestConfigurations.java @@ -0,0 +1,17 @@ +package ru.lanit.at.api.properties; + +import org.aeonbits.owner.Config; + +@Config.LoadPolicy(Config.LoadType.MERGE) +@Config.Sources({ + "classpath:config/configuration.properties", + "system:properties", + "system:env" +}) +public interface RestConfigurations extends Config { + + @Key("baseUrl") + @DefaultValue("") + String getBaseUrl(); + +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/aspects/FragmentsAspect.java b/src/main/java/ru/lanit/at/aspects/FragmentsAspect.java new file mode 100644 index 0000000..f41eaef --- /dev/null +++ b/src/main/java/ru/lanit/at/aspects/FragmentsAspect.java @@ -0,0 +1,54 @@ +package ru.lanit.at.aspects; + +import io.cucumber.core.gherkin.Feature; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.testng.asserts.Assertion; +import ru.lanit.at.corecommonstep.fragment.FragmentReplacer; +import ru.lanit.at.corecommonstep.fragment.GherkinSerializer; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +@Aspect +public class FragmentsAspect { + + /** + * замена фрагментов и данных в фиче + * + * @param features список фич + * @param assertion assert + * @return список фич + * @throws IOException + * @throws IllegalAccessException + */ + public static List replaceSteps(List features, Assertion assertion) throws IOException, IllegalAccessException { + features = features.stream() + .filter(cucumberFeature -> cucumberFeature.getSource() != null) + .collect(Collectors.toList()); + + FragmentReplacer fragmentReplacer = new FragmentReplacer(features, assertion); + fragmentReplacer.replace(); + features = new GherkinSerializer().reserializeFeatures(features, assertion); + return features; + } + + @Pointcut("execution(* io.cucumber.core.runtime.FeaturePathFeatureSupplier.get(..))") + public void cucumberFeatures() { + } + + /** + * тут используется строгий assert + * + * @param joinPoint + * @return + * @throws Throwable + */ + @Around("cucumberFeatures()") + public Object replaceSteps(ProceedingJoinPoint joinPoint) throws Throwable { + return replaceSteps((List) joinPoint.proceed(), new Assertion()); + } +} diff --git a/src/main/java/ru/lanit/at/assertion/AssertErrorType.java b/src/main/java/ru/lanit/at/assertion/AssertErrorType.java new file mode 100644 index 0000000..311487a --- /dev/null +++ b/src/main/java/ru/lanit/at/assertion/AssertErrorType.java @@ -0,0 +1,17 @@ +package ru.lanit.at.assertion; + +public enum AssertErrorType { + + SOFT_ASSERT("SoftAssert"), + CRITICAL_ASSERT("CriticalAssert"); + + private String name; + + AssertErrorType(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/assertion/AssertsManager.java b/src/main/java/ru/lanit/at/assertion/AssertsManager.java new file mode 100644 index 0000000..ba6115c --- /dev/null +++ b/src/main/java/ru/lanit/at/assertion/AssertsManager.java @@ -0,0 +1,62 @@ +package ru.lanit.at.assertion; + + +import org.aeonbits.owner.ConfigFactory; +import ru.lanit.at.utils.web.properties.Configurations; + +public class AssertsManager { + private static final ThreadLocal assertsManagerThreadLocal = new ThreadLocal<>(); + private static final Configurations conf = ConfigFactory.create(Configurations.class, System.getProperties(), + System.getenv()); + private ExtendedAssert asserts; + + public synchronized static AssertsManager getAssertsManager() { + AssertsManager localInstance = assertsManagerThreadLocal.get(); + if (localInstance == null) { + synchronized (AssertsManager.class) { + localInstance = assertsManagerThreadLocal.get(); + if (localInstance == null) { + AssertsManager assertsManager = new AssertsManager(); + assertsManager.setAsserts(new ExtendedAssert()); + assertsManagerThreadLocal.set(assertsManager); + localInstance = assertsManagerThreadLocal.get(); + } + } + } + return localInstance; + + } + + + /** + * Method to get {@link ExtendedAssert} previously marked as critical. So such assert will fail all test. + * + * @return Critical {@link ExtendedAssert}. + */ + public ExtendedAssert criticalAssert() { + asserts.setCritical(); + return asserts; + } + + /** + * Method to get soft {@link ExtendedAssert} that collects assertions. + * + * @return Soft {@link ExtendedAssert}. + */ + public ExtendedAssert softAssert() { + return asserts; + } + + /** + * Recreates instance of {@link ExtendedAssert}. + */ + public void flushAsserts() { + asserts.flush(); + } + + private void setAsserts(ExtendedAssert asserts) { + this.asserts = asserts; + } + + +} diff --git a/src/main/java/ru/lanit/at/assertion/ExtendedAssert.java b/src/main/java/ru/lanit/at/assertion/ExtendedAssert.java new file mode 100644 index 0000000..3fbd7ce --- /dev/null +++ b/src/main/java/ru/lanit/at/assertion/ExtendedAssert.java @@ -0,0 +1,69 @@ +package ru.lanit.at.assertion; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.asserts.IAssert; +import org.testng.asserts.SoftAssert; +import org.testng.collections.Maps; +import ru.lanit.at.utils.allure.AllureHelper; + +import java.util.Map; + + +public class ExtendedAssert extends SoftAssert { + private final static Logger LOGGER = LoggerFactory.getLogger(ExtendedAssert.class); + private final Map> m_errors = Maps.newLinkedHashMap(); + private Boolean isCritical = false; + + + public void setCritical() { + isCritical = true; + } + + + @Override + protected void doAssert(IAssert a) { + onBeforeAssert(a); + try { + a.doAssert(); + onAssertSuccess(a); + LOGGER.debug("Успешно проверено: [{}]", a.getActual()); + } catch (AssertionError ex) { + LOGGER.error(ex.getMessage()); + onAssertFailure(a, ex); + m_errors.put(ex, a); + AllureHelper.setStepStatusBroken("SoftAssert:" + ex.getMessage()); + if (isCritical) { + this.assertAll(); + } + } finally { + this.isCritical = false; + onAfterAssert(a); + } + } + + @Override + public void assertAll() { + if (!m_errors.isEmpty()) { + StringBuilder sb = new StringBuilder("The following asserts failed:"); + boolean first = true; + for (Map.Entry> ae : m_errors.entrySet()) { + if (first) { + first = false; + } else { + sb.append(","); + } + sb.append("\n\t"); + sb.append(ae.getKey().getMessage()); + ae.getKey().printStackTrace(); + } + if (isCritical) sb.append(" [BLOCKER]"); + m_errors.clear(); + throw new AssertionError(sb.toString()); + } + } + + public void flush() { + m_errors.clear(); + } +} diff --git a/src/main/java/ru/lanit/at/corecommonstep/CommonSteps.java b/src/main/java/ru/lanit/at/corecommonstep/CommonSteps.java new file mode 100644 index 0000000..13ec927 --- /dev/null +++ b/src/main/java/ru/lanit/at/corecommonstep/CommonSteps.java @@ -0,0 +1,23 @@ +package ru.lanit.at.corecommonstep; + +import io.cucumber.java.ru.И; +import ru.lanit.at.corecommonstep.fragment.FragmentReplacer; + + +public class CommonSteps { + + @И(FragmentReplacer.REGEX_FRAGMENT) + public void userInsertsFragment(String fragmentName) { + throw new IllegalStateException("фрагмент не подставился"); + } + + /** + * Шаг-заглушка для отчета аллюра и группировки шагов фрагмента под спойлер + * Если будет использоваться в тесте - будет assert "надо использовать другой шаг" + * @param fragmentName - название фрагмента (сценария) + */ + @И(FragmentReplacer.REGEX_FRAGMENT_SPOILER) + public void fragment(String fragmentName) { + } + +} diff --git a/src/main/java/ru/lanit/at/corecommonstep/fragment/CustomGherkinMessagesDataTableArgument.java b/src/main/java/ru/lanit/at/corecommonstep/fragment/CustomGherkinMessagesDataTableArgument.java new file mode 100644 index 0000000..95f8721 --- /dev/null +++ b/src/main/java/ru/lanit/at/corecommonstep/fragment/CustomGherkinMessagesDataTableArgument.java @@ -0,0 +1,27 @@ +package ru.lanit.at.corecommonstep.fragment; + +import io.cucumber.core.gherkin.DataTableArgument; + +import java.util.List; + +/* пришлось написать свою реализацию класса io.cucumber.core.gherkin.messages.GherkinMessagesDataTableArgument */ +public class CustomGherkinMessagesDataTableArgument implements DataTableArgument { + + private List> cells; + private int line; + + public CustomGherkinMessagesDataTableArgument(List> cells, int line) { + this.cells = cells; + this.line = line; + } + + @Override + public List> cells() { + return cells; + } + + @Override + public int getLine() { + return line; + } +} diff --git a/src/main/java/ru/lanit/at/corecommonstep/fragment/CustomGherkinMessagesDocStringArgument.java b/src/main/java/ru/lanit/at/corecommonstep/fragment/CustomGherkinMessagesDocStringArgument.java new file mode 100644 index 0000000..0d0819e --- /dev/null +++ b/src/main/java/ru/lanit/at/corecommonstep/fragment/CustomGherkinMessagesDocStringArgument.java @@ -0,0 +1,38 @@ +package ru.lanit.at.corecommonstep.fragment; + +import io.cucumber.core.gherkin.DocStringArgument; + +public class CustomGherkinMessagesDocStringArgument implements DocStringArgument { + + private String content; + private String contentType; + private String mediaType; + private int line; + + public CustomGherkinMessagesDocStringArgument(String content, String contentType, String mediaType, int line) { + this.content = content; + this.contentType = contentType; + this.mediaType = mediaType; + this.line = line; + } + + @Override + public String getContent() { + return content; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public String getMediaType() { + return mediaType; + } + + @Override + public int getLine() { + return line; + } +} diff --git a/src/main/java/ru/lanit/at/corecommonstep/fragment/CustomStep.java b/src/main/java/ru/lanit/at/corecommonstep/fragment/CustomStep.java new file mode 100644 index 0000000..9876d05 --- /dev/null +++ b/src/main/java/ru/lanit/at/corecommonstep/fragment/CustomStep.java @@ -0,0 +1,116 @@ +package ru.lanit.at.corecommonstep.fragment; + +import io.cucumber.core.gherkin.Argument; +import io.cucumber.core.gherkin.Step; +import io.cucumber.core.gherkin.StepType; +import io.cucumber.gherkin.GherkinDialect; +import io.cucumber.plugin.event.DataTableArgument; +import io.cucumber.plugin.event.DocStringArgument; +import io.cucumber.plugin.event.Location; + +import java.util.List; + + +public class CustomStep implements Step { + + private final Argument argument; + private final String keyWord; + private final StepType stepType; + private final String previousGwtKeyWord; + private final Location location; + private final String id; + private final String text; + + public CustomStep( + String id, + String text, + List> argument, + Class argumentType, + GherkinDialect dialect, + String previousGwtKeyWord, + Location location, + String keyword + ) { + this.id = id; + this.text = text; + this.argument = extractArgument(argument, argumentType, location); + this.keyWord = keyword; + this.stepType = extractKeyWordType(keyWord, dialect); + this.previousGwtKeyWord = previousGwtKeyWord; + this.location = location; + } + + private static Argument extractArgument(List> argument, Class argumentType, Location location) { + if (!argument.isEmpty()) { + if (DataTableArgument.class.equals(argumentType)) { + return new CustomGherkinMessagesDataTableArgument(argument, location.getLine() + 1); + } else if (DocStringArgument.class.equals(argumentType)) { + return new CustomGherkinMessagesDocStringArgument(argument.get(0).get(0), "", "", location.getLine() + 1); + } + throw new IllegalStateException(String.format("Неожиданный тип значения: %s.\n", argumentType.getName())); + } + return null; + } + + private static StepType extractKeyWordType(String keyWord, GherkinDialect dialect) { + if (StepType.isAstrix(keyWord)) { + return StepType.OTHER; + } + if (dialect.getGivenKeywords().contains(keyWord)) { + return StepType.GIVEN; + } + if (dialect.getWhenKeywords().contains(keyWord)) { + return StepType.WHEN; + } + if (dialect.getThenKeywords().contains(keyWord)) { + return StepType.THEN; + } + if (dialect.getAndKeywords().contains(keyWord)) { + return StepType.AND; + } + if (dialect.getButKeywords().contains(keyWord)) { + return StepType.BUT; + } + throw new IllegalStateException("Keyword " + keyWord + " was neither given, when, then, and, but nor *"); + } + + @Override + public StepType getType() { + return stepType; + } + + @Override + public String getPreviousGivenWhenThenKeyword() { + return previousGwtKeyWord; + } + + @Override + public String getId() { + return id; + } + + @Override + public Argument getArgument() { + return argument; + } + + @Override + public String getKeyword() { + return keyWord; + } + + @Override + public String getText() { + return text; + } + + @Override + public int getLine() { + return location.getLine(); + } + + @Override + public Location getLocation() { + return new Location(location.getLine(), location.getColumn()); + } +} diff --git a/src/main/java/ru/lanit/at/corecommonstep/fragment/FragmentReplacer.java b/src/main/java/ru/lanit/at/corecommonstep/fragment/FragmentReplacer.java new file mode 100644 index 0000000..2e7e228 --- /dev/null +++ b/src/main/java/ru/lanit/at/corecommonstep/fragment/FragmentReplacer.java @@ -0,0 +1,425 @@ +package ru.lanit.at.corecommonstep.fragment; + +import com.google.common.graph.EndpointPair; +import com.google.common.graph.MutableValueGraph; +import com.google.common.graph.ValueGraphBuilder; +import io.cucumber.core.feature.FeatureParser; +import io.cucumber.core.feature.FeatureWithLines; +import io.cucumber.core.gherkin.Feature; +import io.cucumber.core.gherkin.Pickle; +import io.cucumber.core.gherkin.Step; +import io.cucumber.gherkin.GherkinDialect; +import io.cucumber.gherkin.GherkinDialectProvider; +import io.cucumber.gherkin.IGherkinDialectProvider; +import io.cucumber.plugin.event.DataTableArgument; +import io.cucumber.plugin.event.DocStringArgument; +import io.cucumber.plugin.event.Location; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.testng.asserts.Assertion; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + + +public class FragmentReplacer { + + public static final String NOT_FOUND_VARS_FOR_DYNAMIC_FRAGMENTS = "Следующие переменные не были переданы в динамический фрагмент из сценария '%s' для шага '%s': %s\n"; + public static final String FOUND_MORE_ONE_FRAGMENT = "Найдено более одного фрагмента с наименованием и путем:\n%s"; + public static final String FRAGMENT_NOT_EXIST = "Нет такого фрагмента с названием - '%s'."; + public static final String LANGUAGE_IS_NOT_SAME = "Язык фрагмента не совпадает с языком сценария! Сценарий: '%s', Фрагмент: '%s'"; + + public static final String REGEX_FRAGMENT = "^вызвать фрагмент \"(.+)\"$"; + public static final String REGEX_FRAGMENT_SPOILER = "^ФРАГМЕНТ \"(.+)\"$"; + public static final String FEATURE_SUFFIX = ".feature"; + public static final String FRAGMENT_TAG = "@fragment"; + private static final String REGEX_VALUE = "<%s>"; + private static final String REGEX_EXAMPLE = "<(.*)>"; + + private List features; + private Map scenarioLanguageMap; + private MutableValueGraph fragmentsGraph; + private Assertion assertion; + + public FragmentReplacer(List features, Assertion assertion) throws IOException { + this.assertion = assertion; + this.features = cacheFragmentsToFeatures(features); + this.scenarioLanguageMap = cacheScenarioLanguage(this.features); + Map fragmentsMap = cacheFragmentsAsMap(this.features); + this.fragmentsGraph = cacheFragmentsAsGraph(this.features, fragmentsMap, this.scenarioLanguageMap); + } + + public static Boolean getMatch(String sentence, String regex) { + if (sentence != null && regex != null) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(sentence); + return matcher.find(); + } + return null; + } + + public static String getMatchValueByGroupNumber(String sentence, String regex, int groupNumber) { + if (sentence != null && regex != null) { + sentence = sentence.trim(); + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(sentence); + if (matcher.find()) { + return matcher.group(groupNumber); + } + } + return null; + } + + public void replace() throws IllegalAccessException { + while (!fragmentsGraph.edges().isEmpty()) { + int fragmentsGraphSize = fragmentsGraph.edges().size(); + + for (EndpointPair edge : new ArrayList<>(fragmentsGraph.edges())) { + Pickle fragment = (Pickle) edge.nodeV(); + Pickle scenario = (Pickle) edge.nodeU(); + + if (isTerminal(fragment)) { + replaceFragmentInScenario(scenario, fragment); + fragmentsGraph.removeEdge(scenario, fragment); + } + } + + if (fragmentsGraphSize == fragmentsGraph.edges().size()) { + throw new AssertionError("Fragments replacing is no longer performed, it will lead to an infinite loop. Interrupting..."); + } + } + } + + /** + * загрузка фича файлов из папки с фрагментами + * + * @return лист Feature файлов + * @throws IOException + */ + private List cacheFragmentsToFeatures(List features) throws IOException { + List featurePaths = new ArrayList(); + StringBuilder pathBuilder = getParentPath("fragments"); + + List collect = Files.walk(Paths.get(pathBuilder.toString())) + .filter(p -> p.toAbsolutePath().toString().endsWith(FEATURE_SUFFIX)) + .collect(Collectors.toList()); + collect.forEach(p -> { + featurePaths.add(FeatureWithLines.create(p.toUri(), Collections.emptyList())); + }); + + List uriList = featurePaths.stream().map(p -> p.uri()).collect(Collectors.toList()); + List loadedFeaturesList = new ArrayList<>(); + FeatureParser featureParser = new FeatureParser(() -> UUID.randomUUID()); + + Iterator iterator = uriList.iterator(); + while (iterator.hasNext()) { + URI uri = iterator.next(); + StringBuilder stringBuilder = new StringBuilder(); + Files.readAllLines(Paths.get(uri)).forEach(p -> stringBuilder.append(p).append("\n")); + GherkinResource gherkinResource = new GherkinResource(stringBuilder.toString(), uri); + Optional feature = featureParser.parseResource(gherkinResource); + feature.ifPresent(loadedFeaturesList::add); + } + + return Stream.concat(features.stream(), loadedFeaturesList.stream()).collect(Collectors.toList()); + } + + private Map cacheScenarioLanguage(List features) { + Map scenarioLanguageMap = new HashMap<>(); + for (Feature feature : features) { + List pickles = feature.getPickles(); + for (Pickle pickle : pickles) { + scenarioLanguageMap.put(pickle, pickle.getLanguage()); + } + } + return scenarioLanguageMap; + } + + private Map cacheFragmentsAsMap(List features) { + Map fragments = new HashMap<>(); + + List collect = features.stream().map(Feature::getPickles).flatMap(List::stream).collect(Collectors.toList()); + + for (Pickle pickle : collect) { + if (isFragmentTagContains(pickle.getTags())) { + List pickles = collect.stream() + .filter(p -> isFragmentTagContains(p.getTags())) + .filter(p -> p.getName().equalsIgnoreCase(pickle.getName())) + .collect(Collectors.toList()); + if (pickles.size() > 1) { + StringBuilder stringBuilder = new StringBuilder(); + pickles.forEach(p -> stringBuilder.append(p.getName()).append(", ").append(p.getUri().getPath()).append(";\n")); + assertion.fail(String.format(FOUND_MORE_ONE_FRAGMENT, stringBuilder.toString())); + } + } + } + + for (Feature feature : features) { + List pickles = feature.getPickles(); + for (Pickle pickle : pickles) { + List tags = pickle.getTags(); + if (isFragmentTagContains(tags)) { + fragments.put(pickle.getName(), pickle); + } + } + } + + return fragments; + } + + private MutableValueGraph cacheFragmentsAsGraph(List features, + Map fragmentsMap, + Map scenarioLanguageMap) { + MutableValueGraph graph = ValueGraphBuilder.directed().allowsSelfLoops(false).build(); + + for (Feature feature : features) { + List pickleList = feature.getPickles().stream().filter(pickle1 -> isScenario(pickle1)).collect(Collectors.toList()); + + for (Pickle pickle : pickleList) { + addGraphNode(graph, pickle, fragmentsMap, scenarioLanguageMap); + } + } + + return graph; + } + + private void addGraphNode(MutableValueGraph graph, + Pickle scenario, + Map fragmentsMap, + Map scenarioLanguageMap) { + graph.addNode(scenario); + String scenarioLanguage = scenarioLanguageMap.get(scenario); + List steps = scenario.getSteps(); + + for (Step step : steps) { + if (getMatch(step.getText(), REGEX_FRAGMENT_SPOILER)) { + assertion.fail(String.format("Для использования фрагментов используйте другой шаг: %s\n Путь к фича-файлу: %s", REGEX_FRAGMENT, scenario.getUri().getPath())); + } + String fragmentName = getFragmentName(step); + if (fragmentName != null) { + String fragmentNameFromMap = fragmentsMap.keySet().stream() + .filter(key -> key.equalsIgnoreCase(fragmentName)) + .findAny().orElse(null); + Pickle pickle = fragmentsMap.get(fragmentNameFromMap); + + if (pickle == null) { + // пофиг на данные @data + assertion.fail(String.format(FRAGMENT_NOT_EXIST, fragmentName)); + } else { + String fragmentLanguage = pickle.getLanguage(); + assertion.assertEquals(scenarioLanguage, fragmentLanguage, String.format(LANGUAGE_IS_NOT_SAME, scenarioLanguage, fragmentLanguage)); + + graph.putEdgeValue(scenario, pickle, ""); + + addGraphNode(graph, pickle, fragmentsMap, scenarioLanguageMap); + } + } + } + } + + private boolean isFragmentTagContains(List tags) { + return tags.stream().anyMatch(tag -> tag.equals(FRAGMENT_TAG)); + } + + private boolean isScenario(Pickle pickle) { + List tags = pickle.getTags(); + return tags.stream().noneMatch(tag -> tag.equals(FRAGMENT_TAG)); + } + + private boolean isTerminal(Object node) { + for (EndpointPair edge : fragmentsGraph.edges()) { + if (edge.nodeU().equals(node)) { + return false; + } + } + + return true; + } + + private void replaceFragmentInScenario(Pickle scenario, Pickle fragment) throws IllegalAccessException { + List replacementSteps = new ArrayList<>(); + + for (Step step : scenario.getSteps()) { + String fragmentName = getFragmentName(step); + if (fragmentName != null && !fragmentName.isEmpty() && fragmentName.equalsIgnoreCase(fragment.getName())) { + Step mockStep = getMockStep(step, fragment); + replacementSteps.add(mockStep); + replacementSteps.addAll(replaceSteps(step, fragment.getSteps(), fragment.getLanguage(), scenario)); + replacementSteps.add(mockStep); + } else { + replacementSteps.add(step); + } + } + FieldUtils.writeField(scenario, "steps", replacementSteps, true); + } + + /** + * Генерирует шаг-заглушки, который в аллюр отчете подменяется на спойлер + * @param step - шаг "вызвать фрагмент" + * @param fragment - сценарий-фрагмент + * @return - шаг ФРАГМЕНТ "название фрагмента" + */ + private Step getMockStep(Step step, Pickle fragment) { + IGherkinDialectProvider dialectProvider = new GherkinDialectProvider(); + GherkinDialect dialect = dialectProvider.getDialect(fragment.getLanguage(), null); + Location location = step.getLocation(); + return new CustomStep( + step.getId(), + String.format("ФРАГМЕНТ \"%s\"", fragment.getName()), + new ArrayList<>(), + Object.class, + dialect, + step.getPreviousGivenWhenThenKeyword(), + new Location(location.getLine(), location.getColumn()), + step.getKeyword()); + } + + private String getFragmentName(Step step) { + return getMatchValueByGroupNumber(step.getText(), REGEX_FRAGMENT, 1); + } + + private List replaceSteps(Step scenarioStep, List fragmentsSteps, String language, Pickle scenario) { + DataTableArgument argument = (DataTableArgument) scenarioStep.getArgument(); + List replacementSteps = new ArrayList<>(); + + if (argument != null) { + Class argumentType = Object.class; + List> lines = argument.cells(); + checkArguments(lines, scenario); + for (Step fragmentStep : fragmentsSteps) { + List> replaceFragmentLines = new ArrayList<>(); + if (fragmentStep.getArgument() instanceof DataTableArgument) { + argumentType = DataTableArgument.class; + /* замена данных в таблице если она есть у шага */ + DataTableArgument fragmentStepArgument = (DataTableArgument) fragmentStep.getArgument(); + if (fragmentStepArgument != null) { + for (List fragmentLines : fragmentStepArgument.cells()) { + for (List line : lines) { + String regex = String.format(REGEX_VALUE, line.get(0)); + fragmentLines = fragmentLines.stream().map(p -> p.replace(regex, line.get(1))).collect(Collectors.toList()); + } + StringBuilder values = new StringBuilder(); + for (String fragmentLine : fragmentLines) { + if (getMatch(fragmentLine, REGEX_EXAMPLE)) { + values.append(getMatchValueByGroupNumber(fragmentLine, REGEX_EXAMPLE, 1)).append(" "); + } + } + if (values.length() != 0) { + assertion.fail( + String.format(NOT_FOUND_VARS_FOR_DYNAMIC_FRAGMENTS, + scenario.getName(), + fragmentStep.getText(), + values.toString())); + } + replaceFragmentLines.add(fragmentLines); + } + } + } else if (fragmentStep.getArgument() instanceof DocStringArgument) { + argumentType = DocStringArgument.class; + /* замена данных в DocStringArgument если есть у шага */ + DocStringArgument fragmentStepArgument = (DocStringArgument) fragmentStep.getArgument(); + if (fragmentStepArgument != null) { + String content = fragmentStepArgument.getContent(); + for (List line : lines) { + String regex = String.format(REGEX_VALUE, line.get(0)); + content = content.replace(regex, line.get(1)); + } + checkVarsForDynamicFragments(content, scenario, fragmentStep); + replaceFragmentLines.add(Collections.singletonList(content)); + } + } + + /* замена в текстовке шага */ + String replaceValue = fragmentStep.getText(); + for (List line : lines) { + String regex = String.format(REGEX_VALUE, line.get(0)); + if (getMatch(fragmentStep.getText(), regex)) { + replaceValue = replaceValue.replace(regex, line.get(1)); + } + } + checkVarsForDynamicFragments(replaceValue, scenario, fragmentStep); + + IGherkinDialectProvider dialectProvider = new GherkinDialectProvider(); + GherkinDialect dialect = dialectProvider.getDialect(language, null); + Location location = fragmentStep.getLocation(); + Step replaceStep = new CustomStep( + fragmentStep.getId(), + replaceValue, + replaceFragmentLines, + argumentType, + dialect, + fragmentStep.getPreviousGivenWhenThenKeyword(), + new Location(location.getLine(), location.getColumn()), + fragmentStep.getKeyword()); + replacementSteps.add(replaceStep); + } + + return replacementSteps; + } else { + return fragmentsSteps; + } + } + + private void checkArguments(List> lines, Pickle scenario) { + lines.forEach(line -> + assertion.assertEquals( + line.size(), + 2, + String.format("Количество значений в таблице не равно двум. Актуальное значение: %s. Путь '%s', наименование сценария '%s'.", + line.size(), + scenario.getUri().getPath(), + scenario.getName() + ) + ) + ); + } + + private void checkVarsForDynamicFragments(String value, Pickle scenario, Step fragmentStep) { + if (getMatch(value, REGEX_EXAMPLE)) { + assertion.fail( + String.format(NOT_FOUND_VARS_FOR_DYNAMIC_FRAGMENTS, + scenario.getName(), + fragmentStep.getText(), + getMatchValueByGroupNumber(value, REGEX_EXAMPLE, 1))); + } + } + + /** + * метод для составления полного пути до директории или файла + * + * @param packages массив наименований папок или файла (в конце) + * @return полный путь до директории или файла + */ + private StringBuilder getParentPath(String... packages) { + StringBuilder pathBuilder = new StringBuilder(); + String separator = System.getProperty("file.separator"); + if (!System.getProperty("base.dir", "").isEmpty()) { + pathBuilder.append(System.getProperty("base.dir")); + } else { + pathBuilder.append(System.getProperty("user.dir")); + } + for (String packageName : packages) { + if (pathBuilder.toString().endsWith(separator)) { + pathBuilder.append(packageName); + } else { + pathBuilder.append(separator).append(packageName); + } + } + + return pathBuilder; + } +} diff --git a/src/main/java/ru/lanit/at/corecommonstep/fragment/GherkinResource.java b/src/main/java/ru/lanit/at/corecommonstep/fragment/GherkinResource.java new file mode 100644 index 0000000..546f004 --- /dev/null +++ b/src/main/java/ru/lanit/at/corecommonstep/fragment/GherkinResource.java @@ -0,0 +1,30 @@ +package ru.lanit.at.corecommonstep.fragment; + +import io.cucumber.core.resource.Resource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.Charset; + +public class GherkinResource implements Resource { + + private final URI path; + private InputStream source; + + public GherkinResource(String source, URI path) { + this.source = new ByteArrayInputStream(source.getBytes(Charset.defaultCharset())); + this.path = path; + } + + @Override + public URI getUri() { + return path; + } + + @Override + public InputStream getInputStream() throws IOException { + return source; + } +} diff --git a/src/main/java/ru/lanit/at/corecommonstep/fragment/GherkinSerializer.java b/src/main/java/ru/lanit/at/corecommonstep/fragment/GherkinSerializer.java new file mode 100644 index 0000000..4430e37 --- /dev/null +++ b/src/main/java/ru/lanit/at/corecommonstep/fragment/GherkinSerializer.java @@ -0,0 +1,146 @@ +package ru.lanit.at.corecommonstep.fragment; + +import io.cucumber.core.feature.FeatureParser; +import io.cucumber.core.gherkin.Feature; +import io.cucumber.core.gherkin.Pickle; +import io.cucumber.core.gherkin.Step; +import io.cucumber.core.resource.Resource; +import io.cucumber.plugin.event.DataTableArgument; +import io.cucumber.plugin.event.DocStringArgument; +import org.testng.asserts.Assertion; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +public class GherkinSerializer { + + public static final String DATA_TAG = "@data=$"; + private static final String NL = "\n"; + private static final String SPACE = " "; + private StringBuilder builder; + private Assertion assertion; + + public GherkinSerializer() { + builder = new StringBuilder(); + } + + public List reserializeFeatures(List cucumberFeatures, Assertion assertion) { + this.assertion = assertion; + FeatureParser featureParser = new FeatureParser(() -> UUID.randomUUID()); + List parsedFeatures = new ArrayList<>(); + cucumberFeatures.forEach(cucumberFeature -> { + builder = new StringBuilder(); + List pickles = cucumberFeature.getPickles(); + if (pickles.size() > 0) { + builder.append("#language: " + pickles.get(0).getLanguage()); + } else { + this.assertion.fail(String.format("\nПроверьте синтаксис фича-файла!:\n%s\n", cucumberFeature.getUri())); + } + nl(1); + Optional featureKeyword = cucumberFeature.getKeyword(); + Optional featureName = cucumberFeature.getName(); + if (featureKeyword.isPresent()) { + builder.append(featureKeyword.get()).append(":").append(SPACE); + } else { + this.assertion.fail("\"Нет кейворда Функционал!\""); + } + + featureName.ifPresent(name -> builder.append(name)); + nl(1); + pickles.forEach(this::buildScenario); + + Resource gherkinResource = new GherkinResource(builder.toString(), cucumberFeature.getUri()); + Optional feature = featureParser.parseResource(gherkinResource); + feature.ifPresent(parsedFeatures::add); + }); + + return parsedFeatures; + } + + private void buildScenario(Pickle pickle) { + buildScenarioTags(pickle); + + tab(1); + builder.append(pickle.getKeyword()).append(":").append(SPACE).append(pickle.getName()); + nl(1); + + List dataTags = pickle.getTags().stream().filter(p -> p.startsWith(DATA_TAG)).collect(Collectors.toList()); + if (!dataTags.isEmpty() && dataTags.size() > 1) { + assertion.fail(String.format("Есть два тега '%s' в сценарии '%s'. Необходимо указать только один тег для данных!\n", dataTags.toString(), pickle.getName())); + } + + if (!dataTags.isEmpty()) { + pickle.getSteps().forEach(step -> buildStep(step, dataTags.get(0), pickle)); + } else { + pickle.getSteps().forEach(step -> buildStep(step, null, pickle)); + } + } + + private void buildScenarioTags(Pickle scenarioDefinition) { + List tags = scenarioDefinition.getTags(); + if (!tags.isEmpty()) { + tags = tags.stream().filter(p -> !p.startsWith(DATA_TAG)).collect(Collectors.toList()); + tab(1); + tags.forEach(tag -> builder.append(tag).append(SPACE)); + nl(1); + } + } + + private void buildStep(Step step, String dataTag, Pickle scenario) { + tab(2); + builder.append(step.getKeyword()).append(SPACE).append(step.getText()); + if (step.getArgument() != null) { + nl(1); + if (step.getArgument() instanceof DataTableArgument) { + DataTableArgument table = (DataTableArgument) step.getArgument(); + table.cells().forEach(tableRow -> buildTableRow(tableRow, dataTag, scenario)); + } else if (step.getArgument() instanceof DocStringArgument) { + tab(2); + builder.append("\"\"\""); + nl(1); + DocStringArgument docString = (DocStringArgument) step.getArgument(); + tab(2); + builder.append(docString.getContent()); + nl(1); + tab(2); + builder.append("\"\"\""); + nl(1); + } + + } else { + builder.append("\n"); + } + } + + private void buildTableRow(List tableRow, String dataTag, Pickle scenario) { + List collect = tableRow.stream() + .map(tableCell -> tableCell.replaceAll("\\|", "\\\\|")) + .collect(Collectors.toList()); + tab(2); + space(2); + builder.append("|").append(String.join("|", collect)).append("|"); + nl(1); + } + + + private void space(int count) { + appendTimes(SPACE, count); + } + + private void nl(int count) { + appendTimes(NL, count); + } + + private void tab(int count) { + appendTimes(SPACE, count * 2); + } + + private void appendTimes(String source, int times) { + for (int i = 0; i < times; i++) { + builder.append(source); + } + } +} diff --git a/src/main/java/ru/lanit/at/hooks/WebHooks.java b/src/main/java/ru/lanit/at/hooks/WebHooks.java new file mode 100644 index 0000000..76d0e35 --- /dev/null +++ b/src/main/java/ru/lanit/at/hooks/WebHooks.java @@ -0,0 +1,50 @@ +package ru.lanit.at.hooks; + +import com.codeborne.selenide.WebDriverRunner; +import io.cucumber.java.After; +import io.cucumber.java.Before; +import io.cucumber.java.Scenario; +import org.aeonbits.owner.ConfigFactory; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.lanit.at.utils.VideoSaveHelper; +import ru.lanit.at.utils.web.properties.Configurations; + +import static ru.lanit.at.assertion.AssertsManager.getAssertsManager; + +public class WebHooks { + private static final Logger LOGGER = LoggerFactory.getLogger(WebHooks.class); + + @Before + public void setup(Scenario scenario) { + } + + @After + public void close() { + if (WebDriverRunner.hasWebDriverStarted()) { + String sessionId = ((RemoteWebDriver) WebDriverRunner.getWebDriver()).getSessionId().toString(); + LOGGER.info("Закрытие сессии драйвера"); + WebDriverRunner.closeWebDriver(); + attachVideo(sessionId); + } + + getAssertsManager().softAssert().assertAll(); + getAssertsManager().softAssert().flush(); + } + + + /** Прикрепление видео в Аллюр отчет при условии запускать с параметром remoteUrl. */ + private void attachVideo(String sessionId) { + Configurations cf = ConfigFactory.create(Configurations.class + , System.getProperties(), + System.getenv()); + if (!cf.getRemoteURL().isEmpty() && cf.getEnableVideo()) { + VideoSaveHelper vs = new VideoSaveHelper(sessionId, cf.getRemoteURL()); + vs.attachVideoFileRest(); + vs.deleteSelenoidVideoRest(); + } + } + + +} diff --git a/src/main/java/ru/lanit/at/pages/GooglePage.java b/src/main/java/ru/lanit/at/pages/GooglePage.java new file mode 100644 index 0000000..e4c64aa --- /dev/null +++ b/src/main/java/ru/lanit/at/pages/GooglePage.java @@ -0,0 +1,17 @@ +package ru.lanit.at.pages; + +import com.codeborne.selenide.SelenideElement; +import ru.lanit.at.utils.web.annotations.Name; +import ru.lanit.at.utils.web.pagecontext.WebPage; + +import static com.codeborne.selenide.Selenide.$x; + +@Name(value = "Google") +public class GooglePage extends WebPage { + + @Name("поле поиска") + private SelenideElement searchField = $x("//input[@name='q']"); + + @Name("кнопка поиска") + private SelenideElement searchButton = $x("//input[@value='Поиск в Google']"); +} diff --git a/src/main/java/ru/lanit/at/pages/GoogleResultPage.java b/src/main/java/ru/lanit/at/pages/GoogleResultPage.java new file mode 100644 index 0000000..fdc2121 --- /dev/null +++ b/src/main/java/ru/lanit/at/pages/GoogleResultPage.java @@ -0,0 +1,15 @@ +package ru.lanit.at.pages; + +import com.codeborne.selenide.SelenideElement; +import org.openqa.selenium.By; +import ru.lanit.at.utils.web.annotations.Name; +import ru.lanit.at.utils.web.pagecontext.WebPage; + +import static com.codeborne.selenide.Selenide.$; + +@Name(value = "Google страница результатов") +public class GoogleResultPage extends WebPage { + + @Name("виджет погоды") + private SelenideElement searchField = $(By.id("wob_wc")); +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/steps/VariableSteps.java b/src/main/java/ru/lanit/at/steps/VariableSteps.java new file mode 100644 index 0000000..9538003 --- /dev/null +++ b/src/main/java/ru/lanit/at/steps/VariableSteps.java @@ -0,0 +1,51 @@ +package ru.lanit.at.steps; + + +import io.cucumber.java.ru.И; +import io.cucumber.java.ru.Когда; +import io.qameta.allure.Allure; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import ru.lanit.at.utils.ContextHolder; +import ru.lanit.at.utils.DataGenerator; + +import java.util.Map; + +import static ru.lanit.at.utils.ContextHolder.replaceVarsIfPresent; + +public class VariableSteps { + private Logger LOGGER = LogManager.getLogger(this.getClass()); + + /** + * Сгенерировать и Сохранить значение + * + * @param field - наименование элемента + * @param key - значение + */ + @Когда("сгенерировать значение {string} и сохранить под именем {string}") + public void generateAndSaveValue(String field, String key) { + String value = DataGenerator.generateValueByMask(field); + ContextHolder.asMap().put(key, value); + LOGGER.info("значение '{}' сохранено под именем '{}'", value, key); + } + + @И("сгенерировать переменные") + public void generateVariables(Map table) { + table.forEach((k, v) -> { + String value = DataGenerator.generateValueByMask(replaceVarsIfPresent(v)); + ContextHolder.put(k, value); + Allure.addAttachment(k, "application/json", k + ": " + value, ".txt"); + LOGGER.info("Сгенерирована переменная: {}={}", k, value); + }); + } + + @И("создать контекстные переменные") + public void createContextVariables(Map table) { + table.forEach((k, v) -> { + ContextHolder.put(k, v); + LOGGER.info("Сохранена переменная: {}={}", k, v); + }); + } + + +} diff --git a/src/main/java/ru/lanit/at/steps/api/ApiSteps.java b/src/main/java/ru/lanit/at/steps/api/ApiSteps.java new file mode 100644 index 0000000..cb4f521 --- /dev/null +++ b/src/main/java/ru/lanit/at/steps/api/ApiSteps.java @@ -0,0 +1,80 @@ +package ru.lanit.at.steps.api; + + +import io.cucumber.datatable.DataTable; +import io.cucumber.java.ru.И; +import io.qameta.allure.Allure; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import ru.lanit.at.api.ApiRequest; +import ru.lanit.at.api.models.RequestModel; +import ru.lanit.at.utils.CompareUtil; +import ru.lanit.at.utils.ContextHolder; +import ru.lanit.at.utils.VariableUtil; + +import java.util.HashMap; +import java.util.Map; + +import static ru.lanit.at.utils.ContextHolder.replaceVarsIfPresent; +import static ru.lanit.at.utils.JsonUtil.getFieldFromJson; + +public class ApiSteps { + private static final Logger LOG = LoggerFactory.getLogger(ApiSteps.class); + private ApiRequest apiRequest; + + @И("создать запрос") + public void createRequest(RequestModel requestModel) { + apiRequest = new ApiRequest(requestModel); + } + + @И("добавить header") + public void addHeaders(DataTable dataTable) { + Map headers = new HashMap<>(); + dataTable.asLists().forEach(it -> headers.put(it.get(0), replaceVarsIfPresent(it.get(1)))); + apiRequest.setHeaders(headers); + } + + @И("добавить query параметры") + public void addQuery(DataTable dataTable) { + Map query = new HashMap<>(); + dataTable.asLists().forEach(it -> query.put(it.get(0), replaceVarsIfPresent(it.get(1)))); + apiRequest.setQuery(query); + } + + @И("отправить запрос") + public void send() { + apiRequest.sendRequest(); + } + + @И("статус код {int}") + public void expectStatusCode(int code) { + int actualStatusCode = apiRequest.getResponse().statusCode(); + Assert.assertEquals(actualStatusCode, code); + } + + @И("извлечь данные") + public void extractVariables(Map vars) { + String responseBody = apiRequest.getResponse().body().asPrettyString(); + vars.forEach((k, jsonPath) -> { + jsonPath = replaceVarsIfPresent(jsonPath); + String extractedValue = VariableUtil.extractBrackets(getFieldFromJson(responseBody, jsonPath)); + ContextHolder.put(k, extractedValue); + Allure.addAttachment(k, "application/json", extractedValue, ".txt"); + LOG.info("Извлечены данные: {}={}", k, extractedValue); + }); + } + + + @И("сравнить значения") + public void compareVars(DataTable table) { + table.asLists().forEach(it -> { + String expect = replaceVarsIfPresent(it.get(0)); + String actual = replaceVarsIfPresent(it.get(2)); + boolean compareResult = CompareUtil.compare(expect, actual, it.get(1)); + Assert.assertTrue(compareResult, String.format("Ожидаемое: '%s'\nФактическое: '%s'\nОператор сравнения: '%s'\n", expect, actual, it.get(1))); + Allure.addAttachment(expect, "application/json", expect + it.get(1) + actual, ".txt"); + LOG.info("Сравнение значений: {} {} {}", expect, it.get(1), actual); + }); + } +} diff --git a/src/main/java/ru/lanit/at/steps/api/type_params/DataTableTypeContainer.java b/src/main/java/ru/lanit/at/steps/api/type_params/DataTableTypeContainer.java new file mode 100644 index 0000000..f1bf16d --- /dev/null +++ b/src/main/java/ru/lanit/at/steps/api/type_params/DataTableTypeContainer.java @@ -0,0 +1,21 @@ +package ru.lanit.at.steps.api.type_params; + + +import io.cucumber.java.DataTableType; +import ru.lanit.at.api.models.RequestModel; + +import java.util.Map; + +public class DataTableTypeContainer { + + @DataTableType + public RequestModel requestModel(Map entry) { + return new RequestModel( + entry.get("method"), + entry.get("body"), + entry.get("path"), + entry.get("url") + ); + } + +} diff --git a/src/main/java/ru/lanit/at/steps/web/AbstractWebSteps.java b/src/main/java/ru/lanit/at/steps/web/AbstractWebSteps.java new file mode 100644 index 0000000..d7347b5 --- /dev/null +++ b/src/main/java/ru/lanit/at/steps/web/AbstractWebSteps.java @@ -0,0 +1,46 @@ +package ru.lanit.at.steps.web; + +import org.aeonbits.owner.ConfigFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import ru.lanit.at.utils.ContextHolder; +import ru.lanit.at.utils.web.pagecontext.Environment; +import ru.lanit.at.utils.web.pagecontext.PageManager; +import ru.lanit.at.utils.web.pagecontext.WebPage; +import ru.lanit.at.utils.web.properties.Configurations; + +import java.util.Map; + + +public abstract class AbstractWebSteps { + protected final Configurations configurations; + protected Logger LOGGER = LogManager.getLogger(this.getClass()); + protected PageManager pageManager; + + + public AbstractWebSteps(PageManager pageManager) { + this.pageManager = pageManager; + configurations = ConfigFactory.create(Configurations.class, System.getProperties(), + System.getenv()); + } + + protected Map getStorage() { + return ContextHolder.asMap(); + } + + protected void saveValueInStorage(String key, Object value) { + getStorage().put(key, value); + } + + protected WebPage getPage(String name) { + WebPage page = Environment.getPage(name); + pageManager.setCurrentPage(page); + return page; + } + + protected T getPage(Class c) { + WebPage page = Environment.getPage(c); + pageManager.setCurrentPage(page); + return (T) page; + } +} diff --git a/src/main/java/ru/lanit/at/steps/web/DebugWebSteps.java b/src/main/java/ru/lanit/at/steps/web/DebugWebSteps.java new file mode 100644 index 0000000..c8b59e1 --- /dev/null +++ b/src/main/java/ru/lanit/at/steps/web/DebugWebSteps.java @@ -0,0 +1,17 @@ +package ru.lanit.at.steps.web; + + +import io.cucumber.java.ru.Если; +import ru.lanit.at.utils.web.pagecontext.PageManager; + +public class DebugWebSteps extends AbstractWebSteps { + + public DebugWebSteps(PageManager pageManager) { + super(pageManager); + } + + @Если("шаг № {string}") + public void stepNumber(String stepNum) { + LOGGER.info("Шаг номер " + stepNum); + } +} diff --git a/src/main/java/ru/lanit/at/steps/web/WebActionWebSteps.java b/src/main/java/ru/lanit/at/steps/web/WebActionWebSteps.java new file mode 100644 index 0000000..41777ec --- /dev/null +++ b/src/main/java/ru/lanit/at/steps/web/WebActionWebSteps.java @@ -0,0 +1,154 @@ +package ru.lanit.at.steps.web; + +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.Selectors; +import com.codeborne.selenide.SelenideElement; +import io.cucumber.java.ru.Если; +import io.cucumber.java.ru.И; +import io.cucumber.java.ru.Когда; +import ru.lanit.at.actions.WebActions; +import ru.lanit.at.utils.Sleep; +import ru.lanit.at.utils.web.pagecontext.PageManager; + +import java.time.Duration; + +import static com.codeborne.selenide.Selenide.$; +import static ru.lanit.at.utils.VariableUtil.replaceVars; + + +public class WebActionWebSteps extends AbstractWebSteps { + + public WebActionWebSteps(PageManager pageManager) { + super(pageManager); + } + + + /** + * нажимает на элемент по тексту + * + * @param text текст элемента + */ + @Когда("кликнуть на элемент по тексту {string}") + public void clickElementWithText(String text) { + $(Selectors.byText(text)) + .shouldBe(Condition.visible) + .click(); + LOGGER.info("клик на элемент по тексту '{}'", text); + } + + @Если("кликнуть на элемент {string}") + public void clickOnElement(String elementName) { + SelenideElement element = pageManager + .getCurrentPage() + .getElement(elementName); + element + .shouldBe(Condition.visible, Duration.ofSeconds(10)) + .click(); + LOGGER.info("клик на элемент '{}'", elementName); + } + + @Если("установить чекбокс на элементе {string}") + public void selectCheckboxOnElement(String elementName) { + SelenideElement element = pageManager + .getCurrentPage() + .getElement(elementName); + element + .shouldBe(Condition.enabled, Duration.ofSeconds(60)); + WebActions.setCheckBox(element, true); + LOGGER.info("чекбокс установлен на элементе '{}'", elementName); + } + + @Если("убрать чекбокс на элементе {string}") + public void unselectCheckboxOnElement(String elementName) { + SelenideElement element = pageManager + .getCurrentPage() + .getElement(elementName); + element + .shouldBe(Condition.enabled, Duration.ofSeconds(60)); + WebActions.setCheckBox(element, false); + LOGGER.info("чекбокс снят на элементе '{}'", elementName); + } + + + /** + * скролл до элемента + * + * @param elementName наименование элемента + */ + @Когда("проскроллить страницу до элемента {string}") + public void scrollToElement(String elementName) { + SelenideElement element = pageManager.getCurrentPage().getElement(elementName); + element.shouldBe(Condition.visible) + .scrollIntoView("{block: 'center'}"); + LOGGER.info("скролл страницы до элемента '{}'", elementName); + } + + /** + * скролл до текста + * + * @param text текст + */ + @Когда("проскроллить страницу до текста {string}") + public void scrollToText(String text) { + SelenideElement element = $(Selectors.byText(text)); + element.shouldBe(Condition.visible) + .scrollIntoView("{block: 'center'}"); + LOGGER.info("скролл страницы до текста '{}'", text); + } + + @И("подождать {int} сек") + public void waitSeconds(int timeout) { + Sleep.pauseSec(timeout); + } + + /** + * Ввод значения в элемент + * + * @param field - наименование элемента + * @param value - значение + */ + @Когда("ввести в поле {string} значение {string}") + public void fillTheField(String field, String value) { + value = replaceVars(value, getStorage()); + SelenideElement fieldElement = pageManager + .getCurrentPage() + .getElement(field); + fieldElement + .shouldBe(Condition.visible, Duration.ofSeconds(60)) + .setValue(value); + LOGGER.info("в поле '{}' введено значение '{}'", field, value); + } + + /** + * Сохранить значения из элемент + * + * @param field - наименование элемента + * @param key - значение + */ + @Когда("сохранить значение из поля {string} под именем {string}") + public void saveTextField(String field, String key) { + SelenideElement fieldElement = pageManager + .getCurrentPage() + .getElement(field); + String elementValue = fieldElement + .shouldBe(Condition.visible, Duration.ofSeconds(60)) + .getValue(); + saveValueInStorage(key, elementValue); + LOGGER.info("значение '{}' сохранено под именем '{}'", elementValue, key); + } + + + /** + * Очистка поля + * + * @param elementName наименование элемента + */ + @Если("очистить поле {string}") + public void clearFiled(String elementName) { + pageManager + .getCurrentPage() + .getElement(elementName) + .shouldBe(Condition.visible) + .clear(); + } +} diff --git a/src/main/java/ru/lanit/at/steps/web/WebCheckWebSteps.java b/src/main/java/ru/lanit/at/steps/web/WebCheckWebSteps.java new file mode 100644 index 0000000..264bb04 --- /dev/null +++ b/src/main/java/ru/lanit/at/steps/web/WebCheckWebSteps.java @@ -0,0 +1,143 @@ +package ru.lanit.at.steps.web; + +import com.codeborne.selenide.SelenideElement; +import io.cucumber.java.ru.Когда; +import io.cucumber.java.ru.Тогда; +import ru.lanit.at.actions.WebChecks; +import ru.lanit.at.utils.web.pagecontext.PageManager; + +import java.util.List; + +public class WebCheckWebSteps extends AbstractWebSteps { + + public WebCheckWebSteps(PageManager pageManager) { + super(pageManager); + } + + + /** + * проверка присутствия текста на странице + * + * @param text текст + */ + @Когда("проверить что элемент {string} содержит текст:") + public void textAppearOnThePage(String elementName, List text) { + SelenideElement element = pageManager + .getCurrentPage() + .getElement(elementName); + WebChecks.elementContainsText(element, text); + LOGGER.info("элемент '{}' содержит текст '{}'", elementName, text); + } + + + /** + * проверка присутствия текста на странице + * + * @param text текст + */ + @Когда("на странице присутствует текст {string}") + public void textAppearOnThePage(String text) { + WebChecks.textVisibleOnPage(text, null); + LOGGER.info("на странице '{}' имеется текст '{}'", pageManager.getCurrentPage().name(), text); + } + + /** + * проверка отсутствия текста на странице + * + * @param text текст + */ + @Когда("на странице отсутствует текст {string}") + public void textVisibleOnPage(String text) { + WebChecks.textAbsentOnPage(text, null); + LOGGER.info("на странице '{}' отсутствует текст '{}'", pageManager.getCurrentPage().name(), text); + } + + /** + * ожидание появления текста на странице в течении некоторого времени + * + * @param text текст + * @param timeoutSeconds количество секунд + */ + @Когда("подождать появления текста {string} в течение {int} секунд") + public void waitUntilTextAppearOnPage(String text, int timeoutSeconds) { + WebChecks.textVisibleOnPage(text, timeoutSeconds); + LOGGER.info("на странице '{}' имеется текст '{}'", pageManager.getCurrentPage().name(), text); + } + + /** + * ожидание исчезновения текста на странице в течении некоторого времени + * + * @param text текст + * @param timeoutSeconds количество секунд + */ + @Когда("подождать исчезновения текста {string} в течение {int} секунд") + public void waitUntilTextAbsentOnPage(String text, int timeoutSeconds) { + WebChecks.textAbsentOnPage(text, timeoutSeconds); + LOGGER.info("на странице '{}' отсутствует текст '{}'", pageManager.getCurrentPage().name(), text); + } + + /** + * ожидание элемента на странице в течении некоторого времени + * + * @param elementName наименование элемента + * @param timeoutSeconds количество секунд + */ + @Когда("подождать появления элемента {string} в течение {int} секунд") + public void waitUntilElementIsVisibleOnPage(String elementName, int timeoutSeconds) { + SelenideElement element = pageManager + .getCurrentPage() + .getElement(elementName); + WebChecks.elementVisibleOnPage(element, timeoutSeconds); + LOGGER.info("на странице '{}' имеется элемент '{}'", pageManager.getCurrentPage().name(), elementName); + } + + /** + * проверка что на странице отображен элемент + * + * @param elementName наименование элемента + */ + @Когда("на странице имеется элемент {string}") + public void elementAppearOnThePage(String elementName) { + SelenideElement element = pageManager + .getCurrentPage() + .getElement(elementName); + WebChecks.elementVisibleOnPage(element, null); + LOGGER.info("на странице '{}' имеется элемент '{}'", pageManager.getCurrentPage().name(), elementName); + } + + /** + * проверка что на странице отсуствует элемент + * + * @param elementName наименование элемента + */ + @Когда("на странице отсутствует элемент {string}") + public void elementAbsentOnPage(String elementName) { + SelenideElement element = pageManager + .getCurrentPage() + .getElement(elementName); + WebChecks.elementAbsentOnPage(element, null); + LOGGER.info("на странице '{}' отсутствует элемент '{}'", pageManager.getCurrentPage().name(), elementName); + } + + /** + * проверка текущего url + *
можно начать написание url с переменной %{apiUrl}% или %{webUrl}% + * + * @param url часть или полный url (также может содержать переменные) + */ + @Тогда("проверить что текущий url соответствует {string}") + public void currentUrlEqualsExpected(String url) { + WebChecks.urlEquals(url); + } + + /** + * проверка текущего url + *
можно начать написание url с переменной %{apiUrl}% или %{webUrl}% + * + * @param url часть url (также может содержать переменные) + */ + @Тогда("проверить что текущий url содержит текст {string}") + public void currentUrlContainsExpected(String url) { + WebChecks.urlContains(url); + } +} diff --git a/src/main/java/ru/lanit/at/steps/web/WindowWebSteps.java b/src/main/java/ru/lanit/at/steps/web/WindowWebSteps.java new file mode 100644 index 0000000..9371e8c --- /dev/null +++ b/src/main/java/ru/lanit/at/steps/web/WindowWebSteps.java @@ -0,0 +1,103 @@ +package ru.lanit.at.steps.web; + + +import com.codeborne.selenide.Selenide; +import com.codeborne.selenide.WebDriverRunner; +import io.cucumber.java.ru.Если; +import io.cucumber.java.ru.И; +import io.cucumber.java.ru.Когда; +import ru.lanit.at.actions.WebActions; +import ru.lanit.at.utils.selenide.DriverManager; +import ru.lanit.at.utils.web.pagecontext.PageManager; +import ru.lanit.at.utils.web.pagecontext.WebPage; + +public class WindowWebSteps extends AbstractWebSteps { + + public WindowWebSteps(PageManager pageManager) { + super(pageManager); + } + + + /** + * открывает браузер + */ + @Если("открыть браузер") + public void openDriver() { + DriverManager.startDriver(); + DriverManager.startApp(); + } + + + /** + * открывает страницу по ссылке + * + * @param url url + */ + @Если("открыть url {string}") + public void open(String url) { + DriverManager.startDriver(); + Selenide.open(url); + } + + /** + * открывает новую вкладку в браузере с url и переключается на нее + * + * @param url url + */ + @И("открыть новую вкладку с url {string}") + public void openNewTab(String url) { + WebActions.openUrlOnNewTab(url); + } + + /** + * если вкладок 2, то переключится на следующую вкладку. + *
по факту переключается на последнюю вкладку + */ + @И("переключиться на следующую вкладку") + public void switchNextTab() { + WebActions.switchToNextTab(null); + } + + /** + * переключается на вкладку по порядковому номеру + * + * @param number порядковый номер вкладки в браузере + */ + @И("пеерключиться на вкладку по порядковому номеру {int}") + public void switchNextTabByNumber(int number) { + WebActions.switchToNextTab(number); + } + + /** + * закрывает текущую вкладку и переходит на предыдущую (последняя вкладка в наборе) + */ + @И("закрыть текущую вкладку и перейти на предыдущую") + public void closeTabAndSwitchTab() { + WebActions.closeCurrentTabAndSwitchToPrevious(); + } + + /** + * закрывает страницу + */ + @Если("закрыть страницу") + public void closeDriver() { + WebDriverRunner.getWebDriver().close(); + } + + /** + * действие обозначает что мы находимся на определенной странице + * для работы с описанными элементами в пейдже + * + * @param pageName наименование страницы + */ + @Если("пользователь на странице {string}") + @Когда("инициализация страницы {string}") + @И("переход на страницу {string}") + + public void setPage(String pageName) { + WebPage page = getPage(pageName); + pageManager.setCurrentPage(page); + } + + +} diff --git a/src/main/java/ru/lanit/at/steps/web/pages/GooglePageWebSteps.java b/src/main/java/ru/lanit/at/steps/web/pages/GooglePageWebSteps.java new file mode 100644 index 0000000..0eb0834 --- /dev/null +++ b/src/main/java/ru/lanit/at/steps/web/pages/GooglePageWebSteps.java @@ -0,0 +1,16 @@ +package ru.lanit.at.steps.web.pages; + +import ru.lanit.at.steps.web.AbstractWebSteps; +import ru.lanit.at.utils.web.pagecontext.PageManager; + + +public class GooglePageWebSteps extends AbstractWebSteps { + + public GooglePageWebSteps(PageManager pageManager) { + super(pageManager); + } + + /// реализация шагов специфичных для страницы GooglePage + + +} diff --git a/src/main/java/ru/lanit/at/utils/CompareUtil.java b/src/main/java/ru/lanit/at/utils/CompareUtil.java new file mode 100644 index 0000000..166306f --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/CompareUtil.java @@ -0,0 +1,62 @@ +package ru.lanit.at.utils; + +import org.apache.commons.lang3.math.NumberUtils; + +import java.math.BigDecimal; + +/** + * Created by Sorokin Boris on 21.01.2022. + */ +public class CompareUtil { + public static boolean compare(String firstValue, String secondValue, String operator) { + if (isNumeric(firstValue) && isNumeric(secondValue)) { + return compareNumbers(firstValue, secondValue, operator); + } else { + return compareStrings(firstValue, secondValue, operator); + } + } + + private static boolean compareStrings(String firstValue, String secondValue, String operator) { + switch (operator.toLowerCase()) { + case "равно": + case "==": + return firstValue.equals(secondValue); + case "не равно": + case "!=": + return !firstValue.equals(secondValue); + case "содержит": + return firstValue.contains(secondValue); + case "не содержит": + return !firstValue.contains(secondValue); + default: + throw new IllegalArgumentException(String + .format("Параметр '%s' является неверным для метода '%s'.", operator, "compareStrings")); + } + } + + private static boolean compareNumbers(String firstValue, String secondValue, String operator) { + BigDecimal number1 = new BigDecimal(firstValue); + BigDecimal number2 = new BigDecimal(secondValue); + switch (operator.toLowerCase()) { + case "равно": + case "==": + return number1.equals(number2); + case "не равно": + case "!=": + return !number1.equals(number2); + case "больше": + case ">": + return number1.compareTo(number2) > 0; + case "меньше": + case "<": + return number1.compareTo(number2) < 0; + default: + throw new IllegalArgumentException(String + .format("Параметр '%s' является неверным для метода '%s'.", operator, "compareNumbers")); + } + } + + private static boolean isNumeric(String strNum) { + return NumberUtils.isParsable(strNum); + } +} diff --git a/src/main/java/ru/lanit/at/utils/ContextHolder.java b/src/main/java/ru/lanit/at/utils/ContextHolder.java new file mode 100644 index 0000000..a3a8439 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/ContextHolder.java @@ -0,0 +1,48 @@ +package ru.lanit.at.utils; + + +import java.util.HashMap; +import java.util.Map; + +/** + * Класс для хранения переменных теста + * Синтаксис %{var_name}% + */ +public class ContextHolder { + + private static final ThreadLocal> THREAD = new ThreadLocal<>(); + + private static Map getThread() { + Map vault = THREAD.get(); + if (vault == null) { + vault = new HashMap<>(); + THREAD.set(vault); + } + return vault; + } + + + public static Map asMap() { + return getThread(); + } + + public static void put(String key, Object value) { + getThread().put(key, value); + } + + public static String replaceVarsIfPresent(String text) { + if (text == null) { + return ""; + } else { + return VariableUtil.replaceVars(text, asMap()); + } + } + + public static T getValue(String key) { + return (T) getThread().get(key); + } + + public static T remove(String key) { + return (T) getThread().remove(key); + } +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/utils/DataGenerator.java b/src/main/java/ru/lanit/at/utils/DataGenerator.java new file mode 100644 index 0000000..bed811a --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/DataGenerator.java @@ -0,0 +1,56 @@ +package ru.lanit.at.utils; + +import java.util.concurrent.ThreadLocalRandom; + +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; +import static org.apache.commons.lang3.RandomStringUtils.randomNumeric; + +public class DataGenerator { + + /** + * Генерирует набор русских, английских букв и цифр по маске + *

+ *
R - русская буква + *
D - цифра + *
E - английская буква + * + * @return - рандомная строка + */ + public static String generateValueByMask(String mask) { + StringBuilder result = new StringBuilder(); + char[] chars = mask.toCharArray(); + for (char aChar : chars) { + switch (String.valueOf(aChar)) { + case "R": + result.append(getRussianLetter()); + break; + case "D": + result.append(randomNumeric(1)); + break; + case "E": + result.append(randomAlphabetic(1).toLowerCase()); + break; + default: + result.append(aChar); + break; + } + } + result.trimToSize(); + return result.toString(); + } + + /** + * рандомная русская буква + * + * @return рандомная русская буква + */ + private static String getRussianLetter() { + int leftLimit = 1040; + int rightLimit = leftLimit + 33; + String res = ""; + int a = ThreadLocalRandom.current().nextInt(leftLimit, rightLimit + 1); + char symbol = (char) a; + res += symbol; + return res; + } +} diff --git a/src/main/java/ru/lanit/at/utils/ErrorMessage.java b/src/main/java/ru/lanit/at/utils/ErrorMessage.java new file mode 100644 index 0000000..e4445b7 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/ErrorMessage.java @@ -0,0 +1,10 @@ +package ru.lanit.at.utils; + +public class ErrorMessage { + + public static final String BROWSER_NOT_SUPPORTED = "Браузер [%s] не поддерживается"; + public static final String BROWSER_TAB_NUMBER_MORE_THAN_TABS = "Номер [%d] переданной вкладки больше, чем количество вкладок"; + public static final String URL_NOT_EQUAL_ACTUAL = "Ожидаемый url [%s] не равен текущему [%s]"; + public static final String URL_NOT_CONTAINS_TEXT = "Текущий url [%s] не содержит в себе [%s]"; + +} diff --git a/src/main/java/ru/lanit/at/utils/FileUtil.java b/src/main/java/ru/lanit/at/utils/FileUtil.java new file mode 100644 index 0000000..582803d --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/FileUtil.java @@ -0,0 +1,95 @@ +package ru.lanit.at.utils; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +public class FileUtil { + + /** + * поиск файла по заданному пути и имени файла + *
если нет ни одного, то ошибка. если более одного, то тоже ошибка + * + * @param path путь + * @param fileName наименование файла + * @return возвращает один файл + */ + public static File searchFileInDirectory(String path, String fileName) { + List paths; + try { + paths = Files.find(Paths.get(path), + Integer.MAX_VALUE, + (path1, basicFileAttributes) -> path1.toFile().getName().equals(fileName)) + .collect(Collectors.toList()); + } catch (IOException e) { + e.printStackTrace(); + throw new FileSystemNotFoundException("Файл не найден"); + } + + if (paths.size() > 1) { + throw new AssertionError("В каталоге более 1 файла с именем " + fileName); + } else if (paths.size() == 0) { + throw new FileSystemNotFoundException("Файл не найден"); + } else { + return paths.get(0).toFile(); + } + } + + /** + * метод для составления полного пути до директории или файла + * + * @param packages массив наименований папок или файла (в конце) + * @return полный путь до директории или файла + */ + public static StringBuilder getParentPath(String... packages) { + StringBuilder pathBuilder = new StringBuilder(); + String separator = System.getProperty("file.separator"); + if (!System.getProperty("project.dir", "").isEmpty()) { + pathBuilder.append(System.getProperty("project.dir")); + } else { + pathBuilder.append(System.getProperty("user.dir")); + } + for (String packageName : packages) { + if (pathBuilder.toString().endsWith(separator)) { + pathBuilder.append(packageName); + } else { + pathBuilder.append(separator).append(packageName); + } + } + + return pathBuilder; + } + + /** + * Считывает и возвращает содержимый текст файла + * + * @param fileName - название файла + * @param packages - директории, в которых лежит файл + * @return - содержимый текст файла + */ + private static String readBodyFromFile(String fileName, String... packages) { + File file = searchFileInDirectory(getParentPath(packages).toString(), fileName); + try { + return new String(Files.readAllBytes(Paths.get(file.getAbsolutePath()))); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * Считывает json-файл из пакеты по пути: "resources", "json" + * Путь часто используется, поэтому вынесен в отдельным метод + * + * @param fileName - название файла + * @return - содержание файла + */ + public static String readBodyFromJsonDir(String fileName) { + return readBodyFromFile(fileName, "src", "test", "resources", "json"); + } +} diff --git a/src/main/java/ru/lanit/at/utils/JsonUtil.java b/src/main/java/ru/lanit/at/utils/JsonUtil.java new file mode 100644 index 0000000..0481515 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/JsonUtil.java @@ -0,0 +1,58 @@ +package ru.lanit.at.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.InvalidJsonException; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import io.qameta.allure.Allure; + +import java.nio.charset.StandardCharsets; + + +public class JsonUtil { + + /** + * парсит к utf-8 + * + * @param text текст + * @return текст в utf-8 (насколько это возможно) + */ + public static String jsonToUtf(String text) { + return new String(text.getBytes(), StandardCharsets.UTF_8); + } + + /** + * извлекает данные по json path из json + * + * @param body json + * @param jsonPath json path + * @return значение из json + */ + public static String getFieldFromJson(String body, String jsonPath) { + Configuration jacksonConfig = Configuration.builder() + .mappingProvider(new JacksonMappingProvider()) + .jsonProvider(new JacksonJsonProvider()) + .build(); + + String val; + JsonNode node; + try { + node = JsonPath.using(jacksonConfig).parse(body).read(jsonPath, JsonNode.class); + } catch (InvalidJsonException e) { + Allure.addAttachment("INVALID JSON", "application/json", body, ".txt"); + throw new InvalidJsonException("Невалидный json."); + } + + if (node == null || node.isNull()) { + val = "null"; + } else { + val = node.toString(); + } + + String matchValue = RegexUtil.getMatchValueByGroupNumber(val, "^\"(.*)\"$", 1); + val = matchValue == null ? val : matchValue; + return val; + } +} diff --git a/src/main/java/ru/lanit/at/utils/RegexUtil.java b/src/main/java/ru/lanit/at/utils/RegexUtil.java new file mode 100644 index 0000000..299d6f7 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/RegexUtil.java @@ -0,0 +1,44 @@ +package ru.lanit.at.utils; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RegexUtil { + + /** + * получение значения из текста по регулярному выражению + * + * @param sentence текст + * @param regex регулярное выражение + * @param groupNumber номер группы + * @return значения из текста по регулярному выражению; если не нашел, то null + */ + public static String getMatchValueByGroupNumber(String sentence, String regex, int groupNumber) { + if (sentence != null && regex != null) { + sentence = sentence.trim(); + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(sentence); + if (matcher.find()) { + return matcher.group(groupNumber); + } + } + return null; + } + + /** + * есть совпадение в тексте по регулярному выражению + * + * @param sentence текст + * @param regex регулярное выражение + * @return true - есть совпадение; false - нет совпадения + */ + public static Boolean getMatch(String sentence, String regex) { + if (sentence != null && regex != null) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(sentence); + return matcher.find(); + } + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/utils/Sleep.java b/src/main/java/ru/lanit/at/utils/Sleep.java new file mode 100644 index 0000000..a609e7c --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/Sleep.java @@ -0,0 +1,26 @@ +package ru.lanit.at.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class Sleep { + + private final static Logger LOGGER = LoggerFactory.getLogger(Sleep.class); + + public static void pauseSec(int sec) { + LOGGER.info("Ожидание {} секунд", sec); + sleep(sec * 1000); + } + + public static void sleep(long ms) { + try { + LOGGER.info("Ожидание {} миллисекунд", ms); + TimeUnit.MILLISECONDS.sleep(ms); + } catch (InterruptedException e) { + LOGGER.warn("Interrupted!", e); + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/ru/lanit/at/utils/Stand.java b/src/main/java/ru/lanit/at/utils/Stand.java new file mode 100644 index 0000000..1cd6615 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/Stand.java @@ -0,0 +1,27 @@ +package ru.lanit.at.utils; + +import java.util.Arrays; + +/** + * Адреса стендов + */ +public enum Stand { + GOOGLE("www.google.ru"), + YANDEX("ya.ru"); + + private String url; + + Stand(String url) { + this.url = url; + } + + public static Stand getByName(String value) { + return Arrays.stream(values()) + .filter(stand -> stand.name().equalsIgnoreCase(value)).findFirst().orElseThrow(() -> new RuntimeException("Не определен подходящий стенд с именем " + value)); + } + + public String getUrlPath() { + return url; + } + +} diff --git a/src/main/java/ru/lanit/at/utils/VariableUtil.java b/src/main/java/ru/lanit/at/utils/VariableUtil.java new file mode 100644 index 0000000..6590672 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/VariableUtil.java @@ -0,0 +1,76 @@ +package ru.lanit.at.utils; + + +import org.apache.commons.lang3.StringUtils; + +import java.math.BigDecimal; +import java.util.Map; + +public class VariableUtil { + + /** + * замена контекстных переменных на значения + * + * @param preBody текст + * @param vars контекстные переменные + * @return значение + */ + public static String replaceVars(String preBody, Map vars) { + StringBuilder replacedText = new StringBuilder(preBody); + + final String patternStartVar = "${"; + final String patternEndVar = "}"; + + while (true) { + int fi = replacedText.indexOf(patternStartVar); + if (fi == -1) { + break; + } + int li = replacedText.indexOf(patternEndVar, fi); + String var = replacedText.substring(fi + patternStartVar.length(), li); + if (vars.containsKey(var)) { + replacedText.replace(fi, li + 1, vars.get(var).toString()); + } else { + break; + } + } + return replacedText.toString(); + } + + /** + * убирает лишние символы (обычно для json) + * + * @param in текст + * @return текст + */ + public static String extractBrackets(Object in) { + if (in == null) { + return "null"; + } + String op = in.toString(); + String replace; + String replace2; + if (op.startsWith("[") && op.endsWith("]") && !op.contains(",")) { + replace = StringUtils.replace(StringUtils.replace(op, "[", ""), "]", ""); + if (StringUtils.startsWith(replace, "\"") && StringUtils.endsWith(replace, "\"")) { + replace2 = StringUtils.replace(replace, "\"", ""); + try { + if (replace2.matches("^[0-9]+$")) { + new BigDecimal(replace2); + } + return replace2; + } catch (NumberFormatException n) { + return replace; + } + } else { + return replace; + } + } + if (op.startsWith("[[") && op.endsWith("]]") || op.startsWith("[") && op.endsWith("]")) { + op = op.replaceFirst("\\[", ""); + int lastIndex = op.lastIndexOf("]"); + return op.substring(0, lastIndex); + } + return op; + } +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/utils/VideoSaveHelper.java b/src/main/java/ru/lanit/at/utils/VideoSaveHelper.java new file mode 100644 index 0000000..06042d8 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/VideoSaveHelper.java @@ -0,0 +1,78 @@ +package ru.lanit.at.utils; + + +import com.codeborne.selenide.Selenide; +import io.restassured.RestAssured; +import io.restassured.response.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.lanit.at.utils.allure.AllureHelper; + +import java.net.URI; +import java.net.URL; + +import static io.restassured.RestAssured.given; + +public class VideoSaveHelper { + private final static Logger LOGGER = LoggerFactory.getLogger(VideoSaveHelper.class); + + private final String sessionId; + private final String hubUrl; + + public VideoSaveHelper(String sessionId, String hubUrl) { + this.sessionId = sessionId; + this.hubUrl = hubUrl; + } + + /** + * Метод для прикрепления видео к Аллюр отчету + */ + public void attachVideoFileRest() { + waitVideoFileDone(); + Response response = RestAssured.given().when() + .get(buildVideoURL(sessionId)); + AllureHelper.attachSelenoidVideo("Video", response + .then().extract().asInputStream()); + } + + /** + * Метод для удаления видео из селенойда. + */ + public void deleteSelenoidVideoRest() { + try { + given().delete(buildVideoURL(sessionId)).then().statusCode(200); + } catch (Exception e) { + LOGGER.warn("Произошла ошибка при удалении видео: " + e.getMessage()); + } + } + + + /** + * Метод для построения урл с видео файлом + */ + private URL buildVideoURL(String sessionId) { + try { + URI uri = new URI("http", hubUrl, "/video/" + sessionId + ".mp4", null, null); + URL url = uri.toURL(); + LOGGER.info("URL для скачивания файла с видео = " + url); + return url; + } catch (Exception e) { + throw new RuntimeException("Не удалось создать url для скачивания видео", e); + } + } + + /** + * Ожидание формирования видео в селенойде + */ + private void waitVideoFileDone() { + for (int i = 0; i < 60; i++) { + if (RestAssured.given().when() + .get(buildVideoURL(sessionId)).statusCode() == 200) { + return; + } + LOGGER.info("Ожидание подготовки видео файла для скачивания"); + Selenide.sleep(5000); + } + throw new RuntimeException("Ошибка ожидания подготовки видео файла для скачивания"); + } +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/utils/allure/AllureHelper.java b/src/main/java/ru/lanit/at/utils/allure/AllureHelper.java new file mode 100644 index 0000000..d8f4701 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/allure/AllureHelper.java @@ -0,0 +1,92 @@ +package ru.lanit.at.utils.allure; + + +import io.qameta.allure.Allure; +import io.qameta.allure.AllureLifecycle; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.StepResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.lanit.at.utils.Sleep; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import static io.qameta.allure.Allure.getLifecycle; + +public class AllureHelper { + private final static Logger LOGGER = LoggerFactory.getLogger(Sleep.class); + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + /* Установка AllureLifecycle для потока выполнения, используется при запуске фреймворка, как веб сервиса **/ + public static void setLifecycle(AllureLifecycle allureLifecycle) { + threadLocal.set(allureLifecycle); + } + + public static void attachScreenShot(String name, byte[] bytes) { + get().addAttachment(name, "image/png", "png", bytes); + } + + public static void attachTxt(String name, String text) { + get().addAttachment(name, "text/plain", "txt", text.getBytes(StandardCharsets.UTF_8)); + } + + public static void attachPageSource(byte[] bytes) { + get().addAttachment("Page source", "text/html", "html", bytes); + } + + public static void setStepStatusBroken(String description) { + get().updateStep(stepResult -> stepResult.setDescription(description)); + } + + public static void addInfoAllureStep(String stepName) { + String uuid = UUID.randomUUID().toString(); + createAllureStep(uuid, stepName, Status.PASSED); + LOGGER.info("выполняется шаг : \"" + stepName + "\""); + stopAllureStep(uuid); + } + + public static void addInfoAllureStep(String uuid, String stepName) { + createAllureStep(uuid, stepName, Status.PASSED); + LOGGER.info("выполняется шаг : \"" + stepName + "\""); + stopAllureStep(uuid); + } + + public static void addBrokenAllureStep(String stepName, String textMessage) { + String uuid = UUID.randomUUID().toString(); + stepName = stepName + " [" + textMessage + "]"; + createAllureStep(uuid, stepName, Status.BROKEN); + stopAllureStep(uuid); + } + + + public static void createAllureStep(String parentUuid, String uuid, String stepName, Status status) { + LOGGER.info("Создание аллюр шага с именем :" + stepName); + StepResult stepResult = new StepResult(); + stepResult.setName(stepName) + .setStatus(status); + get().startStep(parentUuid, uuid, stepResult); + } + + public static void createAllureStep(String uuid, String stepName, Status status) { + LOGGER.info("Создание аллюр шага с именем :" + stepName); + StepResult stepResult = new StepResult(); + stepResult.setName(stepName) + .setStatus(status); + get().startStep(uuid, stepResult); + } + + public static void stopAllureStep(String uuid) { + get().stopStep(uuid); + } + + public static void attachSelenoidVideo(String name, InputStream is) { + Allure.getLifecycle().addAttachment(name, "video/mp4", "mp4", is); + } + + + private static AllureLifecycle get() { + return getLifecycle(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/utils/allure/AllureLogger.java b/src/main/java/ru/lanit/at/utils/allure/AllureLogger.java new file mode 100644 index 0000000..66e0310 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/allure/AllureLogger.java @@ -0,0 +1,125 @@ +package ru.lanit.at.utils.allure; + +import com.codeborne.selenide.WebDriverRunner; +import io.qameta.allure.Allure; +import io.qameta.allure.listener.StepLifecycleListener; +import io.qameta.allure.listener.TestLifecycleListener; +import io.qameta.allure.model.Status; +import io.qameta.allure.model.StepResult; +import io.qameta.allure.model.TestResult; +import org.aeonbits.owner.ConfigFactory; +import org.openqa.selenium.OutputType; +import org.openqa.selenium.TakesScreenshot; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.lanit.at.utils.web.properties.Configurations; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; + +import static ru.lanit.at.assertion.AssertErrorType.CRITICAL_ASSERT; +import static ru.lanit.at.assertion.AssertErrorType.SOFT_ASSERT; + +//import ru.lanit.at.utils.web.properties.Configurations; + +public class AllureLogger implements StepLifecycleListener, TestLifecycleListener { + private final static Logger LOGGER = LoggerFactory.getLogger(AllureLogger.class); + + private final Configurations conf = ConfigFactory.create(Configurations.class, System.getProperties(), + System.getenv()); + + @Override + public void beforeStepStop(StepResult result) { + boolean screenAfterStep = conf.screenAfterStep(); + if (screenAfterStep && !result.getStatus().equals(Status.SKIPPED)) { + Allure.addAttachment(result.getName(), + new ByteArrayInputStream(((TakesScreenshot) + WebDriverRunner.getWebDriver()) + .getScreenshotAs(OutputType.BYTES))); + } + } + + @Override + public void afterStepStop(StepResult result) { + if (!result.getStatus().equals(Status.SKIPPED)) { + if (isHasInnerBrokenStep(result)) { + LOGGER.info("Устанавливаем для шага: '" + result.getName() + "', статус=BROKEN"); + result.setStatus(Status.BROKEN); + } + } + + + if (result.getDescription() != null && (result.getDescription().contains(SOFT_ASSERT.getName()) || result.getDescription().contains(CRITICAL_ASSERT.getName()))) { + LOGGER.info("Устанавливаем для шага: '" + result.getName() + "', статус=BROKEN"); + result.setStatus(Status.BROKEN); + } + } + + private boolean isHasInnerBrokenStep(StepResult result) { + for (StepResult r : result.getSteps()) { + if (r.getStatus().equals(Status.BROKEN)) { + return true; + } + } + return false; + } + + /** + * Ищет шаги-заглушки ФРАГМЕНТ "название фрагмента" и прячет шаги фрагмента под спойлер + * @param testResult - результаты тестов + */ + @Override + public void beforeTestWrite(TestResult testResult) { + List originSteps = testResult.getSteps(); + List newSteps = wrapFragment(originSteps); + testResult.setSteps(newSteps); + } + + /** + * Метод прячет все шаги фрагмента под спойлер + * @param originSteps - оригинальный набор шагов для отчета + * @return - переопределенные шаги отчета + */ + private List wrapFragment(List originSteps) { + List newSteps = new ArrayList<>(); + for (int i=0; i < originSteps.size(); i++) { + StepResult step = originSteps.get(i); + if (step.getName().contains("ФРАГМЕНТ") && i+1 < originSteps.size()) { + i++; + long timeStop = step.getStop(); + List subSteps = new ArrayList<>(); + List statusList = new ArrayList<>(); + //идем дальше по тесту и собираем все шаги фрагмента в отдельный список + do { + StepResult subStep = originSteps.get(i); + if (subStep.getName().contains(step.getName())) { + timeStop = subStep.getStop(); + break; + } else { + subSteps.add(subStep); + statusList.add(subStep.getStatus()); + i++; + } + } while (i < originSteps.size()); + //если есть фрагменты внутри фрагмента + subSteps = wrapFragment(subSteps); + //таки все шаги фрагмента уйдут под спойлер (как саб-шаги) + step.setSteps(subSteps); + //ставим изменяем время конца шага, чтобы была сумма всех саб-шагов + step.setStop(timeStop); + //меняем статус шагу-фрагменту, если статус у одного из шагов не passed или не skipped + if (statusList.contains(Status.FAILED)) { + step.setStatus(Status.FAILED); + } else if (statusList.contains(Status.BROKEN)) { + step.setStatus(Status.BROKEN); + } + newSteps.add(step); + } else { + newSteps.add(step); + } + } + return newSteps; + } + +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/utils/reflections/ReflectionUtil.java b/src/main/java/ru/lanit/at/utils/reflections/ReflectionUtil.java new file mode 100644 index 0000000..961bca0 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/reflections/ReflectionUtil.java @@ -0,0 +1,31 @@ +package ru.lanit.at.utils.reflections; + +import org.reflections.Reflections; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Set; + +public final class ReflectionUtil { + + /** + * Получение списка классов по аннотации + */ + public static Set> getPagesAnnotatedWith(String packageName, Class annotation) { + return new Reflections(packageName).getTypesAnnotatedWith(annotation); + } + + /** + * Получение поля класса с помощью механизма рефлексии + */ + public static Object extractFieldValue(Field field, Object owner) { + field.setAccessible(true); + try { + return field.get(owner); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } finally { + field.setAccessible(false); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/utils/selenide/DriverManager.java b/src/main/java/ru/lanit/at/utils/selenide/DriverManager.java new file mode 100644 index 0000000..d26a9cf --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/selenide/DriverManager.java @@ -0,0 +1,94 @@ +package ru.lanit.at.utils.selenide; + +import com.codeborne.selenide.Configuration; +import com.codeborne.selenide.FileDownloadMode; +import com.codeborne.selenide.Selenide; +import com.codeborne.selenide.WebDriverRunner; +import com.codeborne.selenide.logevents.SelenideLogger; +import io.github.bonigarcia.wdm.WebDriverManager; +import io.qameta.allure.selenide.AllureSelenide; +import org.aeonbits.owner.ConfigFactory; +import ru.lanit.at.utils.ErrorMessage; +import ru.lanit.at.utils.Stand; +import ru.lanit.at.utils.web.pagecontext.Environment; +import ru.lanit.at.utils.web.properties.Configurations; +import ru.lanit.at.utils.web.properties.WebConfigurations; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + + +public class DriverManager { + + public static void startDriver() { + if (!WebDriverRunner.hasWebDriverStarted()) { + createDriver(); + } + } + + + private static void createDriver() { + Configurations cf = ConfigFactory.create(Configurations.class + , System.getProperties(), + System.getenv()); + + + WebConfigurations cfg = ConfigFactory.create(WebConfigurations.class + , System.getProperties(), + System.getenv()); + + SelenideLogger.addListener("AllureSelenide", new AllureSelenide() + .screenshots(true) + .savePageSource(true) + ); + + if (!cf.getRemoteURL().isEmpty()) { + Configuration.remote = "http://" + cf.getRemoteURL() + "/wd/hub"; + Map options = new HashMap<>(); + options.put("enableVNC", cf.getEnableVNC()); + options.put("enableVideo", cf.getEnableVideo()); + options.put("enableLog", cf.getEnableLog()); + Configuration.browserCapabilities.setCapability("selenoid:options", options); + Configuration.browserCapabilities.setCapability("sessionTimeout", "30m"); + Configuration.fileDownload = FileDownloadMode.FOLDER; + } else { + switch (cfg.webDriverBrowserName()) { + case "chrome": + WebDriverManager.chromedriver().setup(); + break; + case "firefox": + WebDriverManager.firefoxdriver().setup(); + break; + case "edge": + WebDriverManager.edgedriver().setup(); + break; + default: { + throw new IllegalArgumentException(String.format(ErrorMessage.BROWSER_NOT_SUPPORTED, cfg.webDriverBrowserName())); + } + } + } + + Configuration.browser = cfg.webDriverBrowserName(); + Configuration.browserSize = cfg.webDriverBrowserSize(); + Configuration.browserVersion = cfg.webDriverVersion(); + Configuration.savePageSource = true; + Configuration.screenshots = true; + Configuration.webdriverLogsEnabled = false; + Configuration.timeout = TimeUnit.SECONDS.toMillis(cfg.webDriverTimeoutSeconds()); + Configuration.pageLoadTimeout = TimeUnit.SECONDS.toMillis(cfg.pageLoadTimeoutSeconds()); + Configuration.pollingInterval = cfg.pollingTimeoutMs(); + Configuration.reportsFolder = System.getProperty("selenide.report.folder"); + Configuration.downloadsFolder = System.getProperty("selenide.download.folder"); + Environment.initPages(cfg.pagesPackage()); + } + + + public static void startApp() { + Configurations conf = ConfigFactory.create(Configurations.class, System.getProperties(), + System.getenv()); + String standParam = conf.getStand(); + Stand stand = Stand.getByName(standParam); + Selenide.open("https://" + stand.getUrlPath()); + } +} diff --git a/src/main/java/ru/lanit/at/utils/selenide/command/Commands.java b/src/main/java/ru/lanit/at/utils/selenide/command/Commands.java new file mode 100644 index 0000000..e402ce2 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/selenide/command/Commands.java @@ -0,0 +1,45 @@ +package ru.lanit.at.utils.selenide.command; + +import com.codeborne.selenide.Command; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.codeborne.selenide.ex.ElementShould; +import com.codeborne.selenide.ex.ElementShouldNot; +import org.aeonbits.owner.ConfigFactory; +import ru.lanit.at.assertion.AssertsManager; +import ru.lanit.at.utils.web.properties.Configurations; + +import java.time.Duration; + +/** + * Кастомные команды selenide элемента + */ +public class Commands { + + public static final Configurations conf = ConfigFactory.create(Configurations.class, System.getProperties(), + System.getenv()); + + private Commands() { + throw new IllegalStateException("Utility class"); + } + + /** + * Мягкая проверка Conditions element + * + * @param condition - тип проверки + * @param duration - таймаут ожидания (может быть null, в таком случае будет использоваться параметр из конфига webdriver.timeoutSeconds ) + * @return - proxy элемент + */ + + public static Command checkSoft(Condition condition, Duration duration) { + return (proxy, locator, args) -> { + try { + return proxy.should(condition, duration); + } catch (ElementShould | ElementShouldNot ex) { + AssertsManager.getAssertsManager().softAssert().fail(ex.getMessage(), ex); + } + return proxy; + }; + } + +} diff --git a/src/main/java/ru/lanit/at/utils/selenide/extensions/CustomCommands.java b/src/main/java/ru/lanit/at/utils/selenide/extensions/CustomCommands.java new file mode 100644 index 0000000..37caf60 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/selenide/extensions/CustomCommands.java @@ -0,0 +1,19 @@ +package ru.lanit.at.utils.selenide.extensions; + +import com.codeborne.selenide.commands.Commands; +import com.codeborne.selenide.impl.WebElementSource; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import java.io.IOException; + +/** Класс для переопределения и добавление методов Selenide элемента */ +@ParametersAreNonnullByDefault +public class CustomCommands + extends Commands { + @Nullable + @Override + public T execute(Object proxy, WebElementSource webElementSource, String methodName, @Nullable Object[] args) throws IOException { + return super.execute(proxy, webElementSource, methodName, args); + } +} diff --git a/src/main/java/ru/lanit/at/utils/selenide/extensions/CustomSelenidePageFactory.java b/src/main/java/ru/lanit/at/utils/selenide/extensions/CustomSelenidePageFactory.java new file mode 100644 index 0000000..c64d886 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/selenide/extensions/CustomSelenidePageFactory.java @@ -0,0 +1,7 @@ +package ru.lanit.at.utils.selenide.extensions; + +import com.codeborne.selenide.impl.SelenidePageFactory; + +/** Класс для переопределения стандартной фабрики иницализации Page объектов */ +public class CustomSelenidePageFactory extends SelenidePageFactory { +} diff --git a/src/main/java/ru/lanit/at/utils/web/annotations/Name.java b/src/main/java/ru/lanit/at/utils/web/annotations/Name.java new file mode 100644 index 0000000..64e9f19 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/web/annotations/Name.java @@ -0,0 +1,15 @@ +package ru.lanit.at.utils.web.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Аннотация для элементов страницы, служащая для их индентификации в cucumber-сценариях + */ +@Target({ElementType.FIELD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Name { + String value(); +} diff --git a/src/main/java/ru/lanit/at/utils/web/pagecontext/Environment.java b/src/main/java/ru/lanit/at/utils/web/pagecontext/Environment.java new file mode 100644 index 0000000..c362475 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/web/pagecontext/Environment.java @@ -0,0 +1,64 @@ +package ru.lanit.at.utils.web.pagecontext; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +/** + * Используется для хранения кеша страниц и драйвера + */ +public class Environment { + + private static final Logger LOGGER = LoggerFactory.getLogger(Environment.class); + + + /** + * Список веб-страниц, заданных пользователем, доступных для использования в сценарии + */ + private static PageCache pages; + + /** + * Метод ищет классы, аннотированные "ru.lanit.ru.lanit.at.utils.web.annotations.Name", + * добавляя ссылки на эти классы в поле "pages" + * + * @param packageName наименование пакета где лежат файлы с описанием страниц + */ + @SuppressWarnings("unchecked") + public static void initPages(String packageName) { + pages = new PageCache(); + ru.lanit.at.utils.reflections.ReflectionUtil + .getPagesAnnotatedWith(packageName, ru.lanit.at.utils.web.annotations.Name.class) + .stream() + .map(it -> { + if (WebPage.class.isAssignableFrom(it)) { + return (Class) it; + } else { + throw new IllegalStateException("Класс " + it.getName() + " должен наследоваться от WebPage"); + } + }) + .forEach(clazz -> pages.put(getClassAnnotationValue(clazz), clazz)); + } + + /** + * Вспомогательный метод, получает значение аннотации "ru.absolut.annotations.Name" для класса + */ + private static String getClassAnnotationValue(Class c) { + return Arrays + .stream(c.getAnnotationsByType(ru.lanit.at.utils.web.annotations.Name.class)) + .findFirst() + .map(ru.lanit.at.utils.web.annotations.Name::value) + .orElseThrow(() -> new AssertionError("Не найдены аннотации Page.Name в классe " + c.getName())); + } + + public static WebPage getPage(String name) { + return pages.get(name); + } + + public static WebPage getPage(Class c) { + return getPage(getClassAnnotationValue(c)); + } + + +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/utils/web/pagecontext/PageCache.java b/src/main/java/ru/lanit/at/utils/web/pagecontext/PageCache.java new file mode 100644 index 0000000..5c170bb --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/web/pagecontext/PageCache.java @@ -0,0 +1,59 @@ +package ru.lanit.at.utils.web.pagecontext; + +import com.codeborne.selenide.Selenide; +import com.google.common.collect.Maps; + +import java.util.Map; + +/** + * Предназначен для хранения страниц, используемых при прогоне тестов + */ +public final class PageCache { + + /** + * Страницы, на которых будет производится тестирование < Имя, Страница > + */ + private Map> pages; + + public PageCache() { + pages = Maps.newHashMap(); + } + + /** + * Получение страницы из "pages" по имени + */ + public WebPage get(String pageName) { + return Selenide.page(getPageFromPagesByName(pageName)).initialize(); + } + + /** + * Получение страницы по классу + */ + @SuppressWarnings("unchecked") + public T get(Class clazz, String name) { + WebPage webPage = Selenide.page(getPageFromPagesByName(name)).initialize(); + if (!clazz.isInstance(webPage)) { + throw new IllegalStateException(name + " page is not a instance of " + clazz + ". Named page is a " + webPage); + } + return (T) webPage; + } + + private Map> getPageMapInstanceInternal() { + return pages; + } + + private Class getPageFromPagesByName(String pageName) throws IllegalArgumentException { + Class page = getPageMapInstanceInternal().get(pageName); + if (page == null) { + throw new IllegalArgumentException("Страница с именем '" + pageName + "' не задекларирована"); + } + return page; + } + + public void put(String pageName, Class page) { + if (page == null) { + throw new IllegalArgumentException("Была передана пустая страница"); + } + pages.put(pageName, page); + } +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/utils/web/pagecontext/PageManager.java b/src/main/java/ru/lanit/at/utils/web/pagecontext/PageManager.java new file mode 100644 index 0000000..cef74f8 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/web/pagecontext/PageManager.java @@ -0,0 +1,30 @@ +package ru.lanit.at.utils.web.pagecontext; + +/** + * Класс, который хранит текущую страницу теста + */ +public class PageManager { + + private WebPage currentPage; + + public PageManager() { + } + + /** + * Возвращает текущую страницу, на которой в текущий момент производится тестирование + */ + public WebPage getCurrentPage() { + if (currentPage == null) { + throw new IllegalStateException("Текущая страница не задана"); + } + return currentPage.initialize(); + } + + /** + * Задает текущую страницу по ее имени + */ + public void setCurrentPage(WebPage webPage) { + this.currentPage = webPage; + } + +} \ No newline at end of file diff --git a/src/main/java/ru/lanit/at/utils/web/pagecontext/WebPage.java b/src/main/java/ru/lanit/at/utils/web/pagecontext/WebPage.java new file mode 100644 index 0000000..c7e1875 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/web/pagecontext/WebPage.java @@ -0,0 +1,126 @@ +package ru.lanit.at.utils.web.pagecontext; + +import com.codeborne.selenide.ElementsCollection; +import com.codeborne.selenide.SelenideElement; +import org.aeonbits.owner.ConfigFactory; +import ru.lanit.at.utils.reflections.ReflectionUtil; +import ru.lanit.at.utils.web.annotations.Name; +import ru.lanit.at.utils.web.properties.Configurations; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.lang.String.format; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +public abstract class WebPage { + + protected final Configurations configurations = ConfigFactory.create(Configurations.class, System.getProperties(), + System.getenv()); + + + /** + * Список всех элементов страницы + */ + private Map namedElements; + + /** + * @param name Annotations.Name + * Возвращает объект SelenideElement по его имени (аннотированного "Annotations.Name") + */ + public SelenideElement getElement(String name) { + Object instance = namedElements.get(name); + if (instance != null && !(instance instanceof SelenideElement)) { + throw new ClassCastException(String.format("Элемент [%s] должен иметь тип 'SelenideElement'", name)); + } + return (SelenideElement) Optional.ofNullable(namedElements.get(name)) + .orElseThrow(() -> new IllegalArgumentException(String.format("Элемент [%s] отсутствует в классе [%s]", name, this.getClass().getName()))); + } + + /** + * @param name Annotations.Name + * @return Возвращает объект ElementsCollection по его имени (аннотированного "Annotations.Name") + */ + public ElementsCollection getElementsCollection(String name) { + Object instance = namedElements.get(name); + if (instance != null && !(instance instanceof ElementsCollection)) { + throw new ClassCastException(String.format("Элемент [%s] должен иметь тип 'ElementsCollection'", name)); + } + return (ElementsCollection) Optional.ofNullable(namedElements.get(name)) + .orElseThrow(() -> new IllegalArgumentException(String.format("Элемент [%s] отсутствует в классе [%s]", name, this.getClass().getName()))); + } + + /** + * @return Возвращает значение аннотации @Name(...) текущей страницы + */ + public String name() { + return this + .getClass() + .getAnnotation(Name.class) + .value(); + } + + public WebPage initialize() { + namedElements = readNamedElements(); + return this; + } + + /** + * Поиск и инициализации элементов страницы + */ + private Map readNamedElements() { + checkNamedAnnotations(); + return Arrays.stream(getClass().getDeclaredFields()) + .filter(f -> f.getDeclaredAnnotation(Name.class) != null) + .peek(this::checkFieldType) + .collect(toMap(f -> f.getDeclaredAnnotation(Name.class).value(), this::extractFieldValueViaReflection)); + } + + private void checkFieldType(Field f) { + if (!SelenideElement.class.isAssignableFrom(f.getType()) + && !WebPage.class.isAssignableFrom(f.getType()) + ) { + this.checkCollectionFieldType(f); + } + } + + private void checkCollectionFieldType(Field f) { + if (ElementsCollection.class.isAssignableFrom(f.getType())) { + return; + } else if (List.class.isAssignableFrom(f.getType())) { + ParameterizedType listType = (ParameterizedType) f.getGenericType(); + Class listClass = (Class) listType.getActualTypeArguments()[0]; + if (SelenideElement.class.isAssignableFrom(listClass) || WebPage.class.isAssignableFrom(listClass)) { + return; + } + } + throw new IllegalStateException( + format("Поле с аннотацией '@Name' должно иметь тип SelenideElement, List или ElementsCollection.\n" + + "Найдено поле с типом %s", f.getType())); + } + + /** + * Поиск по аннотации "Annotations.Name" + */ + private void checkNamedAnnotations() { + List list = Arrays.stream(getClass().getDeclaredFields()) + .filter(f -> f.getDeclaredAnnotation(Name.class) != null) + .map(f -> f.getDeclaredAnnotation(Name.class).value()) + .collect(toList()); + if (list.size() != new HashSet<>(list).size()) { + throw new IllegalStateException("Найдено несколько аннотаций '@Name' с одинаковым значением в классе " + this.getClass().getName()); + } + } + + private Object extractFieldValueViaReflection(Field field) { + return ReflectionUtil.extractFieldValue(field, this); + } + + +} diff --git a/src/main/java/ru/lanit/at/utils/web/properties/Configurations.java b/src/main/java/ru/lanit/at/utils/web/properties/Configurations.java new file mode 100644 index 0000000..49dafdd --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/web/properties/Configurations.java @@ -0,0 +1,40 @@ +package ru.lanit.at.utils.web.properties; + +import org.aeonbits.owner.Config; + + +@Config.LoadPolicy(Config.LoadType.MERGE) +@Config.Sources({ + "classpath:config/configuration.properties", + "system:properties", + "system:env" +}) +public interface Configurations extends Config { + + + @Key("stand") + @DefaultValue("GOOGLE") + String getStand(); + + + @Key("screen_after_step") + @DefaultValue("false") + boolean screenAfterStep(); + + @Key("remoteUrl") + @DefaultValue("") + String getRemoteURL(); + + @Key("enableVNC") + @DefaultValue("false") + boolean getEnableVNC(); + + @Key("enableVideo") + @DefaultValue("false") + boolean getEnableVideo(); + + @Key("enableLog") + @DefaultValue("false") + boolean getEnableLog(); + +} diff --git a/src/main/java/ru/lanit/at/utils/web/properties/WebConfigurations.java b/src/main/java/ru/lanit/at/utils/web/properties/WebConfigurations.java new file mode 100644 index 0000000..82c11d0 --- /dev/null +++ b/src/main/java/ru/lanit/at/utils/web/properties/WebConfigurations.java @@ -0,0 +1,52 @@ +package ru.lanit.at.utils.web.properties; + +import org.aeonbits.owner.Config; + +@Config.LoadPolicy(Config.LoadType.MERGE) +@Config.Sources({ + "classpath:config/${browser}.properties", + "classpath:config/chrome.properties", + "system:properties", + "system:env" +}) +public interface WebConfigurations extends Config { + + @Key("webdriver.browser.size") + @DefaultValue("1920x1080") + String webDriverBrowserSize(); + + @Key("webdriver.version") + @DefaultValue("") + String webDriverVersion(); + + @Key("webdriver.browser.name") + @DefaultValue("") + String webDriverBrowserName(); + + @Key("webdriver.timeoutSeconds") + @DefaultValue("10") + int webDriverTimeoutSeconds(); + + + @Key("webdriver.pageLoadTimeoutSeconds") + @DefaultValue("120") + int pageLoadTimeoutSeconds(); + + @Key("selenoid.enableVNC") + @DefaultValue("false") + boolean enableVnc(); + + @Key("polling.timeoutMs") + @DefaultValue("200") + int pollingTimeoutMs(); + + @Key("pages.package") + @DefaultValue("ru.lanit.at.pages") + String pagesPackage(); + + @Key("site_url") + @DefaultValue("") + String site_url(); + + +} diff --git a/src/main/resources/META-INF/aop.xml b/src/main/resources/META-INF/aop.xml new file mode 100644 index 0000000..73852e4 --- /dev/null +++ b/src/main/resources/META-INF/aop.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/defaultservices/com.codeborne.selenide.commands.Commands b/src/main/resources/META-INF/defaultservices/com.codeborne.selenide.commands.Commands new file mode 100644 index 0000000..c4e0308 --- /dev/null +++ b/src/main/resources/META-INF/defaultservices/com.codeborne.selenide.commands.Commands @@ -0,0 +1 @@ +ru.lanit.at.utils.selenide.extensions.CustomCommands diff --git a/src/main/resources/META-INF/defaultservices/com.codeborne.selenide.impl.PageObjectFactory b/src/main/resources/META-INF/defaultservices/com.codeborne.selenide.impl.PageObjectFactory new file mode 100644 index 0000000..7dd82e8 --- /dev/null +++ b/src/main/resources/META-INF/defaultservices/com.codeborne.selenide.impl.PageObjectFactory @@ -0,0 +1 @@ +ru.lanit.at.utils.selenide.extensions.CustomSelenidePageFactory diff --git a/src/main/resources/META-INF/services/io.qameta.allure.listener.StepLifecycleListener b/src/main/resources/META-INF/services/io.qameta.allure.listener.StepLifecycleListener new file mode 100644 index 0000000..b3091b5 --- /dev/null +++ b/src/main/resources/META-INF/services/io.qameta.allure.listener.StepLifecycleListener @@ -0,0 +1 @@ +ru.lanit.at.utils.allure.AllureLogger \ No newline at end of file diff --git a/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener b/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener new file mode 100644 index 0000000..b3091b5 --- /dev/null +++ b/src/main/resources/META-INF/services/io.qameta.allure.listener.TestLifecycleListener @@ -0,0 +1 @@ +ru.lanit.at.utils.allure.AllureLogger \ No newline at end of file diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 0000000..ed36750 --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=INFO, stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n \ No newline at end of file diff --git a/src/test/java/Runner.java b/src/test/java/Runner.java new file mode 100644 index 0000000..1958636 --- /dev/null +++ b/src/test/java/Runner.java @@ -0,0 +1,22 @@ +import io.cucumber.testng.AbstractTestNGCucumberTests; +import io.cucumber.testng.CucumberOptions; +import org.testng.annotations.DataProvider; + + +@CucumberOptions( + plugin = { + "pretty", + "io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm" + }, + features = "classpath:features", + glue = {"ru.lanit.at.steps", "ru.lanit.at.hooks", "ru.lanit.at.corecommonstep"} +) +public class Runner extends AbstractTestNGCucumberTests { + + @Override + @DataProvider(parallel = true) + + public Object[][] scenarios() { + return super.scenarios(); + } +} diff --git a/src/test/resources/config/chrome.properties b/src/test/resources/config/chrome.properties new file mode 100644 index 0000000..ae9a6dc --- /dev/null +++ b/src/test/resources/config/chrome.properties @@ -0,0 +1,5 @@ +webdriver.browser.size=1920x1080 +webdriver.browser.name=chrome +webdriver.timeoutSeconds=60 +polling.timeoutMs=200 +webdriver.version=99.0 diff --git a/src/test/resources/config/configuration.properties b/src/test/resources/config/configuration.properties new file mode 100644 index 0000000..a83e4e6 --- /dev/null +++ b/src/test/resources/config/configuration.properties @@ -0,0 +1,10 @@ +stand=google +screen_after_step=false +baseUrl=https://petstore.swagger.io/v2/ + +## selenoid options +#remoteUrl=127.0.0.1:4444 +remoteUrl= +enableVNC=true +enableVideo=true +enableLog=true \ No newline at end of file diff --git a/src/test/resources/config/edge.properties b/src/test/resources/config/edge.properties new file mode 100644 index 0000000..99a126d --- /dev/null +++ b/src/test/resources/config/edge.properties @@ -0,0 +1,6 @@ +webdriver.browser.size=1920x1080 +webdriver.browser.name=edge +webdriver.timeoutSeconds=10 +polling.timeoutMs=200 +webdriver.version=91.0 + diff --git a/src/test/resources/config/firefox.properties b/src/test/resources/config/firefox.properties new file mode 100644 index 0000000..8ffe5c5 --- /dev/null +++ b/src/test/resources/config/firefox.properties @@ -0,0 +1,6 @@ +webdriver.browser.size=1920x1080 +webdriver.browser.name=firefox +webdriver.timeoutSeconds=10 +polling.timeoutMs=200 +webdriver.version=98.0 + diff --git a/src/test/resources/cucumber.properties b/src/test/resources/cucumber.properties new file mode 100644 index 0000000..aa51b35 --- /dev/null +++ b/src/test/resources/cucumber.properties @@ -0,0 +1 @@ +cucumber.publish.quiet=true \ No newline at end of file diff --git a/src/test/resources/features/api/Books.feature b/src/test/resources/features/api/Books.feature new file mode 100644 index 0000000..e730678 --- /dev/null +++ b/src/test/resources/features/api/Books.feature @@ -0,0 +1,25 @@ +#language:ru +@test + +Функционал: Образец GET запроса + + Сценарий: Запрос в bookstore к книге Just Spring Integration + + Когда создать контекстные переменные + | book | Just Spring Integration | + + И создать запрос + | method | url | + | GET | https://api.itbook.store/1.0/search/${book} | + Тогда отправить запрос + И статус код 200 + + Когда извлечь данные + | title | $.books[?(@.title=='${book}')].title | + | subtitle | $.books[?(@.title=='${book}')].subtitle | + | price | $.books[?(@.title=='${book}')].price | + + И сравнить значения + | ${title} | == | Just Spring Integration | + | ${subtitle} | == | A Lightweight Introduction to Spring Integration | + | ${price} | содержит | 16.99 | \ No newline at end of file diff --git a/src/test/resources/features/api/Github.feature b/src/test/resources/features/api/Github.feature new file mode 100644 index 0000000..263474e --- /dev/null +++ b/src/test/resources/features/api/Github.feature @@ -0,0 +1,31 @@ +#language:ru +@test + +Функционал: Selenide github test + - Выполнение запроса в репозиторий selenide + - Проверка что статус-код = 200 + - Извлечение данных из тела ответа по jsonpath + - Проверка извлеченных данных + + Сценарий: Выполнение GET запроса в репозиторий selenide + + * создать запрос + | method | url | + | GET | https://api.github.com/orgs/selenide/repos | + * добавить header + | Accept | application/vnd.github.v3+json | + * отправить запрос + * статус код 200 + * извлечь данные + | name | $[?(@.full_name=='selenide/selenide')].name | + | id | $[?(@.full_name=='selenide/selenide')].id | + | language | $[?(@.full_name=='selenide/selenide')].language | + | homepage | $[?(@.full_name=='selenide/selenide')].homepage | + | size | $[?(@.full_name=='selenide/selenide')].size | + + * сравнить значения + | ${name} | == | selenide | + | ${id} | != | null | + | ${language} | == | Java | + | ${homepage} | == | http://selenide.org | + | ${size} | > | 0 | \ No newline at end of file diff --git a/src/test/resources/features/api/PetStore.feature b/src/test/resources/features/api/PetStore.feature new file mode 100644 index 0000000..1ecd393 --- /dev/null +++ b/src/test/resources/features/api/PetStore.feature @@ -0,0 +1,56 @@ +#language:ru +@test + +Функционал: Тестирование сервиса PetStore + - Создание нового юзера POST запросом с телом из json файла, значения которого заполняем сгенерированным значениями + - После создания нового юзера, GET запросом запрашиваем данного юзера и проверяем, что его данные соответствует данными из тела запроса + + Сценарий: Создание юзера + + + # Первая часть теста - Создание юзера. Эти данные подставятся в тело запроса в шаблон тела файла createUser.json + # Генерится дандомная страка по маске + # E - Английская буква, + # R - русская буква, + # D - цифра. Остальные символы игнорятся + # Условна дана строка TEST_EEE_DDD_RRR - снегерится примерно такая - TEST_QRG_904_ЙЦУ + * сгенерировать переменные + | id | 0 | + | username | EEEEEEEE | + | firstName | EEEEEEEE | + | lastName | EEEEEEEE | + | email | EEEEEEE@EEEDDD.EE | + | password | DDDEEEDDDEEE | + | phone | +7DDDDDDDDDD | + | userStatus | 1 | + + # Создаем юзера + * создать запрос + | method | path | body | + | POST | /user | createUser.json | + * добавить header + | Content-Type | application/json | + * отправить запрос + * статус код 200 + * извлечь данные + | user_id | $.message | + * сравнить значения + | ${user_id} | != | null | + + # Вторая часть теста - запрос юзера и проверка его данных + * создать запрос + | method | path | + | GET | /user/${username} | + * добавить header + | accept | application/json | + + # FLAKY - Из-за особенностей сервиса PetStore может возвращать 404 + * отправить запрос + * статус код 200 + * извлечь данные + | resp_firstname | $.firstName | + | resp_user_id | $.id | + + * сравнить значения + | ${user_id} | == | ${resp_user_id} | + | ${firstName} | == | ${resp_firstname} | \ No newline at end of file diff --git a/src/test/resources/features/web/Example.feature b/src/test/resources/features/web/Example.feature new file mode 100644 index 0000000..c472e51 --- /dev/null +++ b/src/test/resources/features/web/Example.feature @@ -0,0 +1,18 @@ +#language:ru +@google +Функционал: Поиск гугл + + Сценарий: Открытие страницы google.com, ввод значения в поиск + + * шаг № "1" + * открыть url "https://www.google.ru/" + * инициализация страницы "Google" + * ввести в поле "поле поиска" значение "Погода в Москве" + + * на странице отсутствует текст "погода в ижевске" + + * шаг № "2" + * на странице имеется элемент "кнопка поиска" + * кликнуть на элемент "кнопка поиска" + * переход на страницу "Google страница результатов" + * на странице имеется элемент "виджет погоды" \ No newline at end of file diff --git a/src/test/resources/features/web/FragmentExample.feature b/src/test/resources/features/web/FragmentExample.feature new file mode 100644 index 0000000..7c7bbd5 --- /dev/null +++ b/src/test/resources/features/web/FragmentExample.feature @@ -0,0 +1,24 @@ +#language:ru +@googleFragment +Функционал: Поиск гугл + + Сценарий: Открытие двух фрагментов - страницы google.com и виджета погоды + + * шаг № "1" + * вызвать фрагмент "открытие страницы google" + + * на странице отсутствует текст "погода в ижевске" + + * шаг № "2" + * вызвать фрагмент "открытие виджета" + + @googleDynamicFragment + Сценарий: Открытие двух фрагментов - страницы google.com и виджета погоды с использованным динамического фрагмента + * шаг № "1" + * вызвать фрагмент "открытие страницы и ввод текста из параметра" + | текст_для_ввода | погода в москве | + + * на странице отсутствует текст "погода в ижевске" + + * шаг № "2" + * вызвать фрагмент "открытие виджета" \ No newline at end of file diff --git a/src/test/resources/json/createUser.json b/src/test/resources/json/createUser.json new file mode 100644 index 0000000..4027892 --- /dev/null +++ b/src/test/resources/json/createUser.json @@ -0,0 +1,10 @@ +{ + "id": ${id}, + "username": "${username}", + "firstName": "${firstName}", + "lastName": "${lastName}", + "email": "${email}", + "password": "${password}", + "phone": "${phone}", + "userStatus": ${userStatus} +} \ No newline at end of file diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 0000000..ed36750 --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=INFO, stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n \ No newline at end of file diff --git a/src/test/resources/suite.xml b/src/test/resources/suite.xml new file mode 100644 index 0000000..00550b5 --- /dev/null +++ b/src/test/resources/suite.xml @@ -0,0 +1,9 @@ + + + + + + + + +