hw4: finalize selenoid and ansible workflow with citrus tests
This commit is contained in:
@@ -1,16 +1,30 @@
|
||||
package ru.kovbasa.config;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.Singleton;
|
||||
import ru.kovbasa.driver.ChromeDriverFactory;
|
||||
import ru.kovbasa.driver.DriverFactory;
|
||||
import ru.kovbasa.driver.MobileChromeDriverFactory;
|
||||
import ru.kovbasa.driver.RemoteDriverFactory;
|
||||
import ru.kovbasa.driver.WebDriverProvider;
|
||||
|
||||
public class DriverModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(DriverFactory.class).to(ChromeDriverFactory.class).in(Singleton.class);
|
||||
bind(WebDriverProvider.class).in(Singleton.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public DriverFactory provideDriverFactory() {
|
||||
if (TestConfig.isSelenoidMode()) {
|
||||
if (TestConfig.isMobileBrowser()) {
|
||||
return new MobileChromeDriverFactory();
|
||||
}
|
||||
return new RemoteDriverFactory();
|
||||
}
|
||||
return new ChromeDriverFactory();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,19 @@ public final class TestConfig {
|
||||
private static final String COURSE_NAME =
|
||||
System.getProperty("course.name", "Python Developer");
|
||||
|
||||
private static final String EXECUTION_MODE =
|
||||
System.getProperty("execution.mode", "local");
|
||||
|
||||
private static final String BROWSER =
|
||||
System.getProperty("browser", "chrome");
|
||||
|
||||
private static final String BROWSER_VERSION =
|
||||
System.getProperty("browser.version", "");
|
||||
|
||||
private static final String SELENOID_URL =
|
||||
System.getProperty("selenoid.url", "http://localhost/wd/hub");
|
||||
|
||||
|
||||
private TestConfig() {
|
||||
}
|
||||
|
||||
@@ -18,4 +31,32 @@ public final class TestConfig {
|
||||
public static String getCourseName() {
|
||||
return COURSE_NAME;
|
||||
}
|
||||
|
||||
public static String getExecutionMode() {
|
||||
return EXECUTION_MODE;
|
||||
}
|
||||
|
||||
public static String getBrowser() {
|
||||
return BROWSER;
|
||||
}
|
||||
|
||||
public static String getSelenoidUrl() {
|
||||
return SELENOID_URL;
|
||||
}
|
||||
|
||||
public static String getBrowserVersion() {
|
||||
return BROWSER_VERSION;
|
||||
}
|
||||
|
||||
public static boolean isSelenoidMode() {
|
||||
return "selenoid".equalsIgnoreCase(EXECUTION_MODE);
|
||||
}
|
||||
|
||||
public static boolean isMobileBrowser() {
|
||||
return "mobile_chrome".equalsIgnoreCase(BROWSER);
|
||||
}
|
||||
|
||||
public static boolean isFirefoxBrowser() {
|
||||
return "firefox".equalsIgnoreCase(BROWSER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.kovbasa.driver;
|
||||
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class MobileChromeDriverFactory extends RemoteDriverFactory {
|
||||
|
||||
@Override
|
||||
public WebDriver createDriver() {
|
||||
final ChromeOptions options = new ChromeOptions();
|
||||
options.setCapability("browserName", "chrome");
|
||||
applyCommonCapabilities(options);
|
||||
options.setCapability("goog:chromeOptions", Map.of(
|
||||
"mobileEmulation", Map.of("deviceName", "Pixel 7")
|
||||
));
|
||||
return createRemote(options);
|
||||
}
|
||||
}
|
||||
60
src/main/java/ru/kovbasa/driver/RemoteDriverFactory.java
Normal file
60
src/main/java/ru/kovbasa/driver/RemoteDriverFactory.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package ru.kovbasa.driver;
|
||||
|
||||
import org.openqa.selenium.MutableCapabilities;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.openqa.selenium.firefox.FirefoxOptions;
|
||||
import org.openqa.selenium.remote.RemoteWebDriver;
|
||||
import ru.kovbasa.config.TestConfig;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class RemoteDriverFactory implements DriverFactory {
|
||||
|
||||
@Override
|
||||
public WebDriver createDriver() {
|
||||
final MutableCapabilities options = TestConfig.isFirefoxBrowser()
|
||||
? buildFirefoxOptions()
|
||||
: buildChromeOptions();
|
||||
return createRemote(options);
|
||||
}
|
||||
|
||||
protected MutableCapabilities buildChromeOptions() {
|
||||
final ChromeOptions options = new ChromeOptions();
|
||||
options.addArguments("--disable-notifications");
|
||||
applyCommonCapabilities(options);
|
||||
return options;
|
||||
}
|
||||
|
||||
protected MutableCapabilities buildFirefoxOptions() {
|
||||
final FirefoxOptions options = new FirefoxOptions();
|
||||
applyCommonCapabilities(options);
|
||||
return options;
|
||||
}
|
||||
|
||||
protected void applyCommonCapabilities(MutableCapabilities options) {
|
||||
final String browserVersion = TestConfig.getBrowserVersion();
|
||||
if (!browserVersion.isBlank()) {
|
||||
options.setCapability("browserVersion", browserVersion);
|
||||
}
|
||||
options.setCapability("selenoid:options", Map.of(
|
||||
"name", "otus-ui-tests",
|
||||
"sessionTimeout", "15m",
|
||||
"env", List.of("TZ=UTC"),
|
||||
"labels", Map.of("manual", "true"),
|
||||
"enableVNC", true,
|
||||
"enableVideo", false
|
||||
));
|
||||
}
|
||||
|
||||
protected WebDriver createRemote(MutableCapabilities capabilities) {
|
||||
try {
|
||||
return new RemoteWebDriver(URI.create(TestConfig.getSelenoidUrl()).toURL(), capabilities);
|
||||
} catch (MalformedURLException e) {
|
||||
throw new IllegalStateException("Invalid selenoid.url: " + TestConfig.getSelenoidUrl(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.support.events.EventFiringDecorator;
|
||||
|
||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||
import ru.kovbasa.config.TestConfig;
|
||||
import ru.kovbasa.listeners.HighlightElementListener;
|
||||
|
||||
public final class WebDriverProvider {
|
||||
@@ -12,10 +13,6 @@ public final class WebDriverProvider {
|
||||
private WebDriver driver;
|
||||
private final DriverFactory driverFactory;
|
||||
|
||||
static {
|
||||
WebDriverManager.chromedriver().setup();
|
||||
}
|
||||
|
||||
@Inject
|
||||
public WebDriverProvider(DriverFactory driverFactory) {
|
||||
this.driverFactory = driverFactory;
|
||||
@@ -29,6 +26,9 @@ public final class WebDriverProvider {
|
||||
}
|
||||
|
||||
private WebDriver createDecoratedDriver() {
|
||||
if (!TestConfig.isSelenoidMode()) {
|
||||
WebDriverManager.chromedriver().setup();
|
||||
}
|
||||
final WebDriver raw = driverFactory.createDriver();
|
||||
return new EventFiringDecorator(new HighlightElementListener())
|
||||
.decorate(raw);
|
||||
|
||||
@@ -3,13 +3,14 @@ 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.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.NoSuchElementException;
|
||||
import org.openqa.selenium.StaleElementReferenceException;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.interactions.Actions;
|
||||
@@ -35,6 +36,8 @@ public class CatalogPage {
|
||||
|
||||
public CatalogPage open() {
|
||||
driver.get(TestConfig.getBaseUrl() + "/catalog/courses");
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));
|
||||
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseCards).isEmpty());
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -67,22 +70,38 @@ public class CatalogPage {
|
||||
}
|
||||
|
||||
public List<CourseCard> getAllCourseCards() {
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));
|
||||
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseCards).isEmpty());
|
||||
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;
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));
|
||||
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseCards).isEmpty());
|
||||
|
||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
final List<CourseItem> parsedCourses = new ArrayList<>();
|
||||
for (WebElement cardElement : driver.findElements(courseCards)) {
|
||||
final CourseItem item = toCourseItem(new CourseCard(cardElement));
|
||||
if (item != null) {
|
||||
parsedCourses.add(item);
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
if (!parsedCourses.isEmpty()) {
|
||||
return parsedCourses;
|
||||
}
|
||||
} catch (StaleElementReferenceException ignored) {
|
||||
// Dynamic catalog can re-render the card list; retry with fresh references.
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"No parsable courses found in catalog after retries. Check course card/date locators."
|
||||
);
|
||||
}
|
||||
|
||||
public List<CourseItem> findEarliestCourses() {
|
||||
@@ -90,8 +109,9 @@ public class CatalogPage {
|
||||
|
||||
final LocalDate minDate = all.stream()
|
||||
.map(CourseItem::startDate)
|
||||
.reduce((left, right) -> left.isBefore(right) ? left : right)
|
||||
.orElseThrow();
|
||||
.min(LocalDate::compareTo)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"Unable to determine earliest date from parsed courses"));
|
||||
|
||||
return all.stream()
|
||||
.filter(c -> c.startDate().isEqual(minDate))
|
||||
@@ -104,8 +124,9 @@ public class CatalogPage {
|
||||
|
||||
final LocalDate maxDate = all.stream()
|
||||
.map(CourseItem::startDate)
|
||||
.reduce((left, right) -> left.isAfter(right) ? left : right)
|
||||
.orElseThrow();
|
||||
.max(LocalDate::compareTo)
|
||||
.orElseThrow(() -> new IllegalStateException(
|
||||
"Unable to determine latest date from parsed courses"));
|
||||
|
||||
return all.stream()
|
||||
.filter(c -> c.startDate().isEqual(maxDate))
|
||||
@@ -116,23 +137,45 @@ public class CatalogPage {
|
||||
public CoursePage openCourse(CourseItem course) {
|
||||
PageUtils.removeBottomBanner(driver);
|
||||
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(12));
|
||||
wait.until((ExpectedCondition<Boolean>) drv -> !drv.findElements(courseCards).isEmpty());
|
||||
|
||||
final WebElement card = wait.until(drv ->
|
||||
drv.findElements(courseCards).stream()
|
||||
.filter(c -> new CourseCard(c).title().equals(course.title()))
|
||||
.findFirst()
|
||||
.orElse(null)
|
||||
);
|
||||
for (int attempt = 1; attempt <= 5; attempt++) {
|
||||
try {
|
||||
for (WebElement card : driver.findElements(courseCards)) {
|
||||
final CourseCard courseCard = new CourseCard(card);
|
||||
final String candidateTitle;
|
||||
final LocalDate candidateDate;
|
||||
try {
|
||||
candidateTitle = courseCard.title();
|
||||
candidateDate = courseCard.startDate();
|
||||
} catch (RuntimeException ignored) {
|
||||
continue;
|
||||
}
|
||||
if (!candidateTitle.equals(course.title()) || !candidateDate.equals(course.startDate())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
((JavascriptExecutor) driver)
|
||||
.executeScript("arguments[0].scrollIntoView({block:'center'});", card);
|
||||
((JavascriptExecutor) driver)
|
||||
.executeScript("arguments[0].scrollIntoView({block:'center'});", card);
|
||||
|
||||
new Actions(driver)
|
||||
.moveToElement(card)
|
||||
.perform();
|
||||
card.click();
|
||||
new Actions(driver).moveToElement(card).perform();
|
||||
card.click();
|
||||
return new CoursePage(driver);
|
||||
}
|
||||
} catch (StaleElementReferenceException ignored) {
|
||||
// Re-query cards when DOM updates during search/click.
|
||||
}
|
||||
}
|
||||
|
||||
return new CoursePage(driver);
|
||||
throw new NoSuchElementException("Course card not found for title: " + course.title());
|
||||
}
|
||||
|
||||
private CourseItem toCourseItem(CourseCard card) {
|
||||
try {
|
||||
return new CourseItem(card.title(), card.startDate());
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.inject.Inject;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.ElementClickInterceptedException;
|
||||
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;
|
||||
@@ -20,7 +21,8 @@ public class MainPage {
|
||||
|
||||
private final WebDriver driver;
|
||||
|
||||
private final By menuLearning = By.cssSelector("span[title='Обучение']");
|
||||
private final By menuLearningByTitle = By.cssSelector("span[title='Обучение']");
|
||||
private final By menuLearningByText = By.xpath("//span[normalize-space()='Обучение']");
|
||||
private final By categories = By.cssSelector("a[href*='/categories/']");
|
||||
|
||||
@Inject
|
||||
@@ -36,17 +38,18 @@ public class MainPage {
|
||||
public String clickRandomCategory() {
|
||||
PageUtils.removeBottomBanner(driver);
|
||||
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
|
||||
final WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(20));
|
||||
|
||||
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;
|
||||
});
|
||||
List<WebElement> els = findVisibleCategories(wait);
|
||||
if (els.isEmpty()) {
|
||||
clickLearningMenu(wait);
|
||||
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");
|
||||
@@ -68,4 +71,29 @@ public class MainPage {
|
||||
|
||||
return href;
|
||||
}
|
||||
|
||||
private List<WebElement> findVisibleCategories(WebDriverWait wait) {
|
||||
return wait.until(drv -> drv.findElements(categories).stream()
|
||||
.filter(WebElement::isDisplayed)
|
||||
.toList());
|
||||
}
|
||||
|
||||
private void clickLearningMenu(WebDriverWait wait) {
|
||||
WebElement menu = null;
|
||||
try {
|
||||
menu = wait.until(drv -> drv.findElement(menuLearningByTitle));
|
||||
} catch (NoSuchElementException ignored) {
|
||||
} catch (org.openqa.selenium.TimeoutException ignored) {
|
||||
}
|
||||
|
||||
if (menu == null) {
|
||||
menu = wait.until(drv -> drv.findElement(menuLearningByText));
|
||||
}
|
||||
|
||||
try {
|
||||
menu.click();
|
||||
} catch (ElementClickInterceptedException ignored) {
|
||||
((JavascriptExecutor) driver).executeScript("arguments[0].click();", menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user