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