Compare commits
6 Commits
6a19b574b8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e731a73838 | |||
| 97c58a29e0 | |||
| bcf831ad99 | |||
| 46f3de4d55 | |||
| aaf63cc438 | |||
| 0cd4e29d02 |
@@ -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
|
||||
@@ -5,3 +5,4 @@ build/
|
||||
.DS_Store
|
||||
allure-results/
|
||||
logcat.txt
|
||||
.env
|
||||
|
||||
@@ -19,14 +19,6 @@
|
||||
- `src/test/java` — только тестовые классы.
|
||||
- `wiremock` — маппинги и APK.
|
||||
|
||||
## Тестовые аккаунты
|
||||
- `user1us / user1us`
|
||||
- `user2us / user2us`
|
||||
- `user3us / user3us`
|
||||
- `user4us / user4us`
|
||||
|
||||
`user4us` используется как владелец подарка в тесте резервирования.
|
||||
|
||||
## Запуск
|
||||
1. Поднять окружение:
|
||||
```bash
|
||||
@@ -38,23 +30,59 @@ docker compose up -d
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
3. Указать доступ к БД:
|
||||
3. Подготовить переменные окружения (использовать один из вариантов):
|
||||
|
||||
Вариант A: через `.env`
|
||||
- скопировать `.env.example` в `.env`;
|
||||
- заполнить `.env` актуальными значениями вашей среды;
|
||||
- загрузить `.env` в текущую shell-сессию.
|
||||
|
||||
PowerShell:
|
||||
```powershell
|
||||
$env:DB_URL="jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist"
|
||||
$env:DB_USER="student"
|
||||
$env:DB_PASSWORD="student"
|
||||
```
|
||||
bash:
|
||||
```bash
|
||||
export DB_URL="jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist"
|
||||
export DB_USER="student"
|
||||
export DB_PASSWORD="student"
|
||||
Get-Content .env | Where-Object { $_ -match '^[^#].+=.+' } | ForEach-Object {
|
||||
$name, $value = $_ -split '=', 2
|
||||
[System.Environment]::SetEnvironmentVariable($name, $value, 'Process')
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
mvn test
|
||||
```
|
||||
|
||||
Тесты запускаются параллельно по классам (2 потока) и распределяются по эмуляторам через очередь.
|
||||
Примечание: эмуляторы зафиксированы в enum `TestEmulator` (порты `4723` и `4725`), в конфигурации задается только хост (`MOBILE_HOST`).
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
package ru.otus.mobile.component;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import com.codeborne.selenide.SelenideElement;
|
||||
|
||||
public final class AlertDialogComponent extends BaseMobileComponent {
|
||||
@Inject
|
||||
public AlertDialogComponent(MobileConfig config) {
|
||||
super(config);
|
||||
private final SelenideElement positiveButton = byIdInRoot("android:id/button1");
|
||||
|
||||
public AlertDialogComponent(SelenideElement root) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
public void acceptIfVisible() {
|
||||
if (exists("android:id/button1")) {
|
||||
tap("android:id/button1");
|
||||
return;
|
||||
}
|
||||
if (byText("OK").exists()) {
|
||||
byText("OK").click();
|
||||
return;
|
||||
}
|
||||
if (byText("ОК").exists()) {
|
||||
byText("ОК").click();
|
||||
if (positiveButton.exists()) {
|
||||
positiveButton.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,35 @@
|
||||
package ru.otus.mobile.component;
|
||||
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
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 BaseMobileComponent(MobileConfig config) {
|
||||
super(config);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package ru.otus.mobile.component;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import com.codeborne.selenide.SelenideElement;
|
||||
|
||||
public final class BottomNavigationComponent extends BaseMobileComponent {
|
||||
@Inject
|
||||
public BottomNavigationComponent(MobileConfig config) {
|
||||
super(config);
|
||||
private final SelenideElement mineMenu = byIdInRoot("mine_menu");
|
||||
private final SelenideElement usersMenu = byIdInRoot("users_menu");
|
||||
|
||||
public BottomNavigationComponent(SelenideElement root) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
public void openWishlists() {
|
||||
tap("mine_menu");
|
||||
mineMenu.click();
|
||||
}
|
||||
|
||||
public void openUsers() {
|
||||
tap("users_menu");
|
||||
usersMenu.click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package ru.otus.mobile.component;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import com.codeborne.selenide.SelenideElement;
|
||||
|
||||
public final class GiftFormComponent extends BaseMobileComponent {
|
||||
@Inject
|
||||
public GiftFormComponent(MobileConfig config) {
|
||||
super(config);
|
||||
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) {
|
||||
type("name_input", name);
|
||||
type("price_input", price);
|
||||
tap("save_button");
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
package ru.otus.mobile.component;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import com.codeborne.selenide.SelenideElement;
|
||||
|
||||
public final class WishlistFormComponent extends BaseMobileComponent {
|
||||
@Inject
|
||||
public WishlistFormComponent(MobileConfig config) {
|
||||
super(config);
|
||||
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) {
|
||||
type("title_input", title);
|
||||
tap("save_button");
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,22 @@
|
||||
package ru.otus.mobile.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class MobileConfig {
|
||||
private final String appPackage;
|
||||
private final String appUrl;
|
||||
private final String reservationOwnerUsername;
|
||||
private final String appiumHost;
|
||||
private final List<Emulator> emulators;
|
||||
|
||||
public MobileConfig(
|
||||
String appPackage,
|
||||
String appUrl,
|
||||
String reservationOwnerUsername,
|
||||
String appiumHost,
|
||||
List<Emulator> emulators
|
||||
) {
|
||||
this.appPackage = appPackage;
|
||||
this.appUrl = appUrl;
|
||||
this.reservationOwnerUsername = reservationOwnerUsername;
|
||||
this.appiumHost = appiumHost;
|
||||
this.emulators = List.copyOf(emulators);
|
||||
}
|
||||
|
||||
@@ -29,33 +28,13 @@ public final class MobileConfig {
|
||||
return appUrl;
|
||||
}
|
||||
|
||||
public String reservationOwnerUsername() {
|
||||
return reservationOwnerUsername;
|
||||
public String appiumHost() {
|
||||
return appiumHost;
|
||||
}
|
||||
|
||||
public List<Emulator> emulators() {
|
||||
return emulators;
|
||||
}
|
||||
|
||||
public record Emulator(String id, String appiumUrl, String deviceName, String appUrl) {
|
||||
public static List<Emulator> parse(String rawValue, String defaultAppUrl) {
|
||||
List<Emulator> emulators = new ArrayList<>();
|
||||
for (String entry : rawValue.split(",")) {
|
||||
String trimmed = entry.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = trimmed.split("\\|");
|
||||
String id = parts.length > 0 ? parts[0].trim() : "emulator-1";
|
||||
String appiumUrl = parts.length > 1 ? parts[1].trim() : "http://127.0.0.1:4723";
|
||||
String deviceName = parts.length > 2 ? parts[2].trim() : "Android Emulator";
|
||||
String emulatorAppUrl = parts.length > 3 ? parts[3].trim() : defaultAppUrl;
|
||||
emulators.add(new Emulator(id, appiumUrl, deviceName, emulatorAppUrl));
|
||||
}
|
||||
if (emulators.isEmpty()) {
|
||||
emulators.add(new Emulator("emulator-1", "http://127.0.0.1:4723", "Android Emulator", defaultAppUrl));
|
||||
}
|
||||
return emulators;
|
||||
}
|
||||
}
|
||||
public record Emulator(String id, String appiumUrl, String deviceName, String appUrl) { }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -1,72 +1,23 @@
|
||||
package ru.otus.mobile.config;
|
||||
|
||||
import ru.otus.mobile.db.DbClient;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public enum TestAccount {
|
||||
WISHLISTS("user1us", "user1us", """
|
||||
WITH target_user AS (
|
||||
SELECT id FROM users WHERE username = ?
|
||||
),
|
||||
delete_gifts AS (
|
||||
DELETE FROM gifts
|
||||
WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM target_user))
|
||||
),
|
||||
delete_wishlists AS (
|
||||
DELETE FROM wishlists WHERE user_id IN (SELECT id FROM target_user)
|
||||
)
|
||||
SELECT 1;
|
||||
"""),
|
||||
GIFTS("user2us", "user2us", """
|
||||
WITH target_user AS (
|
||||
SELECT id FROM users WHERE username = ?
|
||||
),
|
||||
delete_gifts AS (
|
||||
DELETE FROM gifts
|
||||
WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM target_user))
|
||||
),
|
||||
delete_wishlists AS (
|
||||
DELETE FROM wishlists WHERE user_id IN (SELECT id FROM target_user)
|
||||
)
|
||||
SELECT 1;
|
||||
"""),
|
||||
RESERVATION("user3us", "user3us", """
|
||||
WITH owner_user AS (
|
||||
SELECT id FROM users WHERE username = ?
|
||||
),
|
||||
target_user AS (
|
||||
SELECT id FROM users WHERE username = ?
|
||||
),
|
||||
delete_owner_gifts AS (
|
||||
DELETE FROM gifts
|
||||
WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM owner_user))
|
||||
),
|
||||
delete_owner_wishlists AS (
|
||||
DELETE FROM wishlists WHERE user_id IN (SELECT id FROM owner_user)
|
||||
),
|
||||
delete_target_gifts AS (
|
||||
DELETE FROM gifts
|
||||
WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM target_user))
|
||||
),
|
||||
delete_target_wishlists AS (
|
||||
DELETE FROM wishlists WHERE user_id IN (SELECT id FROM target_user)
|
||||
),
|
||||
owner_wishlist AS (
|
||||
INSERT INTO wishlists (id, title, description, user_id)
|
||||
SELECT gen_random_uuid(), 'Owner Wishlist', 'Prepared for reservation test', id
|
||||
FROM owner_user
|
||||
RETURNING id
|
||||
)
|
||||
INSERT INTO gifts (id, name, description, price, is_reserved, store_url, image_url, wish_id)
|
||||
SELECT gen_random_uuid(), 'Owner Gift', 'Prepared for reservation test', 100, false, NULL, NULL, id
|
||||
FROM owner_wishlist;
|
||||
""");
|
||||
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 String resetSql;
|
||||
private final BiConsumer<DbClient, String> dataPreparation;
|
||||
|
||||
TestAccount(String username, String password, String resetSql) {
|
||||
TestAccount(String username, String password, BiConsumer<DbClient, String> dataPreparation) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.resetSql = resetSql;
|
||||
this.dataPreparation = dataPreparation;
|
||||
}
|
||||
|
||||
public String username() {
|
||||
@@ -77,7 +28,7 @@ public enum TestAccount {
|
||||
return password;
|
||||
}
|
||||
|
||||
public String resetSql() {
|
||||
return resetSql;
|
||||
public BiConsumer<DbClient, String> dataPreparation() {
|
||||
return dataPreparation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package ru.otus.mobile.db;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import ru.otus.mobile.config.TestAccount;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
@@ -11,26 +9,56 @@ import java.sql.SQLException;
|
||||
|
||||
public final class DbClient {
|
||||
private final DbConfig config;
|
||||
private final MobileConfig mobileConfig;
|
||||
|
||||
@Inject
|
||||
public DbClient(DbConfig config, MobileConfig mobileConfig) {
|
||||
public DbClient(DbConfig config) {
|
||||
this.config = config;
|
||||
this.mobileConfig = mobileConfig;
|
||||
}
|
||||
|
||||
public void resetData(TestAccount account) {
|
||||
try (Connection connection = DriverManager.getConnection(config.url(), config.user(), config.password());
|
||||
PreparedStatement statement = connection.prepareStatement(account.resetSql())) {
|
||||
if (account == TestAccount.RESERVATION) {
|
||||
statement.setString(1, mobileConfig.reservationOwnerUsername());
|
||||
statement.setString(2, account.username());
|
||||
} else {
|
||||
statement.setString(1, account.username());
|
||||
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();
|
||||
}
|
||||
statement.execute();
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("Failed to prepare test data for account " + account.name(), e);
|
||||
throw new IllegalStateException("Failed to " + operation + " for username " + username, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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;
|
||||
@@ -11,9 +13,11 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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;
|
||||
@@ -7,7 +9,12 @@ 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")
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package ru.otus.mobile.driver;
|
||||
|
||||
import com.codeborne.selenide.WebDriverRunner;
|
||||
import com.google.inject.Injector;
|
||||
import io.appium.java_client.android.AndroidDriver;
|
||||
import ru.otus.mobile.config.TestContext;
|
||||
|
||||
public final class MobileSession {
|
||||
private final TestContext context;
|
||||
private final Injector injector;
|
||||
private final EmulatorQueue emulatorQueue;
|
||||
private final LogcatCollector logcatCollector;
|
||||
|
||||
public MobileSession(
|
||||
TestContext context,
|
||||
Injector injector,
|
||||
EmulatorQueue emulatorQueue,
|
||||
LogcatCollector logcatCollector
|
||||
) {
|
||||
this.context = context;
|
||||
this.injector = injector;
|
||||
this.emulatorQueue = emulatorQueue;
|
||||
this.logcatCollector = logcatCollector;
|
||||
}
|
||||
|
||||
public void activate() {
|
||||
WebDriverRunner.setWebDriver(context.driver());
|
||||
}
|
||||
|
||||
public void injectMembers(Object target) {
|
||||
injector.injectMembers(target);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
logcatCollector.save(context);
|
||||
AndroidDriver driver = context.driver();
|
||||
try {
|
||||
if (driver != null) {
|
||||
driver.quit();
|
||||
}
|
||||
} finally {
|
||||
WebDriverRunner.closeWebDriver();
|
||||
emulatorQueue.release(context.emulator());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package ru.otus.mobile.driver;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Injector;
|
||||
import io.appium.java_client.android.AndroidDriver;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import ru.otus.mobile.config.TestAccount;
|
||||
import ru.otus.mobile.config.TestContext;
|
||||
import ru.otus.mobile.guice.SessionModule;
|
||||
|
||||
public final class MobileSessionFactory {
|
||||
private final Injector rootInjector;
|
||||
private final EmulatorQueue emulatorQueue;
|
||||
private final MobileDriverFactory driverFactory;
|
||||
private final LogcatCollector logcatCollector;
|
||||
|
||||
@Inject
|
||||
public MobileSessionFactory(
|
||||
Injector rootInjector,
|
||||
EmulatorQueue emulatorQueue,
|
||||
MobileDriverFactory driverFactory,
|
||||
LogcatCollector logcatCollector
|
||||
) {
|
||||
this.rootInjector = rootInjector;
|
||||
this.emulatorQueue = emulatorQueue;
|
||||
this.driverFactory = driverFactory;
|
||||
this.logcatCollector = logcatCollector;
|
||||
}
|
||||
|
||||
public MobileSession create(TestAccount account, String testName) {
|
||||
MobileConfig.Emulator emulator = emulatorQueue.acquire();
|
||||
AndroidDriver driver = driverFactory.create(emulator);
|
||||
TestContext context = new TestContext(account, emulator, driver, testName);
|
||||
Injector childInjector = rootInjector.createChildInjector(new SessionModule(context));
|
||||
return new MobileSession(context, childInjector, emulatorQueue, logcatCollector);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,71 @@
|
||||
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.BeforeEachCallback;
|
||||
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.db.DbClient;
|
||||
import ru.otus.mobile.driver.MobileSession;
|
||||
import ru.otus.mobile.driver.MobileSessionFactory;
|
||||
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;
|
||||
import ru.otus.mobile.page.LoginPage;
|
||||
|
||||
public final class MobileExtension implements
|
||||
BeforeEachCallback,
|
||||
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 beforeEach(ExtensionContext context) {
|
||||
Injector injector = rootInjector(context);
|
||||
TestAccount account = resolveAccount(context);
|
||||
injector.getInstance(DbClient.class).resetData(account);
|
||||
public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
|
||||
INJECTOR.injectMembers(testInstance);
|
||||
}
|
||||
|
||||
MobileSessionFactory sessionFactory = injector.getInstance(MobileSessionFactory.class);
|
||||
MobileSession session = sessionFactory.create(account, context.getDisplayName());
|
||||
session.activate();
|
||||
session.injectMembers(context.getRequiredTestInstance());
|
||||
context.getStore(NAMESPACE).put(SESSION_KEY, session);
|
||||
injector.getInstance(LoginPage.class).login(account);
|
||||
@Override
|
||||
public 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) {
|
||||
MobileSession session = context.getStore(NAMESPACE).remove(SESSION_KEY, MobileSession.class);
|
||||
if (session != null) {
|
||||
session.close();
|
||||
TestContext testContext = context.getStore(NAMESPACE).remove(SESSION_KEY, TestContext.class);
|
||||
if (testContext != null) {
|
||||
closeContext(testContext);
|
||||
}
|
||||
}
|
||||
|
||||
private Injector rootInjector(ExtensionContext context) {
|
||||
return context.getRoot()
|
||||
.getStore(NAMESPACE)
|
||||
.getOrComputeIfAbsent("root.injector", key -> Guice.createInjector(new CoreModule()), Injector.class);
|
||||
}
|
||||
|
||||
private TestAccount resolveAccount(ExtensionContext context) {
|
||||
MobileUser annotation = context.getRequiredTestClass().getAnnotation(MobileUser.class);
|
||||
if (annotation == null) {
|
||||
@@ -55,4 +73,30 @@ public final class MobileExtension implements
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,39 +4,23 @@ import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.Singleton;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import ru.otus.mobile.config.ProjectPaths;
|
||||
import ru.otus.mobile.config.TestEmulator;
|
||||
import ru.otus.mobile.db.DbConfig;
|
||||
import ru.otus.mobile.driver.EmulatorQueue;
|
||||
import ru.otus.mobile.driver.LogcatCollector;
|
||||
import ru.otus.mobile.driver.MobileDriverFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public final class CoreModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MobileConfig mobileConfig() {
|
||||
String appPackage = value("app.package", "APP_PACKAGE", "ru.otus.wishlist");
|
||||
String appUrl = value("app.url", "APP_URL", "http://wiremock:8080/wishlist.apk");
|
||||
String reservationOwner = value("reservation.owner", "RESERVATION_OWNER", "user4us");
|
||||
String rawEmulators = value(
|
||||
"mobile.emulators",
|
||||
"MOBILE_EMULATORS",
|
||||
"android-emulator-1|http://127.0.0.1:4723|Android Emulator,"
|
||||
+ "android-emulator-2|http://127.0.0.1:4725|Android Emulator"
|
||||
);
|
||||
List<MobileConfig.Emulator> emulators = MobileConfig.Emulator.parse(rawEmulators, appUrl);
|
||||
return new MobileConfig(appPackage, appUrl, reservationOwner, emulators);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
ProjectPaths projectPaths() {
|
||||
return new ProjectPaths();
|
||||
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
|
||||
@@ -48,24 +32,6 @@ public final class CoreModule extends AbstractModule {
|
||||
return new DbConfig(url, user, password);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
EmulatorQueue emulatorQueue(MobileConfig config) {
|
||||
return new EmulatorQueue(config);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MobileDriverFactory mobileDriverFactory() {
|
||||
return new MobileDriverFactory();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
LogcatCollector logcatCollector(ProjectPaths projectPaths) {
|
||||
return new LogcatCollector(projectPaths);
|
||||
}
|
||||
|
||||
private String requiredValue(String property, String env) {
|
||||
String value = value(property, env, null);
|
||||
if (value == null) {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package ru.otus.mobile.guice;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import io.appium.java_client.android.AndroidDriver;
|
||||
import ru.otus.mobile.config.TestAccount;
|
||||
import ru.otus.mobile.config.TestContext;
|
||||
|
||||
public final class SessionModule extends AbstractModule {
|
||||
private final TestContext testContext;
|
||||
|
||||
public SessionModule(TestContext testContext) {
|
||||
this.testContext = testContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(TestContext.class).toInstance(testContext);
|
||||
}
|
||||
|
||||
@Provides
|
||||
TestAccount testAccount() {
|
||||
return testContext.account();
|
||||
}
|
||||
|
||||
@Provides
|
||||
AndroidDriver androidDriver() {
|
||||
return testContext.driver();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,10 @@
|
||||
package ru.otus.mobile.page;
|
||||
|
||||
import ru.otus.mobile.component.BottomNavigationComponent;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import ru.otus.mobile.component.TopBarComponent;
|
||||
|
||||
public abstract class AbsBasePage extends AbsPageObject {
|
||||
protected final BottomNavigationComponent bottomNavigation;
|
||||
private final MobileConfig config;
|
||||
|
||||
protected AbsBasePage(MobileConfig config, BottomNavigationComponent bottomNavigation) {
|
||||
super(config);
|
||||
this.config = config;
|
||||
this.bottomNavigation = bottomNavigation;
|
||||
}
|
||||
|
||||
protected MobileConfig config() {
|
||||
return config;
|
||||
}
|
||||
protected final BottomNavigationComponent bottomNavigation =
|
||||
new BottomNavigationComponent(byId("bottom_navigation"));
|
||||
protected final TopBarComponent topBar = new TopBarComponent(byId("top_app_bar"));
|
||||
}
|
||||
|
||||
@@ -1,103 +1,34 @@
|
||||
package ru.otus.mobile.page;
|
||||
|
||||
import com.codeborne.selenide.CollectionCondition;
|
||||
import com.codeborne.selenide.Condition;
|
||||
import com.codeborne.selenide.SelenideElement;
|
||||
import com.codeborne.selenide.WebDriverRunner;
|
||||
import com.codeborne.selenide.appium.SelenideAppiumCollection;
|
||||
import io.appium.java_client.AppiumBy;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
|
||||
import static com.codeborne.selenide.appium.SelenideAppium.$$;
|
||||
import static com.codeborne.selenide.appium.SelenideAppium.$;
|
||||
|
||||
public abstract class AbsPageObject {
|
||||
private final MobileConfig config;
|
||||
|
||||
protected AbsPageObject(MobileConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
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(fullId(id)));
|
||||
return $(AppiumBy.id(fullIdValue(id)));
|
||||
}
|
||||
|
||||
protected SelenideAppiumCollection allById(String id) {
|
||||
return $$(AppiumBy.id(fullId(id)));
|
||||
}
|
||||
|
||||
protected SelenideElement byText(String text) {
|
||||
return $(AppiumBy.androidUIAutomator("new UiSelector().text(\"" + escape(text) + "\")"));
|
||||
}
|
||||
|
||||
protected SelenideElement byTextContains(String text) {
|
||||
return $(AppiumBy.androidUIAutomator("new UiSelector().textContains(\"" + escape(text) + "\")"));
|
||||
}
|
||||
|
||||
protected SelenideElement byUiAutomator(String selector) {
|
||||
return $(AppiumBy.androidUIAutomator(selector));
|
||||
}
|
||||
|
||||
protected SelenideElement byXpath(String xpath) {
|
||||
return $(AppiumBy.xpath(xpath));
|
||||
}
|
||||
|
||||
protected void type(String id, String value) {
|
||||
SelenideElement input = byId(id).shouldBe(Condition.visible);
|
||||
input.click();
|
||||
input.clear();
|
||||
input.sendKeys(value);
|
||||
}
|
||||
|
||||
protected void tap(String id) {
|
||||
byId(id).shouldBe(Condition.visible).click();
|
||||
}
|
||||
|
||||
protected boolean exists(String id) {
|
||||
return byId(id).exists();
|
||||
}
|
||||
|
||||
protected void shouldHaveItems(String id) {
|
||||
allById(id).shouldHave(CollectionCondition.sizeGreaterThan(0));
|
||||
}
|
||||
|
||||
protected SelenideElement scrollToText(String text) {
|
||||
String selector = "new UiScrollable(new UiSelector().scrollable(true))"
|
||||
+ ".scrollIntoView(new UiSelector().textContains(\"" + escape(text) + "\"))";
|
||||
return $(AppiumBy.androidUIAutomator(selector));
|
||||
return $$(AppiumBy.id(fullIdValue(id)));
|
||||
}
|
||||
|
||||
protected void back() {
|
||||
WebDriverRunner.getWebDriver().navigate().back();
|
||||
}
|
||||
|
||||
protected String xpathLiteral(String value) {
|
||||
if (!value.contains("\"")) {
|
||||
return "\"" + value + "\"";
|
||||
}
|
||||
if (!value.contains("'")) {
|
||||
return "'" + value + "'";
|
||||
}
|
||||
String[] parts = value.split("\"");
|
||||
StringBuilder builder = new StringBuilder("concat(");
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
if (i > 0) {
|
||||
builder.append(", '\"', ");
|
||||
}
|
||||
builder.append("\"").append(parts[i]).append("\"");
|
||||
}
|
||||
builder.append(")");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String fullId(String id) {
|
||||
protected static String fullIdValue(String id) {
|
||||
if (id.contains(":")) {
|
||||
return id;
|
||||
}
|
||||
return config.appPackage() + ":id/" + id;
|
||||
}
|
||||
|
||||
private String escape(String value) {
|
||||
return value.replace("\"", "\\\"");
|
||||
String appPackage = System.getProperty(APP_PACKAGE_PROPERTY, DEFAULT_APP_PACKAGE);
|
||||
return appPackage + ":id/" + id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
package ru.otus.mobile.page;
|
||||
|
||||
import com.codeborne.selenide.Condition;
|
||||
import com.google.inject.Inject;
|
||||
import ru.otus.mobile.component.BottomNavigationComponent;
|
||||
import com.codeborne.selenide.SelenideElement;
|
||||
import com.google.inject.Singleton;
|
||||
import ru.otus.mobile.component.GiftFormComponent;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import ru.otus.mobile.component.GiftsContentComponent;
|
||||
|
||||
@Singleton
|
||||
public final class GiftsPage extends AbsBasePage {
|
||||
private final GiftFormComponent form;
|
||||
|
||||
@Inject
|
||||
public GiftsPage(MobileConfig config, BottomNavigationComponent bottomNavigation, GiftFormComponent form) {
|
||||
super(config, bottomNavigation);
|
||||
this.form = form;
|
||||
}
|
||||
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) {
|
||||
tap("add_button");
|
||||
addButton.click();
|
||||
form.save(name, "100");
|
||||
content.shouldBe(Condition.visible);
|
||||
}
|
||||
|
||||
public void editGift(String oldName, String newName) {
|
||||
openGift(oldName);
|
||||
tap("edit_button");
|
||||
content.byTitle(oldName).edit();
|
||||
form.save(newName, "100");
|
||||
content.shouldBe(Condition.visible);
|
||||
}
|
||||
|
||||
public void shouldSeeGift(String name) {
|
||||
scrollToText(name).shouldBe(Condition.visible);
|
||||
content.shouldBe(Condition.visible);
|
||||
content.byTitle(name).shouldHaveTitle(name);
|
||||
}
|
||||
|
||||
private void openGift(String name) {
|
||||
scrollToText(name).click();
|
||||
content.byTitle(name).open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
package ru.otus.mobile.page;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.codeborne.selenide.SelenideElement;
|
||||
import com.google.inject.Singleton;
|
||||
import ru.otus.mobile.component.AlertDialogComponent;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import ru.otus.mobile.config.TestAccount;
|
||||
|
||||
@Singleton
|
||||
public final class LoginPage extends AbsPageObject {
|
||||
private final AlertDialogComponent alertDialog;
|
||||
|
||||
@Inject
|
||||
public LoginPage(MobileConfig config, AlertDialogComponent alertDialog) {
|
||||
super(config);
|
||||
this.alertDialog = alertDialog;
|
||||
}
|
||||
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;
|
||||
}
|
||||
type("username_text_input", account.username());
|
||||
type("password_text_input", account.password());
|
||||
tap("log_in_button");
|
||||
usernameInput.click();
|
||||
usernameInput.clear();
|
||||
usernameInput.sendKeys(account.username());
|
||||
passwordInput.click();
|
||||
passwordInput.clear();
|
||||
passwordInput.sendKeys(account.password());
|
||||
loginButton.click();
|
||||
alertDialog.acceptIfVisible();
|
||||
}
|
||||
|
||||
public boolean isOpened() {
|
||||
return exists("username_text_input") && exists("password_text_input");
|
||||
return usernameInput.exists() && passwordInput.exists();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,67 @@
|
||||
package ru.otus.mobile.page;
|
||||
|
||||
import com.codeborne.selenide.Condition;
|
||||
import com.google.inject.Inject;
|
||||
import ru.otus.mobile.component.BottomNavigationComponent;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
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 {
|
||||
@Inject
|
||||
public UsersPage(MobileConfig config, BottomNavigationComponent bottomNavigation) {
|
||||
super(config, bottomNavigation);
|
||||
}
|
||||
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();
|
||||
byId("users_content").shouldBe(Condition.visible);
|
||||
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) {
|
||||
String appPackage = config().appPackage();
|
||||
String selector = "new UiScrollable(new UiSelector().resourceId(\"" + appPackage + ":id/users\"))"
|
||||
+ ".setMaxSearchSwipes(12)"
|
||||
+ ".scrollIntoView(new UiSelector().resourceId(\"" + appPackage + ":id/username\")"
|
||||
+ ".textContains(\"" + username.replace("\"", "\\\"") + "\"))";
|
||||
byUiAutomator(selector).shouldBe(Condition.visible).click();
|
||||
usersContent.byUsername(username).open();
|
||||
}
|
||||
|
||||
public void openFirstWishlist() {
|
||||
allById("wishlist_item").first().shouldBe(Condition.visible).click();
|
||||
wishlistsContent.get(0).open();
|
||||
}
|
||||
|
||||
public void openWishlist(String name) {
|
||||
wishlistsContent.byTitle(name).open();
|
||||
}
|
||||
|
||||
public void openFirstGift() {
|
||||
allById("gift_item").first().shouldBe(Condition.visible).click();
|
||||
currentGift = giftsContent.get(0);
|
||||
}
|
||||
|
||||
public void openGift(String name) {
|
||||
currentGift = giftsContent.byTitle(name);
|
||||
}
|
||||
|
||||
public boolean isReserved() {
|
||||
return Boolean.parseBoolean(byId("reserved").shouldBe(Condition.visible).getAttribute("checked"));
|
||||
return selectedGift().isReserved();
|
||||
}
|
||||
|
||||
public void toggleReservation() {
|
||||
tap("reserved");
|
||||
selectedGift().toggleReservation();
|
||||
}
|
||||
|
||||
private GiftItemComponent selectedGift() {
|
||||
if (currentGift == null) {
|
||||
throw new IllegalStateException("Gift is not selected. Call openFirstGift/openGift first.");
|
||||
}
|
||||
return currentGift;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,43 @@
|
||||
package ru.otus.mobile.page;
|
||||
|
||||
import com.codeborne.selenide.Condition;
|
||||
import com.google.inject.Inject;
|
||||
import ru.otus.mobile.component.BottomNavigationComponent;
|
||||
import com.codeborne.selenide.SelenideElement;
|
||||
import com.google.inject.Singleton;
|
||||
import ru.otus.mobile.component.WishlistsContentComponent;
|
||||
import ru.otus.mobile.component.WishlistFormComponent;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Singleton
|
||||
public final class WishlistsPage extends AbsBasePage {
|
||||
private final WishlistFormComponent form;
|
||||
|
||||
@Inject
|
||||
public WishlistsPage(MobileConfig config, BottomNavigationComponent bottomNavigation, WishlistFormComponent form) {
|
||||
super(config, bottomNavigation);
|
||||
this.form = form;
|
||||
}
|
||||
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();
|
||||
byId("wishlists_content").shouldBe(Condition.visible);
|
||||
content.shouldBe(Condition.visible, Duration.ofSeconds(15));
|
||||
}
|
||||
|
||||
public void createWishlist(String name) {
|
||||
tap("add_button");
|
||||
addButton.click();
|
||||
form.save(name);
|
||||
}
|
||||
|
||||
public void editWishlist(String oldName, String newName) {
|
||||
ensureWishlistsList();
|
||||
editButtonFor(oldName).shouldBe(Condition.visible).click();
|
||||
content.byTitle(oldName).edit();
|
||||
form.save(newName);
|
||||
}
|
||||
|
||||
public void shouldSeeWishlist(String name) {
|
||||
scrollToText(name).shouldBe(Condition.visible);
|
||||
content.byTitle(name).shouldHaveTitle(name);
|
||||
}
|
||||
|
||||
public void openWishlist(String name) {
|
||||
scrollToText(name).click();
|
||||
content.byTitle(name).open();
|
||||
}
|
||||
|
||||
private void ensureWishlistsList() {
|
||||
if (exists("wishlist_item") || exists("wishlists")) {
|
||||
return;
|
||||
}
|
||||
if (exists("gifts_content") || exists("gift_item") || exists("add_button")) {
|
||||
back();
|
||||
}
|
||||
byId("wishlists_content").shouldBe(Condition.visible);
|
||||
}
|
||||
|
||||
private com.codeborne.selenide.SelenideElement editButtonFor(String name) {
|
||||
String full = config().appPackage();
|
||||
String xpath = "//androidx.recyclerview.widget.RecyclerView[@resource-id='" + full + ":id/wishlists']"
|
||||
+ "//android.view.ViewGroup[@resource-id='" + full + ":id/wishlist_item'"
|
||||
+ " and .//android.widget.TextView[@resource-id='" + full + ":id/title'"
|
||||
+ " and contains(@text," + xpathLiteral(name) + ")]]"
|
||||
+ "//android.widget.Button[@resource-id='" + full + ":id/edit_button']";
|
||||
return byXpath(xpath);
|
||||
public void openFirstWishlist() {
|
||||
content.first().open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
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.GiftsPage;
|
||||
import ru.otus.mobile.page.LoginPage;
|
||||
import ru.otus.mobile.page.WishlistsPage;
|
||||
import ru.otus.mobile.util.TestData;
|
||||
|
||||
@@ -23,16 +26,23 @@ public class GiftsTest {
|
||||
@Inject
|
||||
private TestData testData;
|
||||
|
||||
@Inject
|
||||
private DbClient dbClient;
|
||||
|
||||
@Inject
|
||||
private LoginPage loginPage;
|
||||
|
||||
@Test
|
||||
@DisplayName("Создание и редактирование подарка")
|
||||
void createAndEditGift() {
|
||||
String wishlistName = testData.uniqueName("Wishlist");
|
||||
void createAndEditGift(TestContext context) {
|
||||
context.account().dataPreparation().accept(dbClient, context.account().username());
|
||||
loginPage.login(context.account());
|
||||
|
||||
String name = testData.uniqueName("Gift");
|
||||
String updated = name + " updated";
|
||||
|
||||
wishlistsPage.open();
|
||||
wishlistsPage.createWishlist(wishlistName);
|
||||
wishlistsPage.openWishlist(wishlistName);
|
||||
wishlistsPage.openFirstWishlist();
|
||||
giftsPage.createGift(name);
|
||||
giftsPage.shouldSeeGift(name);
|
||||
giftsPage.editGift(name, updated);
|
||||
|
||||
@@ -5,9 +5,11 @@ import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import ru.otus.mobile.annotations.MobileUser;
|
||||
import ru.otus.mobile.config.MobileConfig;
|
||||
import ru.otus.mobile.config.TestAccount;
|
||||
import ru.otus.mobile.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.assertNotEquals;
|
||||
@@ -19,13 +21,20 @@ public class ReservationTest {
|
||||
private UsersPage usersPage;
|
||||
|
||||
@Inject
|
||||
private MobileConfig mobileConfig;
|
||||
private DbClient dbClient;
|
||||
|
||||
@Inject
|
||||
private LoginPage loginPage;
|
||||
|
||||
@Test
|
||||
@DisplayName("Изменение статуса резервирования подарка другого пользователя")
|
||||
void changeReservationStatus() {
|
||||
void changeReservationStatus(TestContext context) {
|
||||
TestAccount.RESERVATION_OWNER.dataPreparation().accept(dbClient, TestAccount.RESERVATION_OWNER.username());
|
||||
loginPage.login(context.account());
|
||||
|
||||
usersPage.open();
|
||||
usersPage.openUser(mobileConfig.reservationOwnerUsername());
|
||||
usersPage.filterByUsername(TestAccount.RESERVATION_OWNER.username());
|
||||
usersPage.openUser(TestAccount.RESERVATION_OWNER.username());
|
||||
usersPage.openFirstWishlist();
|
||||
usersPage.openFirstGift();
|
||||
boolean before = usersPage.isReserved();
|
||||
|
||||
@@ -6,7 +6,10 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
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;
|
||||
|
||||
@@ -19,9 +22,18 @@ public class WishlistsTest {
|
||||
@Inject
|
||||
private TestData testData;
|
||||
|
||||
@Inject
|
||||
private DbClient dbClient;
|
||||
|
||||
@Inject
|
||||
private LoginPage loginPage;
|
||||
|
||||
@Test
|
||||
@DisplayName("Создание и редактирование списка желаний")
|
||||
void createAndEditWishlist() {
|
||||
void createAndEditWishlist(TestContext context) {
|
||||
context.account().dataPreparation().accept(dbClient, context.account().username());
|
||||
loginPage.login(context.account());
|
||||
|
||||
String name = testData.uniqueName("Wishlist");
|
||||
String updated = name + " updated";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user