Update README and stabilize UI tests
This commit is contained in:
52
README.md
52
README.md
@@ -1,6 +1,5 @@
|
|||||||
# OTUS Homework 6: Playwright UI Tests
|
# OTUS Homework 6: Playwright UI Tests
|
||||||
|
UI-автотесты на Playwright для 4 сценариев OTUS с DI и трассировкой.
|
||||||
UI-автотесты на Playwright для 4 сценариев OTUS.
|
|
||||||
|
|
||||||
## Что реализовано
|
## Что реализовано
|
||||||
- 4 UI-сценария из ТЗ (Clickhouse, Catalog, B2B, Subscription).
|
- 4 UI-сценария из ТЗ (Clickhouse, Catalog, B2B, Subscription).
|
||||||
@@ -9,6 +8,18 @@ UI-автотесты на Playwright для 4 сценариев OTUS.
|
|||||||
- Линтеры: Checkstyle и SpotBugs.
|
- Линтеры: Checkstyle и SpotBugs.
|
||||||
- Запуск из консоли (Maven).
|
- Запуск из консоли (Maven).
|
||||||
|
|
||||||
|
## Сценарии
|
||||||
|
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
|
||||||
|
|
||||||
## Структура проекта
|
## Структура проекта
|
||||||
- `src/test/java/ru/kovbasa/tests` — тесты по сценариям.
|
- `src/test/java/ru/kovbasa/tests` — тесты по сценариям.
|
||||||
- `src/test/java/ru/kovbasa/pages` — page-объекты.
|
- `src/test/java/ru/kovbasa/pages` — page-объекты.
|
||||||
@@ -17,38 +28,18 @@ UI-автотесты на Playwright для 4 сценариев OTUS.
|
|||||||
- `traces/` — zip-трейсы по каждому тесту.
|
- `traces/` — zip-трейсы по каждому тесту.
|
||||||
- `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
|
```bash
|
||||||
mvn test
|
mvn test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Один тест
|
Один тест (PowerShell):
|
||||||
```bash
|
```powershell
|
||||||
mvn "-Dtest=ru.kovbasa.tests.CatalogFiltersTest" test
|
mvn "-Dtest=ru.kovbasa.tests.CatalogFiltersTest" test
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры запуска
|
Параметры запуска:
|
||||||
```bash
|
```bash
|
||||||
mvn -Dheadless=false -Dbrowser=chromium -DbaseUrl=https://otus.ru test
|
mvn -Dheadless=false -Dbrowser=chromium -DbaseUrl=https://otus.ru test
|
||||||
```
|
```
|
||||||
@@ -63,7 +54,7 @@ mvn -Dheadless=false -Dbrowser=chromium -DbaseUrl=https://otus.ru test
|
|||||||
## Трейсы
|
## Трейсы
|
||||||
Трейсы автоматически сохраняются в каталог `traces/` в виде zip-файлов по каждому тесту.
|
Трейсы автоматически сохраняются в каталог `traces/` в виде zip-файлов по каждому тесту.
|
||||||
|
|
||||||
Для сдачи ДЗ нужно положить `traces.zip` в корень проекта.
|
Архив `traces.zip` расположен в корне проекта.
|
||||||
|
|
||||||
PowerShell:
|
PowerShell:
|
||||||
```powershell
|
```powershell
|
||||||
@@ -80,10 +71,7 @@ zip -r traces.zip traces
|
|||||||
mvn verify
|
mvn verify
|
||||||
```
|
```
|
||||||
|
|
||||||
## Кросс‑платформенность
|
## Требования к окружению
|
||||||
Проект запускается на Windows / Linux / macOS.
|
|
||||||
|
|
||||||
Минимальные требования:
|
|
||||||
- Java 21
|
- Java 21
|
||||||
- Maven
|
- Maven
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import com.microsoft.playwright.Page;
|
|||||||
import com.microsoft.playwright.PlaywrightException;
|
import com.microsoft.playwright.PlaywrightException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import ru.kovbasa.playwright.TestConfig;
|
import ru.kovbasa.playwright.TestConfig;
|
||||||
@@ -21,6 +20,7 @@ public class CatalogPage extends BasePage {
|
|||||||
|
|
||||||
public void open() {
|
public void open() {
|
||||||
openPath("/catalog/courses");
|
openPath("/catalog/courses");
|
||||||
|
page.getByText("Направление").first().waitFor();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDefaultDirectionSelected() {
|
public boolean isDefaultDirectionSelected() {
|
||||||
@@ -34,25 +34,32 @@ public class CatalogPage extends BasePage {
|
|||||||
|
|
||||||
public void setDurationRange(int minMonths, int maxMonths) {
|
public void setDurationRange(int minMonths, int maxMonths) {
|
||||||
Locator durationSection = getFilterSection("Продолжительность");
|
Locator durationSection = getFilterSection("Продолжительность");
|
||||||
|
for (int attempt = 0; attempt < 2; attempt++) {
|
||||||
|
if (trySetDurationRange(durationSection, minMonths, maxMonths)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Locator rangeLabel = durationSection.locator("label").filter(
|
private boolean trySetDurationRange(Locator durationSection, int minMonths, int maxMonths) {
|
||||||
|
Locator rangeLabel = durationSection.locator("label:visible").filter(
|
||||||
new Locator.FilterOptions().setHasText(String.valueOf(minMonths))
|
new Locator.FilterOptions().setHasText(String.valueOf(minMonths))
|
||||||
).filter(new Locator.FilterOptions().setHasText(String.valueOf(maxMonths))).first();
|
).filter(new Locator.FilterOptions().setHasText(String.valueOf(maxMonths))).first();
|
||||||
if (rangeLabel.count() > 0) {
|
if (rangeLabel.count() > 0) {
|
||||||
rangeLabel.click();
|
rangeLabel.click();
|
||||||
waitForDurationFilterApplied(minMonths, maxMonths);
|
clickApplyIfPresent(durationSection);
|
||||||
return;
|
return waitForDurationFilterApplied(minMonths, maxMonths);
|
||||||
}
|
}
|
||||||
|
|
||||||
Locator rangeText = durationSection.locator(
|
Locator rangeText = durationSection.locator(
|
||||||
"text=/\\b" + minMonths + "\\b.*\\b" + maxMonths + "\\b/").first();
|
"text=/\\b" + minMonths + "\\b.*\\b" + maxMonths + "\\b/").first();
|
||||||
if (rangeText.count() > 0) {
|
if (rangeText.count() > 0) {
|
||||||
rangeText.click();
|
rangeText.click();
|
||||||
waitForDurationFilterApplied(minMonths, maxMonths);
|
clickApplyIfPresent(durationSection);
|
||||||
return;
|
return waitForDurationFilterApplied(minMonths, maxMonths);
|
||||||
}
|
}
|
||||||
|
|
||||||
Locator sliders = page.locator("[role='slider']");
|
Locator sliders = durationSection.locator("[role='slider']:visible");
|
||||||
if (sliders.count() >= 2) {
|
if (sliders.count() >= 2) {
|
||||||
Locator minSlider = sliders.nth(0);
|
Locator minSlider = sliders.nth(0);
|
||||||
Locator maxSlider = sliders.nth(1);
|
Locator maxSlider = sliders.nth(1);
|
||||||
@@ -75,38 +82,43 @@ public class CatalogPage extends BasePage {
|
|||||||
nudgeSliderWithKeyboard(minSlider, minMonths);
|
nudgeSliderWithKeyboard(minSlider, minMonths);
|
||||||
nudgeSliderWithKeyboard(maxSlider, maxMonths);
|
nudgeSliderWithKeyboard(maxSlider, maxMonths);
|
||||||
waitForSliderValues(minSlider, maxSlider, minMonths, maxMonths);
|
waitForSliderValues(minSlider, maxSlider, minMonths, maxMonths);
|
||||||
waitForDurationFilterApplied(minMonths, maxMonths);
|
clickApplyIfPresent(durationSection);
|
||||||
return;
|
return waitForDurationFilterApplied(minMonths, maxMonths);
|
||||||
}
|
}
|
||||||
|
|
||||||
Locator inputs = durationSection.locator("input");
|
Locator inputs = durationSection.locator("input:visible");
|
||||||
if (inputs.count() >= 2) {
|
if (inputs.count() >= 2) {
|
||||||
String type = inputs.nth(0).getAttribute("type");
|
String type = inputs.nth(0).getAttribute("type");
|
||||||
if (type != null && (type.equals("number") || type.equals("text"))) {
|
if (type != null && (type.equals("number") || type.equals("text"))) {
|
||||||
inputs.nth(0).fill(String.valueOf(minMonths));
|
inputs.nth(0).fill(String.valueOf(minMonths));
|
||||||
inputs.nth(1).fill(String.valueOf(maxMonths));
|
inputs.nth(1).fill(String.valueOf(maxMonths));
|
||||||
inputs.nth(1).press("Enter");
|
inputs.nth(1).press("Enter");
|
||||||
return;
|
clickApplyIfPresent(durationSection);
|
||||||
|
return waitForDurationFilterApplied(minMonths, maxMonths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Locator rangeInputs = durationSection.locator("input[type='range']");
|
Locator rangeInputs = durationSection.locator("input[type='range']:visible");
|
||||||
if (rangeInputs.count() >= 2) {
|
if (rangeInputs.count() >= 2) {
|
||||||
rangeInputs.nth(0).fill(String.valueOf(minMonths));
|
rangeInputs.nth(0).fill(String.valueOf(minMonths));
|
||||||
rangeInputs.nth(1).fill(String.valueOf(maxMonths));
|
rangeInputs.nth(1).fill(String.valueOf(maxMonths));
|
||||||
return;
|
clickApplyIfPresent(durationSection);
|
||||||
|
return waitForDurationFilterApplied(minMonths, maxMonths);
|
||||||
}
|
}
|
||||||
|
|
||||||
// no suitable controls found; rely on current state
|
// no suitable controls found; rely on current state
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectDirection(String direction) {
|
public void selectDirection(String direction) {
|
||||||
Locator option = page.getByText(direction).first();
|
Locator directionSection = getFilterSection("Направление");
|
||||||
try {
|
Locator option = directionSection.locator("label")
|
||||||
option.click(new Locator.ClickOptions().setForce(true));
|
.filter(new Locator.FilterOptions().setHasText(direction))
|
||||||
} catch (PlaywrightException ex) {
|
.first();
|
||||||
option.evaluate("el => el.click()");
|
if (option.count() == 0) {
|
||||||
|
option = page.getByText(direction).first();
|
||||||
}
|
}
|
||||||
|
option.click(new Locator.ClickOptions().setForce(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDirectionSelected(String direction) {
|
public boolean isDirectionSelected(String direction) {
|
||||||
@@ -165,13 +177,28 @@ public class CatalogPage extends BasePage {
|
|||||||
|
|
||||||
private Locator getFilterSection(String filterLabel) {
|
private Locator getFilterSection(String filterLabel) {
|
||||||
Locator title = page.getByText(filterLabel).first();
|
Locator title = page.getByText(filterLabel).first();
|
||||||
Locator section = title.locator("xpath=ancestor::*[self::div or self::section][1]");
|
Locator header = title.locator("xpath=ancestor::*[self::div or self::section][1]");
|
||||||
|
Locator section = title.locator("xpath=ancestor::*[self::div or self::section][2]");
|
||||||
if (section.count() == 0) {
|
if (section.count() == 0) {
|
||||||
section = title.locator("xpath=ancestor::*[self::div or self::section][2]");
|
section = header;
|
||||||
|
}
|
||||||
|
if (!hasFilterControls(section)) {
|
||||||
|
Locator toggle = header.locator("button").first();
|
||||||
|
if (toggle.count() > 0) {
|
||||||
|
toggle.click(new Locator.ClickOptions().setForce(true));
|
||||||
|
Locator anyControl = section.locator("label:visible, input:visible, [role='slider']:visible").first();
|
||||||
|
if (anyControl.count() > 0) {
|
||||||
|
anyControl.waitFor(new Locator.WaitForOptions().setTimeout(5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasFilterControls(Locator section) {
|
||||||
|
return section.locator("label:visible, input:visible, [role='slider']:visible").count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isOptionChecked(String optionText) {
|
private boolean isOptionChecked(String optionText) {
|
||||||
Locator label = page.locator("label")
|
Locator label = page.locator("label")
|
||||||
.filter(new Locator.FilterOptions().setHasText(optionText))
|
.filter(new Locator.FilterOptions().setHasText(optionText))
|
||||||
@@ -206,8 +233,7 @@ public class CatalogPage extends BasePage {
|
|||||||
Locator list = page.locator("section, div").filter(
|
Locator list = page.locator("section, div").filter(
|
||||||
new Locator.FilterOptions().setHasText("Показать еще")
|
new Locator.FilterOptions().setHasText("Показать еще")
|
||||||
).first();
|
).first();
|
||||||
Locator cards = list.locator("a[href*='/lessons/']:visible")
|
Locator cards = list.locator("a[href*='/lessons/']:visible");
|
||||||
.filter(new Locator.FilterOptions().setHasNotText("Успеть!"));
|
|
||||||
if (cards.count() == 0) {
|
if (cards.count() == 0) {
|
||||||
cards = list.locator("a:has(h4):visible, a:has(h5):visible, a:has(h6):visible");
|
cards = list.locator("a:has(h4):visible, a:has(h5):visible, a:has(h6):visible");
|
||||||
}
|
}
|
||||||
@@ -228,19 +254,18 @@ public class CatalogPage extends BasePage {
|
|||||||
if (targetValue > max) {
|
if (targetValue > max) {
|
||||||
targetValue = max;
|
targetValue = max;
|
||||||
}
|
}
|
||||||
@SuppressWarnings("unchecked")
|
var track = slider.locator("xpath=..");
|
||||||
Map<String, Object> trackBox = (Map<String, Object>) slider.evaluate("el => {"
|
var trackBox = track.boundingBox();
|
||||||
+ "const track = el.parentElement;"
|
if (trackBox == null) {
|
||||||
+ "const r = track.getBoundingClientRect();"
|
trackBox = slider.boundingBox();
|
||||||
+ "return {x: r.x, y: r.y, width: r.width, height: r.height};"
|
}
|
||||||
+ "}");
|
|
||||||
if (trackBox == null) {
|
if (trackBox == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
double x = ((Number) trackBox.get("x")).doubleValue();
|
double x = trackBox.x;
|
||||||
double width = ((Number) trackBox.get("width")).doubleValue();
|
double width = trackBox.width;
|
||||||
double y = ((Number) trackBox.get("y")).doubleValue();
|
double y = trackBox.y;
|
||||||
double height = ((Number) trackBox.get("height")).doubleValue();
|
double height = trackBox.height;
|
||||||
double ratio = (targetValue - min) / (double) (max - min);
|
double ratio = (targetValue - min) / (double) (max - min);
|
||||||
double targetX = x + width * ratio;
|
double targetX = x + width * ratio;
|
||||||
double targetY = y + height / 2;
|
double targetY = y + height / 2;
|
||||||
@@ -276,15 +301,21 @@ public class CatalogPage extends BasePage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void waitForDurationFilterApplied(int minMonths, int maxMonths) {
|
private boolean waitForDurationFilterApplied(int minMonths, int maxMonths) {
|
||||||
|
boolean appliedOnce = false;
|
||||||
for (int i = 0; i < 40; i++) {
|
for (int i = 0; i < 40; i++) {
|
||||||
List<Integer> durations = getVisibleCourseDurations();
|
List<Integer> durations = getVisibleCourseDurations();
|
||||||
if (!durations.isEmpty()
|
if (!durations.isEmpty()
|
||||||
&& durations.stream().allMatch(v -> v >= minMonths && v <= maxMonths)) {
|
&& durations.stream().allMatch(v -> v >= minMonths && v <= maxMonths)) {
|
||||||
return;
|
return true;
|
||||||
|
}
|
||||||
|
if (!appliedOnce) {
|
||||||
|
clickApplyGlobalIfPresent();
|
||||||
|
appliedOnce = true;
|
||||||
}
|
}
|
||||||
page.waitForTimeout(300);
|
page.waitForTimeout(300);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void waitForFiltersReset() {
|
private void waitForFiltersReset() {
|
||||||
@@ -314,4 +345,25 @@ public class CatalogPage extends BasePage {
|
|||||||
page.waitForTimeout(200);
|
page.waitForTimeout(200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void clickApplyIfPresent(Locator durationSection) {
|
||||||
|
Locator apply = durationSection.locator("button:has-text('Применить'),"
|
||||||
|
+ " button:has-text('Показать'),"
|
||||||
|
+ " button:has-text('Готово'),"
|
||||||
|
+ " button:has-text('Сохранить')").first();
|
||||||
|
if (apply.count() > 0) {
|
||||||
|
apply.click(new Locator.ClickOptions().setForce(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clickApplyGlobalIfPresent() {
|
||||||
|
Locator apply = page.locator("button:has-text('Применить'),"
|
||||||
|
+ " button:has-text('Показать'),"
|
||||||
|
+ " button:has-text('Готово'),"
|
||||||
|
+ " button:has-text('Сохранить')")
|
||||||
|
.first();
|
||||||
|
if (apply.count() > 0) {
|
||||||
|
apply.click(new Locator.ClickOptions().setForce(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package ru.kovbasa.pages;
|
package ru.kovbasa.pages;
|
||||||
|
|
||||||
import com.microsoft.playwright.ElementHandle;
|
|
||||||
import com.microsoft.playwright.Locator;
|
import com.microsoft.playwright.Locator;
|
||||||
import com.microsoft.playwright.Mouse;
|
import com.microsoft.playwright.Mouse;
|
||||||
import com.microsoft.playwright.Page;
|
import com.microsoft.playwright.Page;
|
||||||
import com.microsoft.playwright.options.AriaRole;
|
import com.microsoft.playwright.options.AriaRole;
|
||||||
import java.util.Optional;
|
|
||||||
import ru.kovbasa.playwright.TestConfig;
|
import ru.kovbasa.playwright.TestConfig;
|
||||||
import ru.kovbasa.utils.UiActions;
|
import ru.kovbasa.utils.UiActions;
|
||||||
|
|
||||||
@@ -43,16 +41,11 @@ public class ClickhousePage extends BasePage {
|
|||||||
|
|
||||||
public void dragTeachersCarousel() {
|
public void dragTeachersCarousel() {
|
||||||
Locator section = getTeachersSection();
|
Locator section = getTeachersSection();
|
||||||
ElementHandle scrollContainer = findScrollableContainer(section);
|
Locator activeCard = getActiveTeacherCard();
|
||||||
if (scrollContainer == null) {
|
String activeBefore = getCardName(activeCard);
|
||||||
return;
|
activeCard.scrollIntoViewIfNeeded();
|
||||||
}
|
|
||||||
|
|
||||||
Optional<Double> before = getScrollLeft(scrollContainer);
|
var box = activeCard.boundingBox();
|
||||||
String activeBefore = getCardName(getActiveTeacherCard());
|
|
||||||
scrollContainer.scrollIntoViewIfNeeded();
|
|
||||||
|
|
||||||
var box = scrollContainer.boundingBox();
|
|
||||||
if (box == null) {
|
if (box == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -66,35 +59,25 @@ public class ClickhousePage extends BasePage {
|
|||||||
page.mouse().move(endX, y, new Mouse.MoveOptions().setSteps(12));
|
page.mouse().move(endX, y, new Mouse.MoveOptions().setSteps(12));
|
||||||
page.mouse().up();
|
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());
|
String activeAfter = getCardName(getActiveTeacherCard());
|
||||||
if (activeAfter.equals(activeBefore)) {
|
if (activeAfter.equals(activeBefore)) {
|
||||||
page.evaluate("() => {\n"
|
Locator nextButton = section.locator(
|
||||||
+ " const swiperEl = document.querySelector('.swiper');\n"
|
".swiper-button-next, button[aria-label*='След'], button[aria-label*='Next'],"
|
||||||
+ " if (swiperEl && swiperEl.swiper) { swiperEl.swiper.slideNext(); }\n"
|
+ " button:has-text('>')").first();
|
||||||
+ "}");
|
if (nextButton.count() > 0) {
|
||||||
page.waitForTimeout(300);
|
nextButton.click(new Locator.ClickOptions().setForce(true));
|
||||||
|
} else {
|
||||||
|
page.keyboard().press("ArrowRight");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Double> getTeachersScrollLeft() {
|
|
||||||
Locator section = getTeachersSection();
|
|
||||||
ElementHandle scrollContainer = findScrollableContainer(section);
|
|
||||||
return getScrollLeft(scrollContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPopupTeacherName() {
|
public String getPopupTeacherName() {
|
||||||
|
Locator popup = getPopupContainer();
|
||||||
|
Locator active = popup.locator(".swiper-slide-active").first();
|
||||||
|
if (active.count() > 0) {
|
||||||
|
return getCardName(active);
|
||||||
|
}
|
||||||
return getCardName(getActiveTeacherCard());
|
return getCardName(getActiveTeacherCard());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,57 +94,25 @@ public class ClickhousePage extends BasePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void clickPopupNext() {
|
public void clickPopupNext() {
|
||||||
Locator nextButton = page.locator(
|
String before = getPopupTeacherName();
|
||||||
".swiper-button-next, button[aria-label*='След'], button[aria-label*='Next'],"
|
clickPopupArrow(before, true);
|
||||||
+ " 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() {
|
public void clickPopupPrev() {
|
||||||
String before = getPopupTeacherName();
|
String before = getPopupTeacherName();
|
||||||
Locator prevButton = page.locator(
|
clickPopupArrow(before, false);
|
||||||
".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() {
|
public Locator getTeacherDialog() {
|
||||||
|
Locator dialog = page.locator("[role='dialog'], [class*='modal'], [class*='popup']").first();
|
||||||
|
if (dialog.count() > 0) {
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
Locator active = getActiveTeacherCard();
|
Locator active = getActiveTeacherCard();
|
||||||
if (active.count() > 0) {
|
if (active.count() > 0) {
|
||||||
return active;
|
return active;
|
||||||
}
|
}
|
||||||
return page.locator("[role='dialog'], [class*='modal'], [class*='popup']").first();
|
return page.locator("body");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Locator getActiveTeacherCard() {
|
public Locator getActiveTeacherCard() {
|
||||||
@@ -198,28 +149,41 @@ public class ClickhousePage extends BasePage {
|
|||||||
return section;
|
return section;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ElementHandle findScrollableContainer(Locator section) {
|
private void waitForPopupTeacherChange(String before, boolean forward) {
|
||||||
String script = """
|
for (int i = 0; i < 6; i++) {
|
||||||
section => {
|
page.waitForTimeout(300);
|
||||||
const byClass = section.querySelector(
|
String after = getPopupTeacherName();
|
||||||
'[class*="swiper"], [class*="carousel"], [class*="slider"],'
|
if (!after.equals(before)) {
|
||||||
+ ' [class*="scroll"], [class*="teachers"]'
|
return;
|
||||||
);
|
}
|
||||||
if (byClass) {
|
if (i == 2) {
|
||||||
return byClass;
|
page.keyboard().press(forward ? "ArrowRight" : "ArrowLeft");
|
||||||
}
|
}
|
||||||
return Array.from(section.querySelectorAll('*'))
|
}
|
||||||
.find(el => el.scrollWidth > el.clientWidth);
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
return section.evaluateHandle(script).asElement();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Double> getScrollLeft(ElementHandle handle) {
|
private void clickPopupArrow(String before, boolean forward) {
|
||||||
if (handle == null) {
|
Locator popup = getPopupContainer();
|
||||||
return Optional.empty();
|
Locator buttons = popup.locator(
|
||||||
|
"xpath=.//div[.//*[contains(@class,'swiper-pagination')]]//button");
|
||||||
|
int count = (int) Math.min(buttons.count(), 2);
|
||||||
|
if (count > 0) {
|
||||||
|
int index = forward ? Math.max(0, count - 1) : 0;
|
||||||
|
buttons.nth(index).click(new Locator.ClickOptions().setForce(true));
|
||||||
|
waitForPopupTeacherChange(before, forward);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
Number value = (Number) handle.evaluate("el => el.scrollLeft");
|
page.keyboard().press(forward ? "ArrowRight" : "ArrowLeft");
|
||||||
return value == null ? Optional.empty() : Optional.of(value.doubleValue());
|
waitForPopupTeacherChange(before, forward);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Locator getPopupContainer() {
|
||||||
|
Locator popup = page.locator(".fade-enter-active, .fade-enter-done").first();
|
||||||
|
if (popup.count() > 0) {
|
||||||
|
return popup;
|
||||||
|
}
|
||||||
|
return page.locator("body");
|
||||||
|
}
|
||||||
|
|
||||||
|
// no JS helpers: rely on locators and user-like interactions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,17 +20,17 @@ public class ClickhouseTeachersTest extends BaseTest {
|
|||||||
|
|
||||||
Locator activeCard = clickhouse.getActiveTeacherCard();
|
Locator activeCard = clickhouse.getActiveTeacherCard();
|
||||||
String firstNameBefore = getCardName(activeCard);
|
String firstNameBefore = getCardName(activeCard);
|
||||||
var scrollBefore = clickhouse.getTeachersScrollLeft();
|
|
||||||
|
|
||||||
clickhouse.dragTeachersCarousel();
|
clickhouse.dragTeachersCarousel();
|
||||||
|
|
||||||
activeCard = clickhouse.getActiveTeacherCard();
|
activeCard = clickhouse.getActiveTeacherCard();
|
||||||
String firstNameAfter = getCardName(activeCard);
|
String firstNameAfter = getCardName(activeCard);
|
||||||
var scrollAfter = clickhouse.getTeachersScrollLeft();
|
if (firstNameBefore.equals(firstNameAfter)) {
|
||||||
boolean scrolled = scrollBefore.isPresent() && scrollAfter.isPresent()
|
clickhouse.dragTeachersCarousel();
|
||||||
&& !scrollBefore.get().equals(scrollAfter.get());
|
activeCard = clickhouse.getActiveTeacherCard();
|
||||||
boolean nameChanged = !firstNameBefore.equals(firstNameAfter);
|
firstNameAfter = getCardName(activeCard);
|
||||||
assertTrue(scrolled || nameChanged,
|
}
|
||||||
|
assertTrue(!firstNameBefore.equals(firstNameAfter),
|
||||||
"Teacher list should be scrolled after drag");
|
"Teacher list should be scrolled after drag");
|
||||||
|
|
||||||
clickhouse.openTeacherPopup(activeCard);
|
clickhouse.openTeacherPopup(activeCard);
|
||||||
|
|||||||
BIN
traces.zip
BIN
traces.zip
Binary file not shown.
Reference in New Issue
Block a user