Simplify mobile test architecture and stabilize data setup

This commit is contained in:
2026-04-18 03:27:16 +03:00
parent aaf63cc438
commit 46f3de4d55
17 changed files with 326 additions and 338 deletions
+1 -8
View File
@@ -1,11 +1,4 @@
DB_URL=jdbc:postgresql://<host>:<port>/<db> DB_URL=jdbc:postgresql://<host>:<port>/<db>
DB_USER=<db_user> DB_USER=<db_user>
DB_PASSWORD=<db_password> DB_PASSWORD=<db_password>
MOBILE_HOST=127.0.0.1
WISHLISTS_USERNAME=<login_1>
WISHLISTS_PASSWORD=<password_1>
GIFTS_USERNAME=<login_2>
GIFTS_PASSWORD=<password_2>
RESERVATION_USERNAME=<login_3>
RESERVATION_PASSWORD=<password_3>
RESERVATION_OWNER=<login_owner>
+4
View File
@@ -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
+11 -25
View File
@@ -59,13 +59,7 @@ PowerShell:
$env:DB_URL="jdbc:postgresql://<host>:<port>/<db>" $env:DB_URL="jdbc:postgresql://<host>:<port>/<db>"
$env:DB_USER="<db_user>" $env:DB_USER="<db_user>"
$env:DB_PASSWORD="<db_password>" $env:DB_PASSWORD="<db_password>"
$env:WISHLISTS_USERNAME="<login_1>" $env:MOBILE_HOST="127.0.0.1"
$env:WISHLISTS_PASSWORD="<password_1>"
$env:GIFTS_USERNAME="<login_2>"
$env:GIFTS_PASSWORD="<password_2>"
$env:RESERVATION_USERNAME="<login_3>"
$env:RESERVATION_PASSWORD="<password_3>"
$env:RESERVATION_OWNER="<login_owner>"
``` ```
bash: bash:
@@ -73,30 +67,22 @@ bash:
export DB_URL="jdbc:postgresql://<host>:<port>/<db>" export DB_URL="jdbc:postgresql://<host>:<port>/<db>"
export DB_USER="<db_user>" export DB_USER="<db_user>"
export DB_PASSWORD="<db_password>" export DB_PASSWORD="<db_password>"
export WISHLISTS_USERNAME="<login_1>" export MOBILE_HOST="127.0.0.1"
export WISHLISTS_PASSWORD="<password_1>"
export GIFTS_USERNAME="<login_2>"
export GIFTS_PASSWORD="<password_2>"
export RESERVATION_USERNAME="<login_3>"
export RESERVATION_PASSWORD="<password_3>"
export RESERVATION_OWNER="<login_owner>"
``` ```
Оба варианта эквивалентны: тесты используют переменные окружения процесса. Оба варианта эквивалентны: тесты используют переменные окружения процесса.
Тестовые пользователи зафиксированы в коде (`TestAccount`):
- `user1us` — тест списков желаний;
- `user2us` — тест подарков;
- `user3us` — пользователь, который резервирует подарок;
- `user4us` — пользователь-владелец списка в тесте резервирования.
Для production-подобного запуска используйте шаблон `.env.production.example` и не коммитьте реальные значения в репозиторий.
Тесты запускаются параллельно по классам (2 потока) и распределяются по эмуляторам через очередь. Тесты запускаются параллельно по классам (2 потока) и распределяются по эмуляторам через очередь.
4. Запустить тесты: 4. Запустить тесты:
```bash ```bash
mvn test mvn test
``` ```
Примечание: если нужен запуск только на одном эмуляторе, можно поднять только `wiremock` и `android-emulator-1`, а перед `mvn test` задать: Примечание: эмуляторы зафиксированы в enum `TestEmulator` (порты `4723` и `4725`), в конфигурации задается только хост (`MOBILE_HOST`).
PowerShell:
```powershell
$env:MOBILE_EMULATORS="android-emulator-1|http://127.0.0.1:4723|Android Emulator"
```
bash:
```bash
export MOBILE_EMULATORS="android-emulator-1|http://127.0.0.1:4723|Android Emulator"
```
@@ -1,23 +1,22 @@
package ru.otus.mobile.config; package ru.otus.mobile.config;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public final class MobileConfig { public final class MobileConfig {
private final String appPackage; private final String appPackage;
private final String appUrl; private final String appUrl;
private final String reservationOwnerUsername; private final String appiumHost;
private final List<Emulator> emulators; private final List<Emulator> emulators;
public MobileConfig( public MobileConfig(
String appPackage, String appPackage,
String appUrl, String appUrl,
String reservationOwnerUsername, String appiumHost,
List<Emulator> emulators List<Emulator> emulators
) { ) {
this.appPackage = appPackage; this.appPackage = appPackage;
this.appUrl = appUrl; this.appUrl = appUrl;
this.reservationOwnerUsername = reservationOwnerUsername; this.appiumHost = appiumHost;
this.emulators = List.copyOf(emulators); this.emulators = List.copyOf(emulators);
} }
@@ -29,33 +28,13 @@ public final class MobileConfig {
return appUrl; return appUrl;
} }
public String reservationOwnerUsername() { public String appiumHost() {
return reservationOwnerUsername; return appiumHost;
} }
public List<Emulator> emulators() { public List<Emulator> emulators() {
return emulators; return emulators;
} }
public record Emulator(String id, String appiumUrl, String deviceName, String appUrl) { 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;
}
}
} }
@@ -1,93 +1,34 @@
package ru.otus.mobile.config; package ru.otus.mobile.config;
import ru.otus.mobile.db.DbClient;
import java.util.function.BiConsumer;
public enum TestAccount { public enum TestAccount {
WISHLISTS("WISHLISTS", "user1us", "user1us", """ WISHLISTS("user1us", "user1us", DbClient::clearWishlistsByUsername),
WITH target_user AS ( GIFTS("user2us", "user2us", DbClient::clearGiftsByUsername),
SELECT id FROM users WHERE username = ? RESERVATION("user3us", "user3us", DbClient::doNothing),
), RESERVATION_OWNER("user4us", "user4us", DbClient::clearReservationByUsername);
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("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("RESERVATION", "user3us", "user3us", """
WITH owner_user AS (
SELECT id FROM users WHERE username = ?
),
target_user AS (
SELECT id FROM users WHERE username = ?
),
delete_owner_gifts AS (
DELETE FROM gifts
WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM owner_user))
),
delete_owner_wishlists AS (
DELETE FROM wishlists WHERE user_id IN (SELECT id FROM owner_user)
),
delete_target_gifts AS (
DELETE FROM gifts
WHERE wish_id IN (SELECT id FROM wishlists WHERE user_id IN (SELECT id FROM target_user))
),
delete_target_wishlists AS (
DELETE FROM wishlists WHERE user_id IN (SELECT id FROM target_user)
),
owner_wishlist AS (
INSERT INTO wishlists (id, title, description, user_id)
SELECT gen_random_uuid(), 'Owner Wishlist', 'Prepared for reservation test', id
FROM owner_user
RETURNING id
)
INSERT INTO gifts (id, name, description, price, is_reserved, store_url, image_url, wish_id)
SELECT gen_random_uuid(), 'Owner Gift', 'Prepared for reservation test', 100, false, NULL, NULL, id
FROM owner_wishlist;
""");
private final String envPrefix; private final String username;
private final String defaultUsername; private final String password;
private final String defaultPassword; private final BiConsumer<DbClient, String> dataPreparation;
private final String resetSql;
TestAccount(String envPrefix, String defaultUsername, String defaultPassword, String resetSql) { TestAccount(String username, String password, BiConsumer<DbClient, String> dataPreparation) {
this.envPrefix = envPrefix; this.username = username;
this.defaultUsername = defaultUsername; this.password = password;
this.defaultPassword = defaultPassword; this.dataPreparation = dataPreparation;
this.resetSql = resetSql;
} }
public String username() { public String username() {
return resolve(envPrefix + "_USERNAME", defaultUsername); return username;
} }
public String password() { public String password() {
return resolve(envPrefix + "_PASSWORD", defaultPassword); return password;
} }
public String resetSql() { public BiConsumer<DbClient, String> dataPreparation() {
return resetSql; return dataPreparation;
}
private String resolve(String envKey, String defaultValue) {
String value = System.getenv(envKey);
if (value == null || value.isBlank()) {
return defaultValue;
}
return value;
} }
} }
@@ -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
);
}
}
+43 -26
View File
@@ -1,8 +1,6 @@
package ru.otus.mobile.db; package ru.otus.mobile.db;
import com.google.inject.Inject; import com.google.inject.Inject;
import ru.otus.mobile.config.MobileConfig;
import ru.otus.mobile.config.TestAccount;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DriverManager; import java.sql.DriverManager;
@@ -11,37 +9,56 @@ import java.sql.SQLException;
public final class DbClient { public final class DbClient {
private final DbConfig config; private final DbConfig config;
private final MobileConfig mobileConfig;
@Inject @Inject
public DbClient(DbConfig config, MobileConfig mobileConfig) { public DbClient(DbConfig config) {
this.config = config; this.config = config;
this.mobileConfig = mobileConfig;
} }
public void resetData(TestAccount account) { public void clearWishlistsByUsername(String username) {
try (Connection connection = DriverManager.getConnection(config.url(), config.user(), config.password()); executeUpdate("""
PreparedStatement statement = connection.prepareStatement(account.resetSql())) { DELETE FROM wishlists
if (account == TestAccount.RESERVATION) { WHERE user_id IN (SELECT id FROM users WHERE username = ?)
statement.setString(1, mobileConfig.reservationOwnerUsername()); """, username, "clear wishlists");
statement.setString(2, account.username()); }
} else {
statement.setString(1, account.username()); 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) { } 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);
}
}
public void prepareReservationData(String ownerUsername, String reservationUsername) {
try (Connection connection = DriverManager.getConnection(config.url(), config.user(), config.password());
PreparedStatement statement = connection.prepareStatement(TestAccount.RESERVATION.resetSql())) {
statement.setString(1, ownerUsername);
statement.setString(2, reservationUsername);
statement.execute();
} catch (SQLException e) {
throw new IllegalStateException("Failed to prepare reservation data for owner " + ownerUsername, e);
} }
} }
} }
@@ -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; package ru.otus.mobile.extensions;
import com.codeborne.selenide.WebDriverRunner;
import com.google.inject.Guice; import com.google.inject.Guice;
import com.google.inject.Injector; 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.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.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.annotations.MobileUser;
import ru.otus.mobile.config.MobileConfig;
import ru.otus.mobile.config.TestAccount; import ru.otus.mobile.config.TestAccount;
import ru.otus.mobile.db.DbClient; import ru.otus.mobile.config.TestContext;
import ru.otus.mobile.driver.MobileSession; import ru.otus.mobile.driver.EmulatorQueue;
import ru.otus.mobile.driver.MobileSessionFactory; import ru.otus.mobile.driver.LogcatCollector;
import ru.otus.mobile.driver.MobileDriverFactory;
import ru.otus.mobile.guice.CoreModule; import ru.otus.mobile.guice.CoreModule;
import ru.otus.mobile.page.LoginPage;
public final class MobileExtension implements public final class MobileExtension implements
BeforeEachCallback, TestInstancePostProcessor,
ParameterResolver,
AfterTestExecutionCallback,
AfterEachCallback { AfterEachCallback {
private static final ExtensionContext.Namespace NAMESPACE = private static final ExtensionContext.Namespace NAMESPACE =
ExtensionContext.Namespace.create(MobileExtension.class); ExtensionContext.Namespace.create(MobileExtension.class);
private static final String SESSION_KEY = "mobile.session"; private static final String SESSION_KEY = "mobile.session";
private static final Injector INJECTOR = Guice.createInjector(new CoreModule());
@Override @Override
public void beforeEach(ExtensionContext context) { public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
Injector injector = rootInjector(context); INJECTOR.injectMembers(testInstance);
TestAccount account = resolveAccount(context); }
injector.getInstance(DbClient.class).resetData(account);
MobileSessionFactory sessionFactory = injector.getInstance(MobileSessionFactory.class); @Override
MobileSession session = sessionFactory.create(account, context.getDisplayName()); public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
session.activate(); throws ParameterResolutionException {
session.injectMembers(context.getRequiredTestInstance()); return TestContext.class.equals(parameterContext.getParameter().getType());
context.getStore(NAMESPACE).put(SESSION_KEY, session); }
injector.getInstance(LoginPage.class).login(account);
@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 @Override
public void afterEach(ExtensionContext context) { public void afterEach(ExtensionContext context) {
MobileSession session = context.getStore(NAMESPACE).remove(SESSION_KEY, MobileSession.class); TestContext testContext = context.getStore(NAMESPACE).remove(SESSION_KEY, TestContext.class);
if (session != null) { if (testContext != null) {
session.close(); 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) { private TestAccount resolveAccount(ExtensionContext context) {
MobileUser annotation = context.getRequiredTestClass().getAnnotation(MobileUser.class); MobileUser annotation = context.getRequiredTestClass().getAnnotation(MobileUser.class);
if (annotation == null) { if (annotation == null) {
@@ -55,4 +73,30 @@ public final class MobileExtension implements
} }
return annotation.value(); 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());
}
}
} }
@@ -5,11 +5,13 @@ import com.google.inject.Provides;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import ru.otus.mobile.config.MobileConfig; import ru.otus.mobile.config.MobileConfig;
import ru.otus.mobile.config.ProjectPaths; import ru.otus.mobile.config.ProjectPaths;
import ru.otus.mobile.config.TestEmulator;
import ru.otus.mobile.db.DbConfig; import ru.otus.mobile.db.DbConfig;
import ru.otus.mobile.driver.EmulatorQueue; import ru.otus.mobile.driver.EmulatorQueue;
import ru.otus.mobile.driver.LogcatCollector; import ru.otus.mobile.driver.LogcatCollector;
import ru.otus.mobile.driver.MobileDriverFactory; import ru.otus.mobile.driver.MobileDriverFactory;
import java.util.Arrays;
import java.util.List; import java.util.List;
public final class CoreModule extends AbstractModule { public final class CoreModule extends AbstractModule {
@@ -22,15 +24,11 @@ public final class CoreModule extends AbstractModule {
MobileConfig mobileConfig() { MobileConfig mobileConfig() {
String appPackage = value("app.package", "APP_PACKAGE", "ru.otus.wishlist"); String appPackage = value("app.package", "APP_PACKAGE", "ru.otus.wishlist");
String appUrl = value("app.url", "APP_URL", "http://wiremock:8080/wishlist.apk"); String appUrl = value("app.url", "APP_URL", "http://wiremock:8080/wishlist.apk");
String reservationOwner = value("reservation.owner", "RESERVATION_OWNER", "user4us"); String appiumHost = value("mobile.host", "MOBILE_HOST", "127.0.0.1");
String rawEmulators = value( List<MobileConfig.Emulator> emulators = Arrays.stream(TestEmulator.values())
"mobile.emulators", .map(emulator -> emulator.toEmulator(appiumHost, appUrl))
"MOBILE_EMULATORS", .toList();
"android-emulator-1|http://127.0.0.1:4723|Android Emulator," return new MobileConfig(appPackage, appUrl, appiumHost, emulators);
+ "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 @Provides
@@ -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();
}
}
@@ -20,6 +20,15 @@ public final class UsersPage extends AbsBasePage {
byId("users_content").shouldBe(Condition.visible); byId("users_content").shouldBe(Condition.visible);
} }
public void filterByUsername(String username) {
openFilter();
SelenideElement input = filterInput().shouldBe(Condition.visible, Duration.ofSeconds(15));
input.clear();
input.setValue(username);
applyFilter();
byTextContains(username).shouldBe(Condition.visible, Duration.ofSeconds(15));
}
public void openUser(String username) { public void openUser(String username) {
String appPackage = config().appPackage(); String appPackage = config().appPackage();
String xpath = "//android.widget.TextView[@resource-id='" + appPackage + ":id/username'" String xpath = "//android.widget.TextView[@resource-id='" + appPackage + ":id/username'"
@@ -33,18 +42,6 @@ public final class UsersPage extends AbsBasePage {
byTextContains(username).shouldBe(Condition.visible, Duration.ofSeconds(15)); byTextContains(username).shouldBe(Condition.visible, Duration.ofSeconds(15));
} }
public String firstVisibleUsernameExcluding(String excludedUsername) {
var usernames = allById("username")
.shouldHave(CollectionCondition.sizeGreaterThan(0), Duration.ofSeconds(15));
for (SelenideElement usernameElement : usernames) {
String username = usernameElement.getText().trim();
if (!username.isEmpty() && !username.equals(excludedUsername)) {
return username;
}
}
throw new IllegalStateException("No visible username found excluding '" + excludedUsername + "'.");
}
public void openFirstWishlist() { public void openFirstWishlist() {
allById("wishlist_item") allById("wishlist_item")
.shouldHave(CollectionCondition.sizeGreaterThan(0), Duration.ofSeconds(15)) .shouldHave(CollectionCondition.sizeGreaterThan(0), Duration.ofSeconds(15))
@@ -76,4 +73,85 @@ public final class UsersPage extends AbsBasePage {
public void toggleReservation() { public void toggleReservation() {
tap("reserved"); tap("reserved");
} }
private void openFilter() {
if (byId("users_filter").exists()) {
tap("users_filter");
byId("users_filter_bottom_sheet").shouldBe(Condition.visible, Duration.ofSeconds(15));
return;
}
if (byId("filter_button").exists()) {
tap("filter_button");
byId("users_filter_bottom_sheet").shouldBe(Condition.visible, Duration.ofSeconds(15));
return;
}
if (byId("action_filter").exists()) {
tap("action_filter");
byId("users_filter_bottom_sheet").shouldBe(Condition.visible, Duration.ofSeconds(15));
return;
}
SelenideElement button = byUiAutomator("new UiSelector().descriptionContains(\"Фильтр\")");
if (button.exists()) {
button.click();
byId("users_filter_bottom_sheet").shouldBe(Condition.visible, Duration.ofSeconds(15));
return;
}
button = byUiAutomator("new UiSelector().descriptionContains(\"Filter\")");
if (button.exists()) {
button.click();
byId("users_filter_bottom_sheet").shouldBe(Condition.visible, Duration.ofSeconds(15));
return;
}
throw new IllegalStateException("Filter button was not found on Users screen.");
}
private SelenideElement filterInput() {
if (byId("filter_input").exists()) {
return byId("filter_input");
}
if (byId("username_filter_input").exists()) {
return byId("username_filter_input");
}
if (byId("users_filter_input").exists()) {
return byId("users_filter_input");
}
if (byId("username_input").exists()) {
return byId("username_input");
}
if (byId("search_src_text").exists()) {
return byId("search_src_text");
}
if (byId("query_text").exists()) {
return byId("query_text");
}
String appPackage = config().appPackage();
SelenideElement editText = byXpath(
"//android.view.ViewGroup[@resource-id='" + appPackage + ":id/users_filter_bottom_sheet']"
+ "//android.widget.EditText"
);
if (editText.exists()) {
return editText;
}
throw new IllegalStateException("Filter input was not found on Users screen.");
}
private void applyFilter() {
if (byId("apply_button").exists()) {
tap("apply_button");
return;
}
if (byId("apply").exists()) {
tap("apply");
return;
}
if (byText("ПРИМЕНИТЬ").exists()) {
byText("ПРИМЕНИТЬ").click();
return;
}
if (byText("APPLY").exists()) {
byText("APPLY").click();
return;
}
throw new IllegalStateException("Apply filter button was not found on Users filter screen.");
}
} }
@@ -1,11 +1,14 @@
package ru.otus.mobile.page; package ru.otus.mobile.page;
import com.codeborne.selenide.Condition; import com.codeborne.selenide.Condition;
import com.codeborne.selenide.CollectionCondition;
import com.google.inject.Inject; import com.google.inject.Inject;
import ru.otus.mobile.component.BottomNavigationComponent; import ru.otus.mobile.component.BottomNavigationComponent;
import ru.otus.mobile.component.WishlistFormComponent; import ru.otus.mobile.component.WishlistFormComponent;
import ru.otus.mobile.config.MobileConfig; import ru.otus.mobile.config.MobileConfig;
import java.time.Duration;
public final class WishlistsPage extends AbsBasePage { public final class WishlistsPage extends AbsBasePage {
private final WishlistFormComponent form; private final WishlistFormComponent form;
@@ -39,6 +42,14 @@ public final class WishlistsPage extends AbsBasePage {
scrollToText(name).click(); scrollToText(name).click();
} }
public void openFirstWishlist() {
allById("wishlist_item")
.shouldHave(CollectionCondition.sizeGreaterThan(0), Duration.ofSeconds(15))
.first()
.shouldBe(Condition.visible, Duration.ofSeconds(15))
.click();
}
private void ensureWishlistsList() { private void ensureWishlistsList() {
if (exists("wishlist_item") || exists("wishlists")) { if (exists("wishlist_item") || exists("wishlists")) {
return; return;
@@ -6,8 +6,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import ru.otus.mobile.annotations.MobileUser; import ru.otus.mobile.annotations.MobileUser;
import ru.otus.mobile.config.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.extensions.MobileExtension;
import ru.otus.mobile.page.GiftsPage; import ru.otus.mobile.page.GiftsPage;
import ru.otus.mobile.page.LoginPage;
import ru.otus.mobile.page.WishlistsPage; import ru.otus.mobile.page.WishlistsPage;
import ru.otus.mobile.util.TestData; import ru.otus.mobile.util.TestData;
@@ -23,16 +26,23 @@ public class GiftsTest {
@Inject @Inject
private TestData testData; private TestData testData;
@Inject
private DbClient dbClient;
@Inject
private LoginPage loginPage;
@Test @Test
@DisplayName("Создание и редактирование подарка") @DisplayName("Создание и редактирование подарка")
void createAndEditGift() { void createAndEditGift(TestContext context) {
String wishlistName = testData.uniqueName("Wishlist"); context.account().dataPreparation().accept(dbClient, context.account().username());
loginPage.login(context.account());
String name = testData.uniqueName("Gift"); String name = testData.uniqueName("Gift");
String updated = name + " updated"; String updated = name + " updated";
wishlistsPage.open(); wishlistsPage.open();
wishlistsPage.createWishlist(wishlistName); wishlistsPage.openFirstWishlist();
wishlistsPage.openWishlist(wishlistName);
giftsPage.createGift(name); giftsPage.createGift(name);
giftsPage.shouldSeeGift(name); giftsPage.shouldSeeGift(name);
giftsPage.editGift(name, updated); giftsPage.editGift(name, updated);
@@ -6,8 +6,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import ru.otus.mobile.annotations.MobileUser; import ru.otus.mobile.annotations.MobileUser;
import ru.otus.mobile.config.TestAccount; import ru.otus.mobile.config.TestAccount;
import ru.otus.mobile.config.TestContext;
import ru.otus.mobile.db.DbClient; import ru.otus.mobile.db.DbClient;
import ru.otus.mobile.extensions.MobileExtension; import ru.otus.mobile.extensions.MobileExtension;
import ru.otus.mobile.page.LoginPage;
import ru.otus.mobile.page.UsersPage; import ru.otus.mobile.page.UsersPage;
import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -15,25 +17,26 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ExtendWith(MobileExtension.class) @ExtendWith(MobileExtension.class)
@MobileUser(TestAccount.RESERVATION) @MobileUser(TestAccount.RESERVATION)
public class ReservationTest { public class ReservationTest {
private static final String OWNER_WISHLIST_NAME = "Owner Wishlist";
private static final String OWNER_GIFT_NAME = "Owner Gift";
@Inject @Inject
private UsersPage usersPage; private UsersPage usersPage;
@Inject @Inject
private DbClient dbClient; private DbClient dbClient;
@Inject
private LoginPage loginPage;
@Test @Test
@DisplayName("Изменение статуса резервирования подарка другого пользователя") @DisplayName("Изменение статуса резервирования подарка другого пользователя")
void changeReservationStatus() { void changeReservationStatus(TestContext context) {
TestAccount.RESERVATION_OWNER.dataPreparation().accept(dbClient, TestAccount.RESERVATION_OWNER.username());
loginPage.login(context.account());
usersPage.open(); usersPage.open();
String reservationOwner = usersPage.firstVisibleUsernameExcluding(TestAccount.RESERVATION.username()); usersPage.filterByUsername(TestAccount.RESERVATION_OWNER.username());
dbClient.prepareReservationData(reservationOwner, TestAccount.RESERVATION.username()); usersPage.openUser(TestAccount.RESERVATION_OWNER.username());
usersPage.open(); usersPage.openFirstWishlist();
usersPage.openUser(reservationOwner); usersPage.openFirstGift();
usersPage.openWishlist(OWNER_WISHLIST_NAME);
usersPage.openGift(OWNER_GIFT_NAME);
boolean before = usersPage.isReserved(); boolean before = usersPage.isReserved();
usersPage.toggleReservation(); usersPage.toggleReservation();
assertNotEquals(before, usersPage.isReserved(), "Reservation status was not changed"); assertNotEquals(before, usersPage.isReserved(), "Reservation status was not changed");
@@ -6,7 +6,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import ru.otus.mobile.annotations.MobileUser; import ru.otus.mobile.annotations.MobileUser;
import ru.otus.mobile.config.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.extensions.MobileExtension;
import ru.otus.mobile.page.LoginPage;
import ru.otus.mobile.page.WishlistsPage; import ru.otus.mobile.page.WishlistsPage;
import ru.otus.mobile.util.TestData; import ru.otus.mobile.util.TestData;
@@ -19,9 +22,18 @@ public class WishlistsTest {
@Inject @Inject
private TestData testData; private TestData testData;
@Inject
private DbClient dbClient;
@Inject
private LoginPage loginPage;
@Test @Test
@DisplayName("Создание и редактирование списка желаний") @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 name = testData.uniqueName("Wishlist");
String updated = name + " updated"; String updated = name + " updated";