diff --git a/README.md b/README.md
index bb976c0..d3ab668 100644
--- a/README.md
+++ b/README.md
@@ -1,42 +1,51 @@
-# OTUS Selenium Homework 1
+# OTUS Selenium Homework 2 (BDD + Cucumber)
## Цель проекта
-Автоматизировать 3 UI-сценария на `https://otus.ru` с использованием Selenium WebDriver 4+, JUnit 5, Guice DI, listeners, Stream API, Jsoup и обязательных проверок качества (Checkstyle + SpotBugs).
+Реализовать BDD-подход на `https://otus.ru` с использованием Selenium WebDriver 4+, Cucumber (русские feature), JUnit 5, Guice DI, listeners, Stream API, Jsoup и проверок качества (Checkstyle + SpotBugs).
## Стек технологий
- Java 21
- Maven
- Selenium `4.38.0`
-- WebDriverManager `6.3.3`
- JUnit 5
+- Cucumber (`cucumber-java`, `cucumber-junit-platform-engine`)
- Guice
- Jsoup
- Checkstyle
- SpotBugs
-## Реализованные сценарии
-1. Поиск курса по имени.
- - Открытие каталога `https://otus.ru/catalog/courses`
- - Поиск курса по имени через Stream API
- - Клик по плитке курса
- - Проверка заголовка открытого курса
+## Реализованные BDD-сценарии
+1. Выбор браузера через feature.
+ - Шаг: `Дано Открыт браузер Chrome`
+ - Фабрика драйвера выбирается по `browser` property (`chrome`)
-2. Самые ранние/поздние курсы по дате старта.
- - Открытие каталога `https://otus.ru/catalog/courses`
- - Поиск ранних и поздних курсов через Stream API + `reduce`
- - При совпадении дат проверяются все курсы с этой датой
- - Проверка названия и даты старта на странице курса через Jsoup
+2. Поиск курса по имени и случайный выбор при множественных результатах.
+ - Название курса задается в feature
+ - Выбирается случайный курс из найденных
+ - Проверяется заголовок открытой страницы курса
-3. Случайная категория с главной страницы.
- - Открытие `https://otus.ru`
- - Открытие меню «Обучение»
- - Выбор случайной категории
- - Проверка, что открыта корректная категория
+3. Поиск курсов, стартующих в указанную дату или позже.
+ - Дата задается в feature (`dd.MM.yyyy`)
+ - Выполняется фильтрация `startDate >= dateFrom`
+ - В консоль выводится: название + дата старта
+
+4. Раздел «Обучение» -> «Подготовительные курсы».
+ - Открывается пункт `Подготовительные курсы` из меню `Обучение`
+ - При необходимости нажимается `Показать еще ...`, чтобы загрузить весь список
+ - Из списка выбираются самый дорогой и самый дешевый
+ - Выбор реализован через Stream API + `filter`
+ - Информация о курсах выводится в консоль
+
+5. Общий каталог курсов: самый дорогой и самый дешевый по полной стоимости со скидкой.
+ - Открывается `https://otus.ru/catalog/courses`
+ - Для каждого курса берется цена сравнения:
+ - В приоритете `Полная` -> `Полная стоимость со скидкой`
+ - Для упрощенных online-страниц используется fallback по видимой цене
+ - Через `filter` выбираются max/min, результат выводится в консоль
## Архитектура
-- 2-уровневый тест-дизайн: `tests` + `page objects`
-- DI через Guice для тестов и страниц
-- JUnit 5 Extension (`GuiceExtension`), без базового класса-теста
+- 2-уровневый тест-дизайн: `BDD steps` + `page objects`
+- DI через Guice для step definitions и страниц
- Фабрика драйвера:
- `DriverFactory` (интерфейс)
- `ChromeDriverFactory` (реализация)
@@ -52,8 +61,10 @@
- `src/main/java/ru/kovbasa/listeners` — listener подсветки
- `src/main/java/ru/kovbasa/pages` — Page Object классы
- `src/main/java/ru/kovbasa/elements` — типизированные UI-элементы
-- `src/test/java/ru/kovbasa/config` — JUnit extension для DI
-- `src/test/java/ru/kovbasa/tests` — автотесты
+- `src/test/resources/features` — `.feature` файлы (русский язык)
+- `src/test/java/ru/kovbasa/bdd` — Cucumber runner
+- `src/test/java/ru/kovbasa/bdd/steps` — step definitions
+- `src/test/java/ru/kovbasa/bdd/hooks` — lifecycle hooks
## Требования к окружению
1. Установлен JDK 21 (доступен в `PATH`)
@@ -62,33 +73,36 @@
4. Есть доступ в интернет и к `otus.ru` (тесты запускаются на живом сайте)
## Запуск
-### 1. Запуск только тестов
+### 1. Запуск всех тестов
```bash
mvn test
```
-### 2. Полная проверка (тесты + Checkstyle + SpotBugs)
+### 2. Запуск конкретного Cucumber runner
```bash
-mvn verify
+mvn "-Dtest=ru.kovbasa.bdd.CucumberTest" test
```
-### 3. Запуск отдельного тестового класса
+### 3. Локальная полная проверка (необязательно для сдачи ДЗ)
```bash
-mvn "-Dtest=ru.kovbasa.tests.CourseSearchTest" test
+mvn verify
```
## Параметры запуска
Пробрасываются через Maven Surefire:
- `base.url` (по умолчанию `https://otus.ru`)
- `course.name` (по умолчанию `Python Developer`)
+- `browser` (по умолчанию `chrome`)
Пример переопределения:
```bash
-mvn "-Dcourse.name=Python Developer" test
+mvn "-Dbrowser=chrome" "-Dcourse.name=Python" test
```
## Quality Gates
-- Checkstyle и SpotBugs выполняется в фазе `verify`
+- Checkstyle и SpotBugs выполняются в фазе `verify` (опционально)
## Примечания
- Тесты зависят от текущей верстки/контента `otus.ru`.
+- По умолчанию в `mvn test` запускается только `CucumberTest`.
+- Для сдачи ДЗ достаточно успешного запуска `mvn test`.
diff --git a/pom.xml b/pom.xml
index 0e8899d..f321fea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -16,16 +16,17 @@
https://otus.ru
Python Developer
+ chrome
4.38.0
5.10.0
- 6.3.3
+ 1.10.0
+ 7.20.1
5.1.0
1.21.2
2.0.11
1.4.14
- 32.1.3-jre
3.11.0
@@ -45,13 +46,6 @@
${selenium.version}
-
-
- io.github.bonigarcia
- webdrivermanager
- ${webdrivermanager.version}
-
-
org.junit.jupiter
@@ -60,6 +54,27 @@
test
+
+ org.junit.platform
+ junit-platform-suite
+ ${junit.platform.suite.version}
+ test
+
+
+
+ io.cucumber
+ cucumber-java
+ ${cucumber.version}
+ test
+
+
+
+ io.cucumber
+ cucumber-junit-platform-engine
+ ${cucumber.version}
+ test
+
+
com.google.inject
@@ -87,12 +102,6 @@
${logback.version}
-
-
- com.google.guava
- guava
- ${guava.version}
-
@@ -115,9 +124,16 @@
${surefire.version}
false
+
+ -Djava.util.logging.config.file=${project.basedir}/src/test/resources/logging.properties
+
+
+ **/CucumberTest.java
+
${base.url}
${course.name}
+ ${browser}
diff --git a/src/main/java/ru/kovbasa/config/DriverModule.java b/src/main/java/ru/kovbasa/config/DriverModule.java
index 5248999..a6cad54 100644
--- a/src/main/java/ru/kovbasa/config/DriverModule.java
+++ b/src/main/java/ru/kovbasa/config/DriverModule.java
@@ -2,15 +2,15 @@ package ru.kovbasa.config;
import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
-import ru.kovbasa.driver.ChromeDriverFactory;
import ru.kovbasa.driver.DriverFactory;
+import ru.kovbasa.driver.SelectableDriverFactory;
import ru.kovbasa.driver.WebDriverProvider;
public class DriverModule extends AbstractModule {
@Override
protected void configure() {
- bind(DriverFactory.class).to(ChromeDriverFactory.class).in(Singleton.class);
+ bind(DriverFactory.class).to(SelectableDriverFactory.class).in(Singleton.class);
bind(WebDriverProvider.class).in(Singleton.class);
}
}
diff --git a/src/main/java/ru/kovbasa/config/TestConfig.java b/src/main/java/ru/kovbasa/config/TestConfig.java
index fa7527c..8322d9f 100644
--- a/src/main/java/ru/kovbasa/config/TestConfig.java
+++ b/src/main/java/ru/kovbasa/config/TestConfig.java
@@ -2,20 +2,18 @@ package ru.kovbasa.config;
public final class TestConfig {
- private static final String BASE_URL =
- System.getProperty("base.url", "https://otus.ru");
-
- private static final String COURSE_NAME =
- System.getProperty("course.name", "Python Developer");
-
private TestConfig() {
}
public static String getBaseUrl() {
- return BASE_URL;
+ return System.getProperty("base.url", "https://otus.ru");
}
public static String getCourseName() {
- return COURSE_NAME;
+ return System.getProperty("course.name", "Python Developer");
+ }
+
+ public static String getBrowser() {
+ return System.getProperty("browser", "chrome");
}
}
diff --git a/src/main/java/ru/kovbasa/driver/SelectableDriverFactory.java b/src/main/java/ru/kovbasa/driver/SelectableDriverFactory.java
new file mode 100644
index 0000000..75409a1
--- /dev/null
+++ b/src/main/java/ru/kovbasa/driver/SelectableDriverFactory.java
@@ -0,0 +1,19 @@
+package ru.kovbasa.driver;
+
+import org.openqa.selenium.WebDriver;
+import ru.kovbasa.config.TestConfig;
+
+public class SelectableDriverFactory implements DriverFactory {
+
+ private final DriverFactory chromeFactory = new ChromeDriverFactory();
+
+ @Override
+ public WebDriver createDriver() {
+ final String browser = TestConfig.getBrowser().toLowerCase();
+ if (!"chrome".equals(browser)) {
+ throw new IllegalArgumentException(
+ "Unsupported browser: " + browser + ". Supported browser: chrome");
+ }
+ return chromeFactory.createDriver();
+ }
+}
diff --git a/src/main/java/ru/kovbasa/driver/WebDriverProvider.java b/src/main/java/ru/kovbasa/driver/WebDriverProvider.java
index 2b40ec1..652e3ae 100644
--- a/src/main/java/ru/kovbasa/driver/WebDriverProvider.java
+++ b/src/main/java/ru/kovbasa/driver/WebDriverProvider.java
@@ -3,8 +3,6 @@ package ru.kovbasa.driver;
import com.google.inject.Inject;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.events.EventFiringDecorator;
-
-import io.github.bonigarcia.wdm.WebDriverManager;
import ru.kovbasa.listeners.HighlightElementListener;
public final class WebDriverProvider {
@@ -12,10 +10,6 @@ public final class WebDriverProvider {
private WebDriver driver;
private final DriverFactory driverFactory;
- static {
- WebDriverManager.chromedriver().setup();
- }
-
@Inject
public WebDriverProvider(DriverFactory driverFactory) {
this.driverFactory = driverFactory;
diff --git a/src/main/java/ru/kovbasa/elements/CourseCard.java b/src/main/java/ru/kovbasa/elements/CourseCard.java
index 37a034f..a4be7c7 100644
--- a/src/main/java/ru/kovbasa/elements/CourseCard.java
+++ b/src/main/java/ru/kovbasa/elements/CourseCard.java
@@ -7,11 +7,14 @@ import java.time.LocalDate;
import java.time.Year;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public class CourseCard extends BaseElement {
private final By titleLocator = By.cssSelector("h6 .sc-hrqzy3-1");
private final By dateLocator = By.cssSelector(".sc-157icee-1 .sc-hrqzy3-1");
+ private static final Pattern PRICE_PATTERN = Pattern.compile("(\\d[\\d\\s]*)\\s*[₽р]");
public CourseCard(WebElement element) {
super(element);
@@ -51,4 +54,24 @@ public class CourseCard extends BaseElement {
return LocalDate.parse(normalized, formatter);
}
-}
\ No newline at end of file
+
+ public int price() {
+ final String text = element.getText();
+ final Matcher matcher = PRICE_PATTERN.matcher(text);
+
+ int maxPrice = -1;
+ while (matcher.find()) {
+ final String raw = matcher.group(1).replace(" ", "");
+ final int parsed = Integer.parseInt(raw);
+ if (parsed > maxPrice) {
+ maxPrice = parsed;
+ }
+ }
+
+ if (maxPrice < 0) {
+ throw new RuntimeException("Цена курса не найдена в карточке: " + title());
+ }
+
+ return maxPrice;
+ }
+}
diff --git a/src/main/java/ru/kovbasa/listeners/HighlightElementListener.java b/src/main/java/ru/kovbasa/listeners/HighlightElementListener.java
index 0c40c64..4512f37 100644
--- a/src/main/java/ru/kovbasa/listeners/HighlightElementListener.java
+++ b/src/main/java/ru/kovbasa/listeners/HighlightElementListener.java
@@ -1,5 +1,7 @@
package ru.kovbasa.listeners;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
@@ -8,6 +10,7 @@ import org.openqa.selenium.WrapsDriver;
import org.openqa.selenium.support.events.WebDriverListener;
public class HighlightElementListener implements WebDriverListener {
+ private static final Logger LOG = LoggerFactory.getLogger(HighlightElementListener.class);
private static final String HIGHLIGHT_STYLE =
"outline: 4px solid red !important; " +
@@ -20,16 +23,11 @@ public class HighlightElementListener implements WebDriverListener {
public void beforeClick(WebElement element) {
try {
clearHighlight();
-
- final String tag = safeGetTag(element);
- final String href = safeGetAttr(element, "href");
- final String cls = safeGetAttr(element, "class");
- System.out.println("beforeClick: tag=" + tag + " href=" + href + " class=" + cls);
-
final WebElement tile = findTileContainer(element);
applyHighlight(tile);
- } catch (Exception e) {
- System.out.println("Highlight listener error: " + e.getMessage());
+ pauseForVisibility();
+ } catch (RuntimeException e) {
+ LOG.debug("beforeClick highlight skipped", e);
}
}
@@ -62,8 +60,8 @@ public class HighlightElementListener implements WebDriverListener {
js.executeScript("arguments[0].setAttribute('style', arguments[1] + '; ' + arguments[2]);",
element, original, HIGHLIGHT_STYLE);
} catch (StaleElementReferenceException ignored) {
- } catch (Exception e) {
- System.out.println("applyHighlight error: " + e.getMessage());
+ } catch (RuntimeException e) {
+ LOG.debug("applyHighlight skipped", e);
}
}
@@ -84,8 +82,8 @@ public class HighlightElementListener implements WebDriverListener {
final String original = highlightedOriginalStyle == null ? "" : highlightedOriginalStyle;
js.executeScript("arguments[0].setAttribute('style', arguments[1]);", highlightedElement, original);
} catch (StaleElementReferenceException ignored) {
- } catch (Exception e) {
- System.out.println("clearHighlight error: " + e.getMessage());
+ } catch (RuntimeException e) {
+ LOG.debug("clearHighlight skipped", e);
} finally {
highlightedElement = null;
highlightedOriginalStyle = null;
@@ -134,7 +132,8 @@ public class HighlightElementListener implements WebDriverListener {
return current;
}
}
- } catch (Exception ignored) {
+ } catch (RuntimeException e) {
+ LOG.debug("findTileContainer fallback to original element", e);
}
return element;
}
@@ -148,4 +147,12 @@ public class HighlightElementListener implements WebDriverListener {
try { return el.getTagName(); }
catch (Exception e) { return null; }
}
+
+ private void pauseForVisibility() {
+ try {
+ Thread.sleep(120);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
}
diff --git a/src/main/java/ru/kovbasa/pages/CatalogPage.java b/src/main/java/ru/kovbasa/pages/CatalogPage.java
index 889ce58..27889be 100644
--- a/src/main/java/ru/kovbasa/pages/CatalogPage.java
+++ b/src/main/java/ru/kovbasa/pages/CatalogPage.java
@@ -3,9 +3,13 @@ package ru.kovbasa.pages;
import com.google.inject.Inject;
import java.time.Duration;
import java.time.LocalDate;
+import java.util.ArrayList;
import java.util.Comparator;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.ThreadLocalRandom;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
@@ -26,7 +30,11 @@ public class CatalogPage {
private final WebDriver driver;
private final By courseCards = By.cssSelector("a.sc-zzdkm7-0");
- private final By courseLinks = By.cssSelector("a[href*='/lessons/']");
+ private final By courseLinks = By.cssSelector("a[href*='/lessons/'], a[href*='/online/']");
+ private final By learningMenu = By.cssSelector("span[title='Обучение']");
+ private final By prepCoursesLink = By.xpath(
+ "//a[contains(normalize-space(),'Подготовительные курсы')]");
+ private final By showMoreButton = By.xpath("//button[contains(normalize-space(),'Показать еще')]");
@Inject
public CatalogPage(WebDriverProvider provider) {
@@ -50,6 +58,15 @@ public class CatalogPage {
"Course not found in catalog by name: " + name));
}
+ public List findCoursesByName(String name) {
+ final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
+ wait.until((ExpectedCondition) drv -> !drv.findElements(courseLinks).isEmpty());
+
+ return driver.findElements(courseLinks).stream()
+ .filter(e -> e.getText().toLowerCase().contains(name.toLowerCase()))
+ .toList();
+ }
+
public CoursePage clickCourseByName(String name) {
PageUtils.removeBottomBanner(driver);
@@ -66,6 +83,27 @@ public class CatalogPage {
return new CoursePage(driver);
}
+ public CoursePage clickRandomCourseByName(String name) {
+ PageUtils.removeBottomBanner(driver);
+
+ final List courses = findCoursesByName(name);
+ if (courses.isEmpty()) {
+ throw new NoSuchElementException("Course not found in catalog by name: " + name);
+ }
+
+ final WebElement chosen = courses.get(ThreadLocalRandom.current().nextInt(courses.size()));
+
+ ((JavascriptExecutor) driver)
+ .executeScript("arguments[0].scrollIntoView({block:'center'});", chosen);
+
+ new Actions(driver)
+ .moveToElement(chosen)
+ .perform();
+ chosen.click();
+
+ return new CoursePage(driver);
+ }
+
public List getAllCourseCards() {
return driver.findElements(courseCards).stream()
.map(CourseCard::new)
@@ -113,6 +151,185 @@ public class CatalogPage {
.toList();
}
+ public List findCoursesStartingFrom(LocalDate dateFrom) {
+ return getAllCourses().stream()
+ .filter(course -> !course.startDate().isBefore(dateFrom))
+ .sorted(Comparator.comparing(CourseItem::startDate).thenComparing(CourseItem::title))
+ .toList();
+ }
+
+ public CatalogPage openPreparatoryCourses() {
+ driver.get(TestConfig.getBaseUrl());
+ PageUtils.removeBottomBanner(driver);
+
+ final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
+ final WebElement menu = wait.until(drv -> drv.findElement(learningMenu));
+ menu.click();
+
+ final WebElement prep = wait.until(drv -> drv.findElements(prepCoursesLink).stream()
+ .filter(WebElement::isDisplayed)
+ .findFirst()
+ .orElse(null));
+
+ ((JavascriptExecutor) driver)
+ .executeScript("arguments[0].scrollIntoView({block:'center'});", prep);
+ prep.click();
+
+ wait.until(drv -> drv.findElements(
+ By.xpath("//*[contains(normalize-space(),'Подготовительные курсы')]")
+ ).stream().anyMatch(WebElement::isDisplayed));
+
+ expandAllCourses();
+ return this;
+ }
+
+ public List getAllCoursesWithPrice() {
+ return getAllCourseCards().stream()
+ .map(card -> {
+ try {
+ return new PricedCourseItem(card.title(), card.price());
+ } catch (RuntimeException e) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .toList();
+ }
+
+ public List getPreparatoryCourseLinks() {
+ final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
+ wait.until((ExpectedCondition) drv -> !drv.findElements(courseLinks).isEmpty());
+
+ final Map titleByUrl = new LinkedHashMap<>();
+ for (WebElement link : driver.findElements(courseLinks)) {
+ if (!link.isDisplayed()) {
+ continue;
+ }
+ final String title = link.getText().trim();
+ final String url = link.getAttribute("href");
+ if (title.isEmpty() || url == null || !isCourseUrl(url)) {
+ continue;
+ }
+ titleByUrl.putIfAbsent(url, title);
+ }
+
+ final List> onlineEntries = titleByUrl.entrySet().stream()
+ .filter(entry -> entry.getKey().contains("/online/"))
+ .toList();
+
+ final List items = new ArrayList<>();
+ if (!onlineEntries.isEmpty()) {
+ onlineEntries.forEach(entry -> items.add(new CourseLinkItem(cleanTitle(entry.getValue()), entry.getKey())));
+ return items;
+ }
+
+ titleByUrl.forEach((url, title) -> items.add(new CourseLinkItem(cleanTitle(title), url)));
+ return items;
+ }
+
+ public List getPreparatoryCoursesWithDiscountedFullPrice() {
+ final List links = getPreparatoryCourseLinks();
+ return links.stream()
+ .map(link -> {
+ driver.get(link.url());
+ final CoursePage coursePage = new CoursePage(driver);
+ final int price = coursePage.getPriceForComparison();
+ String title = link.title();
+ try {
+ title = coursePage.getCourseTitle();
+ } catch (RuntimeException ignored) { }
+ return new PricedCourseItem(title, price);
+ })
+ .toList();
+ }
+
+ public List getCatalogLessonCourseLinks() {
+ open();
+
+ final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
+ wait.until((ExpectedCondition) drv -> !drv.findElements(courseLinks).isEmpty());
+
+ final Map titleByUrl = new LinkedHashMap<>();
+ for (WebElement link : driver.findElements(courseLinks)) {
+ if (!link.isDisplayed()) {
+ continue;
+ }
+ final String title = link.getText().trim();
+ final String url = link.getAttribute("href");
+ if (title.isEmpty() || url == null || !url.contains("/lessons/")) {
+ continue;
+ }
+ titleByUrl.putIfAbsent(url, cleanTitle(title));
+ }
+
+ final List items = new ArrayList<>();
+ titleByUrl.forEach((url, title) -> items.add(new CourseLinkItem(title, url)));
+ return items;
+ }
+
+ public List getCatalogCoursesWithDiscountedFullPrice() {
+ final List links = getCatalogLessonCourseLinks();
+ return links.stream()
+ .map(link -> {
+ driver.get(link.url());
+ final CoursePage coursePage = new CoursePage(driver);
+ final int price = coursePage.getPriceForComparison();
+ String title = link.title();
+ try {
+ title = coursePage.getCourseTitle();
+ } catch (RuntimeException ignored) { }
+ return new PricedCourseItem(title, price);
+ })
+ .toList();
+ }
+
+ private void expandAllCourses() {
+ final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
+
+ while (true) {
+ try {
+ final WebElement button = wait.until(drv -> drv.findElements(showMoreButton).stream()
+ .filter(WebElement::isDisplayed)
+ .findFirst()
+ .orElse(null));
+
+ ((JavascriptExecutor) driver)
+ .executeScript("arguments[0].scrollIntoView({block:'center'});", button);
+ button.click();
+ wait.until(drv -> !drv.findElements(courseLinks).isEmpty());
+ } catch (RuntimeException ignored) {
+ break;
+ }
+ }
+ }
+
+ private boolean isCourseUrl(String url) {
+ return url.contains("/lessons/") || url.contains("/online/");
+ }
+
+ private String cleanTitle(String rawTitle) {
+ final String[] lines = rawTitle.split("\\R");
+ for (int i = lines.length - 1; i >= 0; i--) {
+ final String line = lines[i].trim();
+ if (!line.isEmpty()) {
+ return line;
+ }
+ }
+ return rawTitle.trim();
+ }
+
+ public PricedCourseItem findMostExpensiveCourse() {
+ return getAllCoursesWithPrice().stream()
+ .reduce((left, right) -> left.price() >= right.price() ? left : right)
+ .orElseThrow(() -> new NoSuchElementException("Priced course cards are not found"));
+ }
+
+ public PricedCourseItem findCheapestCourse() {
+ return getAllCoursesWithPrice().stream()
+ .reduce((left, right) -> left.price() <= right.price() ? left : right)
+ .orElseThrow(() -> new NoSuchElementException("Priced course cards are not found"));
+ }
+
public CoursePage openCourse(CourseItem course) {
PageUtils.removeBottomBanner(driver);
diff --git a/src/main/java/ru/kovbasa/pages/CourseLinkItem.java b/src/main/java/ru/kovbasa/pages/CourseLinkItem.java
new file mode 100644
index 0000000..6ff0fa7
--- /dev/null
+++ b/src/main/java/ru/kovbasa/pages/CourseLinkItem.java
@@ -0,0 +1,4 @@
+package ru.kovbasa.pages;
+
+public record CourseLinkItem(String title, String url) {
+}
diff --git a/src/main/java/ru/kovbasa/pages/CoursePage.java b/src/main/java/ru/kovbasa/pages/CoursePage.java
index 23740b6..d59a193 100644
--- a/src/main/java/ru/kovbasa/pages/CoursePage.java
+++ b/src/main/java/ru/kovbasa/pages/CoursePage.java
@@ -4,18 +4,31 @@ import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+import org.openqa.selenium.support.ui.WebDriverWait;
import ru.kovbasa.elements.Button;
+import ru.kovbasa.utils.PageUtils;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public class CoursePage {
+ private static final Pattern PRICE_PATTERN = Pattern.compile("(\\d[\\d\\s\\u00A0]*)\\s*₽");
+
private final WebDriver driver;
private final By enrollButton = By.cssSelector("button[data-testid='enroll-button']");
+ private final By fullPriceTab = By.xpath("//div[normalize-space()='Полная']/parent::div");
+ private final By fullDiscountLabel = By.xpath("//p[contains(normalize-space(),'Полная стоимость со скидкой')]");
+ private final By anyPriceText = By.xpath("//*[contains(normalize-space(),'₽')]");
public CoursePage(WebDriver driver) {
this.driver = driver;
@@ -59,4 +72,68 @@ public class CoursePage {
public void clickEnroll() {
getEnrollButton().click();
}
-}
\ No newline at end of file
+
+ public int getDiscountedFullPrice() {
+ PageUtils.removeBottomBanner(driver);
+
+ final WebDriverWait wait = new WebDriverWait(driver, java.time.Duration.ofSeconds(10));
+ final WebElement tab = wait.until(drv -> drv.findElements(fullPriceTab).stream()
+ .filter(WebElement::isDisplayed)
+ .findFirst()
+ .orElse(null));
+
+ ((JavascriptExecutor) driver).executeScript("arguments[0].scrollIntoView({block:'center'});", tab);
+ try {
+ tab.click();
+ } catch (RuntimeException ignored) {
+ new Actions(driver).moveToElement(tab).click().perform();
+ }
+
+ final WebElement label = wait.until(drv -> drv.findElements(fullDiscountLabel).stream()
+ .filter(WebElement::isDisplayed)
+ .findFirst()
+ .orElse(null));
+
+ final WebElement priceElement = findPriceElementNearLabel(label);
+ final String rawPrice = priceElement.getText();
+ return parsePrice(rawPrice);
+ }
+
+ public int getPriceForComparison() {
+ if (hasDisplayed(fullPriceTab)) {
+ try {
+ return getDiscountedFullPrice();
+ } catch (RuntimeException ignored) { }
+ }
+
+ final WebDriverWait wait = new WebDriverWait(driver, java.time.Duration.ofSeconds(10));
+ final WebElement priceElement = wait.until(drv -> drv.findElements(anyPriceText).stream()
+ .filter(WebElement::isDisplayed)
+ .filter(el -> PRICE_PATTERN.matcher(el.getText()).find())
+ .findFirst()
+ .orElse(null));
+
+ return parsePrice(priceElement.getText());
+ }
+
+ private WebElement findPriceElementNearLabel(WebElement label) {
+ try {
+ return label.findElement(By.xpath("following-sibling::div[1]"));
+ } catch (NoSuchElementException ignored) {
+ return label.findElement(By.xpath("following::div[contains(normalize-space(),'₽')][1]"));
+ }
+ }
+
+ private int parsePrice(String rawPrice) {
+ final Matcher matcher = PRICE_PATTERN.matcher(rawPrice);
+ if (!matcher.find()) {
+ throw new IllegalArgumentException("Не удалось распарсить цену из строки: " + rawPrice);
+ }
+ final String normalized = matcher.group(1).replace('\u00A0', ' ').replace(" ", "");
+ return Integer.parseInt(normalized);
+ }
+
+ private boolean hasDisplayed(By by) {
+ return driver.findElements(by).stream().anyMatch(WebElement::isDisplayed);
+ }
+}
diff --git a/src/main/java/ru/kovbasa/pages/MainPage.java b/src/main/java/ru/kovbasa/pages/MainPage.java
index 2499cab..05198a6 100644
--- a/src/main/java/ru/kovbasa/pages/MainPage.java
+++ b/src/main/java/ru/kovbasa/pages/MainPage.java
@@ -50,7 +50,6 @@ public class MainPage {
final WebElement chosen = els.get(ThreadLocalRandom.current().nextInt(els.size()));
final String href = chosen.getAttribute("href");
- System.out.println("Selected category href = " + href);
((JavascriptExecutor) driver)
.executeScript("arguments[0].scrollIntoView({block:'center'});", chosen);
diff --git a/src/main/java/ru/kovbasa/pages/PricedCourseItem.java b/src/main/java/ru/kovbasa/pages/PricedCourseItem.java
new file mode 100644
index 0000000..edc0025
--- /dev/null
+++ b/src/main/java/ru/kovbasa/pages/PricedCourseItem.java
@@ -0,0 +1,4 @@
+package ru.kovbasa.pages;
+
+public record PricedCourseItem(String title, int price) {
+}
diff --git a/src/test/java/ru/kovbasa/bdd/CucumberTest.java b/src/test/java/ru/kovbasa/bdd/CucumberTest.java
new file mode 100644
index 0000000..b9700e8
--- /dev/null
+++ b/src/test/java/ru/kovbasa/bdd/CucumberTest.java
@@ -0,0 +1,17 @@
+package ru.kovbasa.bdd;
+
+import org.junit.platform.suite.api.ConfigurationParameter;
+import org.junit.platform.suite.api.IncludeEngines;
+import org.junit.platform.suite.api.SelectClasspathResource;
+import org.junit.platform.suite.api.Suite;
+
+import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME;
+import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME;
+
+@Suite
+@IncludeEngines("cucumber")
+@SelectClasspathResource("features")
+@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "ru.kovbasa.bdd")
+@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty")
+public class CucumberTest {
+}
diff --git a/src/test/java/ru/kovbasa/bdd/hooks/Hooks.java b/src/test/java/ru/kovbasa/bdd/hooks/Hooks.java
new file mode 100644
index 0000000..a4bfea2
--- /dev/null
+++ b/src/test/java/ru/kovbasa/bdd/hooks/Hooks.java
@@ -0,0 +1,15 @@
+package ru.kovbasa.bdd.hooks;
+
+import com.google.inject.Injector;
+import io.cucumber.java.After;
+import ru.kovbasa.config.InjectorProvider;
+import ru.kovbasa.driver.WebDriverProvider;
+
+public class Hooks {
+
+ @After
+ public void afterScenario() {
+ final Injector injector = InjectorProvider.getInjector();
+ injector.getInstance(WebDriverProvider.class).quit();
+ }
+}
diff --git a/src/test/java/ru/kovbasa/bdd/steps/CatalogSteps.java b/src/test/java/ru/kovbasa/bdd/steps/CatalogSteps.java
new file mode 100644
index 0000000..03e9598
--- /dev/null
+++ b/src/test/java/ru/kovbasa/bdd/steps/CatalogSteps.java
@@ -0,0 +1,159 @@
+package ru.kovbasa.bdd.steps;
+
+import com.google.inject.Injector;
+import io.cucumber.java.ru.Дано;
+import io.cucumber.java.ru.И;
+import io.cucumber.java.ru.Когда;
+import io.cucumber.java.ru.Тогда;
+import org.junit.jupiter.api.Assertions;
+import ru.kovbasa.config.InjectorProvider;
+import ru.kovbasa.pages.CatalogPage;
+import ru.kovbasa.pages.CourseItem;
+import ru.kovbasa.pages.CoursePage;
+import ru.kovbasa.pages.PricedCourseItem;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Locale;
+
+public class CatalogSteps {
+
+ private static final DateTimeFormatter DATE_FORMATTER =
+ DateTimeFormatter.ofPattern("dd.MM.yyyy", Locale.forLanguageTag("ru"));
+
+ private CoursePage openedCoursePage;
+ private List coursesStartingFromDate;
+ private LocalDate searchDate;
+ private PricedCourseItem mostExpensiveCourse;
+ private PricedCourseItem cheapestCourse;
+ private PricedCourseItem mostExpensiveCatalogCourse;
+ private PricedCourseItem cheapestCatalogCourse;
+
+ @Дано("Открыт браузер {word}")
+ public void openBrowser(String browser) {
+ System.setProperty("browser", browser.toLowerCase(Locale.ROOT));
+ }
+
+ @Когда("Открыт каталог курсов")
+ public void openCatalog() {
+ getCatalogPage().open();
+ }
+
+ @Когда("Найден курс {string} и выбран случайный из найденных")
+ public void findCourseAndOpenRandom(String courseName) {
+ openedCoursePage = getCatalogPage().clickRandomCourseByName(courseName);
+ }
+
+ @Тогда("Заголовок страницы курса содержит {string}")
+ public void coursePageTitleContains(String expectedCourseName) {
+ final String title = openedCoursePage.getCourseTitle();
+ Assertions.assertTrue(
+ title.toLowerCase(Locale.ROOT).contains(expectedCourseName.toLowerCase(Locale.ROOT)),
+ "Заголовок страницы курса должен содержать: " + expectedCourseName + ", фактически: " + title
+ );
+ }
+
+ @Когда("Найдены курсы со стартом {string} или позже")
+ public void findCoursesFromDate(String date) {
+ searchDate = LocalDate.parse(date, DATE_FORMATTER);
+ coursesStartingFromDate = getCatalogPage().findCoursesStartingFrom(searchDate);
+ }
+
+ @Тогда("В консоль выведены найденные курсы и даты старта")
+ public void printCoursesAndStartDates() {
+ Assertions.assertFalse(coursesStartingFromDate.isEmpty(), "Не найдено курсов по заданной дате");
+
+ coursesStartingFromDate.forEach(course -> System.out.println(
+ "Курс: " + course.title() + ", дата старта: " + course.startDate().format(DATE_FORMATTER)
+ ));
+
+ final boolean allAfterOrEqual = coursesStartingFromDate.stream()
+ .allMatch(course -> !course.startDate().isBefore(searchDate));
+
+ Assertions.assertTrue(
+ allAfterOrEqual,
+ "Среди найденных есть курсы с датой старта раньше " + searchDate.format(DATE_FORMATTER)
+ );
+ }
+
+ @Когда("Открыт раздел Обучение и Подготовительные курсы")
+ public void openPreparatoryCourses() {
+ getCatalogPage().openPreparatoryCourses();
+ }
+
+ @И("Выбраны самый дорогой и самый дешевый курсы с помощью filter")
+ public void findMostExpensiveAndCheapestByFilter() {
+ final List pricedCourses = getCatalogPage().getPreparatoryCoursesWithDiscountedFullPrice();
+ Assertions.assertFalse(pricedCourses.isEmpty(), "Не найдено курсов с ценой");
+
+ final int maxPrice = pricedCourses.stream().mapToInt(PricedCourseItem::price).max().orElseThrow();
+ final int minPrice = pricedCourses.stream().mapToInt(PricedCourseItem::price).min().orElseThrow();
+
+ mostExpensiveCourse = pricedCourses.stream()
+ .filter(course -> course.price() == maxPrice)
+ .findFirst()
+ .orElseThrow();
+
+ cheapestCourse = pricedCourses.stream()
+ .filter(course -> course.price() == minPrice)
+ .findFirst()
+ .orElseThrow();
+ }
+
+ @Тогда("В консоль выведена информация о самом дорогом и самом дешевом курсе")
+ public void printMostExpensiveAndCheapestCourse() {
+ System.out.println(
+ "Самый дорогой курс: " + mostExpensiveCourse.title() + ", цена: " + mostExpensiveCourse.price()
+ );
+ System.out.println(
+ "Самый дешевый курс: " + cheapestCourse.title() + ", цена: " + cheapestCourse.price()
+ );
+
+ Assertions.assertTrue(
+ mostExpensiveCourse.price() >= cheapestCourse.price(),
+ "Цена самого дорогого курса должна быть не меньше цены самого дешевого"
+ );
+ }
+
+ @Когда("В каталоге курсов выбраны самый дорогой и самый дешевый курсы по полной стоимости со скидкой с помощью filter")
+ public void findMostExpensiveAndCheapestInCatalogByFullDiscountedPrice() {
+ final List pricedCourses = getCatalogPage().getCatalogCoursesWithDiscountedFullPrice();
+ Assertions.assertFalse(pricedCourses.isEmpty(), "Не найдено курсов с ценой в общем каталоге");
+
+ final int maxPrice = pricedCourses.stream().mapToInt(PricedCourseItem::price).max().orElseThrow();
+ final int minPrice = pricedCourses.stream().mapToInt(PricedCourseItem::price).min().orElseThrow();
+
+ mostExpensiveCatalogCourse = pricedCourses.stream()
+ .filter(course -> course.price() == maxPrice)
+ .findFirst()
+ .orElseThrow();
+
+ cheapestCatalogCourse = pricedCourses.stream()
+ .filter(course -> course.price() == minPrice)
+ .findFirst()
+ .orElseThrow();
+ }
+
+ @Тогда("В консоль выведена информация о самом дорогом и самом дешевом курсе в каталоге")
+ public void printMostExpensiveAndCheapestCourseInCatalog() {
+ System.out.println(
+ "Самый дорогой курс в каталоге: "
+ + mostExpensiveCatalogCourse.title() + ", цена: " + mostExpensiveCatalogCourse.price()
+ );
+ System.out.println(
+ "Самый дешевый курс в каталоге: "
+ + cheapestCatalogCourse.title() + ", цена: " + cheapestCatalogCourse.price()
+ );
+
+ Assertions.assertTrue(
+ mostExpensiveCatalogCourse.price() >= cheapestCatalogCourse.price(),
+ "Цена самого дорогого курса в каталоге должна быть не меньше цены самого дешевого"
+ );
+ }
+
+ private CatalogPage getCatalogPage() {
+ final Injector injector = InjectorProvider.getInjector();
+ return injector.getInstance(CatalogPage.class);
+ }
+}
diff --git a/src/test/java/ru/kovbasa/config/GuiceExtension.java b/src/test/java/ru/kovbasa/config/GuiceExtension.java
deleted file mode 100644
index 545ed94..0000000
--- a/src/test/java/ru/kovbasa/config/GuiceExtension.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package ru.kovbasa.config;
-
-import com.google.inject.Injector;
-import org.junit.jupiter.api.extension.AfterEachCallback;
-import org.junit.jupiter.api.extension.BeforeEachCallback;
-import org.junit.jupiter.api.extension.ExtensionContext;
-import org.junit.jupiter.api.extension.TestInstancePostProcessor;
-import ru.kovbasa.driver.WebDriverProvider;
-
-public class GuiceExtension implements TestInstancePostProcessor, BeforeEachCallback, AfterEachCallback {
-
- @Override
- public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
- final Injector injector = InjectorProvider.getInjector();
- injector.injectMembers(testInstance);
- }
-
- @Override
- public void beforeEach(ExtensionContext context) {
- final Injector injector = InjectorProvider.getInjector();
- injector.getInstance(WebDriverProvider.class).getDriver();
- }
-
- @Override
- public void afterEach(ExtensionContext context) {
- final Injector injector = InjectorProvider.getInjector();
- injector.getInstance(WebDriverProvider.class).quit();
- }
-}
diff --git a/src/test/java/ru/kovbasa/tests/CategoryRandomTest.java b/src/test/java/ru/kovbasa/tests/CategoryRandomTest.java
deleted file mode 100644
index 6487c29..0000000
--- a/src/test/java/ru/kovbasa/tests/CategoryRandomTest.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package ru.kovbasa.tests;
-
-import com.google.inject.Inject;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.api.Test;
-import ru.kovbasa.config.GuiceExtension;
-import ru.kovbasa.driver.WebDriverProvider;
-import ru.kovbasa.pages.MainPage;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@ExtendWith(GuiceExtension.class)
-public class CategoryRandomTest {
-
- @Inject
- private MainPage main;
-
- @Inject
- private WebDriverProvider webDriverProvider;
-
- @Test
- void randomCategoryOpensCorrectCatalog() {
- main.open();
-
- final String selectedHref = main.clickRandomCategory();
- final String category = selectedHref.substring(selectedHref.lastIndexOf("/") + 1);
- final String currentUrl = webDriverProvider.getDriver().getCurrentUrl();
-
- assertTrue(
- currentUrl.contains(category),
- "Catalog URL should contain selected category. Selected: "
- + selectedHref + ", current: " + currentUrl
- );
- }
-}
diff --git a/src/test/java/ru/kovbasa/tests/CourseSearchTest.java b/src/test/java/ru/kovbasa/tests/CourseSearchTest.java
deleted file mode 100644
index 0b4c71b..0000000
--- a/src/test/java/ru/kovbasa/tests/CourseSearchTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package ru.kovbasa.tests;
-
-import com.google.inject.Inject;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.api.Test;
-import ru.kovbasa.config.GuiceExtension;
-import ru.kovbasa.config.TestConfig;
-import ru.kovbasa.pages.CatalogPage;
-import ru.kovbasa.pages.CoursePage;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@ExtendWith(GuiceExtension.class)
-public class CourseSearchTest {
-
- @Inject
- private CatalogPage catalog;
-
- @Test
- void findCourseByName() {
- catalog.open();
-
- final String courseName = TestConfig.getCourseName();
-
- final CoursePage page = catalog.clickCourseByName(courseName);
-
- final String title = page.getCourseTitle();
- assertTrue(
- title.toLowerCase().contains(courseName.toLowerCase()),
- "Course page title should contain searched course name"
- );
- }
-}
diff --git a/src/test/java/ru/kovbasa/tests/CoursesDatesTest.java b/src/test/java/ru/kovbasa/tests/CoursesDatesTest.java
deleted file mode 100644
index 51b7e37..0000000
--- a/src/test/java/ru/kovbasa/tests/CoursesDatesTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package ru.kovbasa.tests;
-
-import com.google.inject.Inject;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.api.Test;
-import ru.kovbasa.config.GuiceExtension;
-import ru.kovbasa.pages.CatalogPage;
-import ru.kovbasa.pages.CourseItem;
-import ru.kovbasa.pages.CoursePage;
-
-import java.time.LocalDate;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@ExtendWith(GuiceExtension.class)
-public class CoursesDatesTest {
-
- @Inject
- private CatalogPage catalog;
-
- @Test
- void earliestCourseHasCorrectTitleAndDate() {
- catalog.open();
-
- final List earliestCourses = catalog.findEarliestCourses();
-
- for (CourseItem course : earliestCourses) {
- final CoursePage page = catalog.openCourse(course);
-
- final String pageTitle = page.getCourseTitle();
- assertTrue(
- pageTitle.toLowerCase().contains(course.title().toLowerCase()),
- "Earliest course title on page should contain title from catalog: " + course.title()
- );
-
- final LocalDate pageDate = page.getCourseStartDate(course.startDate());
- assertEquals(
- course.startDate(),
- pageDate,
- "Earliest course start date should match for course: " + course.title()
- );
-
- catalog.open();
- }
- }
-
- @Test
- void latestCourseHasCorrectTitleAndDate() {
- catalog.open();
-
- final List latestCourses = catalog.findLatestCourses();
-
- for (CourseItem course : latestCourses) {
- final CoursePage page = catalog.openCourse(course);
-
- final String pageTitle = page.getCourseTitle();
- assertTrue(
- pageTitle.toLowerCase().contains(course.title().toLowerCase()),
- "Latest course title on page should contain title from catalog: " + course.title()
- );
-
- final LocalDate pageDate = page.getCourseStartDate(course.startDate());
- assertEquals(
- course.startDate(),
- pageDate,
- "Latest course start date should match for course: " + course.title()
- );
-
- catalog.open();
- }
- }
-}
diff --git a/src/test/resources/features/catalog.feature b/src/test/resources/features/catalog.feature
new file mode 100644
index 0000000..cd23d5a
--- /dev/null
+++ b/src/test/resources/features/catalog.feature
@@ -0,0 +1,29 @@
+# language: ru
+
+Функция: Каталог курсов OTUS
+ Как пользователь
+ Я хочу работать с каталогом курсов через BDD
+ Чтобы проверять поиск, фильтрацию по датам и выбор по цене
+
+ Сценарий: Выбор браузера и поиск курса по названию со случайным выбором
+ Дано Открыт браузер Chrome
+ Когда Открыт каталог курсов
+ И Найден курс "Python" и выбран случайный из найденных
+ Тогда Заголовок страницы курса содержит "Python"
+
+ Сценарий: Поиск курсов, стартующих в указанную дату или позже
+ Дано Открыт браузер Chrome
+ Когда Открыт каталог курсов
+ И Найдены курсы со стартом "01.01.2025" или позже
+ Тогда В консоль выведены найденные курсы и даты старта
+
+ Сценарий: Поиск самого дорогого и самого дешевого подготовительного курса
+ Дано Открыт браузер Chrome
+ Когда Открыт раздел Обучение и Подготовительные курсы
+ И Выбраны самый дорогой и самый дешевый курсы с помощью filter
+ Тогда В консоль выведена информация о самом дорогом и самом дешевом курсе
+
+ Сценарий: Поиск самого дорогого и самого дешевого курса в каталоге по полной стоимости со скидкой
+ Дано Открыт браузер Chrome
+ Когда В каталоге курсов выбраны самый дорогой и самый дешевый курсы по полной стоимости со скидкой с помощью filter
+ Тогда В консоль выведена информация о самом дорогом и самом дешевом курсе в каталоге
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..ec7d72c
--- /dev/null
+++ b/src/test/resources/logback-test.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ %d{HH:mm:ss} %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
diff --git a/src/test/resources/logging.properties b/src/test/resources/logging.properties
new file mode 100644
index 0000000..3da1c9c
--- /dev/null
+++ b/src/test/resources/logging.properties
@@ -0,0 +1,8 @@
+handlers= java.util.logging.ConsoleHandler
+.level= WARNING
+
+java.util.logging.ConsoleHandler.level = SEVERE
+java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
+
+org.openqa.selenium.devtools.level = SEVERE
+org.openqa.selenium.devtools.CdpVersionFinder.level = SEVERE