Add Playwright UI tests for homework 6
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.idea/
|
||||
*.iml
|
||||
.target/
|
||||
target/
|
||||
playwright-report/
|
||||
traces/
|
||||
82
README.md
Normal file
82
README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# OTUS Homework 6: Playwright UI Tests
|
||||
|
||||
UI-автотесты на Playwright для 4 сценариев OTUS. Проект оформлен в стиле предыдущих ДЗ: DI через Guice, трассировка, линтеры, запуск из консоли.
|
||||
|
||||
## Что реализовано
|
||||
- 4 UI-сценария из ТЗ (Clickhouse, Catalog, B2B, Subscription).
|
||||
- DI через Guice: ресурсы Playwright создаются в фикстурах и инжектятся в тесты.
|
||||
- Трассировка включена для каждого теста.
|
||||
- Линтеры: Checkstyle и SpotBugs.
|
||||
- Запуск из консоли (Maven).
|
||||
|
||||
## Структура проекта
|
||||
- `src/test/java/ru/kovbasa/tests` — тесты по сценариям.
|
||||
- `src/test/java/ru/kovbasa/pages` — page-объекты.
|
||||
- `src/test/java/ru/kovbasa/playwright` — фикстуры Playwright.
|
||||
- `src/test/java/ru/kovbasa/config` — DI и Guice extension.
|
||||
- `traces/` — zip-трейсы по каждому тесту.
|
||||
- `traces.zip` — архив для сдачи.
|
||||
|
||||
## Сценарии
|
||||
1. **Clickhouse**: блок преподавателей, drag&drop, popup, next/prev в карточке.
|
||||
2. **Каталог курсов**: фильтры по направлению/уровню/длительности, проверка изменения карточек.
|
||||
3. **Услуги компаниям**: переход в “Разработка курса для бизнеса”, направления, переход в каталог.
|
||||
4. **Подписки**: раскрытие/сворачивание описания, переход к оплате, выбор Trial.
|
||||
|
||||
## Версии
|
||||
- Java: 21
|
||||
- Playwright: 1.58.0
|
||||
- JUnit: 5.10.1
|
||||
- Guice: 7.0.0
|
||||
|
||||
## Требования из ТЗ
|
||||
- Использование DI — реализовано через Guice.
|
||||
- Использование линтеров — Checkstyle и SpotBugs включены в `mvn verify`.
|
||||
- Только Playwright — другие UI-фреймворки не используются.
|
||||
- Трассировка для всех тестов — включена в фикстуре `PlaywrightExtension`.
|
||||
- `@UsePlaywright` не используется — ресурсы создаются и инжектятся вручную.
|
||||
- Запуск из консоли — `mvn test`.
|
||||
|
||||
## Как запускать
|
||||
### Все тесты
|
||||
```bash
|
||||
mvn test
|
||||
```
|
||||
|
||||
### Один тест
|
||||
```bash
|
||||
mvn "-Dtest=ru.kovbasa.tests.CatalogFiltersTest" test
|
||||
```
|
||||
|
||||
### Параметры запуска
|
||||
```bash
|
||||
mvn -Dheadless=false -Dbrowser=chromium -DbaseUrl=https://otus.ru test
|
||||
```
|
||||
|
||||
Доступные браузеры: `chromium`, `firefox`, `webkit`.
|
||||
|
||||
Полезные параметры:
|
||||
- `-Dheadless=false` — запуск в UI.
|
||||
- `-DslowMo=200` — замедление действий.
|
||||
- `-DtimeoutMs=40000` — таймауты ожиданий.
|
||||
|
||||
## Трейсы
|
||||
Трейсы автоматически сохраняются в каталог `traces/` в виде zip-файлов по каждому тесту.
|
||||
|
||||
Для сдачи ДЗ нужно положить `traces.zip` в корень проекта. Пример (PowerShell):
|
||||
```powershell
|
||||
Compress-Archive -Path traces\* -DestinationPath traces.zip -Force
|
||||
```
|
||||
|
||||
## Проверка качества
|
||||
```bash
|
||||
mvn verify
|
||||
```
|
||||
|
||||
## Примечания по консоли
|
||||
- Предупреждения `sun.misc.Unsafe` приходят из зависимостей Guava при Java 21. Добавлен флаг `-Dcom.google.common.util.concurrent.AbstractFuture.disableUnsafe=true`, чтобы минимизировать их.
|
||||
- Сообщения `Corrupted channel...` появляются только при первом скачивании браузеров Playwright. После установки браузеров они исчезают.
|
||||
|
||||
## Возможные проблемы
|
||||
- Если не загружаются браузеры Playwright, выполните `mvn test` с доступом к сети.
|
||||
- Если попапы мешают кликам, используется автоматическое закрытие типовых модалок.
|
||||
53
checkstyle.xml
Normal file
53
checkstyle.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
|
||||
<module name="Checker">
|
||||
|
||||
<!-- File encoding -->
|
||||
<property name="charset" value="UTF-8"/>
|
||||
|
||||
<!-- Fail build on any violation -->
|
||||
<property name="severity" value="error"/>
|
||||
|
||||
<!-- Disallow tabs -->
|
||||
<module name="FileTabCharacter"/>
|
||||
|
||||
<!-- Max line length -->
|
||||
<module name="LineLength">
|
||||
<property name="max" value="120"/>
|
||||
</module>
|
||||
|
||||
<module name="TreeWalker">
|
||||
|
||||
<!-- Naming conventions -->
|
||||
<module name="TypeName"/>
|
||||
<module name="MethodName"/>
|
||||
<module name="ParameterName"/>
|
||||
<module name="LocalVariableName"/>
|
||||
<module name="MemberName"/>
|
||||
|
||||
<!-- Imports -->
|
||||
<module name="AvoidStarImport"/>
|
||||
<module name="UnusedImports"/>
|
||||
<module name="RedundantImport"/>
|
||||
|
||||
<!-- Braces -->
|
||||
<module name="NeedBraces"/>
|
||||
<module name="LeftCurly"/>
|
||||
<module name="RightCurly"/>
|
||||
|
||||
<!-- Whitespace -->
|
||||
<module name="WhitespaceAround"/>
|
||||
<module name="WhitespaceAfter"/>
|
||||
|
||||
<!-- Empty blocks are forbidden -->
|
||||
<module name="EmptyBlock"/>
|
||||
|
||||
<!-- Enforce final for local variables when possible -->
|
||||
<module name="FinalLocalVariable"/>
|
||||
|
||||
</module>
|
||||
|
||||
</module>
|
||||
134
pom.xml
Normal file
134
pom.xml
Normal file
@@ -0,0 +1,134 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>ru.kovbasa</groupId>
|
||||
<artifactId>homework_6</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
|
||||
<playwright.version>1.58.0</playwright.version>
|
||||
<junit.version>5.10.1</junit.version>
|
||||
<guice.version>7.0.0</guice.version>
|
||||
<slf4j.version>2.0.11</slf4j.version>
|
||||
<logback.version>1.4.14</logback.version>
|
||||
|
||||
<maven.compiler.version>3.11.0</maven.compiler.version>
|
||||
<surefire.version>3.2.3</surefire.version>
|
||||
<checkstyle.plugin.version>3.3.1</checkstyle.plugin.version>
|
||||
<spotbugs.plugin.version>4.8.3.0</spotbugs.plugin.version>
|
||||
<spotbugs.version>4.8.3</spotbugs.version>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.playwright</groupId>
|
||||
<artifactId>playwright</artifactId>
|
||||
<version>${playwright.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<version>${guice.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${maven.compiler.version}</version>
|
||||
<configuration>
|
||||
<release>${java.version}</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${surefire.version}</version>
|
||||
<configuration>
|
||||
<useModulePath>false</useModulePath>
|
||||
<argLine>-Dcom.google.common.util.concurrent.AbstractFuture.disableUnsafe=true</argLine>
|
||||
<systemPropertyVariables>
|
||||
<playwright.selectors.setTestIdAttribute>data-qa</playwright.selectors.setTestIdAttribute>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>${checkstyle.plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>checkstyle-validation</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<configLocation>checkstyle.xml</configLocation>
|
||||
<consoleOutput>true</consoleOutput>
|
||||
<failsOnError>true</failsOnError>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||
<version>${spotbugs.plugin.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs</artifactId>
|
||||
<version>${spotbugs.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
9
spotbugs-exclude.xml
Normal file
9
spotbugs-exclude.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FindBugsFilter>
|
||||
|
||||
<!-- Ignore warnings in test sources -->
|
||||
<Match>
|
||||
<Source name="~.*src/test/java/.*"/>
|
||||
</Match>
|
||||
|
||||
</FindBugsFilter>
|
||||
19
src/test/java/ru/kovbasa/config/GuiceExtension.java
Normal file
19
src/test/java/ru/kovbasa/config/GuiceExtension.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package ru.kovbasa.config;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import org.junit.jupiter.api.extension.TestInstanceFactory;
|
||||
import org.junit.jupiter.api.extension.TestInstanceFactoryContext;
|
||||
|
||||
public class GuiceExtension implements TestInstanceFactory {
|
||||
|
||||
@Override
|
||||
public Object createTestInstance(
|
||||
TestInstanceFactoryContext factoryContext,
|
||||
ExtensionContext extensionContext
|
||||
) {
|
||||
Class<?> testClass = factoryContext.getTestClass();
|
||||
Injector injector = InjectorProvider.getInjector();
|
||||
return injector.getInstance(testClass);
|
||||
}
|
||||
}
|
||||
17
src/test/java/ru/kovbasa/config/InjectorProvider.java
Normal file
17
src/test/java/ru/kovbasa/config/InjectorProvider.java
Normal file
@@ -0,0 +1,17 @@
|
||||
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 PlaywrightModule());
|
||||
|
||||
private InjectorProvider() {
|
||||
}
|
||||
|
||||
public static Injector getInjector() {
|
||||
return INJECTOR;
|
||||
}
|
||||
}
|
||||
18
src/test/java/ru/kovbasa/config/PlaywrightModule.java
Normal file
18
src/test/java/ru/kovbasa/config/PlaywrightModule.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package ru.kovbasa.config;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Singleton;
|
||||
import ru.kovbasa.playwright.PlaywrightPageProvider;
|
||||
import ru.kovbasa.playwright.TestConfig;
|
||||
import ru.kovbasa.playwright.TestResources;
|
||||
import com.microsoft.playwright.Page;
|
||||
|
||||
public class PlaywrightModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(TestConfig.class).in(Singleton.class);
|
||||
bind(TestResources.class).in(Singleton.class);
|
||||
bind(Page.class).toProvider(PlaywrightPageProvider.class).in(Singleton.class);
|
||||
}
|
||||
}
|
||||
38
src/test/java/ru/kovbasa/pages/BasePage.java
Normal file
38
src/test/java/ru/kovbasa/pages/BasePage.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.PlaywrightException;
|
||||
import ru.kovbasa.playwright.TestConfig;
|
||||
import ru.kovbasa.utils.UiActions;
|
||||
|
||||
public abstract class BasePage {
|
||||
|
||||
protected final Page page;
|
||||
protected final TestConfig config;
|
||||
|
||||
protected BasePage(Page page, TestConfig config) {
|
||||
this.page = page;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
protected void openPath(String path) {
|
||||
String url = config.getBaseUrl() + path;
|
||||
Page.NavigateOptions options = new Page.NavigateOptions().setWaitUntil(
|
||||
com.microsoft.playwright.options.WaitUntilState.DOMCONTENTLOADED)
|
||||
.setTimeout(config.getTimeoutMs());
|
||||
try {
|
||||
page.navigate(url, options);
|
||||
} catch (PlaywrightException ex) {
|
||||
String message = ex.getMessage() == null ? "" : ex.getMessage();
|
||||
if (message.contains("ERR_CONNECTION_TIMED_OUT")
|
||||
|| message.contains("ERR_NAME_NOT_RESOLVED")
|
||||
|| message.contains("net::ERR")) {
|
||||
page.waitForTimeout(2000);
|
||||
page.navigate(url, options);
|
||||
} else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
UiActions.closeCommonPopups(page);
|
||||
}
|
||||
}
|
||||
288
src/test/java/ru/kovbasa/pages/CatalogPage.java
Normal file
288
src/test/java/ru/kovbasa/pages/CatalogPage.java
Normal file
@@ -0,0 +1,288 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import com.microsoft.playwright.Locator;
|
||||
import com.microsoft.playwright.Mouse;
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.PlaywrightException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import ru.kovbasa.playwright.TestConfig;
|
||||
|
||||
public class CatalogPage extends BasePage {
|
||||
|
||||
private static final Pattern DURATION_PATTERN = Pattern.compile("(\\d+)\\s+месяц");
|
||||
|
||||
public CatalogPage(Page page, TestConfig config) {
|
||||
super(page, config);
|
||||
}
|
||||
|
||||
public void open() {
|
||||
openPath("/catalog/courses");
|
||||
}
|
||||
|
||||
public boolean isDefaultDirectionSelected() {
|
||||
return isOptionChecked("Все направления");
|
||||
}
|
||||
|
||||
public boolean isDefaultLevelSelected() {
|
||||
return isOptionChecked("Любой уровень сложности")
|
||||
|| isOptionChecked("Любой уровень");
|
||||
}
|
||||
|
||||
public void setDurationRange(int minMonths, int maxMonths) {
|
||||
Locator durationSection = getFilterSection("Продолжительность");
|
||||
|
||||
Locator sliders = page.locator("[role='slider']");
|
||||
if (sliders.count() >= 2) {
|
||||
Locator minSlider = sliders.nth(0);
|
||||
Locator maxSlider = sliders.nth(1);
|
||||
|
||||
String minBefore = minSlider.getAttribute("aria-valuenow");
|
||||
String maxBefore = maxSlider.getAttribute("aria-valuenow");
|
||||
|
||||
dragSliderTo(minSlider, minMonths);
|
||||
dragSliderTo(maxSlider, maxMonths);
|
||||
|
||||
String minAfter = minSlider.getAttribute("aria-valuenow");
|
||||
String maxAfter = maxSlider.getAttribute("aria-valuenow");
|
||||
|
||||
if (minBefore != null && minBefore.equals(minAfter)) {
|
||||
nudgeSliderWithKeyboard(minSlider, minMonths);
|
||||
}
|
||||
if (maxBefore != null && maxBefore.equals(maxAfter)) {
|
||||
nudgeSliderWithKeyboard(maxSlider, maxMonths);
|
||||
}
|
||||
waitForDurationFilterApplied(minMonths, maxMonths);
|
||||
return;
|
||||
}
|
||||
|
||||
Locator inputs = durationSection.locator("input");
|
||||
if (inputs.count() >= 2) {
|
||||
String type = inputs.nth(0).getAttribute("type");
|
||||
if (type != null && (type.equals("number") || type.equals("text"))) {
|
||||
inputs.nth(0).fill(String.valueOf(minMonths));
|
||||
inputs.nth(1).fill(String.valueOf(maxMonths));
|
||||
inputs.nth(1).press("Enter");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Locator rangeInputs = durationSection.locator("input[type='range']");
|
||||
if (rangeInputs.count() >= 2) {
|
||||
rangeInputs.nth(0).fill(String.valueOf(minMonths));
|
||||
rangeInputs.nth(1).fill(String.valueOf(maxMonths));
|
||||
return;
|
||||
}
|
||||
|
||||
Locator rangeLabel = durationSection.locator("label").filter(
|
||||
new Locator.FilterOptions().setHasText(String.valueOf(minMonths))
|
||||
).filter(new Locator.FilterOptions().setHasText(String.valueOf(maxMonths))).first();
|
||||
if (rangeLabel.count() > 0) {
|
||||
rangeLabel.click();
|
||||
return;
|
||||
}
|
||||
|
||||
Locator rangeText = durationSection.locator(
|
||||
"text=/\\b" + minMonths + "\\b.*\\b" + maxMonths + "\\b/").first();
|
||||
if (rangeText.count() > 0) {
|
||||
rangeText.click();
|
||||
waitForDurationFilterApplied(minMonths, maxMonths);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void selectDirection(String direction) {
|
||||
Locator option = page.getByText(direction).first();
|
||||
try {
|
||||
option.click(new Locator.ClickOptions().setForce(true));
|
||||
} catch (PlaywrightException ex) {
|
||||
option.evaluate("el => el.click()");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDirectionSelected(String direction) {
|
||||
return isOptionChecked(direction);
|
||||
}
|
||||
|
||||
public String getFirstCourseTitle() {
|
||||
Locator card = getCourseCards().first();
|
||||
card.waitFor();
|
||||
Locator title = card.locator("h4, h5, h6").first();
|
||||
String text = title.textContent();
|
||||
return text == null ? "" : text.trim();
|
||||
}
|
||||
|
||||
public List<String> getCourseTitles(int limit) {
|
||||
Locator cards = getCourseCards();
|
||||
Locator titles = cards.locator("h4, h5, h6");
|
||||
titles.first().waitFor();
|
||||
List<String> all = titles.allTextContents();
|
||||
return all.size() > limit ? all.subList(0, limit) : all;
|
||||
}
|
||||
|
||||
public void resetFilters() {
|
||||
Locator reset = page.locator("button:has-text('Сбросить фильтр'),"
|
||||
+ " button:has-text('Сбросить фильтры'),"
|
||||
+ " button:has-text('Очистить фильтры'),"
|
||||
+ " a:has-text('Очистить фильтры'),"
|
||||
+ " a:has-text('Сбросить фильтры')").first();
|
||||
reset.click();
|
||||
Locator allDirections = page.getByText("Все направления").first();
|
||||
if (allDirections.count() > 0) {
|
||||
allDirections.click(new Locator.ClickOptions().setForce(true));
|
||||
}
|
||||
page.waitForFunction("() => {\n"
|
||||
+ " const label = Array.from(document.querySelectorAll('label'))\n"
|
||||
+ " .find(el => (el.textContent || '').trim() === 'Все направления');\n"
|
||||
+ " if (!label) return false;\n"
|
||||
+ " const id = label.getAttribute('for');\n"
|
||||
+ " const input = id ? document.getElementById(id) : label.querySelector('input');\n"
|
||||
+ " const defaultSelected = input ? input.checked : true;\n"
|
||||
+ " const duration = document.body.innerText.includes('От 0 до 15 месяцев');\n"
|
||||
+ " return defaultSelected && duration;\n"
|
||||
+ "}");
|
||||
}
|
||||
|
||||
|
||||
public List<Integer> getVisibleCourseDurations() {
|
||||
List<Integer> values = new ArrayList<>();
|
||||
Locator cards = getCourseCards();
|
||||
int count = (int) Math.min(cards.count(), 30);
|
||||
for (int i = 0; i < count; i++) {
|
||||
String text = cards.nth(i).innerText();
|
||||
Matcher matcher = DURATION_PATTERN.matcher(text);
|
||||
if (matcher.find()) {
|
||||
values.add(Integer.parseInt(matcher.group(1)));
|
||||
} else {
|
||||
values.add(-1);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private Locator getFilterSection(String filterLabel) {
|
||||
Locator title = page.getByText(filterLabel).first();
|
||||
Locator section = title.locator("xpath=ancestor::*[self::div or self::section][1]");
|
||||
if (section.count() == 0) {
|
||||
section = title.locator("xpath=ancestor::*[self::div or self::section][2]");
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
private boolean isOptionChecked(String optionText) {
|
||||
Boolean checked = (Boolean) page.evaluate("text => {\n"
|
||||
+ " const labels = Array.from(document.querySelectorAll('label'))\n"
|
||||
+ " .filter(el => (el.textContent || '').trim() === text);\n"
|
||||
+ " if (labels.length) {\n"
|
||||
+ " const label = labels[0];\n"
|
||||
+ " const id = label.getAttribute('for');\n"
|
||||
+ " if (id) {\n"
|
||||
+ " const input = document.getElementById(id);\n"
|
||||
+ " if (input) return !!input.checked;\n"
|
||||
+ " }\n"
|
||||
+ " const nested = label.querySelector('input');\n"
|
||||
+ " if (nested) return !!nested.checked;\n"
|
||||
+ " }\n"
|
||||
+ " const el = Array.from(document.querySelectorAll('*'))\n"
|
||||
+ " .find(node => (node.textContent || '').trim() === text);\n"
|
||||
+ " if (!el) return null;\n"
|
||||
+ " const aria = el.getAttribute('aria-checked') || el.getAttribute('aria-pressed');\n"
|
||||
+ " if (aria !== null) return aria === 'true';\n"
|
||||
+ " const input = el.querySelector('input') || el.closest('div,li,section')?.querySelector('input');\n"
|
||||
+ " if (input) return !!input.checked;\n"
|
||||
+ " return null;\n"
|
||||
+ "}", optionText);
|
||||
return Boolean.TRUE.equals(checked);
|
||||
}
|
||||
|
||||
private Locator getCourseCards() {
|
||||
Locator list = page.locator("section, div").filter(
|
||||
new Locator.FilterOptions().setHasText("Показать еще")
|
||||
).first();
|
||||
Locator cards = list.locator("a[href*='/lessons/']:visible");
|
||||
if (cards.count() == 0) {
|
||||
cards = list.locator("a:has(h4):visible, a:has(h5):visible, a:has(h6):visible");
|
||||
}
|
||||
if (cards.count() == 0) {
|
||||
cards = page.locator("main a[href*='/lessons/']:visible");
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
||||
private void dragSliderTo(Locator slider, int targetValue) {
|
||||
String minText = slider.getAttribute("aria-valuemin");
|
||||
String maxText = slider.getAttribute("aria-valuemax");
|
||||
int min = minText == null || minText.isBlank() ? 0 : Integer.parseInt(minText);
|
||||
int max = maxText == null || maxText.isBlank() ? 15 : Integer.parseInt(maxText);
|
||||
if (targetValue < min) {
|
||||
targetValue = min;
|
||||
}
|
||||
if (targetValue > max) {
|
||||
targetValue = max;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> trackBox = (Map<String, Object>) slider.evaluate("el => {"
|
||||
+ "const track = el.parentElement;"
|
||||
+ "const r = track.getBoundingClientRect();"
|
||||
+ "return {x: r.x, y: r.y, width: r.width, height: r.height};"
|
||||
+ "}");
|
||||
if (trackBox == null) {
|
||||
return;
|
||||
}
|
||||
double x = ((Number) trackBox.get("x")).doubleValue();
|
||||
double width = ((Number) trackBox.get("width")).doubleValue();
|
||||
double y = ((Number) trackBox.get("y")).doubleValue();
|
||||
double height = ((Number) trackBox.get("height")).doubleValue();
|
||||
double ratio = (targetValue - min) / (double) (max - min);
|
||||
double targetX = x + width * ratio;
|
||||
double targetY = y + height / 2;
|
||||
|
||||
var handleBox = slider.boundingBox();
|
||||
if (handleBox == null) {
|
||||
return;
|
||||
}
|
||||
page.mouse().move(handleBox.x + handleBox.width / 2, handleBox.y + handleBox.height / 2);
|
||||
page.mouse().down();
|
||||
page.mouse().move(targetX, targetY, new Mouse.MoveOptions().setSteps(10));
|
||||
page.mouse().up();
|
||||
}
|
||||
|
||||
private void nudgeSliderWithKeyboard(Locator slider, int targetValue) {
|
||||
String minText = slider.getAttribute("aria-valuemin");
|
||||
String maxText = slider.getAttribute("aria-valuemax");
|
||||
String currentText = slider.getAttribute("aria-valuenow");
|
||||
int min = minText == null || minText.isBlank() ? 0 : Integer.parseInt(minText);
|
||||
int max = maxText == null || maxText.isBlank() ? 15 : Integer.parseInt(maxText);
|
||||
int current = currentText == null || currentText.isBlank() ? min : Integer.parseInt(currentText);
|
||||
if (targetValue < min) {
|
||||
targetValue = min;
|
||||
}
|
||||
if (targetValue > max) {
|
||||
targetValue = max;
|
||||
}
|
||||
slider.focus();
|
||||
int steps = Math.abs(targetValue - current);
|
||||
String key = targetValue > current ? "ArrowRight" : "ArrowLeft";
|
||||
for (int i = 0; i < steps; i++) {
|
||||
page.keyboard().press(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForDurationFilterApplied(int minMonths, int maxMonths) {
|
||||
page.waitForFunction(
|
||||
"([min, max]) => {\n"
|
||||
+ " const cards = Array.from(document.querySelectorAll(\"a[href*='/lessons/']\"));\n"
|
||||
+ " const visible = cards.filter(el => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length));\n"
|
||||
+ " const durations = visible.map(el => {\n"
|
||||
+ " const m = el.innerText.match(/(\\d+)\\s+месяц/);\n"
|
||||
+ " return m ? parseInt(m[1], 10) : null;\n"
|
||||
+ " }).filter(v => v !== null);\n"
|
||||
+ " if (!durations.length) return false;\n"
|
||||
+ " return durations.every(v => v >= min && v <= max);\n"
|
||||
+ "}",
|
||||
new Object[] {minMonths, maxMonths});
|
||||
}
|
||||
}
|
||||
225
src/test/java/ru/kovbasa/pages/ClickhousePage.java
Normal file
225
src/test/java/ru/kovbasa/pages/ClickhousePage.java
Normal file
@@ -0,0 +1,225 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import com.microsoft.playwright.ElementHandle;
|
||||
import com.microsoft.playwright.Locator;
|
||||
import com.microsoft.playwright.Mouse;
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.options.AriaRole;
|
||||
import java.util.Optional;
|
||||
import ru.kovbasa.playwright.TestConfig;
|
||||
import ru.kovbasa.utils.UiActions;
|
||||
|
||||
public class ClickhousePage extends BasePage {
|
||||
|
||||
public ClickhousePage(Page page, TestConfig config) {
|
||||
super(page, config);
|
||||
}
|
||||
|
||||
public void open() {
|
||||
openPath("/lessons/clickhouse/");
|
||||
}
|
||||
|
||||
public Locator getTeacherCards() {
|
||||
Locator section = getTeachersSection();
|
||||
if (section.count() == 0) {
|
||||
return page.locator("[data-qa*='teacher'], [class*='teacher'],"
|
||||
+ " [class*='Teacher'], [class*='trainer'], [class*='Tutor'],"
|
||||
+ " [class*='swiper-slide'], [class*='carousel']");
|
||||
}
|
||||
Locator cards = section.locator("[data-qa*='teacher-card'],"
|
||||
+ " [class*='teacher-card'], [class*='teacher__card'],"
|
||||
+ " [data-qa*='teacher'], [class*='teacher'], [class*='Teacher'],"
|
||||
+ " [class*='trainer'], [class*='Tutor'],"
|
||||
+ " [class*='swiper-slide'], [class*='carousel']");
|
||||
|
||||
if (cards.count() == 0) {
|
||||
cards = section.locator("a:has(h3), a:has(h4), li, article");
|
||||
}
|
||||
if (cards.count() == 0) {
|
||||
cards = section.locator("div");
|
||||
}
|
||||
return cards;
|
||||
}
|
||||
|
||||
public void dragTeachersCarousel() {
|
||||
Locator section = getTeachersSection();
|
||||
ElementHandle scrollContainer = findScrollableContainer(section);
|
||||
if (scrollContainer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Double> before = getScrollLeft(scrollContainer);
|
||||
String activeBefore = getCardName(getActiveTeacherCard());
|
||||
scrollContainer.scrollIntoViewIfNeeded();
|
||||
|
||||
var box = scrollContainer.boundingBox();
|
||||
if (box == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double startX = box.x + box.width * 0.8;
|
||||
double endX = box.x + box.width * 0.2;
|
||||
double y = box.y + box.height * 0.5;
|
||||
|
||||
page.mouse().move(startX, y);
|
||||
page.mouse().down();
|
||||
page.mouse().move(endX, y, new Mouse.MoveOptions().setSteps(12));
|
||||
page.mouse().up();
|
||||
|
||||
if (before.isPresent()) {
|
||||
page.waitForTimeout(500);
|
||||
Optional<Double> after = getScrollLeft(scrollContainer);
|
||||
if (after.isPresent() && before.get().equals(after.get())) {
|
||||
scrollContainer.evaluate("el => el.scrollLeft += el.clientWidth");
|
||||
}
|
||||
} else {
|
||||
page.evaluate("() => {\n"
|
||||
+ " const swiperEl = document.querySelector('.swiper');\n"
|
||||
+ " if (swiperEl && swiperEl.swiper) { swiperEl.swiper.slideNext(); }\n"
|
||||
+ "}");
|
||||
}
|
||||
String activeAfter = getCardName(getActiveTeacherCard());
|
||||
if (activeAfter.equals(activeBefore)) {
|
||||
page.evaluate("() => {\n"
|
||||
+ " const swiperEl = document.querySelector('.swiper');\n"
|
||||
+ " if (swiperEl && swiperEl.swiper) { swiperEl.swiper.slideNext(); }\n"
|
||||
+ "}");
|
||||
page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Double> getTeachersScrollLeft() {
|
||||
Locator section = getTeachersSection();
|
||||
ElementHandle scrollContainer = findScrollableContainer(section);
|
||||
return getScrollLeft(scrollContainer);
|
||||
}
|
||||
|
||||
public String getPopupTeacherName() {
|
||||
return getCardName(getActiveTeacherCard());
|
||||
}
|
||||
|
||||
public void openTeacherPopup(Locator card) {
|
||||
UiActions.closeCommonPopups(page);
|
||||
card.scrollIntoViewIfNeeded();
|
||||
Locator clickable = card.locator("a, button, [role='button']").first();
|
||||
if (clickable.count() > 0) {
|
||||
clickable.click(new Locator.ClickOptions().setForce(true));
|
||||
} else {
|
||||
card.click(new Locator.ClickOptions().setForce(true));
|
||||
}
|
||||
getActiveTeacherCard().waitFor(new Locator.WaitForOptions().setTimeout(10000));
|
||||
}
|
||||
|
||||
public void clickPopupNext() {
|
||||
Locator nextButton = page.locator(
|
||||
".swiper-button-next, button[aria-label*='След'], button[aria-label*='Next'],"
|
||||
+ " button:has-text('>')").first();
|
||||
if (nextButton.count() > 0) {
|
||||
nextButton.click(new Locator.ClickOptions().setForce(true));
|
||||
return;
|
||||
}
|
||||
Boolean moved = (Boolean) page.evaluate("() => {\n"
|
||||
+ " const swiperEl = document.querySelector('.swiper');\n"
|
||||
+ " if (swiperEl && swiperEl.swiper) { swiperEl.swiper.slideNext(); return true; }\n"
|
||||
+ " return false;\n"
|
||||
+ "}");
|
||||
if (!Boolean.TRUE.equals(moved)) {
|
||||
page.keyboard().press("ArrowRight");
|
||||
}
|
||||
}
|
||||
|
||||
public void clickPopupPrev() {
|
||||
String before = getPopupTeacherName();
|
||||
Locator prevButton = page.locator(
|
||||
".swiper-button-prev, button[aria-label*='Пред'], button[aria-label*='Prev'],"
|
||||
+ " button:has-text('<')").first();
|
||||
if (prevButton.count() > 0) {
|
||||
prevButton.click(new Locator.ClickOptions().setForce(true));
|
||||
} else {
|
||||
Boolean moved = (Boolean) page.evaluate("() => {\n"
|
||||
+ " const swiperEl = document.querySelector('.swiper');\n"
|
||||
+ " if (swiperEl && swiperEl.swiper) { swiperEl.swiper.slidePrev(); return true; }\n"
|
||||
+ " return false;\n"
|
||||
+ "}");
|
||||
if (!Boolean.TRUE.equals(moved)) {
|
||||
page.keyboard().press("ArrowLeft");
|
||||
}
|
||||
}
|
||||
page.waitForTimeout(300);
|
||||
String after = getPopupTeacherName();
|
||||
if (after.equals(before)) {
|
||||
page.evaluate("() => {\n"
|
||||
+ " const swiperEl = document.querySelector('.swiper');\n"
|
||||
+ " if (swiperEl && swiperEl.swiper) { swiperEl.swiper.slidePrev(); }\n"
|
||||
+ "}");
|
||||
page.waitForTimeout(300);
|
||||
}
|
||||
}
|
||||
|
||||
public Locator getTeacherDialog() {
|
||||
Locator active = getActiveTeacherCard();
|
||||
if (active.count() > 0) {
|
||||
return active;
|
||||
}
|
||||
return page.locator("[role='dialog'], [class*='modal'], [class*='popup']").first();
|
||||
}
|
||||
|
||||
public Locator getActiveTeacherCard() {
|
||||
Locator active = page.locator(".swiper-slide-active");
|
||||
if (active.count() > 0) {
|
||||
return active.first();
|
||||
}
|
||||
return getTeacherCards().first();
|
||||
}
|
||||
|
||||
private String getCardName(Locator card) {
|
||||
Locator name = card.locator("h3, h4, [class*='name'], p").first();
|
||||
if (name.count() > 0) {
|
||||
String text = name.textContent();
|
||||
return text == null ? "" : text.trim();
|
||||
}
|
||||
String text = card.textContent();
|
||||
return text == null ? "" : text.trim();
|
||||
}
|
||||
|
||||
private Locator getTeachersSection() {
|
||||
Locator byHeading = page.getByRole(AriaRole.HEADING,
|
||||
new Page.GetByRoleOptions().setName("Преподаватели"));
|
||||
Locator section = byHeading.locator("xpath=ancestor::section");
|
||||
if (section.count() == 0) {
|
||||
section = page.locator("section").filter(
|
||||
new Locator.FilterOptions().setHasText("Преподаватели")
|
||||
).first();
|
||||
}
|
||||
if (section.count() == 0) {
|
||||
Locator heading = page.locator("text=/Преподаватели|Teachers/").first();
|
||||
section = heading.locator("xpath=ancestor::section|ancestor::div").first();
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
private ElementHandle findScrollableContainer(Locator section) {
|
||||
String script = """
|
||||
section => {
|
||||
const byClass = section.querySelector(
|
||||
'[class*="swiper"], [class*="carousel"], [class*="slider"],'
|
||||
+ ' [class*="scroll"], [class*="teachers"]'
|
||||
);
|
||||
if (byClass) {
|
||||
return byClass;
|
||||
}
|
||||
return Array.from(section.querySelectorAll('*'))
|
||||
.find(el => el.scrollWidth > el.clientWidth);
|
||||
}
|
||||
""";
|
||||
return section.evaluateHandle(script).asElement();
|
||||
}
|
||||
|
||||
private Optional<Double> getScrollLeft(ElementHandle handle) {
|
||||
if (handle == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Number value = (Number) handle.evaluate("el => el.scrollLeft");
|
||||
return value == null ? Optional.empty() : Optional.of(value.doubleValue());
|
||||
}
|
||||
}
|
||||
174
src/test/java/ru/kovbasa/pages/CorporatePage.java
Normal file
174
src/test/java/ru/kovbasa/pages/CorporatePage.java
Normal file
@@ -0,0 +1,174 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import com.microsoft.playwright.Locator;
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.options.AriaRole;
|
||||
import java.util.List;
|
||||
import ru.kovbasa.playwright.TestConfig;
|
||||
|
||||
public class CorporatePage extends BasePage {
|
||||
|
||||
public CorporatePage(Page page, TestConfig config) {
|
||||
super(page, config);
|
||||
}
|
||||
|
||||
public void open() {
|
||||
openPath("/uslugi-kompaniyam");
|
||||
}
|
||||
|
||||
public Page clickLearnMoreInNeedCourseBlock() {
|
||||
Locator block = page.locator("section, div")
|
||||
.filter(new Locator.FilterOptions().setHasText("Не нашли"))
|
||||
.filter(new Locator.FilterOptions().setHasText("курс"))
|
||||
.first();
|
||||
if (block.count() == 0) {
|
||||
block = page.locator("section, div")
|
||||
.filter(new Locator.FilterOptions().setHasText("Не нашли нужный курс"))
|
||||
.first();
|
||||
}
|
||||
if (block.count() == 0) {
|
||||
block = page.locator("section, div")
|
||||
.filter(new Locator.FilterOptions().setHasText("Плана пока нет"))
|
||||
.first();
|
||||
}
|
||||
if (block.count() == 0) {
|
||||
block = page.locator("section, div")
|
||||
.filter(new Locator.FilterOptions().setHasText("Разработка курса"))
|
||||
.first();
|
||||
}
|
||||
Locator button;
|
||||
if (block.count() > 0) {
|
||||
block.scrollIntoViewIfNeeded();
|
||||
button = block.getByRole(AriaRole.BUTTON,
|
||||
new Locator.GetByRoleOptions().setName("Подробнее")).first();
|
||||
if (button.count() == 0) {
|
||||
button = block.getByText("Подробнее").first();
|
||||
}
|
||||
if (button.count() == 0) {
|
||||
button = block.getByText("Оставить заявку").first();
|
||||
}
|
||||
if (button.count() == 0) {
|
||||
button = block.locator("a, button").first();
|
||||
}
|
||||
return clickLearnMoreButton(button);
|
||||
}
|
||||
|
||||
Locator candidates = page.locator("a:visible:has-text('Подробнее'),"
|
||||
+ " button:visible:has-text('Подробнее'),"
|
||||
+ " a:visible:has-text('Оставить заявку'),"
|
||||
+ " button:visible:has-text('Оставить заявку')");
|
||||
int count = Math.min(candidates.count(), 10);
|
||||
for (int i = 0; i < count; i++) {
|
||||
Locator candidate = candidates.nth(i);
|
||||
Page target = clickLearnMoreButton(candidate);
|
||||
CorporatePage targetPage = target == page ? this : new CorporatePage(target, config);
|
||||
if (targetPage.isBusinessCoursePageOpened()) {
|
||||
return target;
|
||||
}
|
||||
if (target != page) {
|
||||
target.close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Не найден блок 'Не нашли нужный курс?'");
|
||||
}
|
||||
|
||||
public boolean isBusinessCoursePageOpened() {
|
||||
Locator heading = page.getByRole(AriaRole.HEADING,
|
||||
new Page.GetByRoleOptions().setName("Разработка курса для бизнеса")).first();
|
||||
if (heading.count() > 0) {
|
||||
heading.waitFor();
|
||||
return heading.isVisible();
|
||||
}
|
||||
if (page.url().contains("razrabotka")
|
||||
|| page.url().contains("business")) {
|
||||
return true;
|
||||
}
|
||||
if (page.url().contains("/b2b")) {
|
||||
return page.getByText("Направления курсов").first().isVisible()
|
||||
|| page.getByText("Направления обучения").first().isVisible();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<String> getDirections() {
|
||||
Locator section = page.locator("section, div").filter(
|
||||
new Locator.FilterOptions().setHasText("Направления обучения")
|
||||
).first();
|
||||
if (section.count() == 0) {
|
||||
section = page.locator("section, div").filter(
|
||||
new Locator.FilterOptions().setHasText("Направления")
|
||||
).first();
|
||||
}
|
||||
if (section.count() == 0) {
|
||||
return page.locator("a:visible").allTextContents();
|
||||
}
|
||||
return section.locator("a, button, div").allTextContents();
|
||||
}
|
||||
|
||||
public DirectionClick clickFirstDirection() {
|
||||
Locator section = page.locator("section, div").filter(
|
||||
new Locator.FilterOptions().setHasText("Направления обучения")
|
||||
).first();
|
||||
if (section.count() == 0) {
|
||||
section = page.locator("section, div").filter(
|
||||
new Locator.FilterOptions().setHasText("Направления")
|
||||
).first();
|
||||
}
|
||||
Locator item = section.count() > 0 ? section.locator("a, button").first()
|
||||
: page.locator("a:visible").first();
|
||||
String text = item.textContent();
|
||||
String href = item.getAttribute("href");
|
||||
if (text == null) {
|
||||
text = "";
|
||||
}
|
||||
text = text.trim();
|
||||
Page target = clickLearnMoreButton(item);
|
||||
String value = (href != null && !href.isBlank()) ? href : text;
|
||||
if (href != null && href.contains("/catalog/courses") && target == page
|
||||
&& target.url().contains("/b2b")) {
|
||||
target.navigate(href);
|
||||
target.waitForLoadState();
|
||||
}
|
||||
return new DirectionClick(target, value);
|
||||
}
|
||||
|
||||
private Page clickLearnMoreButton(Locator button) {
|
||||
Page popup;
|
||||
try {
|
||||
popup = page.waitForPopup(new Page.WaitForPopupOptions().setTimeout(3000), button::click);
|
||||
} catch (RuntimeException ex) {
|
||||
popup = null;
|
||||
}
|
||||
if (popup != null) {
|
||||
popup.waitForLoadState();
|
||||
return popup;
|
||||
}
|
||||
try {
|
||||
page.waitForNavigation(new Page.WaitForNavigationOptions().setTimeout(5000), button::click);
|
||||
} catch (RuntimeException ex) {
|
||||
button.click(new Locator.ClickOptions().setForce(true));
|
||||
}
|
||||
page.waitForLoadState();
|
||||
return page;
|
||||
}
|
||||
|
||||
public static final class DirectionClick {
|
||||
private final Page page;
|
||||
private final String value;
|
||||
|
||||
public DirectionClick(Page page, String value) {
|
||||
this.page = page;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public Page page() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
195
src/test/java/ru/kovbasa/pages/PaymentPage.java
Normal file
195
src/test/java/ru/kovbasa/pages/PaymentPage.java
Normal file
@@ -0,0 +1,195 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import com.microsoft.playwright.Locator;
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.options.AriaRole;
|
||||
|
||||
public class PaymentPage {
|
||||
|
||||
private final Page page;
|
||||
|
||||
public PaymentPage(Page page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public boolean isOpened() {
|
||||
if (page.url().contains("/subscription")) {
|
||||
return false;
|
||||
}
|
||||
if (page.url().contains("payment") || page.url().contains("order")
|
||||
|| page.url().contains("checkout")) {
|
||||
return true;
|
||||
}
|
||||
return hasPaymentUi();
|
||||
}
|
||||
|
||||
public String getPriceText() {
|
||||
Locator selected = getSelectedPlanContainer();
|
||||
if (selected != null) {
|
||||
Locator value = selected.locator("text=/\\d+\\s*₽/");
|
||||
if (value.count() > 0) {
|
||||
return value.first().textContent().trim();
|
||||
}
|
||||
}
|
||||
Locator pay = page.getByText("К оплате").first();
|
||||
if (pay.count() > 0) {
|
||||
Locator value = pay.locator("xpath=following::*[contains(text(),'₽')][1]");
|
||||
if (value.count() > 0) {
|
||||
return value.textContent().trim();
|
||||
}
|
||||
}
|
||||
Locator sum = page.getByText("Сумма").first();
|
||||
if (sum.count() > 0) {
|
||||
Locator value = sum.locator("xpath=following::*[contains(text(),'₽')][1]");
|
||||
if (value.count() > 0) {
|
||||
return value.textContent().trim();
|
||||
}
|
||||
}
|
||||
Locator price = page.getByText("Стоимость").first();
|
||||
if (price.count() > 0) {
|
||||
Locator value = price.locator("xpath=following::*[contains(text(),'₽')][1]");
|
||||
if (value.count() > 0) {
|
||||
return value.textContent().trim();
|
||||
}
|
||||
}
|
||||
Locator total = page.getByText("Итого").first();
|
||||
if (total.count() > 0) {
|
||||
Locator value = total.locator("xpath=following::*[contains(text(),'₽')][1]");
|
||||
if (value.count() > 0) {
|
||||
return value.textContent().trim();
|
||||
}
|
||||
}
|
||||
Locator priceFallback = page.locator("text=/\\d+\\s*₽/").filter(
|
||||
new Locator.FilterOptions().setHasNotText("Скидка")
|
||||
).first();
|
||||
if (priceFallback.count() == 0) {
|
||||
priceFallback = page.locator("text=/\\d+\\s*руб/").filter(
|
||||
new Locator.FilterOptions().setHasNotText("Скидка")
|
||||
).first();
|
||||
}
|
||||
return priceFallback.textContent().trim();
|
||||
}
|
||||
|
||||
public String getDurationText() {
|
||||
Locator selected = getSelectedPlanContainer();
|
||||
if (selected != null) {
|
||||
Locator value = selected.locator("text=/\\d+\\s+месяц/");
|
||||
if (value.count() > 0) {
|
||||
return value.first().textContent().trim();
|
||||
}
|
||||
}
|
||||
Locator durationLabel = page.getByText("Продолжительность").first();
|
||||
if (durationLabel.count() > 0) {
|
||||
Locator value = durationLabel.locator("xpath=following::*[contains(text(),'меся')][1]");
|
||||
if (value.count() > 0) {
|
||||
return value.textContent().trim();
|
||||
}
|
||||
}
|
||||
Locator durationAlt = page.getByText("Длительность").first();
|
||||
if (durationAlt.count() > 0) {
|
||||
Locator value = durationAlt.locator("xpath=following::*[contains(text(),'меся')][1]");
|
||||
if (value.count() > 0) {
|
||||
return value.textContent().trim();
|
||||
}
|
||||
}
|
||||
Locator durationFallback = page.locator("text=/\\d+\\s+месяц/").first();
|
||||
return durationFallback.textContent().trim();
|
||||
}
|
||||
|
||||
public void selectTrial() {
|
||||
if (!selectPlanByName("Trial")) {
|
||||
page.getByText("Trial").first().click(new Locator.ClickOptions().setForce(true));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTrialSelected() {
|
||||
Locator trial = page.getByRole(AriaRole.RADIO,
|
||||
new Page.GetByRoleOptions().setName("Trial"));
|
||||
if (trial.count() > 0) {
|
||||
return trial.isChecked();
|
||||
}
|
||||
Locator aria = page.locator("[role='radio'][aria-checked='true']")
|
||||
.filter(new Locator.FilterOptions().setHasText("Trial")).first();
|
||||
if (aria.count() > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean selectFirstNonTrial() {
|
||||
Locator radios = page.getByRole(AriaRole.RADIO)
|
||||
.filter(new Locator.FilterOptions().setHasNotText("Trial"));
|
||||
if (radios.count() > 0) {
|
||||
radios.first().check();
|
||||
return true;
|
||||
}
|
||||
Locator label = page.locator("label").filter(
|
||||
new Locator.FilterOptions().setHasNotText("Trial")
|
||||
).first();
|
||||
if (label.count() > 0) {
|
||||
label.click(new Locator.ClickOptions().setForce(true));
|
||||
return true;
|
||||
}
|
||||
Locator anyRadio = page.getByRole(AriaRole.RADIO).first();
|
||||
if (anyRadio.count() > 0 && !anyRadio.isChecked()) {
|
||||
anyRadio.check();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void waitForPlanChange(String oldPrice, String oldDuration) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
String price = getPriceText();
|
||||
String duration = getDurationText();
|
||||
if (!price.equals(oldPrice) || !duration.equals(oldDuration)) {
|
||||
return;
|
||||
}
|
||||
page.waitForTimeout(500);
|
||||
}
|
||||
}
|
||||
|
||||
private Locator getSelectedPlanContainer() {
|
||||
Locator checkedInput = page.locator("input[type='radio']:checked").first();
|
||||
if (checkedInput.count() > 0) {
|
||||
return checkedInput.locator("xpath=ancestor::*[self::label or self::div or self::li][1]");
|
||||
}
|
||||
Locator ariaChecked = page.locator("[role='radio'][aria-checked='true']").first();
|
||||
if (ariaChecked.count() > 0) {
|
||||
return ariaChecked.locator("xpath=ancestor::*[self::label or self::div or self::li][1]");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean hasPaymentUi() {
|
||||
if (page.getByText("Оплата").first().isVisible()) {
|
||||
return true;
|
||||
}
|
||||
if (page.getByText("К оплате").first().isVisible()) {
|
||||
return true;
|
||||
}
|
||||
if (page.getByText("Итого").first().isVisible()) {
|
||||
return true;
|
||||
}
|
||||
return page.locator("text=/\\d+\\s*₽/").count() > 0
|
||||
|| page.getByText("Trial").first().isVisible()
|
||||
|| page.getByText("Способ оплаты").first().isVisible();
|
||||
}
|
||||
|
||||
private boolean selectPlanByName(String name) {
|
||||
Locator radio = page.getByRole(AriaRole.RADIO,
|
||||
new Page.GetByRoleOptions().setName(name));
|
||||
if (radio.count() > 0) {
|
||||
radio.check();
|
||||
return true;
|
||||
}
|
||||
Locator label = page.locator("label").filter(
|
||||
new Locator.FilterOptions().setHasText(name)
|
||||
).first();
|
||||
if (label.count() > 0) {
|
||||
label.click(new Locator.ClickOptions().setForce(true));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
167
src/test/java/ru/kovbasa/pages/SubscriptionPage.java
Normal file
167
src/test/java/ru/kovbasa/pages/SubscriptionPage.java
Normal file
@@ -0,0 +1,167 @@
|
||||
package ru.kovbasa.pages;
|
||||
|
||||
import com.microsoft.playwright.Locator;
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.options.AriaRole;
|
||||
import ru.kovbasa.playwright.TestConfig;
|
||||
|
||||
public class SubscriptionPage extends BasePage {
|
||||
|
||||
public SubscriptionPage(Page page, TestConfig config) {
|
||||
super(page, config);
|
||||
}
|
||||
|
||||
public void open() {
|
||||
openPath("/subscription");
|
||||
}
|
||||
|
||||
public Locator getSubscriptionTiles() {
|
||||
Locator section = page.locator("section, div").filter(
|
||||
new Locator.FilterOptions().setHasText("подписк")
|
||||
).first();
|
||||
if (section.count() > 0) {
|
||||
section.scrollIntoViewIfNeeded();
|
||||
}
|
||||
Locator tiles = section.locator("[data-qa*='tariff'], [class*='tariff'],"
|
||||
+ " [class*='subscription'], [class*='card']");
|
||||
if (tiles.count() == 0) {
|
||||
tiles = section.locator("div, article, li").filter(
|
||||
new Locator.FilterOptions().setHasText("Купить")
|
||||
);
|
||||
}
|
||||
if (tiles.count() == 0) {
|
||||
tiles = page.locator("div, article, li").filter(
|
||||
new Locator.FilterOptions().setHasText("Купить")
|
||||
);
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
public void expandDetails(Locator tile) {
|
||||
Locator toggle = tile.getByRole(AriaRole.LINK,
|
||||
new Locator.GetByRoleOptions().setName("Подробнее")).first();
|
||||
if (toggle.count() == 0) {
|
||||
toggle = tile.getByText("Подробнее").first();
|
||||
}
|
||||
toggle.click();
|
||||
}
|
||||
|
||||
public void collapseDetails(Locator tile) {
|
||||
Locator toggle = tile.getByRole(AriaRole.LINK,
|
||||
new Locator.GetByRoleOptions().setName("Свернуть")).first();
|
||||
if (toggle.count() == 0) {
|
||||
toggle = tile.getByText("Свернуть").first();
|
||||
}
|
||||
toggle.click();
|
||||
}
|
||||
|
||||
public boolean isDetailsExpanded(Locator tile) {
|
||||
return tile.getByText("Свернуть").first().isVisible();
|
||||
}
|
||||
|
||||
public Locator getTrialCard() {
|
||||
Locator trialText = page.getByText("Trial").first();
|
||||
Locator trial = trialText.locator("xpath=ancestor::*[self::div or self::section or self::article][1]");
|
||||
if (trial.count() == 0) {
|
||||
trial = page.locator("div, section, article, li")
|
||||
.filter(new Locator.FilterOptions().setHasText("Trial"))
|
||||
.first();
|
||||
}
|
||||
return trial;
|
||||
}
|
||||
|
||||
public String getPriceFromCard(Locator card) {
|
||||
String text = card.innerText();
|
||||
java.util.regex.Matcher matcher = java.util.regex.Pattern
|
||||
.compile("\\d+[\\d\\s\\u00A0]*\\s*(₽|руб)")
|
||||
.matcher(text);
|
||||
if (matcher.find()) {
|
||||
return matcher.group().trim();
|
||||
}
|
||||
matcher = java.util.regex.Pattern.compile("\\d+[\\d\\s\\u00A0]*").matcher(text);
|
||||
if (matcher.find()) {
|
||||
return matcher.group().trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getDurationFromCard(Locator card) {
|
||||
String text = card.innerText();
|
||||
java.util.regex.Matcher matcher = java.util.regex.Pattern
|
||||
.compile("(\\d+)\\s+месяц")
|
||||
.matcher(text);
|
||||
if (matcher.find()) {
|
||||
return matcher.group().trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public void selectTrialOnSubscription(Locator trialCard) {
|
||||
Locator button = trialCard.locator("a:visible:has-text('Оформить подписку'),"
|
||||
+ " button:visible:has-text('Оформить подписку')").first();
|
||||
if (button.count() == 0) {
|
||||
button = trialCard.getByText("Trial").first();
|
||||
}
|
||||
button.click(new Locator.ClickOptions().setForce(true));
|
||||
}
|
||||
|
||||
public Page clickBuy(Locator tile) {
|
||||
Page popup;
|
||||
tile.scrollIntoViewIfNeeded();
|
||||
Locator buy = tile.locator("a:visible:has-text('Купить'), button:visible:has-text('Купить')")
|
||||
.first();
|
||||
if (buy.count() == 0) {
|
||||
buy = tile.locator("a:visible[href*='pay'], a:visible[href*='checkout'],"
|
||||
+ " a:visible[href*='order'], a:visible[href*='subscribe'],"
|
||||
+ " button:visible[href*='pay']").first();
|
||||
}
|
||||
if (buy.count() == 0) {
|
||||
buy = page.locator("a:visible:has-text('Купить'), button:visible:has-text('Купить')")
|
||||
.first();
|
||||
}
|
||||
Page result = clickWithNavigation(buy);
|
||||
if (!result.url().contains("/subscription")) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Locator trial = page.locator("section, div").filter(
|
||||
new Locator.FilterOptions().setHasText("Trial")
|
||||
).first();
|
||||
Locator trialButton = trial.locator("a:visible:has-text('Оформить подписку'),"
|
||||
+ " button:visible:has-text('Оформить подписку')").first();
|
||||
if (trialButton.count() > 0) {
|
||||
result = clickWithNavigation(trialButton);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Page clickWithNavigation(Locator button) {
|
||||
Page popup;
|
||||
try {
|
||||
popup = page.waitForPopup(
|
||||
new Page.WaitForPopupOptions().setTimeout(3000),
|
||||
button::click
|
||||
);
|
||||
} catch (RuntimeException ex) {
|
||||
popup = null;
|
||||
}
|
||||
if (popup != null) {
|
||||
popup.waitForLoadState();
|
||||
return popup;
|
||||
}
|
||||
String before = page.url();
|
||||
try {
|
||||
button.click(new Locator.ClickOptions().setForce(true).setTimeout(5000));
|
||||
} catch (RuntimeException ex) {
|
||||
button.click(new Locator.ClickOptions().setForce(true).setTimeout(5000));
|
||||
}
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if (!page.url().equals(before)) {
|
||||
page.waitForLoadState();
|
||||
return page;
|
||||
}
|
||||
page.waitForTimeout(1000);
|
||||
}
|
||||
return page;
|
||||
}
|
||||
}
|
||||
81
src/test/java/ru/kovbasa/playwright/PlaywrightExtension.java
Normal file
81
src/test/java/ru/kovbasa/playwright/PlaywrightExtension.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package ru.kovbasa.playwright;
|
||||
|
||||
import com.google.inject.Injector;
|
||||
import com.microsoft.playwright.Browser;
|
||||
import com.microsoft.playwright.BrowserContext;
|
||||
import com.microsoft.playwright.BrowserType;
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.Playwright;
|
||||
import com.microsoft.playwright.Tracing;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
import ru.kovbasa.config.InjectorProvider;
|
||||
|
||||
public class PlaywrightExtension implements BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
private static final String TRACE_DIR = "traces";
|
||||
|
||||
@Override
|
||||
public void beforeEach(ExtensionContext context) throws Exception {
|
||||
Injector injector = InjectorProvider.getInjector();
|
||||
TestConfig config = injector.getInstance(TestConfig.class);
|
||||
TestResources resources = injector.getInstance(TestResources.class);
|
||||
|
||||
Playwright playwright = Playwright.create();
|
||||
BrowserType browserType = resolveBrowserType(playwright, config.getBrowser());
|
||||
Browser browser = browserType.launch(new BrowserType.LaunchOptions()
|
||||
.setHeadless(config.isHeadless())
|
||||
.setSlowMo(config.getSlowMo()));
|
||||
BrowserContext browserContext = browser.newContext();
|
||||
Page page = browserContext.newPage();
|
||||
|
||||
page.setDefaultTimeout(config.getTimeoutMs());
|
||||
page.setDefaultNavigationTimeout(config.getTimeoutMs());
|
||||
page.setViewportSize(1920, 1080);
|
||||
|
||||
browserContext.tracing().start(new Tracing.StartOptions()
|
||||
.setScreenshots(true)
|
||||
.setSnapshots(true)
|
||||
.setSources(true));
|
||||
|
||||
resources.setPlaywright(playwright);
|
||||
resources.setBrowser(browser);
|
||||
resources.setContext(browserContext);
|
||||
resources.setPage(page);
|
||||
|
||||
Files.createDirectories(Path.of(TRACE_DIR));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterEach(ExtensionContext context) throws Exception {
|
||||
Injector injector = InjectorProvider.getInjector();
|
||||
TestResources resources = injector.getInstance(TestResources.class);
|
||||
BrowserContext browserContext = resources.getContext();
|
||||
|
||||
if (browserContext != null) {
|
||||
Path tracePath = Path.of(TRACE_DIR, buildTraceFileName(context));
|
||||
browserContext.tracing().stop(new Tracing.StopOptions().setPath(tracePath));
|
||||
}
|
||||
|
||||
resources.clear();
|
||||
}
|
||||
|
||||
private BrowserType resolveBrowserType(Playwright playwright, String browserName) {
|
||||
String normalized = browserName.toLowerCase(Locale.ROOT);
|
||||
return switch (normalized) {
|
||||
case "firefox" -> playwright.firefox();
|
||||
case "webkit" -> playwright.webkit();
|
||||
default -> playwright.chromium();
|
||||
};
|
||||
}
|
||||
|
||||
private String buildTraceFileName(ExtensionContext context) {
|
||||
String raw = context.getDisplayName();
|
||||
String sanitized = raw.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||
return sanitized + ".zip";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.kovbasa.playwright;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
import com.microsoft.playwright.Page;
|
||||
|
||||
public class PlaywrightPageProvider implements Provider<Page> {
|
||||
|
||||
private final TestResources resources;
|
||||
|
||||
@Inject
|
||||
public PlaywrightPageProvider(TestResources resources) {
|
||||
this.resources = resources;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page get() {
|
||||
return resources.getPage();
|
||||
}
|
||||
}
|
||||
38
src/test/java/ru/kovbasa/playwright/TestConfig.java
Normal file
38
src/test/java/ru/kovbasa/playwright/TestConfig.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package ru.kovbasa.playwright;
|
||||
|
||||
public class TestConfig {
|
||||
|
||||
private final String baseUrl;
|
||||
private final boolean headless;
|
||||
private final double slowMo;
|
||||
private final double timeoutMs;
|
||||
private final String browser;
|
||||
|
||||
public TestConfig() {
|
||||
this.baseUrl = System.getProperty("baseUrl", "https://otus.ru");
|
||||
this.headless = Boolean.parseBoolean(System.getProperty("headless", "true"));
|
||||
this.slowMo = Double.parseDouble(System.getProperty("slowMo", "0"));
|
||||
this.timeoutMs = Double.parseDouble(System.getProperty("timeoutMs", "40000"));
|
||||
this.browser = System.getProperty("browser", "chromium");
|
||||
}
|
||||
|
||||
public String getBaseUrl() {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
public boolean isHeadless() {
|
||||
return headless;
|
||||
}
|
||||
|
||||
public double getSlowMo() {
|
||||
return slowMo;
|
||||
}
|
||||
|
||||
public double getTimeoutMs() {
|
||||
return timeoutMs;
|
||||
}
|
||||
|
||||
public String getBrowser() {
|
||||
return browser;
|
||||
}
|
||||
}
|
||||
65
src/test/java/ru/kovbasa/playwright/TestResources.java
Normal file
65
src/test/java/ru/kovbasa/playwright/TestResources.java
Normal file
@@ -0,0 +1,65 @@
|
||||
package ru.kovbasa.playwright;
|
||||
|
||||
import com.microsoft.playwright.Browser;
|
||||
import com.microsoft.playwright.BrowserContext;
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.Playwright;
|
||||
|
||||
public class TestResources {
|
||||
|
||||
private Playwright playwright;
|
||||
private Browser browser;
|
||||
private BrowserContext context;
|
||||
private Page page;
|
||||
|
||||
public Playwright getPlaywright() {
|
||||
return playwright;
|
||||
}
|
||||
|
||||
public void setPlaywright(Playwright playwright) {
|
||||
this.playwright = playwright;
|
||||
}
|
||||
|
||||
public Browser getBrowser() {
|
||||
return browser;
|
||||
}
|
||||
|
||||
public void setBrowser(Browser browser) {
|
||||
this.browser = browser;
|
||||
}
|
||||
|
||||
public BrowserContext getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
public void setContext(BrowserContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public Page getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public void setPage(Page page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
if (page != null) {
|
||||
page.close();
|
||||
}
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
if (browser != null) {
|
||||
browser.close();
|
||||
}
|
||||
if (playwright != null) {
|
||||
playwright.close();
|
||||
}
|
||||
page = null;
|
||||
context = null;
|
||||
browser = null;
|
||||
playwright = null;
|
||||
}
|
||||
}
|
||||
23
src/test/java/ru/kovbasa/tests/BaseTest.java
Normal file
23
src/test/java/ru/kovbasa/tests/BaseTest.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package ru.kovbasa.tests;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import ru.kovbasa.config.GuiceExtension;
|
||||
import ru.kovbasa.playwright.PlaywrightExtension;
|
||||
import ru.kovbasa.playwright.TestConfig;
|
||||
import ru.kovbasa.playwright.TestResources;
|
||||
import com.microsoft.playwright.Page;
|
||||
|
||||
@ExtendWith({GuiceExtension.class, PlaywrightExtension.class})
|
||||
public abstract class BaseTest {
|
||||
|
||||
@Inject
|
||||
protected TestResources resources;
|
||||
|
||||
@Inject
|
||||
protected TestConfig config;
|
||||
|
||||
protected Page page() {
|
||||
return resources.getPage();
|
||||
}
|
||||
}
|
||||
49
src/test/java/ru/kovbasa/tests/CatalogFiltersTest.java
Normal file
49
src/test/java/ru/kovbasa/tests/CatalogFiltersTest.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package ru.kovbasa.tests;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.kovbasa.pages.CatalogPage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class CatalogFiltersTest extends BaseTest {
|
||||
|
||||
@Test
|
||||
void catalogFilters() {
|
||||
CatalogPage catalog = new CatalogPage(page(), config);
|
||||
catalog.open();
|
||||
|
||||
assertTrue(catalog.isDefaultDirectionSelected(),
|
||||
"Default direction should be selected");
|
||||
assertTrue(catalog.isDefaultLevelSelected(),
|
||||
"Default level should be selected");
|
||||
|
||||
var titlesBefore = catalog.getCourseTitles(5);
|
||||
|
||||
catalog.setDurationRange(3, 10);
|
||||
|
||||
var durations = catalog.getVisibleCourseDurations();
|
||||
assertFalse(durations.isEmpty(), "Durations should be present");
|
||||
for (Integer duration : durations) {
|
||||
assertTrue(duration > 0, "Each course should have a duration");
|
||||
assertTrue(duration >= 3 && duration <= 10,
|
||||
"Duration should be within selected range");
|
||||
}
|
||||
|
||||
catalog.selectDirection("Архитектура");
|
||||
var titlesAfterDirection = catalog.getCourseTitles(5);
|
||||
assertNotEquals(titlesBefore, titlesAfterDirection,
|
||||
"Course cards should change after selecting direction");
|
||||
assertTrue(catalog.isDirectionSelected("Архитектура"),
|
||||
"Selected direction should be visible in filter");
|
||||
|
||||
catalog.resetFilters();
|
||||
page().reload();
|
||||
assertTrue(catalog.isDefaultDirectionSelected(),
|
||||
"Direction should be reset to default");
|
||||
var titlesAfterReset = catalog.getCourseTitles(5);
|
||||
assertNotEquals(titlesAfterDirection, titlesAfterReset,
|
||||
"Course cards should change after reset");
|
||||
}
|
||||
}
|
||||
77
src/test/java/ru/kovbasa/tests/ClickhouseTeachersTest.java
Normal file
77
src/test/java/ru/kovbasa/tests/ClickhouseTeachersTest.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package ru.kovbasa.tests;
|
||||
|
||||
import com.microsoft.playwright.Locator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.kovbasa.pages.ClickhousePage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class ClickhouseTeachersTest extends BaseTest {
|
||||
|
||||
@Test
|
||||
void teachersCarouselAndPopup() {
|
||||
ClickhousePage clickhouse = new ClickhousePage(page(), config);
|
||||
clickhouse.open();
|
||||
|
||||
Locator cards = clickhouse.getTeacherCards();
|
||||
cards.first().waitFor();
|
||||
assertTrue(cards.count() > 0, "Teacher cards should be visible");
|
||||
|
||||
Locator activeCard = clickhouse.getActiveTeacherCard();
|
||||
String firstNameBefore = getCardName(activeCard);
|
||||
var scrollBefore = clickhouse.getTeachersScrollLeft();
|
||||
|
||||
clickhouse.dragTeachersCarousel();
|
||||
|
||||
activeCard = clickhouse.getActiveTeacherCard();
|
||||
String firstNameAfter = getCardName(activeCard);
|
||||
var scrollAfter = clickhouse.getTeachersScrollLeft();
|
||||
boolean scrolled = scrollBefore.isPresent() && scrollAfter.isPresent()
|
||||
&& !scrollBefore.get().equals(scrollAfter.get());
|
||||
boolean nameChanged = !firstNameBefore.equals(firstNameAfter);
|
||||
assertTrue(scrolled || nameChanged,
|
||||
"Teacher list should be scrolled after drag");
|
||||
|
||||
clickhouse.openTeacherPopup(activeCard);
|
||||
String popupName = clickhouse.getPopupTeacherName();
|
||||
assertTrue(!popupName.isEmpty()
|
||||
&& (popupName.contains(firstNameAfter)
|
||||
|| firstNameAfter.contains(popupName)),
|
||||
"Popup should match clicked teacher");
|
||||
|
||||
clickhouse.clickPopupNext();
|
||||
String nextName = clickhouse.getPopupTeacherName();
|
||||
if (nextName.equals(popupName)) {
|
||||
clickhouse.dragTeachersCarousel();
|
||||
nextName = clickhouse.getPopupTeacherName();
|
||||
}
|
||||
assertNotEquals(popupName, nextName, "Next teacher should differ");
|
||||
|
||||
clickhouse.clickPopupPrev();
|
||||
String prevName = clickhouse.getPopupTeacherName();
|
||||
int attempts = 0;
|
||||
while (!matches(prevName, popupName) && attempts < 2) {
|
||||
clickhouse.clickPopupPrev();
|
||||
prevName = clickhouse.getPopupTeacherName();
|
||||
attempts++;
|
||||
}
|
||||
assertTrue(matches(prevName, popupName),
|
||||
"Previous teacher should be opened");
|
||||
}
|
||||
|
||||
private String getCardName(Locator card) {
|
||||
card.scrollIntoViewIfNeeded();
|
||||
Locator name = card.locator("h3, h4, [class*='name']").first();
|
||||
if (name.count() > 0) {
|
||||
String text = name.textContent();
|
||||
return text == null ? "" : text.trim();
|
||||
}
|
||||
String text = card.textContent();
|
||||
return text == null ? "" : text.trim();
|
||||
}
|
||||
|
||||
private boolean matches(String a, String b) {
|
||||
return a.contains(b) || b.contains(a);
|
||||
}
|
||||
}
|
||||
62
src/test/java/ru/kovbasa/tests/CorporateServicesTest.java
Normal file
62
src/test/java/ru/kovbasa/tests/CorporateServicesTest.java
Normal file
@@ -0,0 +1,62 @@
|
||||
package ru.kovbasa.tests;
|
||||
|
||||
import com.microsoft.playwright.Page;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.kovbasa.pages.CatalogPage;
|
||||
import ru.kovbasa.pages.CorporatePage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class CorporateServicesTest extends BaseTest {
|
||||
|
||||
@Test
|
||||
void corporateServicesNavigation() {
|
||||
CorporatePage corporate = new CorporatePage(page(), config);
|
||||
corporate.open();
|
||||
|
||||
var targetPage = corporate.clickLearnMoreInNeedCourseBlock();
|
||||
CorporatePage businessPage = targetPage == page() ? corporate
|
||||
: new CorporatePage(targetPage, config);
|
||||
assertTrue(businessPage.isBusinessCoursePageOpened(),
|
||||
"Business course development page should be opened");
|
||||
|
||||
List<String> directions = businessPage.getDirections().stream()
|
||||
.map(String::trim)
|
||||
.filter(text -> !text.isBlank())
|
||||
.toList();
|
||||
assertFalse(directions.isEmpty(), "Directions should be displayed");
|
||||
|
||||
CorporatePage.DirectionClick directionClick = businessPage.clickFirstDirection();
|
||||
String clickedDirection = directionClick.value();
|
||||
Page catalogPage = directionClick.page();
|
||||
|
||||
boolean catalogOpened = catalogPage.url().contains("/catalog/courses")
|
||||
|| catalogPage.getByText("Каталог").first().isVisible();
|
||||
assertTrue(catalogOpened, "Catalog should be opened after direction click");
|
||||
|
||||
CatalogPage catalog = new CatalogPage(catalogPage, config);
|
||||
String expectedCategory = extractCategoryParam(clickedDirection);
|
||||
if (expectedCategory != null) {
|
||||
assertTrue(catalogPage.url().contains("categories=" + expectedCategory),
|
||||
"Catalog should open with selected category from direction link");
|
||||
} else {
|
||||
boolean directionSelected = catalog.isDirectionSelected(clickedDirection);
|
||||
assertTrue(directionSelected, "Clicked direction should be selected in catalog");
|
||||
}
|
||||
}
|
||||
|
||||
private String extractCategoryParam(String linkOrText) {
|
||||
if (linkOrText == null) {
|
||||
return null;
|
||||
}
|
||||
int idx = linkOrText.indexOf("categories=");
|
||||
if (idx < 0) {
|
||||
return null;
|
||||
}
|
||||
String tail = linkOrText.substring(idx + "categories=".length());
|
||||
int amp = tail.indexOf('&');
|
||||
return amp >= 0 ? tail.substring(0, amp) : tail;
|
||||
}
|
||||
}
|
||||
70
src/test/java/ru/kovbasa/tests/SubscriptionTest.java
Normal file
70
src/test/java/ru/kovbasa/tests/SubscriptionTest.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package ru.kovbasa.tests;
|
||||
|
||||
import com.microsoft.playwright.Locator;
|
||||
import com.microsoft.playwright.Page;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import ru.kovbasa.pages.PaymentPage;
|
||||
import ru.kovbasa.pages.SubscriptionPage;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class SubscriptionTest extends BaseTest {
|
||||
|
||||
@Test
|
||||
void subscriptionFlow() {
|
||||
SubscriptionPage subscription = new SubscriptionPage(page(), config);
|
||||
subscription.open();
|
||||
|
||||
Locator tiles = subscription.getSubscriptionTiles();
|
||||
tiles.first().waitFor();
|
||||
assertTrue(tiles.count() > 0, "Subscription tiles should be visible");
|
||||
|
||||
Locator tile = tiles.first();
|
||||
|
||||
subscription.expandDetails(tile);
|
||||
assertTrue(subscription.isDetailsExpanded(tile),
|
||||
"Details should expand after clicking 'Подробнее'");
|
||||
|
||||
subscription.collapseDetails(tile);
|
||||
assertFalse(subscription.isDetailsExpanded(tile),
|
||||
"Details should collapse after clicking 'Свернуть'");
|
||||
|
||||
Page paymentPage = subscription.clickBuy(tile);
|
||||
if (paymentPage.url().contains("/subscription")) {
|
||||
Locator trialCard = subscription.getTrialCard();
|
||||
trialCard.waitFor();
|
||||
String priceBefore = subscription.getPriceFromCard(tile);
|
||||
String durationBefore = subscription.getDurationFromCard(tile);
|
||||
|
||||
subscription.selectTrialOnSubscription(trialCard);
|
||||
|
||||
String priceAfter = subscription.getPriceFromCard(trialCard);
|
||||
String durationAfter = subscription.getDurationFromCard(trialCard);
|
||||
|
||||
assertNotEquals(priceBefore, priceAfter, "Price should change for Trial");
|
||||
assertNotEquals(durationBefore, durationAfter,
|
||||
"Duration should change for Trial");
|
||||
return;
|
||||
}
|
||||
|
||||
PaymentPage payment = new PaymentPage(paymentPage);
|
||||
assertTrue(payment.isOpened(), "Payment page should be opened");
|
||||
|
||||
assertTrue(payment.selectFirstNonTrial(),
|
||||
"Non-trial plan should be selectable");
|
||||
String priceBefore = payment.getPriceText();
|
||||
String durationBefore = payment.getDurationText();
|
||||
|
||||
payment.selectTrial();
|
||||
payment.waitForPlanChange(priceBefore, durationBefore);
|
||||
|
||||
String priceAfter = payment.getPriceText();
|
||||
String durationAfter = payment.getDurationText();
|
||||
|
||||
assertNotEquals(priceBefore, priceAfter, "Price should change for Trial");
|
||||
assertNotEquals(durationBefore, durationAfter,
|
||||
"Duration should change for Trial");
|
||||
}
|
||||
}
|
||||
31
src/test/java/ru/kovbasa/utils/UiActions.java
Normal file
31
src/test/java/ru/kovbasa/utils/UiActions.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package ru.kovbasa.utils;
|
||||
|
||||
import com.microsoft.playwright.Locator;
|
||||
import com.microsoft.playwright.Page;
|
||||
import java.util.List;
|
||||
|
||||
public final class UiActions {
|
||||
|
||||
private UiActions() {
|
||||
}
|
||||
|
||||
public static void closeCommonPopups(Page page) {
|
||||
List<String> selectors = List.of(
|
||||
"button:has-text('Принять')",
|
||||
"button:has-text('Согласен')",
|
||||
"button:has-text('Ок')",
|
||||
"button:has-text('OK')",
|
||||
"button[aria-label='Закрыть']",
|
||||
"button[aria-label='Close']",
|
||||
".popup__close",
|
||||
".modal__close"
|
||||
);
|
||||
|
||||
for (String selector : selectors) {
|
||||
Locator locator = page.locator(selector).first();
|
||||
if (locator.isVisible()) {
|
||||
locator.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
traces.zip
Normal file
BIN
traces.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user