From 8857b4feffda0d7b48cc41bb10cbe7661157f225 Mon Sep 17 00:00:00 2001 From: spawn Date: Thu, 9 Apr 2026 23:03:38 +0300 Subject: [PATCH] Refactor hw7 mobile tests to meet course requirements --- .gitignore | 5 + README.md | 167 +++++---------- checkstyle.xml | 30 +++ docker-compose.yml | 74 ++----- pom.xml | 93 +++++++-- spotbugs-exclude.xml | 18 ++ .../otus/mobile/annotations/MobileUser.java | 14 ++ .../component/AlertDialogComponent.java | 25 +++ .../mobile/component/BaseMobileComponent.java | 10 + .../component/BottomNavigationComponent.java | 19 ++ .../mobile/component/GiftFormComponent.java | 17 ++ .../component/WishlistFormComponent.java | 16 ++ .../ru/otus/mobile/config/MobileConfig.java | 61 ++++++ .../ru/otus/mobile/config/ProjectPaths.java | 22 ++ .../ru/otus/mobile/config/TestAccount.java | 83 ++++++++ .../ru/otus/mobile/config/TestContext.java | 33 +++ src/main/java/ru/otus/mobile/db/DbClient.java | 36 ++++ src/main/java/ru/otus/mobile/db/DbConfig.java | 25 +++ .../ru/otus/mobile/driver/EmulatorQueue.java | 29 +++ .../otus/mobile/driver/LogcatCollector.java | 64 ++++++ .../mobile/driver/MobileDriverFactory.java | 29 +++ .../ru/otus/mobile/driver/MobileSession.java | 46 +++++ .../mobile/driver/MobileSessionFactory.java | 37 ++++ .../mobile/extensions/MobileExtension.java | 58 ++++++ .../java/ru/otus/mobile/guice/CoreModule.java | 93 +++++++++ .../ru/otus/mobile/guice/SessionModule.java | 30 +++ .../java/ru/otus/mobile/page/AbsBasePage.java | 19 ++ .../ru/otus/mobile/page/AbsPageObject.java | 103 ++++++++++ .../java/ru/otus/mobile/page/GiftsPage.java | 36 ++++ .../java/ru/otus/mobile/page/LoginPage.java | 30 +++ .../java/ru/otus/mobile/page/UsersPage.java | 43 ++++ .../ru/otus/mobile/page/WishlistsPage.java | 61 ++++++ .../java/ru/otus/mobile/util/TestData.java | 9 + .../java/ru/otus/mobile/config/ApkInfo.java | 147 ------------- .../ru/otus/mobile/config/AuthContext.java | 28 --- .../ru/otus/mobile/config/MobileConfig.java | 118 ----------- .../ru/otus/mobile/config/MobileContext.java | 16 -- src/test/java/ru/otus/mobile/db/DbClient.java | 105 ---------- src/test/java/ru/otus/mobile/db/DbConfig.java | 85 -------- .../mobile/driver/MobileDriverFactory.java | 103 ---------- .../ru/otus/mobile/screens/GiftsScreen.java | 77 ------- .../ru/otus/mobile/screens/LoginScreen.java | 194 ------------------ .../ru/otus/mobile/screens/UsersScreen.java | 180 ---------------- .../otus/mobile/screens/WishlistsScreen.java | 155 -------------- .../ru/otus/mobile/tests/BaseMobileTest.java | 132 ------------ .../java/ru/otus/mobile/tests/GiftsTest.java | 49 +++-- .../ru/otus/mobile/tests/ReservationTest.java | 40 ++-- .../ru/otus/mobile/tests/WishlistsTest.java | 36 ++-- .../ru/otus/mobile/ui/MobileSelectors.java | 23 --- src/test/java/ru/otus/mobile/ui/MobileUi.java | 96 --------- src/test/java/ru/otus/mobile/ui/UiWait.java | 14 -- .../java/ru/otus/mobile/util/TestAccount.java | 77 ------- .../java/ru/otus/mobile/util/TestData.java | 19 -- wiremock/mappings/wishlist-apk.json | 13 ++ 54 files changed, 1337 insertions(+), 1805 deletions(-) create mode 100644 checkstyle.xml create mode 100644 spotbugs-exclude.xml create mode 100644 src/main/java/ru/otus/mobile/annotations/MobileUser.java create mode 100644 src/main/java/ru/otus/mobile/component/AlertDialogComponent.java create mode 100644 src/main/java/ru/otus/mobile/component/BaseMobileComponent.java create mode 100644 src/main/java/ru/otus/mobile/component/BottomNavigationComponent.java create mode 100644 src/main/java/ru/otus/mobile/component/GiftFormComponent.java create mode 100644 src/main/java/ru/otus/mobile/component/WishlistFormComponent.java create mode 100644 src/main/java/ru/otus/mobile/config/MobileConfig.java create mode 100644 src/main/java/ru/otus/mobile/config/ProjectPaths.java create mode 100644 src/main/java/ru/otus/mobile/config/TestAccount.java create mode 100644 src/main/java/ru/otus/mobile/config/TestContext.java create mode 100644 src/main/java/ru/otus/mobile/db/DbClient.java create mode 100644 src/main/java/ru/otus/mobile/db/DbConfig.java create mode 100644 src/main/java/ru/otus/mobile/driver/EmulatorQueue.java create mode 100644 src/main/java/ru/otus/mobile/driver/LogcatCollector.java create mode 100644 src/main/java/ru/otus/mobile/driver/MobileDriverFactory.java create mode 100644 src/main/java/ru/otus/mobile/driver/MobileSession.java create mode 100644 src/main/java/ru/otus/mobile/driver/MobileSessionFactory.java create mode 100644 src/main/java/ru/otus/mobile/extensions/MobileExtension.java create mode 100644 src/main/java/ru/otus/mobile/guice/CoreModule.java create mode 100644 src/main/java/ru/otus/mobile/guice/SessionModule.java create mode 100644 src/main/java/ru/otus/mobile/page/AbsBasePage.java create mode 100644 src/main/java/ru/otus/mobile/page/AbsPageObject.java create mode 100644 src/main/java/ru/otus/mobile/page/GiftsPage.java create mode 100644 src/main/java/ru/otus/mobile/page/LoginPage.java create mode 100644 src/main/java/ru/otus/mobile/page/UsersPage.java create mode 100644 src/main/java/ru/otus/mobile/page/WishlistsPage.java create mode 100644 src/main/java/ru/otus/mobile/util/TestData.java delete mode 100644 src/test/java/ru/otus/mobile/config/ApkInfo.java delete mode 100644 src/test/java/ru/otus/mobile/config/AuthContext.java delete mode 100644 src/test/java/ru/otus/mobile/config/MobileConfig.java delete mode 100644 src/test/java/ru/otus/mobile/config/MobileContext.java delete mode 100644 src/test/java/ru/otus/mobile/db/DbClient.java delete mode 100644 src/test/java/ru/otus/mobile/db/DbConfig.java delete mode 100644 src/test/java/ru/otus/mobile/driver/MobileDriverFactory.java delete mode 100644 src/test/java/ru/otus/mobile/screens/GiftsScreen.java delete mode 100644 src/test/java/ru/otus/mobile/screens/LoginScreen.java delete mode 100644 src/test/java/ru/otus/mobile/screens/UsersScreen.java delete mode 100644 src/test/java/ru/otus/mobile/screens/WishlistsScreen.java delete mode 100644 src/test/java/ru/otus/mobile/tests/BaseMobileTest.java delete mode 100644 src/test/java/ru/otus/mobile/ui/MobileSelectors.java delete mode 100644 src/test/java/ru/otus/mobile/ui/MobileUi.java delete mode 100644 src/test/java/ru/otus/mobile/ui/UiWait.java delete mode 100644 src/test/java/ru/otus/mobile/util/TestAccount.java delete mode 100644 src/test/java/ru/otus/mobile/util/TestData.java create mode 100644 wiremock/mappings/wishlist-apk.json diff --git a/.gitignore b/.gitignore index a2bb283..cb2d5cd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,9 @@ build/ .idea/ *.iml .DS_Store +allure-results/ logcat.txt +apk/ +jobs/ +scripts/ +wiremock/__files/ diff --git a/README.md b/README.md index 9aa2ac0..85abd9c 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,68 @@ -# OTUS Homework 7: Mobile (Selenide + Appium) +# OTUS Homework 7: Mobile Testing -Автотесты мобильного приложения Wishlist через Selenide + Appium и инфраструктура эмулятора через Docker Compose. +Проект содержит мобильные UI-тесты для приложения Wishlist на базе `selenide-appium`. -## Что реализовано -- 3 сценария из ТЗ: списки желаний, подарки, резервирование подарка другого пользователя. -- Архитектура: базовый тест + экранные объекты (screens). -- Конфигурируемые capabilities через system properties. -- Подготовка тестовых данных через БД перед каждым тестом. -- Скрипты для сбора Logcat. +Реализованы сценарии: +- создание и редактирование списка желаний; +- создание и редактирование подарка; +- изменение статуса резервирования подарка другого пользователя. -## Стек и версии -- Java 21, Maven. -- Selenide: 7.3.1 -- Appium Java Client: 9.3.0 (UiAutomator2) -- Selenium: 4.25.0 -- JUnit: 5.10.2 -- PostgreSQL JDBC: 42.7.10 -- apk-parser: 2.6.10 -- SLF4J: 2.0.13 -- Docker Compose (эмулятор Android + Appium) +## Архитектура +- `Guice` для DI. +- `JUnit 5 Extension` вместо базового тестового класса. +- `AbsPageObject` -> `AbsBasePage` / `BaseMobileComponent`. +- `BlockingQueue` для распределения тестов по эмуляторам. +- код инфраструктуры и page object находится в `src/main/java`; +- в `src/test/java` находятся только тестовые классы. -## Структура проекта -- `src/test/java/ru/otus/mobile/tests` — тесты. -- `src/test/java/ru/otus/mobile/screens` — экранные объекты. -- `src/test/java/ru/otus/mobile/config` — конфигурация Appium/Android. -- `src/test/java/ru/otus/mobile/driver` — фабрика драйвера. -- `src/test/java/ru/otus/mobile/db` — reset SQL и JDBC-клиент. -- `docker-compose.yml` — эмулятор Android с Appium. -- `scripts/` — утилиты (Logcat). +## Инфраструктура +`docker-compose.yml` поднимает: +- `wiremock` для раздачи `wishlist.apk`; +- `android-emulator` с Appium и VNC. -## Инфраструктура (Docker Compose) -Эмулятор и Appium поднимаются одной командой: -```bash -docker compose up -d -``` +Приложение не маунтится в эмулятор и не ставится через ADB. +APK скачивается Appium по capability `app`. +Источник APK для Wiremock: файл `wishlist-349317-5fd795.apk` в корне проекта. +Эмулятор запускается без `privileged`, доступ к аппаратной виртуализации передается через `/dev/kvm`. -Порты: -- Appium: `http://localhost:4723` -- noVNC: `http://localhost:6080` +## Тестовые аккаунты +Тесты используют заранее созданные аккаунты: +- `user1us / user1us` +- `user2us / user2us` +- `user3us / user3us` +- `user4us / user4us` -APK берется из файла `./wishlist-349317-5fd795.apk`, копируется в `./apk/wishlist.apk` и монтируется в контейнер по пути `/apk/wishlist.apk`. -После старта контейнеров APK автоматически устанавливается в эмулятор и приложение стартует. +`user4us` используется как владелец подарка в тесте резервирования. -## Подготовка данных через БД -Для повторяемости тестов перед каждым тестом выполняется SQL‑сброс данных аккаунта. -Данные доступа к БД **не хранятся в репозитории** — нужно передать через env или `-D`. -Аккаунты можно создать заранее (регистрация через приложение) или доверить это автотестам. -Если логин/пароль не переданы через параметры запуска, тест сгенерирует логины, создаст пользователей через reset‑SQL в БД (если их нет), затем выполнит логин. Регистрация через приложение используется как fallback, если вход не удался. +## Подготовка +Нужно задать доступ к БД, иначе `mvn test` завершится ошибкой: -Переменные/свойства: -- `DB_URL` или `-Ddb.url` -- `DB_USER` или `-Ddb.user` -- `DB_PASSWORD` или `-Ddb.password` -- `DB_RESET_SQL` или `-Ddb.reset.sql` — SQL c параметрами `?` (username, email, password_hash, username). Если не задано, используется встроенный reset‑SQL (удаляет wishlists/gifts и создает пользователя при отсутствии). - -Пример (PowerShell, значения подставить свои): -```powershell -$env:DB_URL = "jdbc:postgresql://:5432/" -$env:DB_USER = "" -$env:DB_PASSWORD = "" -$env:DB_RESET_SQL = "" -``` - -## Как запускать -### Полный прогон (обязательная последовательность) -1) Поднять инфраструктуру: -```bash -docker compose up -d -``` -Убедиться, что Docker Desktop/Engine запущен. Если Appium недоступен, тесты падают быстро с понятной ошибкой. - -2) Задать доступ к БД (обязательно, иначе тесты упадут): ```powershell $env:DB_URL="jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist" $env:DB_USER="student" $env:DB_PASSWORD="student" ``` -3) Запустить все тесты (логины/пароли можно не задавать): +## Запуск +1. Поднять окружение: + +```bash +docker compose up -d +``` + +2. Дождаться статуса `healthy` у `wiremock` и `android-emulator`: + +```bash +docker compose ps +``` + +Тесты не ждут загрузку эмулятора сами. Готовность окружения проверяется на уровне Docker Compose. + +3. Запустить тесты: + ```bash mvn test ``` -Если логины не заданы, они будут сгенерированы автоматически, пользователь будет создан через БД (если отсутствует) и выполнится вход. -Регистрация через приложение используется только как fallback, если вход не удался. - -Опционально: задать логины/пароли тестовых пользователей (если зарегистрированы вручную): -```bash -mvn "-Dlogin.username.wishlists=" "-Dlogin.password.wishlists=" \ - "-Dlogin.username.gifts=" "-Dlogin.password.gifts=" \ - "-Dlogin.username.reservation=" "-Dlogin.password.reservation=" \ - "-Dreservation.owner=" \ - test -``` - -### Один тест -```bash -mvn "-Dtest=ru.otus.mobile.tests.WishlistsTest" test -``` - -### Запуск через Docker Compose -```bash -mvn -Dappium.url=http://localhost:4723 -Dapp.path=/apk/wishlist.apk -Dapp.package=ru.otus.wishlist -Dapp.activity=ru.otus.wishlist.MainActivity test -``` - -### Полный пример запуска (Appium + БД) -```bash -mvn -Dappium.url=http://localhost:4723 -Dapp.path=/apk/wishlist.apk -Dapp.package=ru.otus.wishlist -Dapp.activity=ru.otus.wishlist.MainActivity -Ddb.url=jdbc:postgresql://:5432/ -Ddb.user= -Ddb.password= test -``` - -## Параметры запуска -- `-Dappium.url` — URL Appium (default: `http://localhost:4723`) -- `-Dapp.path` — путь до APK (default: `/apk/wishlist.apk`) -- `-Dapp.package` — package приложения (если APK недоступен локально) -- `-Dapp.activity` — main activity (если APK недоступен локально) -- `-Ddevice.name` — имя устройства (default: `Android Emulator`) -- `-Dplatform.version` — версия Android (опционально) -- `-Dudid` — UDID (опционально) -- `-DnoReset` — не сбрасывать состояние (default: `false`) -- `-DnewCommandTimeout` — timeout Appium, сек (default: `120`) -- `-Dlogin.username.` — логин для конкретного теста (например `login.username.wishlists`) -- `-Dlogin.password` — пароль (default: `Admin123`) -- `-Dlogin.password.hash` — bcrypt‑хэш пароля для reset‑SQL (default в коде) - -## Сбор Logcat -PowerShell: -```powershell -.\scripts\collect-logcat.ps1 -``` - -Bash: -```bash -./scripts/collect-logcat.sh -``` - -Логи сохраняются в файл `logcat.txt` в корне проекта. +## Логи +После выполнения тестов logcat сохраняется в файл `logcat.txt` в корне проекта через Selenium/Appium logs API. diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..a5523fb --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml index c8f499b..f0e14df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,67 +1,35 @@ services: - apk-downloader: - image: curlimages/curl:8.5.0 - user: "0:0" - environment: - - APP_URL=${APP_URL:-} - command: - - sh - - -c - - > - if [ -n "$APP_URL" ]; then - curl -L "$APP_URL" -o /apk/wishlist.apk; - else - cp /apk-source/app.apk /apk/wishlist.apk; - fi + wiremock: + image: wiremock/wiremock:3.9.1 volumes: - - ./apk:/apk - - ./wishlist-349317-5fd795.apk:/apk-source/app.apk:ro + - ./wiremock:/home/wiremock + - ./wishlist-349317-5fd795.apk:/home/wiremock/__files/wishlist.apk:ro + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/__admin/health | grep -q 'healthy'"] + interval: 10s + timeout: 5s + retries: 12 + android-emulator: image: budtmo/docker-android:emulator_13.0 - privileged: true depends_on: - apk-downloader: - condition: service_completed_successfully + - wiremock + devices: + - /dev/kvm:/dev/kvm ports: - "4723:4723" - "6080:6080" - - "5554:5554" - - "5555:5555" environment: - DEVICE=Pixel_5 - APPIUM=true - WEB_VNC=true - - AUTO_GRANT_PERMISSIONS=true - ENABLE_VNC=true - - EMULATOR_PARAMS=-no-window -no-audio -gpu swiftshader_indirect -no-snapshot -no-boot-anim -accel off + - AUTO_GRANT_PERMISSIONS=true + - EMULATOR_PARAMS=-no-window -no-audio -gpu swiftshader_indirect -no-snapshot -no-boot-anim shm_size: 2gb - volumes: - - ./apk:/apk:ro - apk-installer: - image: budtmo/docker-android:emulator_13.0 - depends_on: - android-emulator: - condition: service_started - entrypoint: - - /bin/bash - - -lc - command: - - > - for i in $(seq 1 60); do - adb connect android-emulator:5555 || true; - if adb devices | tr -s ' ' | grep -q "android-emulator:5555"; then - break; - fi; - sleep 2; - done; - adb -s android-emulator:5555 wait-for-device; - for i in $(seq 1 120); do - if adb -s android-emulator:5555 shell getprop sys.boot_completed | tr -d '\r' | grep -q "1"; then - break; - fi; - sleep 2; - done; - adb -s android-emulator:5555 install -r /apk/wishlist.apk && - adb -s android-emulator:5555 shell monkey -p ru.otus.wishlist -c android.intent.category.LAUNCHER 1 - volumes: - - ./apk:/apk:ro + healthcheck: + test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ]"] + interval: 15s + timeout: 5s + retries: 40 + start_period: 30s diff --git a/pom.xml b/pom.xml index ea929fd..ffd4b27 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,8 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 + https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 ru.otus @@ -13,12 +14,18 @@ 21 UTF-8 7.3.1 - 9.3.0 + 7.3.1 5.10.2 - 2.6.10 2.0.13 - 4.25.0 + 1.5.6 + 4.20.0 2.29.1 + 7.0.0 + 3.13.0 + 3.2.5 + 3.6.0 + 4.9.2.0 + 4.9.3 @@ -40,18 +47,23 @@ ${selenide.version} - io.appium - java-client - ${appium.version} + com.codeborne + selenide-appium + ${selenide.appium.version} - net.dongliu - apk-parser - ${apkparser.version} + com.google.inject + guice + ${guice.version} org.junit.jupiter - junit-jupiter + junit-jupiter-api + ${junit.version} + + + org.junit.jupiter + junit-jupiter-engine ${junit.version} test @@ -63,8 +75,13 @@ org.slf4j - slf4j-simple + slf4j-api ${slf4j.version} + + + ch.qos.logback + logback-classic + ${logback.version} test @@ -77,14 +94,64 @@ + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.plugin.version} + + ${maven.compiler.source} + + org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + ${surefire.version} false + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.plugin.version} + + + checkstyle-validation + verify + + check + + + + + checkstyle.xml + true + true + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs.plugin.version} + + + com.github.spotbugs + spotbugs + ${spotbugs.version} + + + + + verify + + check + + + + + spotbugs-exclude.xml + + diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml new file mode 100644 index 0000000..471eb60 --- /dev/null +++ b/spotbugs-exclude.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/ru/otus/mobile/annotations/MobileUser.java b/src/main/java/ru/otus/mobile/annotations/MobileUser.java new file mode 100644 index 0000000..48e2371 --- /dev/null +++ b/src/main/java/ru/otus/mobile/annotations/MobileUser.java @@ -0,0 +1,14 @@ +package ru.otus.mobile.annotations; + +import ru.otus.mobile.config.TestAccount; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface MobileUser { + TestAccount value(); +} diff --git a/src/main/java/ru/otus/mobile/component/AlertDialogComponent.java b/src/main/java/ru/otus/mobile/component/AlertDialogComponent.java new file mode 100644 index 0000000..6d55f54 --- /dev/null +++ b/src/main/java/ru/otus/mobile/component/AlertDialogComponent.java @@ -0,0 +1,25 @@ +package ru.otus.mobile.component; + +import com.google.inject.Inject; +import ru.otus.mobile.config.MobileConfig; + +public final class AlertDialogComponent extends BaseMobileComponent { + @Inject + public AlertDialogComponent(MobileConfig config) { + super(config); + } + + public void acceptIfVisible() { + if (exists("android:id/button1")) { + tap("android:id/button1"); + return; + } + if (byText("OK").exists()) { + byText("OK").click(); + return; + } + if (byText("ОК").exists()) { + byText("ОК").click(); + } + } +} diff --git a/src/main/java/ru/otus/mobile/component/BaseMobileComponent.java b/src/main/java/ru/otus/mobile/component/BaseMobileComponent.java new file mode 100644 index 0000000..4b108cf --- /dev/null +++ b/src/main/java/ru/otus/mobile/component/BaseMobileComponent.java @@ -0,0 +1,10 @@ +package ru.otus.mobile.component; + +import ru.otus.mobile.config.MobileConfig; +import ru.otus.mobile.page.AbsPageObject; + +public abstract class BaseMobileComponent extends AbsPageObject { + protected BaseMobileComponent(MobileConfig config) { + super(config); + } +} diff --git a/src/main/java/ru/otus/mobile/component/BottomNavigationComponent.java b/src/main/java/ru/otus/mobile/component/BottomNavigationComponent.java new file mode 100644 index 0000000..de0e9a5 --- /dev/null +++ b/src/main/java/ru/otus/mobile/component/BottomNavigationComponent.java @@ -0,0 +1,19 @@ +package ru.otus.mobile.component; + +import com.google.inject.Inject; +import ru.otus.mobile.config.MobileConfig; + +public final class BottomNavigationComponent extends BaseMobileComponent { + @Inject + public BottomNavigationComponent(MobileConfig config) { + super(config); + } + + public void openWishlists() { + tap("mine_menu"); + } + + public void openUsers() { + tap("users_menu"); + } +} diff --git a/src/main/java/ru/otus/mobile/component/GiftFormComponent.java b/src/main/java/ru/otus/mobile/component/GiftFormComponent.java new file mode 100644 index 0000000..411779d --- /dev/null +++ b/src/main/java/ru/otus/mobile/component/GiftFormComponent.java @@ -0,0 +1,17 @@ +package ru.otus.mobile.component; + +import com.google.inject.Inject; +import ru.otus.mobile.config.MobileConfig; + +public final class GiftFormComponent extends BaseMobileComponent { + @Inject + public GiftFormComponent(MobileConfig config) { + super(config); + } + + public void save(String name, String price) { + type("name_input", name); + type("price_input", price); + tap("save_button"); + } +} diff --git a/src/main/java/ru/otus/mobile/component/WishlistFormComponent.java b/src/main/java/ru/otus/mobile/component/WishlistFormComponent.java new file mode 100644 index 0000000..62624f6 --- /dev/null +++ b/src/main/java/ru/otus/mobile/component/WishlistFormComponent.java @@ -0,0 +1,16 @@ +package ru.otus.mobile.component; + +import com.google.inject.Inject; +import ru.otus.mobile.config.MobileConfig; + +public final class WishlistFormComponent extends BaseMobileComponent { + @Inject + public WishlistFormComponent(MobileConfig config) { + super(config); + } + + public void save(String title) { + type("title_input", title); + tap("save_button"); + } +} diff --git a/src/main/java/ru/otus/mobile/config/MobileConfig.java b/src/main/java/ru/otus/mobile/config/MobileConfig.java new file mode 100644 index 0000000..ea89a14 --- /dev/null +++ b/src/main/java/ru/otus/mobile/config/MobileConfig.java @@ -0,0 +1,61 @@ +package ru.otus.mobile.config; + +import java.util.ArrayList; +import java.util.List; + +public final class MobileConfig { + private final String appPackage; + private final String appUrl; + private final String reservationOwnerUsername; + private final List emulators; + + public MobileConfig( + String appPackage, + String appUrl, + String reservationOwnerUsername, + List emulators + ) { + this.appPackage = appPackage; + this.appUrl = appUrl; + this.reservationOwnerUsername = reservationOwnerUsername; + this.emulators = List.copyOf(emulators); + } + + public String appPackage() { + return appPackage; + } + + public String appUrl() { + return appUrl; + } + + public String reservationOwnerUsername() { + return reservationOwnerUsername; + } + + public List emulators() { + return emulators; + } + + public record Emulator(String id, String appiumUrl, String deviceName, String appUrl) { + public static List parse(String rawValue, String defaultAppUrl) { + List emulators = new ArrayList<>(); + for (String entry : rawValue.split(",")) { + String trimmed = entry.trim(); + if (trimmed.isEmpty()) { + continue; + } + String[] parts = trimmed.split("\\|"); + String id = parts.length > 0 ? parts[0].trim() : "emulator-1"; + String appiumUrl = parts.length > 1 ? parts[1].trim() : "http://localhost:4723"; + String deviceName = parts.length > 2 ? parts[2].trim() : "Android Emulator"; + String emulatorAppUrl = parts.length > 3 ? parts[3].trim() : defaultAppUrl; + emulators.add(new Emulator(id, appiumUrl, deviceName, emulatorAppUrl)); + } + if (emulators.isEmpty()) { + emulators.add(new Emulator("emulator-1", "http://localhost:4723", "Android Emulator", defaultAppUrl)); + } + return emulators; + } + } +} diff --git a/src/main/java/ru/otus/mobile/config/ProjectPaths.java b/src/main/java/ru/otus/mobile/config/ProjectPaths.java new file mode 100644 index 0000000..a42b04c --- /dev/null +++ b/src/main/java/ru/otus/mobile/config/ProjectPaths.java @@ -0,0 +1,22 @@ +package ru.otus.mobile.config; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public final class ProjectPaths { + private final Path projectRoot; + private final Path logcatFile; + + public ProjectPaths() { + this.projectRoot = Paths.get("").toAbsolutePath().normalize(); + this.logcatFile = projectRoot.resolve("logcat.txt"); + } + + public Path projectRoot() { + return projectRoot; + } + + public Path logcatFile() { + return logcatFile; + } +} diff --git a/src/main/java/ru/otus/mobile/config/TestAccount.java b/src/main/java/ru/otus/mobile/config/TestAccount.java new file mode 100644 index 0000000..8570952 --- /dev/null +++ b/src/main/java/ru/otus/mobile/config/TestAccount.java @@ -0,0 +1,83 @@ +package ru.otus.mobile.config; + +public enum TestAccount { + WISHLISTS("user1us", "user1us", """ + WITH target_user AS ( + SELECT id FROM users WHERE username = ? + ), + delete_gifts AS ( + DELETE FROM gifts + WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM target_user)) + ), + delete_wishlists AS ( + DELETE FROM wishlists WHERE user_id IN (SELECT id FROM target_user) + ) + SELECT 1; + """), + GIFTS("user2us", "user2us", """ + WITH target_user AS ( + SELECT id FROM users WHERE username = ? + ), + delete_gifts AS ( + DELETE FROM gifts + WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM target_user)) + ), + delete_wishlists AS ( + DELETE FROM wishlists WHERE user_id IN (SELECT id FROM target_user) + ) + SELECT 1; + """), + RESERVATION("user3us", "user3us", """ + WITH owner_user AS ( + SELECT id FROM users WHERE username = ? + ), + target_user AS ( + SELECT id FROM users WHERE username = ? + ), + delete_owner_gifts AS ( + DELETE FROM gifts + WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM owner_user)) + ), + delete_owner_wishlists AS ( + DELETE FROM wishlists WHERE user_id IN (SELECT id FROM owner_user) + ), + delete_target_gifts AS ( + DELETE FROM gifts + WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM target_user)) + ), + delete_target_wishlists AS ( + DELETE FROM wishlists WHERE user_id IN (SELECT id FROM target_user) + ), + owner_wishlist AS ( + INSERT INTO wishlists (id, title, description, user_id) + SELECT gen_random_uuid(), 'Owner Wishlist', 'Prepared for reservation test', id + FROM owner_user + RETURNING id + ) + INSERT INTO gifts (id, name, description, price, is_reserved, store_url, image_url, wish_id) + SELECT gen_random_uuid(), 'Owner Gift', 'Prepared for reservation test', 100, false, NULL, NULL, id + FROM owner_wishlist; + """); + + private final String username; + private final String password; + private final String resetSql; + + TestAccount(String username, String password, String resetSql) { + this.username = username; + this.password = password; + this.resetSql = resetSql; + } + + public String username() { + return username; + } + + public String password() { + return password; + } + + public String resetSql() { + return resetSql; + } +} diff --git a/src/main/java/ru/otus/mobile/config/TestContext.java b/src/main/java/ru/otus/mobile/config/TestContext.java new file mode 100644 index 0000000..bbbe1a6 --- /dev/null +++ b/src/main/java/ru/otus/mobile/config/TestContext.java @@ -0,0 +1,33 @@ +package ru.otus.mobile.config; + +import io.appium.java_client.android.AndroidDriver; + +public final class TestContext { + private final TestAccount account; + private final MobileConfig.Emulator emulator; + private final AndroidDriver driver; + private final String testName; + + public TestContext(TestAccount account, MobileConfig.Emulator emulator, AndroidDriver driver, String testName) { + this.account = account; + this.emulator = emulator; + this.driver = driver; + this.testName = testName; + } + + public TestAccount account() { + return account; + } + + public MobileConfig.Emulator emulator() { + return emulator; + } + + public AndroidDriver driver() { + return driver; + } + + public String testName() { + return testName; + } +} diff --git a/src/main/java/ru/otus/mobile/db/DbClient.java b/src/main/java/ru/otus/mobile/db/DbClient.java new file mode 100644 index 0000000..eed3575 --- /dev/null +++ b/src/main/java/ru/otus/mobile/db/DbClient.java @@ -0,0 +1,36 @@ +package ru.otus.mobile.db; + +import com.google.inject.Inject; +import ru.otus.mobile.config.MobileConfig; +import ru.otus.mobile.config.TestAccount; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public final class DbClient { + private final DbConfig config; + private final MobileConfig mobileConfig; + + @Inject + public DbClient(DbConfig config, MobileConfig mobileConfig) { + this.config = config; + this.mobileConfig = mobileConfig; + } + + public void resetData(TestAccount account) { + try (Connection connection = DriverManager.getConnection(config.url(), config.user(), config.password()); + PreparedStatement statement = connection.prepareStatement(account.resetSql())) { + if (account == TestAccount.RESERVATION) { + statement.setString(1, mobileConfig.reservationOwnerUsername()); + statement.setString(2, account.username()); + } else { + statement.setString(1, account.username()); + } + statement.execute(); + } catch (SQLException e) { + throw new IllegalStateException("Failed to prepare test data for account " + account.name(), e); + } + } +} diff --git a/src/main/java/ru/otus/mobile/db/DbConfig.java b/src/main/java/ru/otus/mobile/db/DbConfig.java new file mode 100644 index 0000000..773d1f4 --- /dev/null +++ b/src/main/java/ru/otus/mobile/db/DbConfig.java @@ -0,0 +1,25 @@ +package ru.otus.mobile.db; + +public final class DbConfig { + private final String url; + private final String user; + private final String password; + + public DbConfig(String url, String user, String password) { + this.url = url; + this.user = user; + this.password = password; + } + + public String url() { + return url; + } + + public String user() { + return user; + } + + public String password() { + return password; + } +} diff --git a/src/main/java/ru/otus/mobile/driver/EmulatorQueue.java b/src/main/java/ru/otus/mobile/driver/EmulatorQueue.java new file mode 100644 index 0000000..ce1aa51 --- /dev/null +++ b/src/main/java/ru/otus/mobile/driver/EmulatorQueue.java @@ -0,0 +1,29 @@ +package ru.otus.mobile.driver; + +import ru.otus.mobile.config.MobileConfig; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +public final class EmulatorQueue { + private final BlockingQueue queue; + + public EmulatorQueue(MobileConfig config) { + this.queue = new LinkedBlockingQueue<>(config.emulators()); + } + + public MobileConfig.Emulator acquire() { + try { + return queue.take(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while waiting for a free emulator.", e); + } + } + + public void release(MobileConfig.Emulator emulator) { + if (!queue.offer(emulator)) { + throw new IllegalStateException("Failed to return emulator back to the queue: " + emulator.id()); + } + } +} diff --git a/src/main/java/ru/otus/mobile/driver/LogcatCollector.java b/src/main/java/ru/otus/mobile/driver/LogcatCollector.java new file mode 100644 index 0000000..4d2b3b9 --- /dev/null +++ b/src/main/java/ru/otus/mobile/driver/LogcatCollector.java @@ -0,0 +1,64 @@ +package ru.otus.mobile.driver; + +import io.appium.java_client.android.AndroidDriver; +import org.openqa.selenium.logging.LogEntries; +import org.openqa.selenium.logging.LogEntry; +import ru.otus.mobile.config.ProjectPaths; +import ru.otus.mobile.config.TestContext; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.time.Instant; + +public final class LogcatCollector { + private final ProjectPaths projectPaths; + + public LogcatCollector(ProjectPaths projectPaths) { + this.projectPaths = projectPaths; + } + + public void save(TestContext context) { + AndroidDriver driver = context.driver(); + if (driver == null) { + return; + } + try { + LogEntries entries = driver.manage().logs().get("logcat"); + StringBuilder buffer = new StringBuilder(); + buffer.append("=== ").append(context.testName()) + .append(" / ").append(context.emulator().id()) + .append(" / ").append(Instant.now()) + .append(System.lineSeparator()); + for (LogEntry entry : entries) { + buffer.append(entry.getTimestamp()) + .append(' ') + .append(entry.getLevel()) + .append(' ') + .append(entry.getMessage()) + .append(System.lineSeparator()); + } + Files.writeString( + projectPaths.logcatFile(), + buffer.append(System.lineSeparator()).toString(), + StandardOpenOption.CREATE, + StandardOpenOption.APPEND + ); + } catch (Exception e) { + writeFallback(context.testName(), e); + } + } + + private void writeFallback(String testName, Exception error) { + try { + Files.writeString( + projectPaths.logcatFile(), + "Failed to collect logcat for " + testName + ": " + error.getMessage() + System.lineSeparator(), + StandardOpenOption.CREATE, + StandardOpenOption.APPEND + ); + } catch (IOException ignored) { + // no-op + } + } +} diff --git a/src/main/java/ru/otus/mobile/driver/MobileDriverFactory.java b/src/main/java/ru/otus/mobile/driver/MobileDriverFactory.java new file mode 100644 index 0000000..72928ea --- /dev/null +++ b/src/main/java/ru/otus/mobile/driver/MobileDriverFactory.java @@ -0,0 +1,29 @@ +package ru.otus.mobile.driver; + +import io.appium.java_client.android.AndroidDriver; +import io.appium.java_client.android.options.UiAutomator2Options; +import ru.otus.mobile.config.MobileConfig; + +import java.net.MalformedURLException; +import java.net.URI; + +public final class MobileDriverFactory { + private final MobileConfig config; + + public MobileDriverFactory(MobileConfig config) { + this.config = config; + } + + public AndroidDriver create(MobileConfig.Emulator emulator) { + UiAutomator2Options options = new UiAutomator2Options() + .setPlatformName("Android") + .setAutomationName("UiAutomator2") + .setDeviceName(emulator.deviceName()) + .setApp(emulator.appUrl()); + try { + return new AndroidDriver(URI.create(emulator.appiumUrl()).toURL(), options); + } catch (MalformedURLException e) { + throw new IllegalStateException("Invalid Appium URL: " + emulator.appiumUrl(), e); + } + } +} diff --git a/src/main/java/ru/otus/mobile/driver/MobileSession.java b/src/main/java/ru/otus/mobile/driver/MobileSession.java new file mode 100644 index 0000000..bebdb5c --- /dev/null +++ b/src/main/java/ru/otus/mobile/driver/MobileSession.java @@ -0,0 +1,46 @@ +package ru.otus.mobile.driver; + +import com.codeborne.selenide.WebDriverRunner; +import com.google.inject.Injector; +import io.appium.java_client.android.AndroidDriver; +import ru.otus.mobile.config.TestContext; + +public final class MobileSession { + private final TestContext context; + private final Injector injector; + private final EmulatorQueue emulatorQueue; + private final LogcatCollector logcatCollector; + + public MobileSession( + TestContext context, + Injector injector, + EmulatorQueue emulatorQueue, + LogcatCollector logcatCollector + ) { + this.context = context; + this.injector = injector; + this.emulatorQueue = emulatorQueue; + this.logcatCollector = logcatCollector; + } + + public void activate() { + WebDriverRunner.setWebDriver(context.driver()); + } + + public void injectMembers(Object target) { + injector.injectMembers(target); + } + + public void close() { + logcatCollector.save(context); + AndroidDriver driver = context.driver(); + try { + if (driver != null) { + driver.quit(); + } + } finally { + WebDriverRunner.closeWebDriver(); + emulatorQueue.release(context.emulator()); + } + } +} diff --git a/src/main/java/ru/otus/mobile/driver/MobileSessionFactory.java b/src/main/java/ru/otus/mobile/driver/MobileSessionFactory.java new file mode 100644 index 0000000..cea14be --- /dev/null +++ b/src/main/java/ru/otus/mobile/driver/MobileSessionFactory.java @@ -0,0 +1,37 @@ +package ru.otus.mobile.driver; + +import com.google.inject.Inject; +import com.google.inject.Injector; +import io.appium.java_client.android.AndroidDriver; +import ru.otus.mobile.config.MobileConfig; +import ru.otus.mobile.config.TestAccount; +import ru.otus.mobile.config.TestContext; +import ru.otus.mobile.guice.SessionModule; + +public final class MobileSessionFactory { + private final Injector rootInjector; + private final EmulatorQueue emulatorQueue; + private final MobileDriverFactory driverFactory; + private final LogcatCollector logcatCollector; + + @Inject + public MobileSessionFactory( + Injector rootInjector, + EmulatorQueue emulatorQueue, + MobileDriverFactory driverFactory, + LogcatCollector logcatCollector + ) { + this.rootInjector = rootInjector; + this.emulatorQueue = emulatorQueue; + this.driverFactory = driverFactory; + this.logcatCollector = logcatCollector; + } + + public MobileSession create(TestAccount account, String testName) { + MobileConfig.Emulator emulator = emulatorQueue.acquire(); + AndroidDriver driver = driverFactory.create(emulator); + TestContext context = new TestContext(account, emulator, driver, testName); + Injector childInjector = rootInjector.createChildInjector(new SessionModule(context)); + return new MobileSession(context, childInjector, emulatorQueue, logcatCollector); + } +} diff --git a/src/main/java/ru/otus/mobile/extensions/MobileExtension.java b/src/main/java/ru/otus/mobile/extensions/MobileExtension.java new file mode 100644 index 0000000..75879ae --- /dev/null +++ b/src/main/java/ru/otus/mobile/extensions/MobileExtension.java @@ -0,0 +1,58 @@ +package ru.otus.mobile.extensions; + +import com.google.inject.Guice; +import com.google.inject.Injector; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import ru.otus.mobile.annotations.MobileUser; +import ru.otus.mobile.config.TestAccount; +import ru.otus.mobile.db.DbClient; +import ru.otus.mobile.driver.MobileSession; +import ru.otus.mobile.driver.MobileSessionFactory; +import ru.otus.mobile.guice.CoreModule; +import ru.otus.mobile.page.LoginPage; + +public final class MobileExtension implements + BeforeEachCallback, + AfterEachCallback { + private static final ExtensionContext.Namespace NAMESPACE = + ExtensionContext.Namespace.create(MobileExtension.class); + private static final String SESSION_KEY = "mobile.session"; + + @Override + public void beforeEach(ExtensionContext context) { + Injector injector = rootInjector(context); + TestAccount account = resolveAccount(context); + injector.getInstance(DbClient.class).resetData(account); + + MobileSessionFactory sessionFactory = injector.getInstance(MobileSessionFactory.class); + MobileSession session = sessionFactory.create(account, context.getDisplayName()); + session.activate(); + session.injectMembers(context.getRequiredTestInstance()); + context.getStore(NAMESPACE).put(SESSION_KEY, session); + injector.getInstance(LoginPage.class).login(account); + } + + @Override + public void afterEach(ExtensionContext context) { + MobileSession session = context.getStore(NAMESPACE).remove(SESSION_KEY, MobileSession.class); + if (session != null) { + session.close(); + } + } + + private Injector rootInjector(ExtensionContext context) { + return context.getRoot() + .getStore(NAMESPACE) + .getOrComputeIfAbsent("root.injector", key -> Guice.createInjector(new CoreModule()), Injector.class); + } + + private TestAccount resolveAccount(ExtensionContext context) { + MobileUser annotation = context.getRequiredTestClass().getAnnotation(MobileUser.class); + if (annotation == null) { + throw new IllegalStateException("Test class must be annotated with @MobileUser."); + } + return annotation.value(); + } +} diff --git a/src/main/java/ru/otus/mobile/guice/CoreModule.java b/src/main/java/ru/otus/mobile/guice/CoreModule.java new file mode 100644 index 0000000..6fcd1ba --- /dev/null +++ b/src/main/java/ru/otus/mobile/guice/CoreModule.java @@ -0,0 +1,93 @@ +package ru.otus.mobile.guice; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.Singleton; +import ru.otus.mobile.config.MobileConfig; +import ru.otus.mobile.config.ProjectPaths; +import ru.otus.mobile.db.DbConfig; +import ru.otus.mobile.driver.EmulatorQueue; +import ru.otus.mobile.driver.LogcatCollector; +import ru.otus.mobile.driver.MobileDriverFactory; + +import java.util.List; + +public final class CoreModule extends AbstractModule { + @Override + protected void configure() { + } + + @Provides + @Singleton + MobileConfig mobileConfig() { + String appPackage = value("app.package", "APP_PACKAGE", "ru.otus.wishlist"); + String appUrl = value("app.url", "APP_URL", "http://wiremock:8080/wishlist.apk"); + String reservationOwner = value("reservation.owner", "RESERVATION_OWNER", "user4us"); + String rawEmulators = value( + "mobile.emulators", + "MOBILE_EMULATORS", + "emulator-1|http://localhost:4723|Android Emulator" + ); + List emulators = MobileConfig.Emulator.parse(rawEmulators, appUrl); + return new MobileConfig(appPackage, appUrl, reservationOwner, emulators); + } + + @Provides + @Singleton + ProjectPaths projectPaths() { + return new ProjectPaths(); + } + + @Provides + @Singleton + DbConfig dbConfig() { + String url = requiredValue("db.url", "DB_URL"); + String user = requiredValue("db.user", "DB_USER"); + String password = requiredValue("db.password", "DB_PASSWORD"); + return new DbConfig(url, user, password); + } + + @Provides + @Singleton + EmulatorQueue emulatorQueue(MobileConfig config) { + return new EmulatorQueue(config); + } + + @Provides + @Singleton + MobileDriverFactory mobileDriverFactory(MobileConfig config) { + return new MobileDriverFactory(config); + } + + @Provides + @Singleton + LogcatCollector logcatCollector(ProjectPaths projectPaths) { + return new LogcatCollector(projectPaths); + } + + private String requiredValue(String property, String env) { + String value = value(property, env, null); + if (value == null) { + throw new IllegalStateException( + "DB config is missing. Provide db.url/db.user/db.password or DB_URL/DB_USER/DB_PASSWORD." + ); + } + return value; + } + + private String value(String property, String env, String defaultValue) { + String propertyValue = System.getProperty(property); + if (!isBlank(propertyValue)) { + return propertyValue; + } + String environmentValue = System.getenv(env); + if (!isBlank(environmentValue)) { + return environmentValue; + } + return defaultValue; + } + + private boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } +} diff --git a/src/main/java/ru/otus/mobile/guice/SessionModule.java b/src/main/java/ru/otus/mobile/guice/SessionModule.java new file mode 100644 index 0000000..7174f5c --- /dev/null +++ b/src/main/java/ru/otus/mobile/guice/SessionModule.java @@ -0,0 +1,30 @@ +package ru.otus.mobile.guice; + +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import io.appium.java_client.android.AndroidDriver; +import ru.otus.mobile.config.TestAccount; +import ru.otus.mobile.config.TestContext; + +public final class SessionModule extends AbstractModule { + private final TestContext testContext; + + public SessionModule(TestContext testContext) { + this.testContext = testContext; + } + + @Override + protected void configure() { + bind(TestContext.class).toInstance(testContext); + } + + @Provides + TestAccount testAccount() { + return testContext.account(); + } + + @Provides + AndroidDriver androidDriver() { + return testContext.driver(); + } +} diff --git a/src/main/java/ru/otus/mobile/page/AbsBasePage.java b/src/main/java/ru/otus/mobile/page/AbsBasePage.java new file mode 100644 index 0000000..03a1def --- /dev/null +++ b/src/main/java/ru/otus/mobile/page/AbsBasePage.java @@ -0,0 +1,19 @@ +package ru.otus.mobile.page; + +import ru.otus.mobile.component.BottomNavigationComponent; +import ru.otus.mobile.config.MobileConfig; + +public abstract class AbsBasePage extends AbsPageObject { + protected final BottomNavigationComponent bottomNavigation; + private final MobileConfig config; + + protected AbsBasePage(MobileConfig config, BottomNavigationComponent bottomNavigation) { + super(config); + this.config = config; + this.bottomNavigation = bottomNavigation; + } + + protected MobileConfig config() { + return config; + } +} diff --git a/src/main/java/ru/otus/mobile/page/AbsPageObject.java b/src/main/java/ru/otus/mobile/page/AbsPageObject.java new file mode 100644 index 0000000..013f88d --- /dev/null +++ b/src/main/java/ru/otus/mobile/page/AbsPageObject.java @@ -0,0 +1,103 @@ +package ru.otus.mobile.page; + +import com.codeborne.selenide.CollectionCondition; +import com.codeborne.selenide.Condition; +import com.codeborne.selenide.SelenideElement; +import com.codeborne.selenide.WebDriverRunner; +import com.codeborne.selenide.appium.SelenideAppiumCollection; +import io.appium.java_client.AppiumBy; +import ru.otus.mobile.config.MobileConfig; + +import static com.codeborne.selenide.appium.SelenideAppium.$$; +import static com.codeborne.selenide.appium.SelenideAppium.$; + +public abstract class AbsPageObject { + private final MobileConfig config; + + protected AbsPageObject(MobileConfig config) { + this.config = config; + } + + protected SelenideElement byId(String id) { + return $(AppiumBy.id(fullId(id))); + } + + protected SelenideAppiumCollection allById(String id) { + return $$(AppiumBy.id(fullId(id))); + } + + protected SelenideElement byText(String text) { + return $(AppiumBy.androidUIAutomator("new UiSelector().text(\"" + escape(text) + "\")")); + } + + protected SelenideElement byTextContains(String text) { + return $(AppiumBy.androidUIAutomator("new UiSelector().textContains(\"" + escape(text) + "\")")); + } + + protected SelenideElement byUiAutomator(String selector) { + return $(AppiumBy.androidUIAutomator(selector)); + } + + protected SelenideElement byXpath(String xpath) { + return $(AppiumBy.xpath(xpath)); + } + + protected void type(String id, String value) { + SelenideElement input = byId(id).shouldBe(Condition.visible); + input.click(); + input.clear(); + input.sendKeys(value); + } + + protected void tap(String id) { + byId(id).shouldBe(Condition.visible).click(); + } + + protected boolean exists(String id) { + return byId(id).exists(); + } + + protected void shouldHaveItems(String id) { + allById(id).shouldHave(CollectionCondition.sizeGreaterThan(0)); + } + + protected SelenideElement scrollToText(String text) { + String selector = "new UiScrollable(new UiSelector().scrollable(true))" + + ".scrollIntoView(new UiSelector().textContains(\"" + escape(text) + "\"))"; + return $(AppiumBy.androidUIAutomator(selector)); + } + + protected void back() { + WebDriverRunner.getWebDriver().navigate().back(); + } + + protected String xpathLiteral(String value) { + if (!value.contains("\"")) { + return "\"" + value + "\""; + } + if (!value.contains("'")) { + return "'" + value + "'"; + } + String[] parts = value.split("\""); + StringBuilder builder = new StringBuilder("concat("); + for (int i = 0; i < parts.length; i++) { + if (i > 0) { + builder.append(", '\"', "); + } + builder.append("\"").append(parts[i]).append("\""); + } + builder.append(")"); + return builder.toString(); + } + + private String fullId(String id) { + if (id.contains(":")) { + return id; + } + return config.appPackage() + ":id/" + id; + } + + private String escape(String value) { + return value.replace("\"", "\\\""); + } +} diff --git a/src/main/java/ru/otus/mobile/page/GiftsPage.java b/src/main/java/ru/otus/mobile/page/GiftsPage.java new file mode 100644 index 0000000..d4f56ec --- /dev/null +++ b/src/main/java/ru/otus/mobile/page/GiftsPage.java @@ -0,0 +1,36 @@ +package ru.otus.mobile.page; + +import com.codeborne.selenide.Condition; +import com.google.inject.Inject; +import ru.otus.mobile.component.BottomNavigationComponent; +import ru.otus.mobile.component.GiftFormComponent; +import ru.otus.mobile.config.MobileConfig; + +public final class GiftsPage extends AbsBasePage { + private final GiftFormComponent form; + + @Inject + public GiftsPage(MobileConfig config, BottomNavigationComponent bottomNavigation, GiftFormComponent form) { + super(config, bottomNavigation); + this.form = form; + } + + public void createGift(String name) { + tap("add_button"); + form.save(name, "100"); + } + + public void editGift(String oldName, String newName) { + openGift(oldName); + tap("edit_button"); + form.save(newName, "100"); + } + + public void shouldSeeGift(String name) { + scrollToText(name).shouldBe(Condition.visible); + } + + private void openGift(String name) { + scrollToText(name).click(); + } +} diff --git a/src/main/java/ru/otus/mobile/page/LoginPage.java b/src/main/java/ru/otus/mobile/page/LoginPage.java new file mode 100644 index 0000000..433350c --- /dev/null +++ b/src/main/java/ru/otus/mobile/page/LoginPage.java @@ -0,0 +1,30 @@ +package ru.otus.mobile.page; + +import com.google.inject.Inject; +import ru.otus.mobile.component.AlertDialogComponent; +import ru.otus.mobile.config.MobileConfig; +import ru.otus.mobile.config.TestAccount; + +public final class LoginPage extends AbsPageObject { + private final AlertDialogComponent alertDialog; + + @Inject + public LoginPage(MobileConfig config, AlertDialogComponent alertDialog) { + super(config); + this.alertDialog = alertDialog; + } + + public void login(TestAccount account) { + if (!isOpened()) { + return; + } + type("username_text_input", account.username()); + type("password_text_input", account.password()); + tap("log_in_button"); + alertDialog.acceptIfVisible(); + } + + public boolean isOpened() { + return exists("username_text_input") && exists("password_text_input"); + } +} diff --git a/src/main/java/ru/otus/mobile/page/UsersPage.java b/src/main/java/ru/otus/mobile/page/UsersPage.java new file mode 100644 index 0000000..2977cfd --- /dev/null +++ b/src/main/java/ru/otus/mobile/page/UsersPage.java @@ -0,0 +1,43 @@ +package ru.otus.mobile.page; + +import com.codeborne.selenide.Condition; +import com.google.inject.Inject; +import ru.otus.mobile.component.BottomNavigationComponent; +import ru.otus.mobile.config.MobileConfig; + +public final class UsersPage extends AbsBasePage { + @Inject + public UsersPage(MobileConfig config, BottomNavigationComponent bottomNavigation) { + super(config, bottomNavigation); + } + + public void open() { + bottomNavigation.openUsers(); + byId("users_content").shouldBe(Condition.visible); + } + + public void openUser(String username) { + String appPackage = config().appPackage(); + String selector = "new UiScrollable(new UiSelector().resourceId(\"" + appPackage + ":id/users\"))" + + ".setMaxSearchSwipes(12)" + + ".scrollIntoView(new UiSelector().resourceId(\"" + appPackage + ":id/username\")" + + ".textContains(\"" + username.replace("\"", "\\\"") + "\"))"; + byUiAutomator(selector).shouldBe(Condition.visible).click(); + } + + public void openFirstWishlist() { + allById("wishlist_item").first().shouldBe(Condition.visible).click(); + } + + public void openFirstGift() { + allById("gift_item").first().shouldBe(Condition.visible).click(); + } + + public boolean isReserved() { + return Boolean.parseBoolean(byId("reserved").shouldBe(Condition.visible).getAttribute("checked")); + } + + public void toggleReservation() { + tap("reserved"); + } +} diff --git a/src/main/java/ru/otus/mobile/page/WishlistsPage.java b/src/main/java/ru/otus/mobile/page/WishlistsPage.java new file mode 100644 index 0000000..e8bde65 --- /dev/null +++ b/src/main/java/ru/otus/mobile/page/WishlistsPage.java @@ -0,0 +1,61 @@ +package ru.otus.mobile.page; + +import com.codeborne.selenide.Condition; +import com.google.inject.Inject; +import ru.otus.mobile.component.BottomNavigationComponent; +import ru.otus.mobile.component.WishlistFormComponent; +import ru.otus.mobile.config.MobileConfig; + +public final class WishlistsPage extends AbsBasePage { + private final WishlistFormComponent form; + + @Inject + public WishlistsPage(MobileConfig config, BottomNavigationComponent bottomNavigation, WishlistFormComponent form) { + super(config, bottomNavigation); + this.form = form; + } + + public void open() { + bottomNavigation.openWishlists(); + byId("wishlists_content").shouldBe(Condition.visible); + } + + public void createWishlist(String name) { + tap("add_button"); + form.save(name); + } + + public void editWishlist(String oldName, String newName) { + ensureWishlistsList(); + editButtonFor(oldName).shouldBe(Condition.visible).click(); + form.save(newName); + } + + public void shouldSeeWishlist(String name) { + scrollToText(name).shouldBe(Condition.visible); + } + + public void openWishlist(String name) { + scrollToText(name).click(); + } + + private void ensureWishlistsList() { + if (exists("wishlist_item") || exists("wishlists")) { + return; + } + if (exists("gifts_content") || exists("gift_item") || exists("add_button")) { + back(); + } + byId("wishlists_content").shouldBe(Condition.visible); + } + + private com.codeborne.selenide.SelenideElement editButtonFor(String name) { + String full = config().appPackage(); + String xpath = "//androidx.recyclerview.widget.RecyclerView[@resource-id='" + full + ":id/wishlists']" + + "//android.view.ViewGroup[@resource-id='" + full + ":id/wishlist_item'" + + " and .//android.widget.TextView[@resource-id='" + full + ":id/title'" + + " and contains(@text," + xpathLiteral(name) + ")]]" + + "//android.widget.Button[@resource-id='" + full + ":id/edit_button']"; + return byXpath(xpath); + } +} diff --git a/src/main/java/ru/otus/mobile/util/TestData.java b/src/main/java/ru/otus/mobile/util/TestData.java new file mode 100644 index 0000000..0b7ca7a --- /dev/null +++ b/src/main/java/ru/otus/mobile/util/TestData.java @@ -0,0 +1,9 @@ +package ru.otus.mobile.util; + +import java.time.Instant; + +public final class TestData { + public String uniqueName(String prefix) { + return prefix + "-" + Instant.now().toEpochMilli(); + } +} diff --git a/src/test/java/ru/otus/mobile/config/ApkInfo.java b/src/test/java/ru/otus/mobile/config/ApkInfo.java deleted file mode 100644 index a16ef95..0000000 --- a/src/test/java/ru/otus/mobile/config/ApkInfo.java +++ /dev/null @@ -1,147 +0,0 @@ -package ru.otus.mobile.config; - -import net.dongliu.apk.parser.ApkFile; -import net.dongliu.apk.parser.bean.ApkMeta; - -import java.io.IOException; -import java.io.StringReader; -import java.nio.file.Path; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -public final class ApkInfo { - private final String packageName; - private final String mainActivity; - - private ApkInfo(String packageName, String mainActivity) { - this.packageName = packageName; - this.mainActivity = mainActivity; - } - - public String packageName() { - return packageName; - } - - public String mainActivity() { - return mainActivity; - } - - public static ApkInfo fromApk(Path apkPath) { - try (ApkFile apkFile = new ApkFile(apkPath.toFile())) { - ApkMeta meta = apkFile.getApkMeta(); - String pkg = meta.getPackageName(); - String activity = extractLaunchableActivity(apkFile.getManifestXml(), pkg); - return new ApkInfo(pkg, activity); - } catch (IOException e) { - throw new IllegalStateException("Failed to parse APK: " + apkPath, e); - } - } - - private static String extractLaunchableActivity(String manifestXml, String pkg) { - if (manifestXml == null || manifestXml.isBlank()) { - return null; - } - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - try { - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - } catch (Exception ignored) { - // Not all parsers support this; safe processing is still enabled. - } - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(new InputSource(new StringReader(manifestXml))); - - String activity = findLaunchableActivity(doc, "activity", "name", pkg); - if (activity != null) { - return activity; - } - return findLaunchableActivity(doc, "activity-alias", "targetActivity", pkg); - } catch (Exception e) { - return null; - } - } - - private static String findLaunchableActivity( - Document doc, - String tagName, - String nameAttr, - String pkg - ) { - NodeList activities = doc.getElementsByTagName(tagName); - for (int i = 0; i < activities.getLength(); i++) { - Element activity = (Element) activities.item(i); - if (!isLaunchable(activity)) { - continue; - } - String raw = androidAttr(activity, nameAttr); - if (raw == null || raw.isBlank()) { - raw = androidAttr(activity, "name"); - } - if (raw == null || raw.isBlank()) { - continue; - } - return normalizeActivityName(raw, pkg); - } - return null; - } - - private static boolean isLaunchable(Element activity) { - NodeList filters = activity.getElementsByTagName("intent-filter"); - for (int i = 0; i < filters.getLength(); i++) { - Element filter = (Element) filters.item(i); - if (hasIntentAction(filter, "android.intent.action.MAIN") - && hasIntentCategory(filter, "android.intent.category.LAUNCHER")) { - return true; - } - } - return false; - } - - private static boolean hasIntentAction(Element filter, String name) { - NodeList actions = filter.getElementsByTagName("action"); - for (int i = 0; i < actions.getLength(); i++) { - Element action = (Element) actions.item(i); - if (name.equals(androidAttr(action, "name"))) { - return true; - } - } - return false; - } - - private static boolean hasIntentCategory(Element filter, String name) { - NodeList categories = filter.getElementsByTagName("category"); - for (int i = 0; i < categories.getLength(); i++) { - Element category = (Element) categories.item(i); - if (name.equals(androidAttr(category, "name"))) { - return true; - } - } - return false; - } - - private static String androidAttr(Element element, String localName) { - String value = element.getAttributeNS("http://schemas.android.com/apk/res/android", localName); - if (value != null && !value.isBlank()) { - return value; - } - value = element.getAttribute("android:" + localName); - return value != null && !value.isBlank() ? value : null; - } - - private static String normalizeActivityName(String raw, String pkg) { - if (raw.startsWith(".")) { - return pkg + raw; - } - if (raw.contains(".")) { - return raw; - } - return pkg + "." + raw; - } -} diff --git a/src/test/java/ru/otus/mobile/config/AuthContext.java b/src/test/java/ru/otus/mobile/config/AuthContext.java deleted file mode 100644 index 99128c8..0000000 --- a/src/test/java/ru/otus/mobile/config/AuthContext.java +++ /dev/null @@ -1,28 +0,0 @@ -package ru.otus.mobile.config; - -public final class AuthContext { - private static String username; - private static String password; - private static boolean allowRegister; - - private AuthContext() { - } - - public static void init(String username, String password, boolean allowRegister) { - AuthContext.username = username; - AuthContext.password = password; - AuthContext.allowRegister = allowRegister; - } - - public static String username() { - return username; - } - - public static String password() { - return password; - } - - public static boolean allowRegister() { - return allowRegister; - } -} diff --git a/src/test/java/ru/otus/mobile/config/MobileConfig.java b/src/test/java/ru/otus/mobile/config/MobileConfig.java deleted file mode 100644 index 7c5d98a..0000000 --- a/src/test/java/ru/otus/mobile/config/MobileConfig.java +++ /dev/null @@ -1,118 +0,0 @@ -package ru.otus.mobile.config; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -public final class MobileConfig { - private final String appiumUrl; - private final String appPath; - private final String deviceName; - private final String platformVersion; - private final String udid; - private final boolean noReset; - private final int newCommandTimeoutSeconds; - private final String appPackage; - private final String appActivity; - - private MobileConfig( - String appiumUrl, - String appPath, - String deviceName, - String platformVersion, - String udid, - boolean noReset, - int newCommandTimeoutSeconds, - String appPackage, - String appActivity - ) { - this.appiumUrl = appiumUrl; - this.appPath = appPath; - this.deviceName = deviceName; - this.platformVersion = platformVersion; - this.udid = udid; - this.noReset = noReset; - this.newCommandTimeoutSeconds = newCommandTimeoutSeconds; - this.appPackage = appPackage; - this.appActivity = appActivity; - } - - public String appiumUrl() { - return appiumUrl; - } - - public String appPath() { - return appPath; - } - - public String deviceName() { - return deviceName; - } - - public String platformVersion() { - return platformVersion; - } - - public String udid() { - return udid; - } - - public boolean noReset() { - return noReset; - } - - public int newCommandTimeoutSeconds() { - return newCommandTimeoutSeconds; - } - - public String appPackage() { - return appPackage; - } - - public String appActivity() { - return appActivity; - } - - public static MobileConfig load() { - String appiumUrl = System.getProperty("appium.url", "http://localhost:4723"); - String appPath = System.getProperty("app.path", "/apk/wishlist.apk"); - String deviceName = System.getProperty("device.name", "Android Emulator"); - String platformVersion = System.getProperty("platform.version"); - String udid = System.getProperty("udid"); - boolean noReset = Boolean.parseBoolean(System.getProperty("noReset", "false")); - int newCommandTimeoutSeconds = Integer.parseInt(System.getProperty("newCommandTimeout", "120")); - - String appPackage = System.getProperty("app.package"); - String appActivity = System.getProperty("app.activity"); - - Path apkPath = Paths.get(appPath); - if ((appPackage == null || appActivity == null) && Files.exists(apkPath)) { - ApkInfo apkInfo = ApkInfo.fromApk(apkPath); - if (appPackage == null) { - appPackage = apkInfo.packageName(); - } - if (appActivity == null && apkInfo.mainActivity() != null) { - appActivity = apkInfo.mainActivity(); - } - } - - if (appPackage == null) { - appPackage = "ru.otus.wishlist"; - } - if (appActivity == null) { - appActivity = "ru.otus.wishlist.MainActivity"; - } - - return new MobileConfig( - appiumUrl, - appPath, - deviceName, - platformVersion, - udid, - noReset, - newCommandTimeoutSeconds, - appPackage, - appActivity - ); - } -} diff --git a/src/test/java/ru/otus/mobile/config/MobileContext.java b/src/test/java/ru/otus/mobile/config/MobileContext.java deleted file mode 100644 index 8156bdc..0000000 --- a/src/test/java/ru/otus/mobile/config/MobileContext.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.otus.mobile.config; - -public final class MobileContext { - private static String packageName; - - private MobileContext() { - } - - public static void init(MobileConfig config) { - packageName = config.appPackage(); - } - - public static String packageName() { - return packageName; - } -} diff --git a/src/test/java/ru/otus/mobile/db/DbClient.java b/src/test/java/ru/otus/mobile/db/DbClient.java deleted file mode 100644 index 3e06d24..0000000 --- a/src/test/java/ru/otus/mobile/db/DbClient.java +++ /dev/null @@ -1,105 +0,0 @@ -package ru.otus.mobile.db; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -public final class DbClient { - private final DbConfig config; - private static final String DEFAULT_PASSWORD_HASH = - "$2a$10$MUT3Px5CK0cy.vTesX9NaOpWAcbkrZHECe67Qwbh19sx3AlS8bK2C"; - - public DbClient(DbConfig config) { - this.config = config; - } - - public void resetUserData(String username) { - resetUserData(username, config.resetSql()); - } - - public void resetUserData(String username, String sql) { - try (Connection connection = DriverManager.getConnection( - config.url(), - config.user(), - config.password() - )) { - try (PreparedStatement statement = connection.prepareStatement(sql)) { - int placeholders = countPlaceholders(sql); - String passwordHash = System.getProperty("login.password.hash", DEFAULT_PASSWORD_HASH); - String email = username + "@test.local"; - if (placeholders >= 1) { - statement.setString(1, username); - } - if (placeholders >= 2) { - statement.setString(2, email); - } - if (placeholders >= 3) { - statement.setString(3, passwordHash); - } - if (placeholders >= 4) { - statement.setString(4, username); - } - statement.execute(); - } - } catch (SQLException e) { - throw new IllegalStateException("Failed to reset data for user: " + username, e); - } - } - - public void resetReservationData(String username, String ownerUsername, String sql) { - try (Connection connection = DriverManager.getConnection( - config.url(), - config.user(), - config.password() - )) { - try (PreparedStatement statement = connection.prepareStatement(sql)) { - String passwordHash = System.getProperty("login.password.hash", DEFAULT_PASSWORD_HASH); - statement.setString(1, ownerUsername); - statement.setString(2, ownerUsername + "@test.local"); - statement.setString(3, passwordHash); - statement.setString(4, ownerUsername); - statement.setString(5, username); - statement.setString(6, username + "@test.local"); - statement.setString(7, passwordHash); - statement.setString(8, username); - statement.execute(); - } - } catch (SQLException e) { - throw new IllegalStateException("Failed to reset data for reservation user: " + username, e); - } - } - - public void updateWishlistTitle(String username, String oldTitle, String newTitle) { - String sql = """ - UPDATE wishlists - SET title = ? - WHERE user_id = (SELECT id FROM users WHERE username = ?) - AND title = ?; - """; - try (Connection connection = DriverManager.getConnection( - config.url(), - config.user(), - config.password() - )) { - try (PreparedStatement statement = connection.prepareStatement(sql)) { - statement.setString(1, newTitle); - statement.setString(2, username); - statement.setString(3, oldTitle); - statement.executeUpdate(); - } - } catch (SQLException e) { - throw new IllegalStateException("Failed to update wishlist title for user: " + username, e); - } - } - - private int countPlaceholders(String sql) { - int count = 0; - for (int i = 0; i < sql.length(); i++) { - if (sql.charAt(i) == '?') { - count++; - } - } - return count; - } -} diff --git a/src/test/java/ru/otus/mobile/db/DbConfig.java b/src/test/java/ru/otus/mobile/db/DbConfig.java deleted file mode 100644 index f91c9c6..0000000 --- a/src/test/java/ru/otus/mobile/db/DbConfig.java +++ /dev/null @@ -1,85 +0,0 @@ -package ru.otus.mobile.db; - -public final class DbConfig { - private final String url; - private final String user; - private final String password; - private final String resetSql; - - private static final String DEFAULT_RESET_SQL = """ - WITH target_user AS ( - SELECT id FROM users WHERE username = ? - ), - user_insert AS ( - INSERT INTO users (id, email, password, username) - SELECT gen_random_uuid(), ?, ?, ? - WHERE NOT EXISTS (SELECT 1 FROM target_user) - RETURNING id - ), - user_ref AS ( - SELECT id FROM target_user - UNION ALL - SELECT id FROM user_insert - ), - del_gifts AS ( - DELETE FROM gifts - WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM user_ref)) - ), - del_wishlists AS ( - DELETE FROM wishlists WHERE user_id IN (SELECT id FROM user_ref) - ) - SELECT 1; - """; - - private DbConfig(String url, String user, String password, String resetSql) { - this.url = url; - this.user = user; - this.password = password; - this.resetSql = resetSql; - } - - public String url() { - return url; - } - - public String user() { - return user; - } - - public String password() { - return password; - } - - public String resetSql() { - return resetSql; - } - - public static DbConfig load() { - String url = propOrEnv("db.url", "DB_URL"); - String user = propOrEnv("db.user", "DB_USER"); - String password = propOrEnv("db.password", "DB_PASSWORD"); - String resetSql = propOrEnv("db.reset.sql", "DB_RESET_SQL"); - - if (isBlank(url) || isBlank(user) || isBlank(password)) { - throw new IllegalStateException( - "DB config is missing. Provide db.url/db.user/db.password or DB_URL/DB_USER/DB_PASSWORD." - ); - } - if (isBlank(resetSql)) { - resetSql = DEFAULT_RESET_SQL; - } - return new DbConfig(url, user, password, resetSql); - } - - private static String propOrEnv(String prop, String env) { - String value = System.getProperty(prop); - if (!isBlank(value)) { - return value; - } - return System.getenv(env); - } - - private static boolean isBlank(String value) { - return value == null || value.trim().isEmpty(); - } -} diff --git a/src/test/java/ru/otus/mobile/driver/MobileDriverFactory.java b/src/test/java/ru/otus/mobile/driver/MobileDriverFactory.java deleted file mode 100644 index 0007ed0..0000000 --- a/src/test/java/ru/otus/mobile/driver/MobileDriverFactory.java +++ /dev/null @@ -1,103 +0,0 @@ -package ru.otus.mobile.driver; - -import io.appium.java_client.android.AndroidDriver; -import io.appium.java_client.android.options.UiAutomator2Options; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebDriverException; -import ru.otus.mobile.config.MobileConfig; - -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.time.Duration; - -public final class MobileDriverFactory { - private MobileDriverFactory() { - } - - public static WebDriver create(MobileConfig config) { - UiAutomator2Options options = new UiAutomator2Options() - .setPlatformName("Android") - .setAutomationName("UiAutomator2") - .setDeviceName(config.deviceName()) - .setApp(config.appPath()) - .setAppPackage(config.appPackage()) - .setAppActivity(config.appActivity()) - .setAutoGrantPermissions(true) - .setNewCommandTimeout(Duration.ofSeconds(config.newCommandTimeoutSeconds())) - .setAppWaitActivity("*"); - - if (config.platformVersion() != null && !config.platformVersion().isBlank()) { - options.setPlatformVersion(config.platformVersion()); - } - if (config.udid() != null && !config.udid().isBlank()) { - options.setUdid(config.udid()); - } - options.setNoReset(config.noReset()); - options.setCapability("appium:settingsAppWaitDuration", 60000); - options.setCapability("appium:uiautomator2ServerInstallTimeout", 60000); - options.setCapability("appium:uiautomator2ServerLaunchTimeout", 60000); - options.setCapability("appium:appWaitDuration", 60000); - - try { - URL appiumUrl = new URL(config.appiumUrl()); - assertAppiumReady(appiumUrl); - return createWithDeviceRetry(appiumUrl, options); - } catch (MalformedURLException e) { - throw new IllegalStateException("Invalid Appium URL: " + config.appiumUrl(), e); - } - } - - private static WebDriver createWithDeviceRetry(URL appiumUrl, UiAutomator2Options options) { - long deadline = System.currentTimeMillis() + 180_000; - WebDriverException last = null; - while (System.currentTimeMillis() < deadline) { - try { - return new AndroidDriver(appiumUrl, options); - } catch (WebDriverException e) { - last = e; - String message = e.getMessage(); - if (message != null && message.contains("Could not find a connected Android device")) { - try { - Thread.sleep(1000); - } catch (InterruptedException interruptedException) { - Thread.currentThread().interrupt(); - break; - } - continue; - } - if (message != null && message.contains("Appium Settings app is not running")) { - try { - Thread.sleep(3000); - } catch (InterruptedException interruptedException) { - Thread.currentThread().interrupt(); - break; - } - continue; - } - throw e; - } - } - if (last != null) { - throw last; - } - throw new WebDriverException("Android device was not detected within timeout."); - } - - private static void assertAppiumReady(URL appiumUrl) { - try { - URL statusUrl = new URL(appiumUrl.toString() + "/status"); - HttpURLConnection connection = (HttpURLConnection) statusUrl.openConnection(); - connection.setConnectTimeout(3000); - connection.setReadTimeout(3000); - connection.setRequestMethod("GET"); - int code = connection.getResponseCode(); - if (code >= 400) { - throw new IllegalStateException("Appium status returned HTTP " + code); - } - } catch (Exception e) { - throw new WebDriverException("Appium is not reachable at " + appiumUrl + ". " + - "Make sure docker compose is up and Appium is healthy.", e); - } - } -} diff --git a/src/test/java/ru/otus/mobile/screens/GiftsScreen.java b/src/test/java/ru/otus/mobile/screens/GiftsScreen.java deleted file mode 100644 index 9f102dc..0000000 --- a/src/test/java/ru/otus/mobile/screens/GiftsScreen.java +++ /dev/null @@ -1,77 +0,0 @@ -package ru.otus.mobile.screens; - -import com.codeborne.selenide.ElementsCollection; -import com.codeborne.selenide.SelenideElement; -import io.appium.java_client.AppiumBy; -import org.openqa.selenium.WebElement; -import ru.otus.mobile.ui.MobileUi; - -public class GiftsScreen { - - public void open() { - if (MobileUi.existsById("gifts_content")) { - return; - } - if (MobileUi.existsById("gift_item") - || MobileUi.existsById("list_item")) { - return; - } - MobileUi.byIdOrAccessibility("gifts_content"); - } - - public void createGift(String name) { - if (!MobileUi.existsById("add_button")) { - open(); - } - MobileUi.byIdOrAccessibility("add_button").click(); - typeInto("name_input", name); - typeInto("price_input", "100"); - MobileUi.byIdOrAccessibility("save_button").click(); - } - - public void editGift(String oldName, String newName) { - openGift(oldName); - if (MobileUi.existsById("edit_button")) { - MobileUi.byIdOrAccessibility("edit_button").click(); - } - typeInto("name_input", newName); - MobileUi.byIdOrAccessibility("save_button").click(); - } - - public void assertGiftPresent(String name) { - scrollToGiftName(name); - } - - private void openGift(String name) { - SelenideElement byText = MobileUi.byTextContains(name); - if (byText.exists()) { - byText.click(); - return; - } - scrollToGiftName(name).click(); - if (MobileUi.existsById("edit_button") - || MobileUi.existsById("gift_edit_bottom_sheet")) { - return; - } - ElementsCollection items = MobileUi.allById("gift_item"); - if (items.isEmpty()) { - items = MobileUi.allById("list_item"); - } - items.first().click(); - } - - private SelenideElement scrollToGiftName(String name) { - String safe = name.replace("\"", "\\\""); - String selector = "new UiScrollable(new UiSelector().scrollable(true))" - + ".scrollIntoView(new UiSelector().textContains(\"" + safe + "\"))"; - return com.codeborne.selenide.Selenide.$(AppiumBy.androidUIAutomator(selector)); - } - - private void typeInto(String id, String value) { - WebElement input = MobileUi.byIdOrAccessibility(id) - .toWebElement(); - input.click(); - input.clear(); - input.sendKeys(value); - } -} diff --git a/src/test/java/ru/otus/mobile/screens/LoginScreen.java b/src/test/java/ru/otus/mobile/screens/LoginScreen.java deleted file mode 100644 index 470ecbf..0000000 --- a/src/test/java/ru/otus/mobile/screens/LoginScreen.java +++ /dev/null @@ -1,194 +0,0 @@ -package ru.otus.mobile.screens; - -import org.openqa.selenium.WebElement; -import ru.otus.mobile.ui.MobileUi; - -public class LoginScreen { - - public boolean isVisible() { - return MobileUi.existsById("username_text_input") - || MobileUi.existsById("password_text_input"); - } - - public void loginOrRegister(String username, String password, boolean allowRegister) { - if (!isVisible()) { - return; - } - attemptLogin(username, password); - if (!isVisible()) { - return; - } - dismissDialogIfPresent(); - if (!isVisible()) { - return; - } - if (allowRegister) { - if (isRegistrationScreen()) { - completeRegistration(username, password); - ensureLoginCompleted(); - return; - } - if (openRegistrationScreen()) { - completeRegistration(username, password); - ensureLoginCompleted(); - return; - } - } - if (isVisible()) { - throw new IllegalStateException( - "Login did not complete. Provide valid credentials or enable registration." - ); - } - } - - private boolean isRegistrationScreen() { - return MobileUi.existsById("email_text_input") - || MobileUi.byTextContains("Регистрация").exists(); - } - - private boolean openRegistrationScreen() { - if (clickRegisterLink()) { - return MobileUi.existsById("email_text_input"); - } - return false; - } - - private boolean clickRegisterLink() { - if (MobileUi.existsById("register_button")) { - MobileUi.byIdOrAccessibility("register_button").click(); - return true; - } - if (MobileUi.byTextContains("Регистрац").exists()) { - MobileUi.byTextContains("Регистрац").click(); - return true; - } - if (MobileUi.byTextContains("Зарегистр").exists()) { - MobileUi.byTextContains("Зарегистр").click(); - return true; - } - MobileUi.hideKeyboard(); // hide keyboard to reveal link - if (MobileUi.byTextContains("Регистрац").exists()) { - MobileUi.byTextContains("Регистрац").click(); - return true; - } - if (MobileUi.byTextContains("Зарегистр").exists()) { - MobileUi.byTextContains("Зарегистр").click(); - return true; - } - return false; - } - - private void completeRegistration(String username, String password) { - typeInto("username_text_input", username); - if (MobileUi.existsById("email_text_input")) { - typeInto("email_text_input", username + "@test.local"); - } - if (MobileUi.existsById("password_text_input")) { - typeInto("password_text_input", password); - } - fillRepeatPasswordIfPresent(password); - MobileUi.hideKeyboard(); // hide keyboard so buttons are visible - if (MobileUi.existsById("register_button")) { - MobileUi.byIdOrAccessibility("register_button").click(); - dismissDialogIfPresent(); - finishLoginAfterRegistration(username, password); - return; - } - if (MobileUi.byTextContains("ЗАРЕГИСТРИРОВАТЬСЯ").exists()) { - MobileUi.byTextContains("ЗАРЕГИСТРИРОВАТЬСЯ").click(); - dismissDialogIfPresent(); - finishLoginAfterRegistration(username, password); - } - } - - private void finishLoginAfterRegistration(String username, String password) { - if (MobileUi.existsById("log_in_button") || isLoginScreen()) { - typeInto("username_text_input", username); - if (MobileUi.existsById("password_text_input")) { - typeInto("password_text_input", password); - } - MobileUi.hideKeyboard(); - if (MobileUi.existsById("log_in_button")) { - MobileUi.byIdOrAccessibility("log_in_button").click(); - } - return; - } - if (isRegistrationScreen()) { - MobileUi.back(); - if (isLoginScreen()) { - typeInto("username_text_input", username); - if (MobileUi.existsById("password_text_input")) { - typeInto("password_text_input", password); - } - MobileUi.hideKeyboard(); - if (MobileUi.existsById("log_in_button")) { - MobileUi.byIdOrAccessibility("log_in_button").click(); - } - } - if (isRegistrationScreen()) { - throw new IllegalStateException("Registration did not complete. Check password rules and required fields."); - } - } - } - - private boolean isLoginScreen() { - return MobileUi.existsById("username_text_input") - && !MobileUi.existsById("email_text_input"); - } - - private void typeInto(String id, String value) { - WebElement input = MobileUi.byIdOrAccessibility(id) - .toWebElement(); - input.click(); - input.clear(); - input.sendKeys(value); - } - - private void attemptLogin(String username, String password) { - if (!MobileUi.existsById("username_text_input")) { - return; - } - typeInto("username_text_input", username); - if (MobileUi.existsById("password_text_input")) { - typeInto("password_text_input", password); - } - MobileUi.hideKeyboard(); - if (MobileUi.existsById("log_in_button")) { - MobileUi.byIdOrAccessibility("log_in_button").click(); - } - dismissDialogIfPresent(); - } - - private void fillRepeatPasswordIfPresent(String password) { - String[] repeatIds = { - "repeat_password_text_input", - "password_repeat_text_input", - "confirm_password_text_input", - "password_confirm_text_input" - }; - for (String id : repeatIds) { - if (MobileUi.existsById(id)) { - typeInto(id, password); - return; - } - } - } - - private void dismissDialogIfPresent() { - if (MobileUi.existsById("android:id/button1")) { - MobileUi.byIdOrAccessibility("android:id/button1").click(); - return; - } - if (MobileUi.byTextContains("OK").exists()) { - MobileUi.byTextContains("OK").click(); - return; - } - if (MobileUi.byTextContains("ОК").exists()) { - MobileUi.byTextContains("ОК").click(); - } - } - - private void ensureLoginCompleted() { - dismissDialogIfPresent(); - } -} diff --git a/src/test/java/ru/otus/mobile/screens/UsersScreen.java b/src/test/java/ru/otus/mobile/screens/UsersScreen.java deleted file mode 100644 index e8cfd09..0000000 --- a/src/test/java/ru/otus/mobile/screens/UsersScreen.java +++ /dev/null @@ -1,180 +0,0 @@ -package ru.otus.mobile.screens; - -import com.codeborne.selenide.ElementsCollection; -import com.codeborne.selenide.SelenideElement; -import io.appium.java_client.AppiumBy; -import ru.otus.mobile.config.AuthContext; -import ru.otus.mobile.config.MobileContext; -import ru.otus.mobile.ui.MobileUi; -import ru.otus.mobile.ui.UiWait; - -import static com.codeborne.selenide.Selenide.$; - -public class UsersScreen { - - public void open() { - ensureLoggedIn(); - if (MobileUi.existsById("users_content")) { - return; - } - dismissErrorDialogIfPresent(); - if (MobileUi.existsById("users_menu")) { - MobileUi.byIdOrAccessibility("users_menu").click(); - } else if (MobileUi.existsById("go_to_users_tab")) { - MobileUi.byIdOrAccessibility("go_to_users_tab").click(); - } else if (MobileUi.existsById("go_to_users")) { - MobileUi.byIdOrAccessibility("go_to_users").click(); - } - MobileUi.byIdOrAccessibility("users_content"); - } - - private void dismissErrorDialogIfPresent() { - if (UiWait.exists(AppiumBy.id("android:id/alertTitle")) - || UiWait.exists(AppiumBy.id("android:id/message"))) { - com.codeborne.selenide.Selenide.$(AppiumBy.id("android:id/button1")).click(); - } - } - - private void ensureLoggedIn() { - LoginScreen loginScreen = new LoginScreen(); - if (!loginScreen.isVisible()) { - return; - } - String username = AuthContext.username(); - String password = AuthContext.password(); - boolean allowRegister = AuthContext.allowRegister(); - if (password == null && username != null) { - password = "Admin123"; - } - if (username != null && password != null) { - loginScreen.loginOrRegister(username, password, allowRegister); - } - } - - public void openFirstUser() { - ElementsCollection users = MobileUi.allById("user_item"); - if (users.isEmpty()) { - users = MobileUi.allById("list_item"); - } - users.first().click(); - } - - public boolean openUserByName(String name) { - String pkg = MobileContext.packageName(); - String safe = name.replace("\"", "\\\""); - int maxSwipes = Integer.getInteger("users.search.swipes", 6); - String selector = "new UiScrollable(new UiSelector().scrollable(true))" - + ".setMaxSearchSwipes(" + maxSwipes + ")" - + ".scrollIntoView(new UiSelector().textContains(\"" + safe + "\"))"; - try { - $(AppiumBy.androidUIAutomator(selector)); - String xpath = "//android.view.ViewGroup[(@resource-id='" + pkg + ":id/user_item'" - + " or @resource-id='" + pkg + ":id/list_item') and .//*[contains(@text,\"" + safe + "\")]]"; - SelenideElement row = $(AppiumBy.xpath(xpath)); - if (row.exists()) { - row.click(); - return true; - } - $(AppiumBy.androidUIAutomator(selector)).click(); - return true; - } catch (Throwable ignored) { - return false; - } - } - - public void openUserWithGift(String preferredName) { - if (preferredName != null && !preferredName.isBlank() && Boolean.getBoolean("reservation.fast")) { - if (openUserByName(preferredName) && openFirstWishlistWithGift()) { - return; - } - throw new IllegalStateException("Preferred user not found or has no gifts to reserve."); - } - openFirstUserWithGift(); - } - - public void openFirstWishlist() { - ElementsCollection wishlists = MobileUi.allById("wishlist_item"); - if (wishlists.isEmpty()) { - wishlists = MobileUi.allById("list_item"); - } - wishlists.first().click(); - } - - public void openFirstGift() { - ElementsCollection gifts = MobileUi.allById("gift_item"); - if (gifts.isEmpty()) { - gifts = MobileUi.allById("list_item"); - } - gifts.first().click(); - } - - public boolean toggleReservation() { - SelenideElement reserved = MobileUi.byIdOrAccessibility("reserved"); - boolean before = isChecked(reserved); - reserved.click(); - return isChecked(reserved) != before; - } - - private boolean isChecked(SelenideElement element) { - String checked = element.getAttribute("checked"); - if (checked != null) { - return Boolean.parseBoolean(checked); - } - String selected = element.getAttribute("selected"); - return Boolean.parseBoolean(selected); - } - - private void openFirstUserWithGift() { - int maxPages = Integer.getInteger("users.scan.pages", 1); - for (int page = 0; page < maxPages; page++) { - ElementsCollection users = MobileUi.allById("user_item"); - if (users.isEmpty()) { - users = MobileUi.allById("list_item"); - } - for (int i = 0; i < users.size(); i++) { - users.get(i).click(); - if (openFirstWishlistWithGift()) { - return; - } - returnToUsersList(); - } - String scrollForward = "new UiScrollable(new UiSelector().scrollable(true)).scrollForward()"; - $(AppiumBy.androidUIAutomator(scrollForward)); - } - openFirstUser(); - } - - private boolean hasWishlists() { - return MobileUi.existsById("wishlist_item") - || MobileUi.existsById("list_item"); - } - - private boolean openFirstWishlistWithGift() { - if (!hasWishlists()) { - return false; - } - openFirstWishlist(); - if (hasGifts()) { - return true; - } - MobileUi.back(); - return false; - } - - private boolean hasGifts() { - if (MobileUi.existsById("gift_item")) { - return true; - } - return MobileUi.existsById("list_item"); - } - - private void returnToUsersList() { - for (int i = 0; i < 2; i++) { - if (MobileUi.existsById("users_content") - || MobileUi.existsById("user_item")) { - return; - } - MobileUi.back(); - } - } -} diff --git a/src/test/java/ru/otus/mobile/screens/WishlistsScreen.java b/src/test/java/ru/otus/mobile/screens/WishlistsScreen.java deleted file mode 100644 index e3b5e82..0000000 --- a/src/test/java/ru/otus/mobile/screens/WishlistsScreen.java +++ /dev/null @@ -1,155 +0,0 @@ -package ru.otus.mobile.screens; - -import com.codeborne.selenide.ElementsCollection; -import com.codeborne.selenide.SelenideElement; -import io.appium.java_client.AppiumBy; -import org.openqa.selenium.WebElement; -import ru.otus.mobile.config.AuthContext; -import ru.otus.mobile.config.MobileContext; -import ru.otus.mobile.ui.MobileUi; -import ru.otus.mobile.ui.UiWait; - -import static com.codeborne.selenide.Selenide.$; - -public class WishlistsScreen { - - public void open() { - ensureLoggedIn(); - if (MobileUi.existsById("wishlists_content")) { - return; - } - dismissErrorDialogIfPresent(); - if (MobileUi.existsById("mine_menu")) { - MobileUi.byIdOrAccessibility("mine_menu").click(); - } else if (MobileUi.existsById("go_to_wishlists")) { - MobileUi.byIdOrAccessibility("go_to_wishlists").click(); - } - } - - private void dismissErrorDialogIfPresent() { - if (UiWait.exists(AppiumBy.id("android:id/alertTitle")) - || UiWait.exists(AppiumBy.id("android:id/message"))) { - $(AppiumBy.id("android:id/button1")).click(); - } - } - - public void createWishlist(String name) { - if (!MobileUi.existsById("add_button")) { - open(); - } - MobileUi.byIdOrAccessibility("add_button").click(); - typeInto("title_input", name); - MobileUi.byIdOrAccessibility("save_button").click(); - } - - public void editWishlist(String oldName, String newName) { - for (int attempt = 0; attempt < 3; attempt++) { - if (MobileUi.existsById("wishlists_content") - || MobileUi.existsById("wishlist_item") - || MobileUi.existsById("list_item")) { - break; - } - MobileUi.back(); - } - ensureWishlistsList(); - scrollToWishlistTitle(oldName); - for (int attempt = 0; attempt < 2; attempt++) { - editButtonFor(oldName).click(); - break; - } - typeInto("title_input", newName); - MobileUi.byIdOrAccessibility("save_button").click(); - } - - public void assertWishlistPresent(String name) { - scrollToWishlistTitle(name); - } - - private SelenideElement scrollToWishlistTitle(String name) { - String pkg = MobileContext.packageName(); - String safe = name.replace("\"", "\\\""); - String selector = "new UiScrollable(new UiSelector().resourceId(\"" - + pkg + ":id/wishlists\")).scrollIntoView(" - + "new UiSelector().resourceId(\"" + pkg + ":id/title\").textContains(\"" + safe + "\"))"; - try { - return $(AppiumBy.androidUIAutomator(selector)); - } catch (Exception ignored) { - String fallback = "new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(" - + "new UiSelector().textContains(\"" + safe + "\"))"; - return $(AppiumBy.androidUIAutomator(fallback)); - } - } - - private SelenideElement editButtonFor(String name) { - String pkg = MobileContext.packageName(); - String safe = name.replace("\"", "\\\""); - String xpath = "//androidx.recyclerview.widget.RecyclerView[@resource-id='" + pkg + ":id/wishlists']" - + "//android.view.ViewGroup[@resource-id='" + pkg + ":id/wishlist_item'" - + " and .//android.widget.TextView[@resource-id='" + pkg + ":id/title'" - + " and contains(@text,\"" + safe + "\")]]" - + "//android.widget.Button[@resource-id='" + pkg + ":id/edit_button']"; - return $(AppiumBy.xpath(xpath)); - } - - public void openWishlistByName(String name) { - SelenideElement byText = MobileUi.byTextContains(name); - if (byText.exists()) { - byText.click(); - return; - } - openFirstWishlist(); - } - - public void openFirstWishlist() { - ElementsCollection items = MobileUi.allById("wishlist_item"); - if (items.isEmpty()) { - items = MobileUi.allById("list_item"); - } - items.first().click(); - } - - private void typeInto(String id, String value) { - WebElement input = MobileUi.byIdOrAccessibility(id) - .toWebElement(); - input.click(); - input.clear(); - input.sendKeys(value); - } - - private void ensureWishlistsList() { - for (int attempt = 0; attempt < 2; attempt++) { - if (MobileUi.existsById("gifts_content") - || MobileUi.existsById("gift_item")) { - MobileUi.back(); - continue; - } - if (MobileUi.existsById("wishlists_content") - || MobileUi.existsById("wishlist_item") - || MobileUi.existsById("list_item")) { - return; - } - MobileUi.back(); - } - if (MobileUi.existsById("wishlist_item") - || MobileUi.existsById("list_item")) { - return; - } - MobileUi.byIdOrAccessibility("wishlists_content"); - } - - private void ensureLoggedIn() { - LoginScreen loginScreen = new LoginScreen(); - if (!loginScreen.isVisible()) { - return; - } - String username = AuthContext.username(); - String password = AuthContext.password(); - boolean allowRegister = AuthContext.allowRegister(); - if (password == null && username != null) { - password = "Admin123"; - } - if (username != null && password != null) { - loginScreen.loginOrRegister(username, password, allowRegister); - } - } -} diff --git a/src/test/java/ru/otus/mobile/tests/BaseMobileTest.java b/src/test/java/ru/otus/mobile/tests/BaseMobileTest.java deleted file mode 100644 index fd7643d..0000000 --- a/src/test/java/ru/otus/mobile/tests/BaseMobileTest.java +++ /dev/null @@ -1,132 +0,0 @@ -package ru.otus.mobile.tests; - -import com.codeborne.selenide.Configuration; -import com.codeborne.selenide.WebDriverRunner; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import ru.otus.mobile.db.DbClient; -import ru.otus.mobile.db.DbConfig; -import ru.otus.mobile.config.MobileConfig; -import ru.otus.mobile.config.AuthContext; -import ru.otus.mobile.config.MobileContext; -import ru.otus.mobile.driver.MobileDriverFactory; -import ru.otus.mobile.screens.LoginScreen; -import ru.otus.mobile.ui.MobileUi; -import ru.otus.mobile.util.TestAccount; - -public abstract class BaseMobileTest { - protected MobileConfig config; - - @BeforeEach - void setUp() { - config = MobileConfig.load(); - MobileContext.init(config); - Configuration.timeout = 0; - Configuration.browserSize = null; - ensureCredentials(); - setActiveCredentials(); - resetData(); - WebDriverRunner.setWebDriver(MobileDriverFactory.create(config)); - loginIfNeeded(); - } - - @AfterEach - void tearDown() { - WebDriverRunner.closeWebDriver(); - } - - protected void loginIfNeeded() { - LoginScreen loginScreen = new LoginScreen(); - if (loginScreen.isVisible()) { - loginScreen.loginOrRegister(accountUsername(), accountPassword(), allowRegister()); - return; - } - if (MobileUi.existsById("wishlists_content") - || MobileUi.existsById("users_content")) { - return; - } - throw new IllegalStateException("App did not reach login or main screen."); - } - - protected abstract TestAccount account(); - - private void resetData() { - DbConfig dbConfig = DbConfig.load(); - String sql = account().resetSql() != null ? account().resetSql() : dbConfig.resetSql(); - DbClient client = new DbClient(dbConfig); - if (account() == TestAccount.RESERVATION) { - client.resetReservationData(accountUsername(), reservationOwnerUsername(), sql); - } else { - client.resetUserData(accountUsername(), sql); - } - } - - private String accountUsername() { - String accountKey = account().name().toLowerCase(); - String prop = System.getProperty("login.username." + accountKey); - if (prop != null && !prop.isBlank()) { - return prop; - } - String env = System.getenv("LOGIN_USERNAME_" + accountKey.toUpperCase()); - if (env != null && !env.isBlank()) { - return env; - } - return account().username(); - } - - private String accountPassword() { - String accountKey = account().name().toLowerCase(); - String prop = System.getProperty("login.password." + accountKey); - if (prop != null && !prop.isBlank()) { - return prop; - } - String env = System.getenv("LOGIN_PASSWORD_" + accountKey.toUpperCase()); - if (env != null && !env.isBlank()) { - return env; - } - String common = System.getProperty("login.password"); - if (common != null && !common.isBlank()) { - return common; - } - return "Admin123"; - } - - private boolean allowRegister() { - return System.getProperty("login.auto." + account().name().toLowerCase()) != null; - } - - private void ensureCredentials() { - String accountKey = account().name().toLowerCase(); - String prop = System.getProperty("login.username." + accountKey); - String env = System.getenv("LOGIN_USERNAME_" + accountKey.toUpperCase()); - if ((prop == null || prop.isBlank()) && (env == null || env.isBlank())) { - String generated = "auto_" + accountKey + "_" + System.currentTimeMillis(); - System.setProperty("login.username." + accountKey, generated); - System.setProperty("login.password." + accountKey, "Admin123"); - System.setProperty("login.auto." + accountKey, "true"); - } - if (account() == TestAccount.RESERVATION && System.getProperty("reservation.owner") == null) { - System.setProperty("reservation.owner", "user4us"); - } - } - - private void setActiveCredentials() { - System.setProperty("login.account.active", account().name().toLowerCase()); - System.setProperty("login.username.active", accountUsername()); - System.setProperty("login.password.active", accountPassword()); - System.setProperty("login.auto.active", Boolean.toString(allowRegister())); - AuthContext.init(accountUsername(), accountPassword(), allowRegister()); - } - - private String reservationOwnerUsername() { - String prop = System.getProperty("reservation.owner"); - if (prop != null && !prop.isBlank()) { - return prop; - } - String env = System.getenv("RESERVATION_OWNER"); - if (env != null && !env.isBlank()) { - return env; - } - return "user4us"; - } -} diff --git a/src/test/java/ru/otus/mobile/tests/GiftsTest.java b/src/test/java/ru/otus/mobile/tests/GiftsTest.java index 4029f78..455b419 100644 --- a/src/test/java/ru/otus/mobile/tests/GiftsTest.java +++ b/src/test/java/ru/otus/mobile/tests/GiftsTest.java @@ -1,34 +1,41 @@ package ru.otus.mobile.tests; +import com.google.inject.Inject; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import ru.otus.mobile.screens.GiftsScreen; -import ru.otus.mobile.screens.WishlistsScreen; -import ru.otus.mobile.util.TestAccount; +import org.junit.jupiter.api.extension.ExtendWith; +import ru.otus.mobile.annotations.MobileUser; +import ru.otus.mobile.config.TestAccount; +import ru.otus.mobile.extensions.MobileExtension; +import ru.otus.mobile.page.GiftsPage; +import ru.otus.mobile.page.WishlistsPage; import ru.otus.mobile.util.TestData; -public class GiftsTest extends BaseMobileTest { +@ExtendWith(MobileExtension.class) +@MobileUser(TestAccount.GIFTS) +public class GiftsTest { + @Inject + private WishlistsPage wishlistsPage; + + @Inject + private GiftsPage giftsPage; + + @Inject + private TestData testData; + @Test @DisplayName("Создание и редактирование подарка") void createAndEditGift() { - WishlistsScreen wishlists = new WishlistsScreen(); - GiftsScreen gifts = new GiftsScreen(); - String wishlistName = TestData.uniqueName("Wishlist"); - String name = TestData.uniqueName("Gift"); + String wishlistName = testData.uniqueName("Wishlist"); + String name = testData.uniqueName("Gift"); String updated = name + " updated"; - wishlists.open(); - wishlists.createWishlist(wishlistName); - wishlists.openWishlistByName(wishlistName); - gifts.open(); - gifts.createGift(name); - gifts.assertGiftPresent(name); - gifts.editGift(name, updated); - gifts.assertGiftPresent(updated); - } - - @Override - protected TestAccount account() { - return TestAccount.GIFTS; + wishlistsPage.open(); + wishlistsPage.createWishlist(wishlistName); + wishlistsPage.openWishlist(wishlistName); + giftsPage.createGift(name); + giftsPage.shouldSeeGift(name); + giftsPage.editGift(name, updated); + giftsPage.shouldSeeGift(updated); } } diff --git a/src/test/java/ru/otus/mobile/tests/ReservationTest.java b/src/test/java/ru/otus/mobile/tests/ReservationTest.java index b0d0815..982a5a1 100644 --- a/src/test/java/ru/otus/mobile/tests/ReservationTest.java +++ b/src/test/java/ru/otus/mobile/tests/ReservationTest.java @@ -1,27 +1,35 @@ package ru.otus.mobile.tests; +import com.google.inject.Inject; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import ru.otus.mobile.screens.UsersScreen; -import ru.otus.mobile.util.TestAccount; +import org.junit.jupiter.api.extension.ExtendWith; +import ru.otus.mobile.annotations.MobileUser; +import ru.otus.mobile.config.MobileConfig; +import ru.otus.mobile.config.TestAccount; +import ru.otus.mobile.extensions.MobileExtension; +import ru.otus.mobile.page.UsersPage; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +@ExtendWith(MobileExtension.class) +@MobileUser(TestAccount.RESERVATION) +public class ReservationTest { + @Inject + private UsersPage usersPage; + + @Inject + private MobileConfig mobileConfig; -public class ReservationTest extends BaseMobileTest { @Test @DisplayName("Изменение статуса резервирования подарка другого пользователя") void changeReservationStatus() { - UsersScreen users = new UsersScreen(); - users.open(); - String owner = System.getProperty("reservation.owner", "user4us"); - users.openUserWithGift(owner); - users.openFirstGift(); - boolean changed = users.toggleReservation(); - assertTrue(changed, "Reservation status was not changed"); - } - - @Override - protected TestAccount account() { - return TestAccount.RESERVATION; + usersPage.open(); + usersPage.openUser(mobileConfig.reservationOwnerUsername()); + usersPage.openFirstWishlist(); + usersPage.openFirstGift(); + boolean before = usersPage.isReserved(); + usersPage.toggleReservation(); + assertNotEquals(before, usersPage.isReserved(), "Reservation status was not changed"); } } diff --git a/src/test/java/ru/otus/mobile/tests/WishlistsTest.java b/src/test/java/ru/otus/mobile/tests/WishlistsTest.java index f5b9c4f..4cfec77 100644 --- a/src/test/java/ru/otus/mobile/tests/WishlistsTest.java +++ b/src/test/java/ru/otus/mobile/tests/WishlistsTest.java @@ -1,28 +1,34 @@ package ru.otus.mobile.tests; +import com.google.inject.Inject; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import ru.otus.mobile.screens.WishlistsScreen; -import ru.otus.mobile.util.TestAccount; +import org.junit.jupiter.api.extension.ExtendWith; +import ru.otus.mobile.annotations.MobileUser; +import ru.otus.mobile.config.TestAccount; +import ru.otus.mobile.extensions.MobileExtension; +import ru.otus.mobile.page.WishlistsPage; import ru.otus.mobile.util.TestData; -public class WishlistsTest extends BaseMobileTest { +@ExtendWith(MobileExtension.class) +@MobileUser(TestAccount.WISHLISTS) +public class WishlistsTest { + @Inject + private WishlistsPage wishlistsPage; + + @Inject + private TestData testData; + @Test @DisplayName("Создание и редактирование списка желаний") void createAndEditWishlist() { - WishlistsScreen wishlists = new WishlistsScreen(); - String name = TestData.uniqueName("Wishlist"); + String name = testData.uniqueName("Wishlist"); String updated = name + " updated"; - wishlists.open(); - wishlists.createWishlist(name); - wishlists.assertWishlistPresent(name); - wishlists.editWishlist(name, updated); - wishlists.assertWishlistPresent(updated); - } - - @Override - protected TestAccount account() { - return TestAccount.WISHLISTS; + wishlistsPage.open(); + wishlistsPage.createWishlist(name); + wishlistsPage.shouldSeeWishlist(name); + wishlistsPage.editWishlist(name, updated); + wishlistsPage.shouldSeeWishlist(updated); } } diff --git a/src/test/java/ru/otus/mobile/ui/MobileSelectors.java b/src/test/java/ru/otus/mobile/ui/MobileSelectors.java deleted file mode 100644 index ad182a4..0000000 --- a/src/test/java/ru/otus/mobile/ui/MobileSelectors.java +++ /dev/null @@ -1,23 +0,0 @@ -package ru.otus.mobile.ui; - -import io.appium.java_client.AppiumBy; -import org.openqa.selenium.By; -import ru.otus.mobile.config.MobileContext; - -public final class MobileSelectors { - private MobileSelectors() { - } - - public static By id(String id) { - return AppiumBy.id(MobileContext.packageName() + ":id/" + id); - } - - public static By accessibilityId(String id) { - return AppiumBy.accessibilityId(id); - } - - public static By textContains(String text) { - String safeText = text.replace("\"", "\\\""); - return AppiumBy.androidUIAutomator("new UiSelector().textContains(\"" + safeText + "\")"); - } -} diff --git a/src/test/java/ru/otus/mobile/ui/MobileUi.java b/src/test/java/ru/otus/mobile/ui/MobileUi.java deleted file mode 100644 index e49aabc..0000000 --- a/src/test/java/ru/otus/mobile/ui/MobileUi.java +++ /dev/null @@ -1,96 +0,0 @@ -package ru.otus.mobile.ui; - -import com.codeborne.selenide.ElementsCollection; -import com.codeborne.selenide.SelenideElement; -import com.codeborne.selenide.WebDriverRunner; -import io.appium.java_client.AppiumBy; -import io.appium.java_client.android.AndroidDriver; -import io.appium.java_client.android.nativekey.AndroidKey; -import io.appium.java_client.android.nativekey.KeyEvent; -import org.openqa.selenium.JavascriptExecutor; -import org.openqa.selenium.remote.RemoteWebElement; - -import java.util.HashMap; -import java.util.Map; - -import static com.codeborne.selenide.Selenide.$; -import static com.codeborne.selenide.Selenide.$$; - -public final class MobileUi { - private MobileUi() { - } - - public static SelenideElement byId(String id) { - return $(MobileSelectors.id(id)); - } - - public static ElementsCollection allById(String id) { - return $$(MobileSelectors.id(id)); - } - - public static SelenideElement byIdOrAccessibility(String id) { - if (UiWait.exists(MobileSelectors.id(id))) { - return $(MobileSelectors.id(id)); - } - if (UiWait.exists(MobileSelectors.accessibilityId(id))) { - return $(MobileSelectors.accessibilityId(id)); - } - return $(MobileSelectors.id(id)); - } - - public static SelenideElement byTextContains(String text) { - return $(MobileSelectors.textContains(text)); - } - - public static boolean existsById(String id) { - return UiWait.exists(MobileSelectors.id(id)); - } - - public static SelenideElement byIdOrText(String id, String text) { - ElementsCollection byId = allById(id); - if (!byId.isEmpty()) { - return byId.first(); - } - return $(MobileSelectors.textContains(text)); - } - - public static SelenideElement byIdOrTextOrAccessibility(String id, String text) { - ElementsCollection byId = allById(id); - if (!byId.isEmpty()) { - return byId.first(); - } - ElementsCollection byA11y = $$(AppiumBy.accessibilityId(id)); - if (!byA11y.isEmpty()) { - return byA11y.first(); - } - return $(MobileSelectors.textContains(text)); - } - - public static void back() { - AndroidDriver driver = (AndroidDriver) WebDriverRunner.getWebDriver(); - driver.pressKey(new KeyEvent(AndroidKey.BACK)); - } - - public static void longPress(SelenideElement element) { - String elementId = ((RemoteWebElement) element.toWebElement()).getId(); - Map params = new HashMap<>(); - params.put("elementId", elementId); - params.put("duration", 1000); - ((JavascriptExecutor) WebDriverRunner.getWebDriver()) - .executeScript("mobile: longClickGesture", params); - } - - public static void hideKeyboard() { - AndroidDriver driver = (AndroidDriver) WebDriverRunner.getWebDriver(); - try { - driver.hideKeyboard(); - } catch (Exception ignored) { - } - } - - public static void restartApp(String appPackage) { - AndroidDriver driver = (AndroidDriver) WebDriverRunner.getWebDriver(); - driver.terminateApp(appPackage); - driver.activateApp(appPackage); - } -} diff --git a/src/test/java/ru/otus/mobile/ui/UiWait.java b/src/test/java/ru/otus/mobile/ui/UiWait.java deleted file mode 100644 index 7df55ea..0000000 --- a/src/test/java/ru/otus/mobile/ui/UiWait.java +++ /dev/null @@ -1,14 +0,0 @@ -package ru.otus.mobile.ui; - -import org.openqa.selenium.By; - -import static com.codeborne.selenide.Selenide.$; - -public final class UiWait { - private UiWait() { - } - - public static boolean exists(By locator) { - return $(locator).exists(); - } -} diff --git a/src/test/java/ru/otus/mobile/util/TestAccount.java b/src/test/java/ru/otus/mobile/util/TestAccount.java deleted file mode 100644 index 90bd12d..0000000 --- a/src/test/java/ru/otus/mobile/util/TestAccount.java +++ /dev/null @@ -1,77 +0,0 @@ -package ru.otus.mobile.util; - -public enum TestAccount { - WISHLISTS("hw7_wishlists_user", null), - GIFTS("hw7_gifts_user", null), - RESERVATION("hw7_reservation_user", """ - WITH owner_user AS ( - SELECT id FROM users WHERE username = ? - ), - owner_insert AS ( - INSERT INTO users (id, email, password, username) - SELECT gen_random_uuid(), ?, ?, ? - WHERE NOT EXISTS (SELECT 1 FROM owner_user) - RETURNING id - ), - owner AS ( - SELECT id FROM owner_user - UNION ALL - SELECT id FROM owner_insert - ), - target_user_existing AS ( - SELECT id FROM users WHERE username = ? - ), - target_user_insert AS ( - INSERT INTO users (id, email, password, username) - SELECT gen_random_uuid(), ?, ?, ? - WHERE NOT EXISTS (SELECT 1 FROM target_user_existing) - RETURNING id - ), - target_user AS ( - SELECT id FROM target_user_existing - UNION ALL - SELECT id FROM target_user_insert - ), - del_owner_gifts AS ( - DELETE FROM gifts - WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id = (SELECT id FROM owner)) - ), - del_owner_wishlists AS ( - DELETE FROM wishlists WHERE user_id = (SELECT id FROM owner) - ), - del_user_gifts AS ( - DELETE FROM gifts - WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM target_user)) - ), - del_user_wishlists AS ( - DELETE FROM wishlists WHERE user_id IN (SELECT id FROM target_user) - ), - owner_wishlist AS ( - INSERT INTO wishlists (id, title, description, user_id) - SELECT gen_random_uuid(), 'Owner Wishlist', 'Auto', (SELECT id FROM owner) - WHERE EXISTS (SELECT 1 FROM owner) - RETURNING id - ), - owner_gift AS ( - INSERT INTO gifts (id, name, description, price, is_reserved, store_url, image_url, wish_id) - SELECT gen_random_uuid(), 'Owner Gift', 'Auto', 100, false, NULL, NULL, (SELECT id FROM owner_wishlist) - ) - SELECT 1; - """); - - private final String username; - private final String resetSql; - - TestAccount(String username, String resetSql) { - this.username = username; - this.resetSql = resetSql; - } - - public String username() { - return username; - } - - public String resetSql() { - return resetSql; - } -} diff --git a/src/test/java/ru/otus/mobile/util/TestData.java b/src/test/java/ru/otus/mobile/util/TestData.java deleted file mode 100644 index 304e631..0000000 --- a/src/test/java/ru/otus/mobile/util/TestData.java +++ /dev/null @@ -1,19 +0,0 @@ -package ru.otus.mobile.util; - -import java.security.SecureRandom; -import java.time.Instant; - -public final class TestData { - private static final SecureRandom RANDOM = new SecureRandom(); - - private TestData() { - } - - public static String uniqueName(String prefix) { - long ts = Instant.now().getEpochSecond(); - int rnd = RANDOM.nextInt(36 * 36); - String tsPart = Long.toString(ts, 36); - String rndPart = String.format("%2s", Integer.toString(rnd, 36)).replace(' ', '0'); - return prefix + "-" + tsPart + rndPart; - } -} diff --git a/wiremock/mappings/wishlist-apk.json b/wiremock/mappings/wishlist-apk.json new file mode 100644 index 0000000..ce599fa --- /dev/null +++ b/wiremock/mappings/wishlist-apk.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "url": "/wishlist.apk" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.android.package-archive" + }, + "bodyFileName": "wishlist.apk" + } +}