Compare commits

..

2 Commits

Author SHA1 Message Date
c6688f6dbf Update README and stabilize UI tests 2026-03-26 00:40:10 +03:00
ce6691bf64 Remove JS checks and simplify waits 2026-03-19 21:04:46 +03:00
7 changed files with 254 additions and 225 deletions

View File

@@ -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

View File

@@ -8,9 +8,6 @@ public final class InjectorProvider {
private static final Injector INJECTOR = private static final Injector INJECTOR =
Guice.createInjector(new PlaywrightModule()); Guice.createInjector(new PlaywrightModule());
private InjectorProvider() {
}
public static Injector getInjector() { public static Injector getInjector() {
return INJECTOR; return INJECTOR;
} }

View File

@@ -27,7 +27,6 @@ public abstract class BasePage {
if (message.contains("ERR_CONNECTION_TIMED_OUT") if (message.contains("ERR_CONNECTION_TIMED_OUT")
|| message.contains("ERR_NAME_NOT_RESOLVED") || message.contains("ERR_NAME_NOT_RESOLVED")
|| message.contains("net::ERR")) { || message.contains("net::ERR")) {
page.waitForTimeout(2000);
page.navigate(url, options); page.navigate(url, options);
} else { } else {
throw ex; throw ex;

View File

@@ -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,8 +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 sliders = page.locator("[role='slider']"); private boolean trySetDurationRange(Locator durationSection, int minMonths, int maxMonths) {
Locator rangeLabel = durationSection.locator("label:visible").filter(
new Locator.FilterOptions().setHasText(String.valueOf(minMonths))
).filter(new Locator.FilterOptions().setHasText(String.valueOf(maxMonths))).first();
if (rangeLabel.count() > 0) {
rangeLabel.click();
clickApplyIfPresent(durationSection);
return waitForDurationFilterApplied(minMonths, maxMonths);
}
Locator rangeText = durationSection.locator(
"text=/\\b" + minMonths + "\\b.*\\b" + maxMonths + "\\b/").first();
if (rangeText.count() > 0) {
rangeText.click();
clickApplyIfPresent(durationSection);
return waitForDurationFilterApplied(minMonths, maxMonths);
}
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);
@@ -55,52 +79,46 @@ public class CatalogPage extends BasePage {
if (maxBefore != null && maxBefore.equals(maxAfter)) { if (maxBefore != null && maxBefore.equals(maxAfter)) {
nudgeSliderWithKeyboard(maxSlider, maxMonths); nudgeSliderWithKeyboard(maxSlider, maxMonths);
} }
waitForDurationFilterApplied(minMonths, maxMonths); nudgeSliderWithKeyboard(minSlider, minMonths);
return; nudgeSliderWithKeyboard(maxSlider, maxMonths);
waitForSliderValues(minSlider, maxSlider, minMonths, maxMonths);
clickApplyIfPresent(durationSection);
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);
} }
Locator rangeLabel = durationSection.locator("label").filter( // no suitable controls found; rely on current state
new Locator.FilterOptions().setHasText(String.valueOf(minMonths)) return false;
).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) { 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) {
@@ -134,16 +152,7 @@ public class CatalogPage extends BasePage {
if (allDirections.count() > 0) { if (allDirections.count() > 0) {
allDirections.click(new Locator.ClickOptions().setForce(true)); allDirections.click(new Locator.ClickOptions().setForce(true));
} }
page.waitForFunction("() => {\n" waitForFiltersReset();
+ " 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"
+ "}");
} }
@@ -152,12 +161,15 @@ public class CatalogPage extends BasePage {
Locator cards = getCourseCards(); Locator cards = getCourseCards();
int count = (int) Math.min(cards.count(), 30); int count = (int) Math.min(cards.count(), 30);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
String text = cards.nth(i).innerText(); String text;
try {
text = cards.nth(i).innerText(new Locator.InnerTextOptions().setTimeout(2000));
} catch (PlaywrightException ex) {
continue;
}
Matcher matcher = DURATION_PATTERN.matcher(text); Matcher matcher = DURATION_PATTERN.matcher(text);
if (matcher.find()) { if (matcher.find()) {
values.add(Integer.parseInt(matcher.group(1))); values.add(Integer.parseInt(matcher.group(1)));
} else {
values.add(-1);
} }
} }
return values; return values;
@@ -165,37 +177,56 @@ 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) {
Boolean checked = (Boolean) page.evaluate("text => {\n" Locator label = page.locator("label")
+ " const labels = Array.from(document.querySelectorAll('label'))\n" .filter(new Locator.FilterOptions().setHasText(optionText))
+ " .filter(el => (el.textContent || '').trim() === text);\n" .first();
+ " if (labels.length) {\n" if (label.count() > 0) {
+ " const label = labels[0];\n" String forId = label.getAttribute("for");
+ " const id = label.getAttribute('for');\n" Locator input = forId != null
+ " if (id) {\n" ? page.locator("input[id='" + forId + "']")
+ " const input = document.getElementById(id);\n" : label.locator("input");
+ " if (input) return !!input.checked;\n" if (input.count() > 0) {
+ " }\n" return input.first().isChecked();
+ " const nested = label.querySelector('input');\n" }
+ " if (nested) return !!nested.checked;\n" }
+ " }\n" Locator element = page.getByText(optionText).first();
+ " const el = Array.from(document.querySelectorAll('*'))\n" if (element.count() > 0) {
+ " .find(node => (node.textContent || '').trim() === text);\n" String aria = element.getAttribute("aria-checked");
+ " if (!el) return null;\n" if (aria == null) {
+ " const aria = el.getAttribute('aria-checked') || el.getAttribute('aria-pressed');\n" aria = element.getAttribute("aria-pressed");
+ " if (aria !== null) return aria === 'true';\n" }
+ " const input = el.querySelector('input') || el.closest('div,li,section')?.querySelector('input');\n" if (aria != null) {
+ " if (input) return !!input.checked;\n" return "true".equals(aria);
+ " return null;\n" }
+ "}", optionText); Locator input = element.locator("input").first();
return Boolean.TRUE.equals(checked); if (input.count() > 0) {
return input.isChecked();
}
}
return false;
} }
private Locator getCourseCards() { private Locator getCourseCards() {
@@ -223,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;
@@ -271,18 +301,69 @@ public class CatalogPage extends BasePage {
} }
} }
private void waitForDurationFilterApplied(int minMonths, int maxMonths) { private boolean waitForDurationFilterApplied(int minMonths, int maxMonths) {
page.waitForFunction( boolean appliedOnce = false;
"([min, max]) => {\n" for (int i = 0; i < 40; i++) {
+ " const cards = Array.from(document.querySelectorAll(\"a[href*='/lessons/']\"));\n" List<Integer> durations = getVisibleCourseDurations();
+ " const visible = cards.filter(el => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length));\n" if (!durations.isEmpty()
+ " const durations = visible.map(el => {\n" && durations.stream().allMatch(v -> v >= minMonths && v <= maxMonths)) {
+ " const m = el.innerText.match(/(\\d+)\\s+месяц/);\n" return true;
+ " return m ? parseInt(m[1], 10) : null;\n" }
+ " }).filter(v => v !== null);\n" if (!appliedOnce) {
+ " if (!durations.length) return false;\n" clickApplyGlobalIfPresent();
+ " return durations.every(v => v >= min && v <= max);\n" appliedOnce = true;
+ "}", }
new Object[] {minMonths, maxMonths}); page.waitForTimeout(300);
}
return false;
}
private void waitForFiltersReset() {
for (int i = 0; i < 20; i++) {
if (isDefaultDirectionSelected() && getCourseCards().count() > 0) {
return;
}
page.waitForTimeout(300);
}
}
private void waitForSliderValues(Locator minSlider, Locator maxSlider, int minMonths, int maxMonths) {
for (int i = 0; i < 20; i++) {
String minValue = minSlider.getAttribute("aria-valuenow");
String maxValue = maxSlider.getAttribute("aria-valuenow");
if (minValue != null && maxValue != null) {
try {
int min = Integer.parseInt(minValue);
int max = Integer.parseInt(maxValue);
if (min == minMonths && max == maxMonths) {
return;
}
} catch (NumberFormatException ignored) {
// ignore and retry
}
}
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));
}
} }
} }

View File

@@ -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
} }

View File

@@ -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);

Binary file not shown.