Compare commits
9 Commits
6ca010ed17
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e731a73838 | |||
| 97c58a29e0 | |||
| bcf831ad99 | |||
| 46f3de4d55 | |||
| aaf63cc438 | |||
| 0cd4e29d02 | |||
| 6a19b574b8 | |||
| ea52943000 | |||
| 8857b4feff |
@@ -0,0 +1,4 @@
|
|||||||
|
DB_URL=jdbc:postgresql://<host>:<port>/<db>
|
||||||
|
DB_USER=<db_user>
|
||||||
|
DB_PASSWORD=<db_password>
|
||||||
|
MOBILE_HOST=127.0.0.1
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
DB_URL=jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist
|
||||||
|
DB_USER=<prod_db_user>
|
||||||
|
DB_PASSWORD=<prod_db_password>
|
||||||
|
MOBILE_HOST=127.0.0.1
|
||||||
@@ -3,4 +3,6 @@ build/
|
|||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
allure-results/
|
||||||
logcat.txt
|
logcat.txt
|
||||||
|
.env
|
||||||
|
|||||||
@@ -1,137 +1,88 @@
|
|||||||
# 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.
|
- `docker-compose` поднимает `wiremock` и 2 Android-эмулятора (`android-emulator-1`, `android-emulator-2`) для параллельного запуска.
|
||||||
- Selenide: 7.3.1
|
- APK хранится в `wiremock/__files/wishlist.apk` и устанавливается через Appium capability `app`.
|
||||||
- Appium Java Client: 9.3.0 (UiAutomator2)
|
- DI на `Guice`, запуск через `JUnit 5 Extension`, балансировка эмуляторов через `BlockingQueue`.
|
||||||
- Selenium: 4.25.0
|
- Подготовка тестовых данных выполняется через JDBC перед каждым тестом.
|
||||||
- JUnit: 5.10.2
|
- Логи `logcat` сохраняются в `logcat.txt` через Appium logs API (без ADB-скриптов).
|
||||||
- PostgreSQL JDBC: 42.7.10
|
|
||||||
- apk-parser: 2.6.10
|
|
||||||
- SLF4J: 2.0.13
|
|
||||||
- Docker Compose (эмулятор Android + Appium)
|
|
||||||
|
|
||||||
## Структура проекта
|
## Структура
|
||||||
- `src/test/java/ru/otus/mobile/tests` — тесты.
|
- `src/main/java` — инфраструктура, конфиги, page/component object.
|
||||||
- `src/test/java/ru/otus/mobile/screens` — экранные объекты.
|
- `src/test/java` — только тестовые классы.
|
||||||
- `src/test/java/ru/otus/mobile/config` — конфигурация Appium/Android.
|
- `wiremock` — маппинги и APK.
|
||||||
- `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)
|
## Запуск
|
||||||
Эмулятор и Appium поднимаются одной командой:
|
1. Поднять окружение:
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Порты:
|
2. Убедиться, что сервисы `wiremock`, `android-emulator-1`, `android-emulator-2` имеют статус `healthy`:
|
||||||
- Appium: `http://localhost:4723`
|
|
||||||
- noVNC: `http://localhost:6080`
|
|
||||||
|
|
||||||
APK берется из файла `./wishlist-349317-5fd795.apk`, копируется в `./apk/wishlist.apk` и монтируется в контейнер по пути `/apk/wishlist.apk`.
|
|
||||||
После старта контейнеров APK автоматически устанавливается в эмулятор и приложение стартует.
|
|
||||||
|
|
||||||
## Подготовка данных через БД
|
|
||||||
Для повторяемости тестов перед каждым тестом выполняется SQL‑сброс данных аккаунта.
|
|
||||||
Данные доступа к БД **не хранятся в репозитории** — нужно передать через env или `-D`.
|
|
||||||
Аккаунты можно создать заранее (регистрация через приложение) или доверить это автотестам.
|
|
||||||
Если логин/пароль не переданы через параметры запуска, тест сгенерирует логины, создаст пользователей через reset‑SQL в БД (если их нет), затем выполнит логин. Регистрация через приложение используется как fallback, если вход не удался.
|
|
||||||
|
|
||||||
Переменные/свойства:
|
|
||||||
- `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://<host>:5432/<db>"
|
|
||||||
$env:DB_USER = "<user>"
|
|
||||||
$env:DB_PASSWORD = "<password>"
|
|
||||||
$env:DB_RESET_SQL = "<one-statement reset sql with ? placeholders>"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Как запускать
|
|
||||||
### Полный прогон (обязательная последовательность)
|
|
||||||
1) Поднять инфраструктуру:
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose ps
|
||||||
```
|
```
|
||||||
Убедиться, что Docker Desktop/Engine запущен. Если Appium недоступен, тесты падают быстро с понятной ошибкой.
|
|
||||||
|
|
||||||
2) Задать доступ к БД (обязательно, иначе тесты упадут):
|
3. Подготовить переменные окружения (использовать один из вариантов):
|
||||||
|
|
||||||
|
Вариант A: через `.env`
|
||||||
|
- скопировать `.env.example` в `.env`;
|
||||||
|
- заполнить `.env` актуальными значениями вашей среды;
|
||||||
|
- загрузить `.env` в текущую shell-сессию.
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
```powershell
|
```powershell
|
||||||
$env:DB_URL="jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist"
|
Get-Content .env | Where-Object { $_ -match '^[^#].+=.+' } | ForEach-Object {
|
||||||
$env:DB_USER="student"
|
$name, $value = $_ -split '=', 2
|
||||||
$env:DB_PASSWORD="student"
|
[System.Environment]::SetEnvironmentVariable($name, $value, 'Process')
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
3) Запустить все тесты (логины/пароли можно не задавать):
|
bash:
|
||||||
|
```bash
|
||||||
|
set -a
|
||||||
|
source .env
|
||||||
|
set +a
|
||||||
|
```
|
||||||
|
|
||||||
|
Вариант B: напрямую в shell-сессии
|
||||||
|
|
||||||
|
PowerShell:
|
||||||
|
```powershell
|
||||||
|
$env:DB_URL="jdbc:postgresql://<host>:<port>/<db>"
|
||||||
|
$env:DB_USER="<db_user>"
|
||||||
|
$env:DB_PASSWORD="<db_password>"
|
||||||
|
$env:MOBILE_HOST="127.0.0.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
bash:
|
||||||
|
```bash
|
||||||
|
export DB_URL="jdbc:postgresql://<host>:<port>/<db>"
|
||||||
|
export DB_USER="<db_user>"
|
||||||
|
export DB_PASSWORD="<db_password>"
|
||||||
|
export MOBILE_HOST="127.0.0.1"
|
||||||
|
```
|
||||||
|
Оба варианта эквивалентны: тесты используют переменные окружения процесса.
|
||||||
|
|
||||||
|
Тестовые пользователи зафиксированы в коде (`TestAccount`):
|
||||||
|
- `user1us` — тест списков желаний;
|
||||||
|
- `user2us` — тест подарков;
|
||||||
|
- `user3us` — пользователь, который резервирует подарок;
|
||||||
|
- `user4us` — пользователь-владелец списка в тесте резервирования.
|
||||||
|
|
||||||
|
Для production-подобного запуска используйте шаблон `.env.production.example` и не коммитьте реальные значения в репозиторий.
|
||||||
|
|
||||||
|
Тесты запускаются параллельно по классам (2 потока) и распределяются по эмуляторам через очередь.
|
||||||
|
4. Запустить тесты:
|
||||||
```bash
|
```bash
|
||||||
mvn test
|
mvn test
|
||||||
```
|
```
|
||||||
|
|
||||||
Если логины не заданы, они будут сгенерированы автоматически, пользователь будет создан через БД (если отсутствует) и выполнится вход.
|
Примечание: эмуляторы зафиксированы в enum `TestEmulator` (порты `4723` и `4725`), в конфигурации задается только хост (`MOBILE_HOST`).
|
||||||
Регистрация через приложение используется только как fallback, если вход не удался.
|
|
||||||
|
|
||||||
Опционально: задать логины/пароли тестовых пользователей (если зарегистрированы вручную):
|
|
||||||
```bash
|
|
||||||
mvn "-Dlogin.username.wishlists=<user1>" "-Dlogin.password.wishlists=<pass1>" \
|
|
||||||
"-Dlogin.username.gifts=<user2>" "-Dlogin.password.gifts=<pass2>" \
|
|
||||||
"-Dlogin.username.reservation=<user3>" "-Dlogin.password.reservation=<pass3>" \
|
|
||||||
"-Dreservation.owner=<user4>" \
|
|
||||||
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://<host>:5432/<db> -Ddb.user=<user> -Ddb.password=<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.<account>` — логин для конкретного теста (например `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` в корне проекта.
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE module PUBLIC
|
||||||
|
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||||
|
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||||
|
|
||||||
|
<module name="Checker">
|
||||||
|
<property name="charset" value="UTF-8"/>
|
||||||
|
<property name="severity" value="error"/>
|
||||||
|
|
||||||
|
<module name="FileTabCharacter"/>
|
||||||
|
|
||||||
|
<module name="LineLength">
|
||||||
|
<property name="max" value="120"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<module name="TreeWalker">
|
||||||
|
<module name="TypeName"/>
|
||||||
|
<module name="MethodName"/>
|
||||||
|
<module name="ParameterName"/>
|
||||||
|
<module name="LocalVariableName"/>
|
||||||
|
<module name="MemberName"/>
|
||||||
|
<module name="AvoidStarImport"/>
|
||||||
|
<module name="UnusedImports"/>
|
||||||
|
<module name="RedundantImport"/>
|
||||||
|
<module name="NeedBraces"/>
|
||||||
|
<module name="WhitespaceAround"/>
|
||||||
|
<module name="WhitespaceAfter"/>
|
||||||
|
<module name="EmptyBlock"/>
|
||||||
|
</module>
|
||||||
|
</module>
|
||||||
+44
-53
@@ -1,67 +1,58 @@
|
|||||||
services:
|
services:
|
||||||
apk-downloader:
|
wiremock:
|
||||||
image: curlimages/curl:8.5.0
|
image: wiremock/wiremock:3.9.1
|
||||||
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
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./apk:/apk
|
- ./wiremock:/home/wiremock
|
||||||
- ./wishlist-349317-5fd795.apk:/apk-source/app.apk:ro
|
healthcheck:
|
||||||
android-emulator:
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/__admin/health | grep -q 'healthy'"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
|
||||||
|
android-emulator-1:
|
||||||
image: budtmo/docker-android:emulator_13.0
|
image: budtmo/docker-android:emulator_13.0
|
||||||
privileged: true
|
|
||||||
depends_on:
|
depends_on:
|
||||||
apk-downloader:
|
- wiremock
|
||||||
condition: service_completed_successfully
|
devices:
|
||||||
|
- /dev/kvm:/dev/kvm
|
||||||
ports:
|
ports:
|
||||||
- "4723:4723"
|
- "4723:4723"
|
||||||
- "6080:6080"
|
- "6080:6080"
|
||||||
- "5554:5554"
|
|
||||||
- "5555:5555"
|
|
||||||
environment:
|
environment:
|
||||||
- DEVICE=Pixel_5
|
- DEVICE=Pixel_5
|
||||||
- APPIUM=true
|
- APPIUM=true
|
||||||
- WEB_VNC=true
|
- WEB_VNC=true
|
||||||
- AUTO_GRANT_PERMISSIONS=true
|
|
||||||
- ENABLE_VNC=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
|
shm_size: 2gb
|
||||||
volumes:
|
healthcheck:
|
||||||
- ./apk:/apk:ro
|
test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ]"]
|
||||||
apk-installer:
|
interval: 15s
|
||||||
image: budtmo/docker-android:emulator_13.0
|
timeout: 5s
|
||||||
|
retries: 40
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
android-emulator-2:
|
||||||
|
image: budtmo/docker-android:emulator_12.0
|
||||||
depends_on:
|
depends_on:
|
||||||
android-emulator:
|
- wiremock
|
||||||
condition: service_started
|
devices:
|
||||||
entrypoint:
|
- /dev/kvm:/dev/kvm
|
||||||
- /bin/bash
|
ports:
|
||||||
- -lc
|
- "4725:4723"
|
||||||
command:
|
- "6081:6080"
|
||||||
- >
|
environment:
|
||||||
for i in $(seq 1 60); do
|
- DEVICE=Pixel_4
|
||||||
adb connect android-emulator:5555 || true;
|
- APPIUM=true
|
||||||
if adb devices | tr -s ' ' | grep -q "android-emulator:5555"; then
|
- WEB_VNC=true
|
||||||
break;
|
- ENABLE_VNC=true
|
||||||
fi;
|
- AUTO_GRANT_PERMISSIONS=true
|
||||||
sleep 2;
|
- EMULATOR_PARAMS=-no-window -no-audio -gpu swiftshader_indirect -no-snapshot -no-boot-anim
|
||||||
done;
|
shm_size: 2gb
|
||||||
adb -s android-emulator:5555 wait-for-device;
|
healthcheck:
|
||||||
for i in $(seq 1 120); do
|
test: ["CMD-SHELL", "[ \"$(cat /home/androidusr/device_status 2>/dev/null)\" = \"READY\" ]"]
|
||||||
if adb -s android-emulator:5555 shell getprop sys.boot_completed | tr -d '\r' | grep -q "1"; then
|
interval: 15s
|
||||||
break;
|
timeout: 5s
|
||||||
fi;
|
retries: 40
|
||||||
sleep 2;
|
start_period: 30s
|
||||||
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
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<groupId>ru.otus</groupId>
|
<groupId>ru.otus</groupId>
|
||||||
@@ -12,42 +13,33 @@
|
|||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<selenide.version>7.3.1</selenide.version>
|
<selenide.appium.version>7.3.1</selenide.appium.version>
|
||||||
<appium.version>9.3.0</appium.version>
|
|
||||||
<junit.version>5.10.2</junit.version>
|
<junit.version>5.10.2</junit.version>
|
||||||
<apkparser.version>2.6.10</apkparser.version>
|
|
||||||
<slf4j.version>2.0.13</slf4j.version>
|
<slf4j.version>2.0.13</slf4j.version>
|
||||||
<selenium.version>4.25.0</selenium.version>
|
<logback.version>1.5.6</logback.version>
|
||||||
<allure.version>2.29.1</allure.version>
|
<guice.version>7.0.0</guice.version>
|
||||||
|
<maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
|
||||||
|
<surefire.version>3.2.5</surefire.version>
|
||||||
|
<checkstyle.plugin.version>3.6.0</checkstyle.plugin.version>
|
||||||
|
<spotbugs.plugin.version>4.9.2.0</spotbugs.plugin.version>
|
||||||
|
<spotbugs.version>4.9.3</spotbugs.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.seleniumhq.selenium</groupId>
|
|
||||||
<artifactId>selenium-bom</artifactId>
|
|
||||||
<version>${selenium.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</dependencyManagement>
|
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.codeborne</groupId>
|
<groupId>com.codeborne</groupId>
|
||||||
<artifactId>selenide</artifactId>
|
<artifactId>selenide-appium</artifactId>
|
||||||
<version>${selenide.version}</version>
|
<version>${selenide.appium.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.appium</groupId>
|
<groupId>com.google.inject</groupId>
|
||||||
<artifactId>java-client</artifactId>
|
<artifactId>guice</artifactId>
|
||||||
<version>${appium.version}</version>
|
<version>${guice.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.dongliu</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>apk-parser</artifactId>
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
<version>${apkparser.version}</version>
|
<version>${junit.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
@@ -56,15 +48,14 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.qameta.allure</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>allure-junit5</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
<version>${allure.version}</version>
|
<version>${slf4j.version}</version>
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>ch.qos.logback</groupId>
|
||||||
<artifactId>slf4j-simple</artifactId>
|
<artifactId>logback-classic</artifactId>
|
||||||
<version>${slf4j.version}</version>
|
<version>${logback.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -77,12 +68,69 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>${maven.compiler.plugin.version}</version>
|
||||||
|
<configuration>
|
||||||
|
<release>${maven.compiler.source}</release>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
<version>3.2.5</version>
|
<version>${surefire.version}</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<useModulePath>false</useModulePath>
|
<useModulePath>false</useModulePath>
|
||||||
|
<systemPropertyVariables>
|
||||||
|
<junit.jupiter.execution.parallel.enabled>true</junit.jupiter.execution.parallel.enabled>
|
||||||
|
<junit.jupiter.execution.parallel.mode.default>same_thread</junit.jupiter.execution.parallel.mode.default>
|
||||||
|
<junit.jupiter.execution.parallel.mode.classes.default>concurrent</junit.jupiter.execution.parallel.mode.classes.default>
|
||||||
|
<junit.jupiter.execution.parallel.config.strategy>fixed</junit.jupiter.execution.parallel.config.strategy>
|
||||||
|
<junit.jupiter.execution.parallel.config.fixed.parallelism>2</junit.jupiter.execution.parallel.config.fixed.parallelism>
|
||||||
|
</systemPropertyVariables>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||||
|
<version>${checkstyle.plugin.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>checkstyle-validation</id>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<configLocation>checkstyle.xml</configLocation>
|
||||||
|
<consoleOutput>true</consoleOutput>
|
||||||
|
<failsOnError>true</failsOnError>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.github.spotbugs</groupId>
|
||||||
|
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||||
|
<version>${spotbugs.plugin.version}</version>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.spotbugs</groupId>
|
||||||
|
<artifactId>spotbugs</artifactId>
|
||||||
|
<version>${spotbugs.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>verify</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<FindBugsFilter>
|
||||||
|
<Match>
|
||||||
|
<Source name="~.*src/test/java/.*"/>
|
||||||
|
</Match>
|
||||||
|
<Match>
|
||||||
|
<Class name="ru.otus.mobile.config.TestContext"/>
|
||||||
|
<Bug pattern="EI_EXPOSE_REP,EI_EXPOSE_REP2"/>
|
||||||
|
</Match>
|
||||||
|
<Match>
|
||||||
|
<Class name="ru.otus.mobile.driver.MobileSession"/>
|
||||||
|
<Bug pattern="EI_EXPOSE_REP2"/>
|
||||||
|
</Match>
|
||||||
|
<Match>
|
||||||
|
<Class name="ru.otus.mobile.driver.MobileSessionFactory"/>
|
||||||
|
<Bug pattern="EI_EXPOSE_REP2"/>
|
||||||
|
</Match>
|
||||||
|
</FindBugsFilter>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
public final class AlertDialogComponent extends BaseMobileComponent {
|
||||||
|
private final SelenideElement positiveButton = byIdInRoot("android:id/button1");
|
||||||
|
|
||||||
|
public AlertDialogComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acceptIfVisible() {
|
||||||
|
if (positiveButton.exists()) {
|
||||||
|
positiveButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.ElementsCollection;
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
import com.codeborne.selenide.WebElementCondition;
|
||||||
|
import io.appium.java_client.AppiumBy;
|
||||||
|
import ru.otus.mobile.page.AbsPageObject;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public abstract class BaseMobileComponent extends AbsPageObject {
|
||||||
|
protected final SelenideElement root;
|
||||||
|
|
||||||
|
protected BaseMobileComponent(SelenideElement root) {
|
||||||
|
this.root = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SelenideElement byIdInRoot(String id) {
|
||||||
|
return root.$(AppiumBy.id(fullIdValue(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ElementsCollection allByIdInRoot(String id) {
|
||||||
|
return root.$$(AppiumBy.id(fullIdValue(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseMobileComponent shouldBe(WebElementCondition... conditions) {
|
||||||
|
root.shouldBe(conditions);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseMobileComponent shouldBe(WebElementCondition condition, Duration timeout) {
|
||||||
|
root.shouldBe(condition, timeout);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
public final class BottomNavigationComponent extends BaseMobileComponent {
|
||||||
|
private final SelenideElement mineMenu = byIdInRoot("mine_menu");
|
||||||
|
private final SelenideElement usersMenu = byIdInRoot("users_menu");
|
||||||
|
|
||||||
|
public BottomNavigationComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openWishlists() {
|
||||||
|
mineMenu.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openUsers() {
|
||||||
|
usersMenu.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
public final class GiftFormComponent extends BaseMobileComponent {
|
||||||
|
private final SelenideElement nameInput = byIdInRoot("name_input");
|
||||||
|
private final SelenideElement priceInput = byIdInRoot("price_input");
|
||||||
|
private final SelenideElement saveButton = byIdInRoot("save_button");
|
||||||
|
|
||||||
|
public GiftFormComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(String name, String price) {
|
||||||
|
nameInput.click();
|
||||||
|
nameInput.clear();
|
||||||
|
nameInput.sendKeys(name);
|
||||||
|
priceInput.click();
|
||||||
|
priceInput.clear();
|
||||||
|
priceInput.sendKeys(price);
|
||||||
|
saveButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.Condition;
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
import static com.codeborne.selenide.Condition.text;
|
||||||
|
|
||||||
|
public final class GiftItemComponent extends BaseMobileComponent {
|
||||||
|
private final SelenideElement title = byIdInRoot("title");
|
||||||
|
private final SelenideElement editButton = byIdInRoot("edit_button");
|
||||||
|
private final SelenideElement reservedToggle = byIdInRoot("reserved");
|
||||||
|
|
||||||
|
public GiftItemComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String titleText() {
|
||||||
|
return title.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shouldHaveTitle(String value) {
|
||||||
|
title.shouldHave(text(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
title.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void edit() {
|
||||||
|
editButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReserved() {
|
||||||
|
return Boolean.parseBoolean(reservedToggle.shouldBe(Condition.visible).getAttribute("checked"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleReservation() {
|
||||||
|
reservedToggle.shouldBe(Condition.visible).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.CollectionCondition;
|
||||||
|
import com.codeborne.selenide.Condition;
|
||||||
|
import com.codeborne.selenide.ElementsCollection;
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public final class GiftsContentComponent extends BaseMobileComponent {
|
||||||
|
private final ElementsCollection items = allByIdInRoot("gift_item");
|
||||||
|
|
||||||
|
public GiftsContentComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GiftItemComponent get(int index) {
|
||||||
|
return new GiftItemComponent(items.get(index).shouldBe(Condition.visible, Duration.ofSeconds(15)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public GiftItemComponent first() {
|
||||||
|
return get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GiftItemComponent byTitle(String title) {
|
||||||
|
items.shouldHave(CollectionCondition.sizeGreaterThan(0), Duration.ofSeconds(15));
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
GiftItemComponent item = get(i);
|
||||||
|
if (title.equals(item.titleText())) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Gift with title '" + title + "' was not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
public final class TopBarComponent extends BaseMobileComponent {
|
||||||
|
private final SelenideElement filterButton = byIdInRoot("filter");
|
||||||
|
|
||||||
|
public TopBarComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openUsersFilter() {
|
||||||
|
filterButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
public final class UserItemComponent extends BaseMobileComponent {
|
||||||
|
private final SelenideElement username = byIdInRoot("username");
|
||||||
|
|
||||||
|
public UserItemComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String usernameText() {
|
||||||
|
return username.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
username.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.CollectionCondition;
|
||||||
|
import com.codeborne.selenide.Condition;
|
||||||
|
import com.codeborne.selenide.ElementsCollection;
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public final class UsersContentComponent extends BaseMobileComponent {
|
||||||
|
private final ElementsCollection items = allByIdInRoot("user_item");
|
||||||
|
|
||||||
|
public UsersContentComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserItemComponent get(int index) {
|
||||||
|
return new UserItemComponent(items.get(index).shouldBe(Condition.visible, Duration.ofSeconds(15)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserItemComponent first() {
|
||||||
|
return get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserItemComponent byUsername(String username) {
|
||||||
|
items.shouldHave(CollectionCondition.sizeGreaterThan(0), Duration.ofSeconds(15));
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
UserItemComponent item = get(i);
|
||||||
|
if (username.equals(item.usernameText())) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("User with username '" + username + "' was not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
public final class UsersFilterComponent extends BaseMobileComponent {
|
||||||
|
private final SelenideElement usernameInput = byIdInRoot("username_input");
|
||||||
|
private final SelenideElement applyButton = byIdInRoot("apply_button");
|
||||||
|
|
||||||
|
public UsersFilterComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyByUsername(String username) {
|
||||||
|
usernameInput.click();
|
||||||
|
usernameInput.clear();
|
||||||
|
usernameInput.sendKeys(username);
|
||||||
|
applyButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
public final class WishlistFormComponent extends BaseMobileComponent {
|
||||||
|
private final SelenideElement titleInput = byIdInRoot("title_input");
|
||||||
|
private final SelenideElement saveButton = byIdInRoot("save_button");
|
||||||
|
|
||||||
|
public WishlistFormComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(String title) {
|
||||||
|
titleInput.click();
|
||||||
|
titleInput.clear();
|
||||||
|
titleInput.sendKeys(title);
|
||||||
|
saveButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
import static com.codeborne.selenide.Condition.text;
|
||||||
|
|
||||||
|
public final class WishlistItemComponent extends BaseMobileComponent {
|
||||||
|
private final SelenideElement title = byIdInRoot("title");
|
||||||
|
private final SelenideElement editButton = byIdInRoot("edit_button");
|
||||||
|
|
||||||
|
public WishlistItemComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String titleText() {
|
||||||
|
return title.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shouldHaveTitle(String value) {
|
||||||
|
title.shouldHave(text(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
title.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void edit() {
|
||||||
|
editButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package ru.otus.mobile.component;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.CollectionCondition;
|
||||||
|
import com.codeborne.selenide.Condition;
|
||||||
|
import com.codeborne.selenide.ElementsCollection;
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public final class WishlistsContentComponent extends BaseMobileComponent {
|
||||||
|
private final ElementsCollection items = allByIdInRoot("wishlist_item");
|
||||||
|
|
||||||
|
public WishlistsContentComponent(SelenideElement root) {
|
||||||
|
super(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WishlistItemComponent get(int index) {
|
||||||
|
return new WishlistItemComponent(items.get(index).shouldBe(Condition.visible, Duration.ofSeconds(15)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public WishlistItemComponent first() {
|
||||||
|
return get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WishlistItemComponent byTitle(String title) {
|
||||||
|
items.shouldHave(CollectionCondition.sizeGreaterThan(0), Duration.ofSeconds(15));
|
||||||
|
for (int i = 0; i < items.size(); i++) {
|
||||||
|
WishlistItemComponent item = get(i);
|
||||||
|
if (title.equals(item.titleText())) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Wishlist with title '" + title + "' was not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package ru.otus.mobile.config;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class MobileConfig {
|
||||||
|
private final String appPackage;
|
||||||
|
private final String appUrl;
|
||||||
|
private final String appiumHost;
|
||||||
|
private final List<Emulator> emulators;
|
||||||
|
|
||||||
|
public MobileConfig(
|
||||||
|
String appPackage,
|
||||||
|
String appUrl,
|
||||||
|
String appiumHost,
|
||||||
|
List<Emulator> emulators
|
||||||
|
) {
|
||||||
|
this.appPackage = appPackage;
|
||||||
|
this.appUrl = appUrl;
|
||||||
|
this.appiumHost = appiumHost;
|
||||||
|
this.emulators = List.copyOf(emulators);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String appPackage() {
|
||||||
|
return appPackage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String appUrl() {
|
||||||
|
return appUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String appiumHost() {
|
||||||
|
return appiumHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Emulator> emulators() {
|
||||||
|
return emulators;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Emulator(String id, String appiumUrl, String deviceName, String appUrl) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ru.otus.mobile.config;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public final class ProjectPaths {
|
||||||
|
private final Path logcatFile;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ProjectPaths() {
|
||||||
|
this.logcatFile = Paths.get("").toAbsolutePath().normalize().resolve("logcat.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path logcatFile() {
|
||||||
|
return logcatFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package ru.otus.mobile.config;
|
||||||
|
|
||||||
|
import ru.otus.mobile.db.DbClient;
|
||||||
|
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
public enum TestAccount {
|
||||||
|
WISHLISTS("user1us", "user1us", DbClient::clearWishlistsByUsername),
|
||||||
|
GIFTS("user2us", "user2us", DbClient::clearGiftsByUsername),
|
||||||
|
RESERVATION("user3us", "user3us", DbClient::doNothing),
|
||||||
|
RESERVATION_OWNER("user4us", "user4us", DbClient::clearReservationByUsername);
|
||||||
|
|
||||||
|
private final String username;
|
||||||
|
private final String password;
|
||||||
|
private final BiConsumer<DbClient, String> dataPreparation;
|
||||||
|
|
||||||
|
TestAccount(String username, String password, BiConsumer<DbClient, String> dataPreparation) {
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.dataPreparation = dataPreparation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String username() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String password() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BiConsumer<DbClient, String> dataPreparation() {
|
||||||
|
return dataPreparation;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package ru.otus.mobile.config;
|
||||||
|
|
||||||
|
public enum TestEmulator {
|
||||||
|
EMULATOR_1("android-emulator-1", 4723, "Android Emulator"),
|
||||||
|
EMULATOR_2("android-emulator-2", 4725, "Android Emulator");
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final int appiumPort;
|
||||||
|
private final String deviceName;
|
||||||
|
|
||||||
|
TestEmulator(String id, int appiumPort, String deviceName) {
|
||||||
|
this.id = id;
|
||||||
|
this.appiumPort = appiumPort;
|
||||||
|
this.deviceName = deviceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MobileConfig.Emulator toEmulator(String appiumHost, String appUrl) {
|
||||||
|
return new MobileConfig.Emulator(
|
||||||
|
id,
|
||||||
|
"http://" + appiumHost + ":" + appiumPort,
|
||||||
|
deviceName,
|
||||||
|
appUrl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package ru.otus.mobile.db;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public final class DbClient {
|
||||||
|
private final DbConfig config;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public DbClient(DbConfig config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearWishlistsByUsername(String username) {
|
||||||
|
executeUpdate("""
|
||||||
|
DELETE FROM wishlists
|
||||||
|
WHERE user_id IN (SELECT id FROM users WHERE username = ?)
|
||||||
|
""", username, "clear wishlists");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearGiftsByUsername(String username) {
|
||||||
|
executeUpdate("""
|
||||||
|
DELETE FROM gifts
|
||||||
|
WHERE wish_id IN (
|
||||||
|
SELECT w.id
|
||||||
|
FROM wishlists w
|
||||||
|
JOIN users u ON u.id = w.user_id
|
||||||
|
WHERE u.username = ?
|
||||||
|
)
|
||||||
|
""", username, "clear gifts");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearReservationByUsername(String username) {
|
||||||
|
executeUpdate("""
|
||||||
|
UPDATE gifts
|
||||||
|
SET is_reserved = false
|
||||||
|
WHERE wish_id IN (
|
||||||
|
SELECT w.id
|
||||||
|
FROM wishlists w
|
||||||
|
JOIN users u ON u.id = w.user_id
|
||||||
|
WHERE u.username = ?
|
||||||
|
)
|
||||||
|
""", username, "reset reservation status");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doNothing(String username) {
|
||||||
|
// no-op preparation for accounts that do not require DB setup
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeUpdate(String sql, String username, String operation) {
|
||||||
|
try (Connection connection = DriverManager.getConnection(config.url(), config.user(), config.password())) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||||
|
statement.setString(1, username);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IllegalStateException("Failed to " + operation + " for username " + username, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package ru.otus.mobile.driver;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import ru.otus.mobile.config.MobileConfig;
|
||||||
|
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public final class EmulatorQueue {
|
||||||
|
private final BlockingQueue<MobileConfig.Emulator> queue;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package ru.otus.mobile.driver;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public final class LogcatCollector {
|
||||||
|
private final ProjectPaths projectPaths;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package ru.otus.mobile.driver;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public final class MobileDriverFactory {
|
||||||
|
@Inject
|
||||||
|
public MobileDriverFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public AndroidDriver create(MobileConfig.Emulator emulator) {
|
||||||
|
UiAutomator2Options options = new UiAutomator2Options()
|
||||||
|
.setPlatformName("Android")
|
||||||
|
.setAutomationName("UiAutomator2")
|
||||||
|
.setDeviceName(emulator.deviceName())
|
||||||
|
.setApp(emulator.appUrl())
|
||||||
|
.setSkipDeviceInitialization(true);
|
||||||
|
try {
|
||||||
|
return new AndroidDriver(URI.create(emulator.appiumUrl()).toURL(), options);
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new IllegalStateException("Invalid Appium URL: " + emulator.appiumUrl(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package ru.otus.mobile.extensions;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.WebDriverRunner;
|
||||||
|
import com.google.inject.Guice;
|
||||||
|
import com.google.inject.Injector;
|
||||||
|
import io.appium.java_client.android.AndroidDriver;
|
||||||
|
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||||
|
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
|
||||||
|
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||||
|
import org.junit.jupiter.api.extension.ParameterContext;
|
||||||
|
import org.junit.jupiter.api.extension.ParameterResolutionException;
|
||||||
|
import org.junit.jupiter.api.extension.ParameterResolver;
|
||||||
|
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
|
||||||
|
import ru.otus.mobile.annotations.MobileUser;
|
||||||
|
import ru.otus.mobile.config.MobileConfig;
|
||||||
|
import ru.otus.mobile.config.TestAccount;
|
||||||
|
import ru.otus.mobile.config.TestContext;
|
||||||
|
import ru.otus.mobile.driver.EmulatorQueue;
|
||||||
|
import ru.otus.mobile.driver.LogcatCollector;
|
||||||
|
import ru.otus.mobile.driver.MobileDriverFactory;
|
||||||
|
import ru.otus.mobile.guice.CoreModule;
|
||||||
|
|
||||||
|
public final class MobileExtension implements
|
||||||
|
TestInstancePostProcessor,
|
||||||
|
ParameterResolver,
|
||||||
|
AfterTestExecutionCallback,
|
||||||
|
AfterEachCallback {
|
||||||
|
private static final ExtensionContext.Namespace NAMESPACE =
|
||||||
|
ExtensionContext.Namespace.create(MobileExtension.class);
|
||||||
|
private static final String SESSION_KEY = "mobile.session";
|
||||||
|
private static final Injector INJECTOR = Guice.createInjector(new CoreModule());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
|
||||||
|
INJECTOR.injectMembers(testInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
|
||||||
|
throws ParameterResolutionException {
|
||||||
|
return TestContext.class.equals(parameterContext.getParameter().getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext context)
|
||||||
|
throws ParameterResolutionException {
|
||||||
|
TestAccount account = resolveAccount(context);
|
||||||
|
TestContext testContext = createContext(account, context.getDisplayName());
|
||||||
|
context.getStore(NAMESPACE).put(SESSION_KEY, testContext);
|
||||||
|
return testContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTestExecution(ExtensionContext context) {
|
||||||
|
TestContext testContext = context.getStore(NAMESPACE).get(SESSION_KEY, TestContext.class);
|
||||||
|
if (testContext != null) {
|
||||||
|
INJECTOR.getInstance(LogcatCollector.class).save(testContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterEach(ExtensionContext context) {
|
||||||
|
TestContext testContext = context.getStore(NAMESPACE).remove(SESSION_KEY, TestContext.class);
|
||||||
|
if (testContext != null) {
|
||||||
|
closeContext(testContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestContext createContext(TestAccount account, String testName) {
|
||||||
|
EmulatorQueue queue = INJECTOR.getInstance(EmulatorQueue.class);
|
||||||
|
MobileConfig.Emulator emulator = queue.acquire();
|
||||||
|
try {
|
||||||
|
AndroidDriver driver = INJECTOR.getInstance(MobileDriverFactory.class).create(emulator);
|
||||||
|
WebDriverRunner.setWebDriver(driver);
|
||||||
|
return new TestContext(account, emulator, driver, testName);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
queue.release(emulator);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeContext(TestContext context) {
|
||||||
|
AndroidDriver driver = context.driver();
|
||||||
|
EmulatorQueue queue = INJECTOR.getInstance(EmulatorQueue.class);
|
||||||
|
try {
|
||||||
|
if (driver != null) {
|
||||||
|
driver.quit();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
WebDriverRunner.closeWebDriver();
|
||||||
|
queue.release(context.emulator());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
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.TestEmulator;
|
||||||
|
import ru.otus.mobile.db.DbConfig;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class CoreModule extends AbstractModule {
|
||||||
|
@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 appiumHost = value("mobile.host", "MOBILE_HOST", "127.0.0.1");
|
||||||
|
List<MobileConfig.Emulator> emulators = Arrays.stream(TestEmulator.values())
|
||||||
|
.map(emulator -> emulator.toEmulator(appiumHost, appUrl))
|
||||||
|
.toList();
|
||||||
|
return new MobileConfig(appPackage, appUrl, appiumHost, emulators);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package ru.otus.mobile.page;
|
||||||
|
|
||||||
|
import ru.otus.mobile.component.BottomNavigationComponent;
|
||||||
|
import ru.otus.mobile.component.TopBarComponent;
|
||||||
|
|
||||||
|
public abstract class AbsBasePage extends AbsPageObject {
|
||||||
|
protected final BottomNavigationComponent bottomNavigation =
|
||||||
|
new BottomNavigationComponent(byId("bottom_navigation"));
|
||||||
|
protected final TopBarComponent topBar = new TopBarComponent(byId("top_app_bar"));
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package ru.otus.mobile.page;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
import com.codeborne.selenide.WebDriverRunner;
|
||||||
|
import com.codeborne.selenide.appium.SelenideAppiumCollection;
|
||||||
|
import io.appium.java_client.AppiumBy;
|
||||||
|
|
||||||
|
import static com.codeborne.selenide.appium.SelenideAppium.$$;
|
||||||
|
import static com.codeborne.selenide.appium.SelenideAppium.$;
|
||||||
|
|
||||||
|
public abstract class AbsPageObject {
|
||||||
|
private static final String APP_PACKAGE_PROPERTY = "app.package";
|
||||||
|
private static final String DEFAULT_APP_PACKAGE = "ru.otus.wishlist";
|
||||||
|
|
||||||
|
protected SelenideElement byId(String id) {
|
||||||
|
return $(AppiumBy.id(fullIdValue(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SelenideAppiumCollection allById(String id) {
|
||||||
|
return $$(AppiumBy.id(fullIdValue(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void back() {
|
||||||
|
WebDriverRunner.getWebDriver().navigate().back();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String fullIdValue(String id) {
|
||||||
|
if (id.contains(":")) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
String appPackage = System.getProperty(APP_PACKAGE_PROPERTY, DEFAULT_APP_PACKAGE);
|
||||||
|
return appPackage + ":id/" + id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package ru.otus.mobile.page;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.Condition;
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import ru.otus.mobile.component.GiftFormComponent;
|
||||||
|
import ru.otus.mobile.component.GiftsContentComponent;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public final class GiftsPage extends AbsBasePage {
|
||||||
|
private final SelenideElement addButton = byId("add_button");
|
||||||
|
private final GiftsContentComponent content = new GiftsContentComponent(byId("gifts_content"));
|
||||||
|
private final GiftFormComponent form = new GiftFormComponent(byId("gift_edit_bottom_sheet"));
|
||||||
|
|
||||||
|
public void createGift(String name) {
|
||||||
|
addButton.click();
|
||||||
|
form.save(name, "100");
|
||||||
|
content.shouldBe(Condition.visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void editGift(String oldName, String newName) {
|
||||||
|
openGift(oldName);
|
||||||
|
content.byTitle(oldName).edit();
|
||||||
|
form.save(newName, "100");
|
||||||
|
content.shouldBe(Condition.visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shouldSeeGift(String name) {
|
||||||
|
content.shouldBe(Condition.visible);
|
||||||
|
content.byTitle(name).shouldHaveTitle(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openGift(String name) {
|
||||||
|
content.byTitle(name).open();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package ru.otus.mobile.page;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import ru.otus.mobile.component.AlertDialogComponent;
|
||||||
|
import ru.otus.mobile.config.TestAccount;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public final class LoginPage extends AbsPageObject {
|
||||||
|
private final SelenideElement usernameInput = byId("username_text_input");
|
||||||
|
private final SelenideElement passwordInput = byId("password_text_input");
|
||||||
|
private final SelenideElement loginButton = byId("log_in_button");
|
||||||
|
private final SelenideElement appMainContainer = byId("app_main_fragment_container");
|
||||||
|
private final AlertDialogComponent alertDialog = new AlertDialogComponent(appMainContainer);
|
||||||
|
|
||||||
|
public void login(TestAccount account) {
|
||||||
|
if (!isOpened()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
usernameInput.click();
|
||||||
|
usernameInput.clear();
|
||||||
|
usernameInput.sendKeys(account.username());
|
||||||
|
passwordInput.click();
|
||||||
|
passwordInput.clear();
|
||||||
|
passwordInput.sendKeys(account.password());
|
||||||
|
loginButton.click();
|
||||||
|
alertDialog.acceptIfVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpened() {
|
||||||
|
return usernameInput.exists() && passwordInput.exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package ru.otus.mobile.page;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.Condition;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import ru.otus.mobile.component.GiftItemComponent;
|
||||||
|
import ru.otus.mobile.component.GiftsContentComponent;
|
||||||
|
import ru.otus.mobile.component.UsersContentComponent;
|
||||||
|
import ru.otus.mobile.component.UsersFilterComponent;
|
||||||
|
import ru.otus.mobile.component.WishlistsContentComponent;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public final class UsersPage extends AbsBasePage {
|
||||||
|
private final UsersContentComponent usersContent = new UsersContentComponent(byId("users_content"));
|
||||||
|
private final UsersFilterComponent usersFilter = new UsersFilterComponent(byId("users_filter_bottom_sheet"));
|
||||||
|
private final WishlistsContentComponent wishlistsContent = new WishlistsContentComponent(byId("wishlists_content"));
|
||||||
|
private final GiftsContentComponent giftsContent = new GiftsContentComponent(byId("gifts_content"));
|
||||||
|
private GiftItemComponent currentGift;
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
bottomNavigation.openUsers();
|
||||||
|
usersContent.shouldBe(Condition.visible, Duration.ofSeconds(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void filterByUsername(String username) {
|
||||||
|
topBar.openUsersFilter();
|
||||||
|
usersFilter.shouldBe(Condition.visible, Duration.ofSeconds(15));
|
||||||
|
usersFilter.applyByUsername(username);
|
||||||
|
usersContent.byUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openUser(String username) {
|
||||||
|
usersContent.byUsername(username).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openFirstWishlist() {
|
||||||
|
wishlistsContent.get(0).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openWishlist(String name) {
|
||||||
|
wishlistsContent.byTitle(name).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openFirstGift() {
|
||||||
|
currentGift = giftsContent.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openGift(String name) {
|
||||||
|
currentGift = giftsContent.byTitle(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReserved() {
|
||||||
|
return selectedGift().isReserved();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleReservation() {
|
||||||
|
selectedGift().toggleReservation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private GiftItemComponent selectedGift() {
|
||||||
|
if (currentGift == null) {
|
||||||
|
throw new IllegalStateException("Gift is not selected. Call openFirstGift/openGift first.");
|
||||||
|
}
|
||||||
|
return currentGift;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package ru.otus.mobile.page;
|
||||||
|
|
||||||
|
import com.codeborne.selenide.Condition;
|
||||||
|
import com.codeborne.selenide.SelenideElement;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import ru.otus.mobile.component.WishlistsContentComponent;
|
||||||
|
import ru.otus.mobile.component.WishlistFormComponent;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public final class WishlistsPage extends AbsBasePage {
|
||||||
|
private final SelenideElement addButton = byId("add_button");
|
||||||
|
private final WishlistsContentComponent content = new WishlistsContentComponent(byId("wishlists_content"));
|
||||||
|
private final WishlistFormComponent form = new WishlistFormComponent(byId("wishlist_edit_bottom_sheet"));
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
bottomNavigation.openWishlists();
|
||||||
|
content.shouldBe(Condition.visible, Duration.ofSeconds(15));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createWishlist(String name) {
|
||||||
|
addButton.click();
|
||||||
|
form.save(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void editWishlist(String oldName, String newName) {
|
||||||
|
content.byTitle(oldName).edit();
|
||||||
|
form.save(newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shouldSeeWishlist(String name) {
|
||||||
|
content.byTitle(name).shouldHaveTitle(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openWishlist(String name) {
|
||||||
|
content.byTitle(name).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openFirstWishlist() {
|
||||||
|
content.first().open();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,51 @@
|
|||||||
package ru.otus.mobile.tests;
|
package ru.otus.mobile.tests;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import ru.otus.mobile.screens.GiftsScreen;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import ru.otus.mobile.screens.WishlistsScreen;
|
import ru.otus.mobile.annotations.MobileUser;
|
||||||
import ru.otus.mobile.util.TestAccount;
|
import ru.otus.mobile.config.TestAccount;
|
||||||
|
import ru.otus.mobile.config.TestContext;
|
||||||
|
import ru.otus.mobile.db.DbClient;
|
||||||
|
import ru.otus.mobile.extensions.MobileExtension;
|
||||||
|
import ru.otus.mobile.page.GiftsPage;
|
||||||
|
import ru.otus.mobile.page.LoginPage;
|
||||||
|
import ru.otus.mobile.page.WishlistsPage;
|
||||||
import ru.otus.mobile.util.TestData;
|
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;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private DbClient dbClient;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private LoginPage loginPage;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Создание и редактирование подарка")
|
@DisplayName("Создание и редактирование подарка")
|
||||||
void createAndEditGift() {
|
void createAndEditGift(TestContext context) {
|
||||||
WishlistsScreen wishlists = new WishlistsScreen();
|
context.account().dataPreparation().accept(dbClient, context.account().username());
|
||||||
GiftsScreen gifts = new GiftsScreen();
|
loginPage.login(context.account());
|
||||||
String wishlistName = TestData.uniqueName("Wishlist");
|
|
||||||
String name = TestData.uniqueName("Gift");
|
String name = testData.uniqueName("Gift");
|
||||||
String updated = name + " updated";
|
String updated = name + " updated";
|
||||||
|
|
||||||
wishlists.open();
|
wishlistsPage.open();
|
||||||
wishlists.createWishlist(wishlistName);
|
wishlistsPage.openFirstWishlist();
|
||||||
wishlists.openWishlistByName(wishlistName);
|
giftsPage.createGift(name);
|
||||||
gifts.open();
|
giftsPage.shouldSeeGift(name);
|
||||||
gifts.createGift(name);
|
giftsPage.editGift(name, updated);
|
||||||
gifts.assertGiftPresent(name);
|
giftsPage.shouldSeeGift(updated);
|
||||||
gifts.editGift(name, updated);
|
|
||||||
gifts.assertGiftPresent(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected TestAccount account() {
|
|
||||||
return TestAccount.GIFTS;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,44 @@
|
|||||||
package ru.otus.mobile.tests;
|
package ru.otus.mobile.tests;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import ru.otus.mobile.screens.UsersScreen;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import ru.otus.mobile.util.TestAccount;
|
import ru.otus.mobile.annotations.MobileUser;
|
||||||
|
import ru.otus.mobile.config.TestAccount;
|
||||||
|
import ru.otus.mobile.config.TestContext;
|
||||||
|
import ru.otus.mobile.db.DbClient;
|
||||||
|
import ru.otus.mobile.extensions.MobileExtension;
|
||||||
|
import ru.otus.mobile.page.LoginPage;
|
||||||
|
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 DbClient dbClient;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private LoginPage loginPage;
|
||||||
|
|
||||||
public class ReservationTest extends BaseMobileTest {
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Изменение статуса резервирования подарка другого пользователя")
|
@DisplayName("Изменение статуса резервирования подарка другого пользователя")
|
||||||
void changeReservationStatus() {
|
void changeReservationStatus(TestContext context) {
|
||||||
UsersScreen users = new UsersScreen();
|
TestAccount.RESERVATION_OWNER.dataPreparation().accept(dbClient, TestAccount.RESERVATION_OWNER.username());
|
||||||
users.open();
|
loginPage.login(context.account());
|
||||||
String owner = System.getProperty("reservation.owner", "user4us");
|
|
||||||
users.openUserWithGift(owner);
|
|
||||||
users.openFirstGift();
|
|
||||||
boolean changed = users.toggleReservation();
|
|
||||||
assertTrue(changed, "Reservation status was not changed");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
usersPage.open();
|
||||||
protected TestAccount account() {
|
usersPage.filterByUsername(TestAccount.RESERVATION_OWNER.username());
|
||||||
return TestAccount.RESERVATION;
|
usersPage.openUser(TestAccount.RESERVATION_OWNER.username());
|
||||||
|
usersPage.openFirstWishlist();
|
||||||
|
usersPage.openFirstGift();
|
||||||
|
boolean before = usersPage.isReserved();
|
||||||
|
usersPage.toggleReservation();
|
||||||
|
assertNotEquals(before, usersPage.isReserved(), "Reservation status was not changed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,46 @@
|
|||||||
package ru.otus.mobile.tests;
|
package ru.otus.mobile.tests;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import ru.otus.mobile.screens.WishlistsScreen;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import ru.otus.mobile.util.TestAccount;
|
import ru.otus.mobile.annotations.MobileUser;
|
||||||
|
import ru.otus.mobile.config.TestAccount;
|
||||||
|
import ru.otus.mobile.config.TestContext;
|
||||||
|
import ru.otus.mobile.db.DbClient;
|
||||||
|
import ru.otus.mobile.extensions.MobileExtension;
|
||||||
|
import ru.otus.mobile.page.LoginPage;
|
||||||
|
import ru.otus.mobile.page.WishlistsPage;
|
||||||
import ru.otus.mobile.util.TestData;
|
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;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private DbClient dbClient;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private LoginPage loginPage;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Создание и редактирование списка желаний")
|
@DisplayName("Создание и редактирование списка желаний")
|
||||||
void createAndEditWishlist() {
|
void createAndEditWishlist(TestContext context) {
|
||||||
WishlistsScreen wishlists = new WishlistsScreen();
|
context.account().dataPreparation().accept(dbClient, context.account().username());
|
||||||
String name = TestData.uniqueName("Wishlist");
|
loginPage.login(context.account());
|
||||||
|
|
||||||
|
String name = testData.uniqueName("Wishlist");
|
||||||
String updated = name + " updated";
|
String updated = name + " updated";
|
||||||
|
|
||||||
wishlists.open();
|
wishlistsPage.open();
|
||||||
wishlists.createWishlist(name);
|
wishlistsPage.createWishlist(name);
|
||||||
wishlists.assertWishlistPresent(name);
|
wishlistsPage.shouldSeeWishlist(name);
|
||||||
wishlists.editWishlist(name, updated);
|
wishlistsPage.editWishlist(name, updated);
|
||||||
wishlists.assertWishlistPresent(updated);
|
wishlistsPage.shouldSeeWishlist(updated);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected TestAccount account() {
|
|
||||||
return TestAccount.WISHLISTS;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 + "\")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String, Object> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "/wishlist.apk"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": "application/vnd.android.package-archive"
|
||||||
|
},
|
||||||
|
"bodyFileName": "wishlist.apk"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user