Implement HW5 stubs, API helpers, optional SQL/MQ helpers, and test coverage

This commit is contained in:
2026-03-09 01:40:48 +03:00
parent d1247eec18
commit 507e14bc2e
53 changed files with 2320 additions and 56 deletions

View File

@@ -0,0 +1,16 @@
package ru.kovbasa.config;
import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
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(SelectableDriverFactory.class).in(Singleton.class);
bind(WebDriverProvider.class).in(Singleton.class);
}
}

View File

@@ -0,0 +1,16 @@
package ru.kovbasa.config;
import com.google.inject.Guice;
import com.google.inject.Injector;
public final class InjectorProvider {
private static final Injector INJECTOR = Guice.createInjector(new DriverModule());
private InjectorProvider() {
}
public static Injector getInjector() {
return INJECTOR;
}
}

View File

@@ -0,0 +1,30 @@
package ru.kovbasa.config;
public final class TestConfig {
private TestConfig() {
}
public static String getBaseUrl() {
final String configuredBaseUrl = System.getProperty("base.url");
if (configuredBaseUrl != null && !configuredBaseUrl.isBlank()) {
return configuredBaseUrl;
}
if ("selenoid".equalsIgnoreCase(getBrowser())) {
return "http://host.docker.internal:8089";
}
return "http://localhost:8089";
}
public static String getCourseName() {
return System.getProperty("course.name", "Python Developer");
}
public static String getBrowser() {
return System.getProperty("browser", "chrome");
}
public static String getSelenoidUrl() {
return System.getProperty("selenoid.url", "http://localhost:4444/wd/hub");
}
}

View File

@@ -0,0 +1,16 @@
package ru.kovbasa.driver;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
public class ChromeDriverFactory implements DriverFactory {
@Override
public WebDriver createDriver() {
final ChromeOptions options = new ChromeOptions();
options.addArguments("--start-maximized");
options.addArguments("--disable-notifications");
return new ChromeDriver(options);
}
}

View File

@@ -0,0 +1,7 @@
package ru.kovbasa.driver;
import org.openqa.selenium.WebDriver;
public interface DriverFactory {
WebDriver createDriver();
}

View File

@@ -0,0 +1,23 @@
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();
private final DriverFactory selenoidFactory = new SelenoidDriverFactory();
@Override
public WebDriver createDriver() {
final String browser = TestConfig.getBrowser().toLowerCase();
if ("chrome".equals(browser)) {
return chromeFactory.createDriver();
}
if ("selenoid".equals(browser)) {
return selenoidFactory.createDriver();
}
throw new IllegalArgumentException(
"Unsupported browser: " + browser + ". Supported browsers: chrome, selenoid");
}
}

View File

@@ -0,0 +1,34 @@
package ru.kovbasa.driver;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import ru.kovbasa.config.TestConfig;
import java.net.MalformedURLException;
import java.net.URI;
public class SelenoidDriverFactory implements DriverFactory {
@Override
public WebDriver createDriver() {
final ChromeOptions options = new ChromeOptions();
options.setCapability("browserName", "chrome");
options.setCapability("browserVersion", "126.0");
options.setCapability("selenoid:options", java.util.Map.of(
"enableVNC", true,
"enableVideo", false
));
try {
return new RemoteWebDriver(URI.create(TestConfig.getSelenoidUrl()).toURL(), options);
} catch (MalformedURLException exception) {
throw new IllegalArgumentException("Invalid selenoid.url: " + TestConfig.getSelenoidUrl(), exception);
} catch (RuntimeException exception) {
throw new IllegalStateException(
"Cannot create RemoteWebDriver session via selenoid.url=" + TestConfig.getSelenoidUrl()
+ ". Ensure Docker is running and execute 'docker compose up -d' first.",
exception
);
}
}
}

View File

@@ -0,0 +1,37 @@
package ru.kovbasa.driver;
import com.google.inject.Inject;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.events.EventFiringDecorator;
import ru.kovbasa.listeners.HighlightElementListener;
public final class WebDriverProvider {
private WebDriver driver;
private final DriverFactory driverFactory;
@Inject
public WebDriverProvider(DriverFactory driverFactory) {
this.driverFactory = driverFactory;
}
public WebDriver getDriver() {
if (driver == null) {
driver = createDecoratedDriver();
}
return driver;
}
private WebDriver createDecoratedDriver() {
final WebDriver raw = driverFactory.createDriver();
return new EventFiringDecorator(new HighlightElementListener())
.decorate(raw);
}
public void quit() {
if (driver != null) {
driver.quit();
driver = null;
}
}
}

View File

@@ -0,0 +1,27 @@
package ru.kovbasa.elements;
import org.openqa.selenium.WebElement;
public abstract class BaseElement implements UIElement {
protected final WebElement element;
protected BaseElement(WebElement element) {
this.element = element;
}
@Override
public void click() {
element.click();
}
@Override
public String getText() {
return element.getText();
}
@Override
public boolean isDisplayed() {
return element.isDisplayed();
}
}

View File

@@ -0,0 +1,14 @@
package ru.kovbasa.elements;
import org.openqa.selenium.WebElement;
public class Button extends BaseElement {
public Button(WebElement element) {
super(element);
}
public boolean isEnabled() {
return element.isEnabled();
}
}

View File

@@ -0,0 +1,77 @@
package ru.kovbasa.elements;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
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);
}
public String title() {
return element.findElement(titleLocator).getText().trim();
}
public LocalDate startDate() {
String raw = element.findElement(dateLocator).getText().trim();
if (raw.isEmpty()) {
throw new RuntimeException("Пустая дата на карточке курса: " + title());
}
String[] parts = raw.split("·");
String datePart = parts[0].trim();
if (datePart.startsWith("С ")) {
datePart = datePart.substring(2).trim();
}
datePart = datePart.replace(",", "").replace("года", "").trim();
boolean hasYear = datePart.matches(".*\\d{4}.*");
String normalized;
DateTimeFormatter formatter;
if (hasYear) {
normalized = datePart;
formatter = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.forLanguageTag("ru"));
} else {
normalized = datePart + " " + Year.now().getValue();
formatter = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.forLanguageTag("ru"));
}
return LocalDate.parse(normalized, formatter);
}
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;
}
}

View File

@@ -0,0 +1,14 @@
package ru.kovbasa.elements;
import org.openqa.selenium.WebElement;
public class Link extends BaseElement {
public Link(WebElement element) {
super(element);
}
public String getHref() {
return element.getAttribute("href");
}
}

View File

@@ -0,0 +1,7 @@
package ru.kovbasa.elements;
public interface UIElement {
void click();
String getText();
boolean isDisplayed();
}

View File

@@ -0,0 +1,158 @@
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;
import org.openqa.selenium.WebElement;
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; " +
"box-shadow: 0 0 0 4px rgba(255,0,0,0.20) !important; " +
"background: rgba(255,0,0,0.06) !important;";
private WebElement highlightedElement;
private String highlightedOriginalStyle;
@Override
public void beforeClick(WebElement element) {
try {
clearHighlight();
final WebElement tile = findTileContainer(element);
applyHighlight(tile);
pauseForVisibility();
} catch (RuntimeException e) {
LOG.debug("beforeClick highlight skipped", e);
}
}
@Override
public void afterClick(WebElement element) {
clearHighlight();
}
private void applyHighlight(WebElement element) {
if (element == null) {
return;
}
try {
final WebDriver raw = ((WrapsDriver) element).getWrappedDriver();
if (!(raw instanceof JavascriptExecutor)) {
return;
}
final JavascriptExecutor js = (JavascriptExecutor) raw;
String original = safeGetAttr(element, "style");
if (original == null) {
original = "";
}
this.highlightedElement = element;
this.highlightedOriginalStyle = original;
js.executeScript("arguments[0].setAttribute('style', arguments[1] + '; ' + arguments[2]);",
element, original, HIGHLIGHT_STYLE);
} catch (StaleElementReferenceException ignored) {
} catch (RuntimeException e) {
LOG.debug("applyHighlight skipped", e);
}
}
private void clearHighlight() {
if (highlightedElement == null) {
return;
}
try {
final WebDriver raw = ((WrapsDriver) highlightedElement).getWrappedDriver();
if (!(raw instanceof JavascriptExecutor)) {
highlightedElement = null;
highlightedOriginalStyle = null;
return;
}
final JavascriptExecutor js = (JavascriptExecutor) raw;
final String original = highlightedOriginalStyle == null ? "" : highlightedOriginalStyle;
js.executeScript("arguments[0].setAttribute('style', arguments[1]);", highlightedElement, original);
} catch (StaleElementReferenceException ignored) {
} catch (RuntimeException e) {
LOG.debug("clearHighlight skipped", e);
} finally {
highlightedElement = null;
highlightedOriginalStyle = null;
}
}
private WebElement findTileContainer(WebElement element) {
try {
final String tag = safeGetTag(element);
final String href = safeGetAttr(element, "href");
final String cls = safeGetAttr(element, "class");
if ("a".equalsIgnoreCase(tag) && href != null &&
(href.contains("/lessons/") || href.contains("/categories/"))) {
return element;
}
if (cls != null && cls.contains("sc-zzdkm7-0")) {
return element;
}
final WebDriver rawDriver = ((WrapsDriver) element).getWrappedDriver();
if (!(rawDriver instanceof JavascriptExecutor)) {
return element;
}
final JavascriptExecutor js = (JavascriptExecutor) rawDriver;
WebElement current = element;
for (int i = 0; i < 8; i++) {
final Object parentObj = js.executeScript("return arguments[0].parentElement;", current);
if (!(parentObj instanceof WebElement)) {
break;
}
current = (WebElement) parentObj;
final String t = safeGetTag(current);
final String h = safeGetAttr(current, "href");
final String c = safeGetAttr(current, "class");
if ("a".equalsIgnoreCase(t) && h != null &&
(h.contains("/lessons/") || h.contains("/categories/"))) {
return current;
}
if (c != null && c.contains("sc-zzdkm7-0")) {
return current;
}
}
} catch (RuntimeException e) {
LOG.debug("findTileContainer fallback to original element", e);
}
return element;
}
private String safeGetAttr(WebElement el, String name) {
try { return el.getAttribute(name); }
catch (Exception e) { return null; }
}
private String safeGetTag(WebElement el) {
try { return el.getTagName(); }
catch (Exception e) { return null; }
}
private void pauseForVisibility() {
try {
Thread.sleep(120);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

View File

@@ -0,0 +1,355 @@
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;
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.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import ru.kovbasa.config.TestConfig;
import ru.kovbasa.driver.WebDriverProvider;
import ru.kovbasa.elements.CourseCard;
import ru.kovbasa.utils.PageUtils;
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/'], 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) {
this.driver = provider.getDriver();
}
public CatalogPage open() {
driver.get(TestConfig.getBaseUrl() + "/catalog/courses");
return this;
}
public WebElement findCourseByName(String name) {
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseLinks).isEmpty());
final List<WebElement> links = driver.findElements(courseLinks);
return links.stream()
.filter(e -> e.getText().toLowerCase().contains(name.toLowerCase()))
.findFirst()
.orElseThrow(() -> new NoSuchElementException(
"Course not found in catalog by name: " + name));
}
public List<WebElement> findCoursesByName(String name) {
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
wait.until((ExpectedCondition<Boolean>) 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);
final WebElement course = findCourseByName(name);
((JavascriptExecutor) driver)
.executeScript("arguments[0].scrollIntoView({block:'center'});", course);
new Actions(driver)
.moveToElement(course)
.perform();
course.click();
return new CoursePage(driver);
}
public CoursePage clickRandomCourseByName(String name) {
PageUtils.removeBottomBanner(driver);
final List<WebElement> 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<CourseCard> getAllCourseCards() {
return driver.findElements(courseCards).stream()
.map(CourseCard::new)
.toList();
}
public List<CourseItem> getAllCourses() {
return getAllCourseCards().stream()
.map(card -> {
try {
return new CourseItem(card.title(), card.startDate());
} catch (RuntimeException e) {
return null;
}
})
.filter(Objects::nonNull)
.toList();
}
public List<CourseItem> findEarliestCourses() {
final List<CourseItem> all = getAllCourses();
final LocalDate minDate = all.stream()
.map(CourseItem::startDate)
.reduce((left, right) -> left.isBefore(right) ? left : right)
.orElseThrow();
return all.stream()
.filter(c -> c.startDate().isEqual(minDate))
.sorted(Comparator.comparing(CourseItem::title))
.toList();
}
public List<CourseItem> findLatestCourses() {
final List<CourseItem> all = getAllCourses();
final LocalDate maxDate = all.stream()
.map(CourseItem::startDate)
.reduce((left, right) -> left.isAfter(right) ? left : right)
.orElseThrow();
return all.stream()
.filter(c -> c.startDate().isEqual(maxDate))
.sorted(Comparator.comparing(CourseItem::title))
.toList();
}
public List<CourseItem> 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<PricedCourseItem> 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<CourseLinkItem> getPreparatoryCourseLinks() {
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseLinks).isEmpty());
final Map<String, String> 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<Map.Entry<String, String>> onlineEntries = titleByUrl.entrySet().stream()
.filter(entry -> entry.getKey().contains("/online/"))
.toList();
final List<CourseLinkItem> 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<PricedCourseItem> getPreparatoryCoursesWithDiscountedFullPrice() {
final List<CourseLinkItem> 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<CourseLinkItem> getCatalogLessonCourseLinks() {
open();
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseLinks).isEmpty());
final Map<String, String> 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<CourseLinkItem> items = new ArrayList<>();
titleByUrl.forEach((url, title) -> items.add(new CourseLinkItem(title, url)));
return items;
}
public List<PricedCourseItem> getCatalogCoursesWithDiscountedFullPrice() {
final List<CourseLinkItem> 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);
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
final WebElement card = wait.until(drv ->
drv.findElements(courseCards).stream()
.filter(c -> new CourseCard(c).title().equals(course.title()))
.findFirst()
.orElse(null)
);
((JavascriptExecutor) driver)
.executeScript("arguments[0].scrollIntoView({block:'center'});", card);
new Actions(driver)
.moveToElement(card)
.perform();
card.click();
return new CoursePage(driver);
}
}

View File

@@ -0,0 +1,24 @@
package ru.kovbasa.pages;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.openqa.selenium.WebDriver;
public class CategoryPage {
private final WebDriver driver;
public CategoryPage(WebDriver driver) {
this.driver = driver;
}
public String getHeader() {
Document doc = Jsoup.parse(driver.getPageSource());
return doc.select("h1")
.stream()
.map(e -> e.text().trim())
.filter(t -> !t.isEmpty())
.findFirst()
.orElseThrow(() -> new RuntimeException("Category header not found"));
}
}

View File

@@ -0,0 +1,6 @@
package ru.kovbasa.pages;
import java.time.LocalDate;
public record CourseItem(String title, LocalDate startDate) {
}

View File

@@ -0,0 +1,4 @@
package ru.kovbasa.pages;
public record CourseLinkItem(String title, String url) {
}

View File

@@ -0,0 +1,139 @@
package ru.kovbasa.pages;
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;
}
public String getCourseTitle() {
return getFirstH1();
}
public String getFirstH1() {
Document doc = Jsoup.parse(driver.getPageSource());
return doc.select("h1")
.stream()
.map(e -> e.text().trim())
.filter(t -> !t.isEmpty())
.findFirst()
.orElseThrow(() -> new RuntimeException("Course title not found"));
}
public LocalDate getCourseStartDate(LocalDate catalogDate) {
Document doc = Jsoup.parse(driver.getPageSource());
Element dateElement = doc.selectFirst("p.sc-3cb1l3-0");
if (dateElement == null) {
throw new RuntimeException("Start date <p> not found on course page");
}
String text = dateElement.text().trim();
String fullDate = text + ", " + catalogDate.getYear();
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("d MMMM, yyyy", Locale.forLanguageTag("ru"));
return LocalDate.parse(fullDate, formatter);
}
public Button getEnrollButton() {
return new Button(driver.findElement(enrollButton));
}
public void clickEnroll() {
getEnrollButton().click();
}
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);
}
}

View File

@@ -0,0 +1,70 @@
package ru.kovbasa.pages;
import com.google.inject.Inject;
import org.openqa.selenium.By;
import org.openqa.selenium.ElementClickInterceptedException;
import org.openqa.selenium.JavascriptExecutor;
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.config.TestConfig;
import ru.kovbasa.driver.WebDriverProvider;
import ru.kovbasa.utils.PageUtils;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class MainPage {
private final WebDriver driver;
private final By menuLearning = By.cssSelector("span[title='Обучение']");
private final By categories = By.cssSelector("a[href*='/categories/']");
@Inject
public MainPage(WebDriverProvider provider) {
this.driver = provider.getDriver();
}
public MainPage open() {
driver.get(TestConfig.getBaseUrl());
return this;
}
public String clickRandomCategory() {
PageUtils.removeBottomBanner(driver);
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
final WebElement menu = wait.until(drv -> drv.findElement(menuLearning));
menu.click();
final List<WebElement> els = wait.until(drv -> {
final List<WebElement> found = drv.findElements(categories).stream()
.filter(WebElement::isDisplayed)
.toList();
return found.isEmpty() ? null : found;
});
final WebElement chosen = els.get(ThreadLocalRandom.current().nextInt(els.size()));
final String href = chosen.getAttribute("href");
((JavascriptExecutor) driver)
.executeScript("arguments[0].scrollIntoView({block:'center'});", chosen);
try {
chosen.click();
} catch (ElementClickInterceptedException ignored) {
new Actions(driver).moveToElement(chosen).click().perform();
}
final String categorySlug = href.substring(href.lastIndexOf('/') + 1);
if (!driver.getCurrentUrl().contains(categorySlug)) {
driver.get(TestConfig.getBaseUrl() + "/catalog/courses?categories=" + categorySlug);
}
return href;
}
}

View File

@@ -0,0 +1,4 @@
package ru.kovbasa.pages;
public record PricedCourseItem(String title, int price) {
}

View File

@@ -0,0 +1,22 @@
package ru.kovbasa.utils;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
public final class PageUtils {
private PageUtils() {
}
public static void removeBottomBanner(WebDriver driver) {
try {
((JavascriptExecutor) driver)
.executeScript(
"document.querySelectorAll('.sc-11pdrud-1.cmIXWc')" +
".forEach(e => e.remove());"
);
} catch (Exception ignored) {
}
}
}

View File

@@ -0,0 +1,56 @@
package ru.otus.stub.helper;
import io.restassured.RestAssured;
import io.restassured.common.mapper.TypeRef;
import ru.otus.stub.model.Course;
import ru.otus.stub.model.User;
import ru.otus.stub.model.UserScore;
import java.util.List;
public class HttpHelper {
private final String baseUri;
private final String basePath;
public HttpHelper(String baseUri, String basePath) {
this.baseUri = baseUri;
this.basePath = basePath;
}
public List<User> getAllUsers() {
return RestAssured.given()
.baseUri(baseUri)
.basePath(basePath)
.when()
.get("/user/get/all")
.then()
.statusCode(200)
.extract()
.as(new TypeRef<>() { });
}
public List<Course> getAllCourses() {
return RestAssured.given()
.baseUri(baseUri)
.basePath(basePath)
.when()
.get("/cource/get/all")
.then()
.statusCode(200)
.extract()
.as(new TypeRef<>() { });
}
public UserScore getUserScore(long id) {
return RestAssured.given()
.baseUri(baseUri)
.basePath(basePath)
.when()
.get("/user/get/{id}", id)
.then()
.statusCode(200)
.extract()
.as(UserScore.class);
}
}

View File

@@ -0,0 +1,38 @@
package ru.otus.stub.helper;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class MqHelper {
private final Map<String, BlockingQueue<String>> queues = new ConcurrentHashMap<>();
public void send(String queueName, String payload) {
queue(queueName).add(payload);
}
public String receive(String queueName, Duration timeout) {
try {
return queue(queueName).poll(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while receiving from queue: " + queueName, exception);
}
}
public int size(String queueName) {
return queue(queueName).size();
}
public void clear(String queueName) {
queue(queueName).clear();
}
private BlockingQueue<String> queue(String queueName) {
return queues.computeIfAbsent(queueName, key -> new LinkedBlockingQueue<>());
}
}

View File

@@ -0,0 +1,48 @@
package ru.otus.stub.helper;
import io.restassured.RestAssured;
import io.restassured.path.xml.XmlPath;
import ru.otus.stub.model.UserScore;
public class SoapHelper {
private final String baseUri;
private final String basePath;
public SoapHelper(String baseUri, String basePath) {
this.baseUri = baseUri;
this.basePath = basePath;
}
public UserScore getUserScore(long id) {
final String request = """
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:stu="http://otus.ru/stub">
<soapenv:Header/>
<soapenv:Body>
<stu:GetUserScoreRequest>
<stu:id>${id}</stu:id>
</stu:GetUserScoreRequest>
</soapenv:Body>
</soapenv:Envelope>
""".replace("${id}", String.valueOf(id));
final String response = RestAssured.given()
.baseUri(baseUri)
.basePath(basePath)
.contentType("text/xml; charset=UTF-8")
.body(request)
.when()
.post("/soap/user/score")
.then()
.statusCode(200)
.extract()
.asString();
final XmlPath xml = new XmlPath(response);
final String name = xml.getString("Envelope.Body.GetUserScoreResponse.name");
final int score = xml.getInt("Envelope.Body.GetUserScoreResponse.score");
return new UserScore(name, score);
}
}

View File

@@ -0,0 +1,63 @@
package ru.otus.stub.helper;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class SqlHelper {
private final String jdbcUrl;
private final String username;
private final String password;
public SqlHelper(String jdbcUrl, String username, String password) {
this.jdbcUrl = jdbcUrl;
this.username = username;
this.password = password;
}
public int executeUpdate(String sql) {
try (Connection connection = openConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
return statement.executeUpdate();
} catch (SQLException exception) {
throw new IllegalStateException("SQL update failed: " + sql, exception);
}
}
public List<Map<String, Object>> selectRows(String sql) {
try (Connection connection = openConnection();
PreparedStatement statement = connection.prepareStatement(sql);
ResultSet resultSet = statement.executeQuery()) {
return mapRows(resultSet);
} catch (SQLException exception) {
throw new IllegalStateException("SQL query failed: " + sql, exception);
}
}
private Connection openConnection() throws SQLException {
return DriverManager.getConnection(jdbcUrl, username, password);
}
private List<Map<String, Object>> mapRows(ResultSet resultSet) throws SQLException {
final List<Map<String, Object>> rows = new ArrayList<>();
final ResultSetMetaData metaData = resultSet.getMetaData();
final int columnsCount = metaData.getColumnCount();
while (resultSet.next()) {
final Map<String, Object> row = new LinkedHashMap<>();
for (int column = 1; column <= columnsCount; column++) {
row.put(metaData.getColumnLabel(column), resultSet.getObject(column));
}
rows.add(row);
}
return rows;
}
}

View File

@@ -0,0 +1,4 @@
package ru.otus.stub.model;
public record Course(String name, int price) {
}

View File

@@ -0,0 +1,4 @@
package ru.otus.stub.model;
public record User(String name, String cource, String email, int age) {
}

View File

@@ -0,0 +1,4 @@
package ru.otus.stub.model;
public record UserScore(String name, int score) {
}