Привет, хабровчане! С вами снова Евгений Иванов, QA-lead в компании Fix Price. В этот раз поделюсь с Вами опытом внедрения автоматизации для гибридного мобильного приложения на Android. 

У этого решения есть свои плюсы и минусы, и мы продолжаем работать над его развитием. Но уже сейчас понятно: оно приносит реальную пользу команде во время регрессионных и предрелизных прогонов. Расскажу подробнее, как мы собрали связку Playwright + Appium + WebdriverIO и что из этого вышло.

Предпосылки выбора стека

В начале расскажу, почему мы пошли именно по этому пути. Наш выбор обусловлен несколькими ключевыми факторами:

  1. Опыт команды. Наша команда уже имеет опыт написания автотестов на связке Playwright + TypeScript. Использование Playwright как основы для структуры тестовых файлов поможет быстро войти в проект мобильных тестов и снижению порога входа для новых сотрудников.

  2. Производительность. Playwright показывает более высокую скорость выполнения по сравнению с WebdriverIO. Это делает его предпочтительным выбором для тестирования веб-части (WebView), позволяя получать результат быстрее без потери стабильности.

  3. Управление авторизацией. Playwright умеет легко сохранять и переиспользовать состояния авторизации (cookies, localStorage). В нашем мобильном приложении на Android процессы авторизации и аутентификации выполняются именно через веб-часть SSO Keyсloak, что делает эту функцию незаменимой.

  4. Поддержка мобильных браузеров. У Playwright есть экспериментальная поддержка для тестирования браузерных приложений на Android, что дает дополнительный инструментарий для работы с веб-контекстом.

  5. Тестирование нативной части. Нативную часть мы тестируем через связку Appium + WebdriverIO. Синтаксис последнего во многом схож с Playwright, что позволяет поддерживать единый стиль кода в проекте.

Архитектура решения

Архитектура данного проекта значительно отличается от традиционной структуры веб-автотестов на Playwright. Мы решаем специфическую задачу: тестирование гибридного мобильного приложения, где подход к тестированию нативного Android-приложения используется совместно с тестированием веб-части и WebView. Ниже приведено подробное сравнение и описание архитектурных особенностей.(см. таблица 1)

Таблица1. Сравнение с традиционной веб-структурой Playwright.

Характеристика

Традиционный Веб-проект (Playwright)

Гибридный проект (PW + Appium + WDIO)

Объект тестирования

Браузер (Chromium, Firefox, WebKit).

Android-устройство (Эмулятор/Реал), APK и Браузер.

Основной драйвер

Playwright (через протокол CDP).

WebdriverIO (через Appium Server и UiAutomator2).

Авторизация

Вкладка в браузере.

Keycloak в браузере → Перенос сессии в Android Chrome.

Слои абстракции

Page Object Model (POM).

Screen Object (для native) + Page Object (для web).

Управление сессией

storageState используется внутри PW.

storageState извлекается PW и инжектируется в Android Chrome через хелперы.

Схема1.Общая схема архитектуры.

Описание ключевых слоев архитектуры.

Архитектура проекта (см. Схема1) построена по принципу разделения ответственности между шестью основными слоями. Такой подход позволяет эффективно комбинировать веб и нативные мобильные инструменты, сохраняя код поддерживаемым и понятным.

1. Слой тестов (Tests)

  • Setup-тесты (auth-android.setup.ts): Отвечают за предварительную авторизацию. В отличие от классических веб-тестов, здесь Playwright используется только для получения сессии (cookies, local storage), которая впоследствии будет передана на устройство.

  • Специфические тесты (.spec.ts): Описывают бизнес-логику и сценарии, которые выполняются непосредственно на мобильном устройстве.

2. Слой фикстур и конфигурации

  • Фикстура (mainScreen.fixture.ts): Объединяет в себе инициализацию WebdriverIO (remote) и объекты экранов. Служит точкой входа для подготовки окружения перед каждым тестом.

  • Конфиг Playwright: Управляет запуском тестов, проектами и глобальными настройками раннера.

3. Слой Page / Screen Object

  • LoginPage (Playwright + Web): Традиционный Page Object для работы с веб-формой авторизации через браузер.

  • MainScreen (WebdriverIO + Native): Использует синтаксис WebdriverIO (например, $, $$, tap) для управления элементами внутри нативного мобильного приложения.

4. Слой утилит и хелперов

  • Этот слой обеспечивает взаимодействие компонентов. Классы AndroidBrowserManager и HelperAuth отвечают за передачу cookies, полученных Playwright в начале теста, в браузер Chrome на Android-устройстве.

  • Здесь также хранится DriverData.ts — файл с настройками (capabilities) для подключения к Appium.

5. Слой инструментов

  • Playwright: Используется как тест-раннер и инструмент для работы с веб-контекстом.

  • WebdriverIO: Выступает в качестве удобной обертки (драйвера) для управления мобильной сессией Appium.

  • Appium: Прослойка между драйвером и устройством, которая транслирует команды в действия на устройстве.

6. Слой устройства

  • Конечная точка, где происходит реальное взаимодействие. Это может быть эмулятор или физический смартфон, на котором одновременно работают: браузер Chrome (с инжектированной сессией), целевое APK-приложение и системные компоненты (диалоги разрешений, уведомления).

1. Тесты.

1.1 Web-авторизация и генерация storageState.

Файл tests/auth-android.setup.ts отвечает за авторизацию в Keycloak. В нём:

  • открывается web-страница (Playwright),

  • вводятся учетные данные,

  • сохраняется storageState в ./support/.auth/storage.json.

tests/auth-android.setup.ts

import { test as setup, expect } from '@playwright/test';
import { LoginPage } from '@pages/LoginPage';
import { EnvHelper } from '@utilities/EnvHelper';


const adminFile = './support/.auth/storage.json';


setup('Авторизация в keycloak', async ({ page }) => {
    const loginPage: LoginPage = new LoginPage(page);
    await loginPage.open();
    await loginPage.openLoginPageButton.click();
    await loginPage.fillUsername(EnvHelper.login);
    await loginPage.fillPassword(EnvHelper.password);
    await loginPage.loginButton.click();
    await expect(loginPage.textMyChecks).toBeVisible();
    await page.context().storageState({ path: adminFile });
    await page.context().close();
});

«Обратите внимание: мы не просто выполняем вход, а сохраняем состояние в storage.json. Этот файл становится ключевым артефактом — позже мы используем его на Android-устройстве для восстановления сессии. Такой подход позволяет избежать повторного прохождения процедуры логина на каждом запуске тестов.»

1.2 Пример сценария: установка, запуск и синхронизация в нативном приложении Android.

tests/android/mainScreen/installAndRunApp.spec.

import { test } from './mainScreen.fixture';
import { Data } from '@testData/data/Data';
import { expect } from '@playwright/test';
import { setupAndroidBrowserWithAuth } from '@utilities/HelperAuth';


test.describe('Проверка установки, запуска и первой синхронизации в приложении', () => {
    const storageState = './support/.auth/storage.json';
    test.beforeAll(async ({ }) => {
        await setupAndroidBrowserWithAuth(storageState);
    });


    test('Удаление, установка, запуск + вход в мобильное приложение', async ({ mainScreen, driver }) => {
        await driver.removeApp(Data.pkgName);
        await driver.installApp(Data.apkPath);
        await test.step('Авторизация', async () => {
            await driver.activateApp(Data.pkgName);
            await driver.switchContext('NATIVE_APP');
            await mainScreen.keycloakServerInput.addValue(Data.keycloakServerUrl);
            await mainScreen.authKeycloakButton.tap();
        });
        await test.step('Запуск', async () => {
            await mainScreen.allowButtonDialogPermission.tap();
        });
        await mainScreen.snackBar.waitForDisplayed({ timeout: 20000 });
        await expect.poll(async () => {
            return await mainScreen.snackBar.getText();
        }, {
            timeout: 5000,
            intervals: [200],
        }).toMatch(/Синхронизация (чеклистов|задач) выполнена/);
    });
});

2. Фикстуры и конфигурации.

2.1 Конфигурация Playwright.

Playwright используется для:

  • Выполнения авторизации в web-окружении перед выполнением всех тестов;

  • Сохранения  storageState;

  • Построения структуры файлов тестов и использования хуков;

  • Запуска всех Android-тестов в рамках общего тестового раннера;

  • Инжектирования cookies в браузер устройства Android.

playwright.config.ts

import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';
import process from 'node:process';
import { EnvHelper } from '@utilities/EnvHelper';

dotenv.config({
    path: '.env'
});
export default defineConfig({
    timeout: 150000,
    testDir: './tests',
    fullyParallel: true,
    forbidOnly: !!process.env.CI,
    retries: process.env.CI ? 0 : 0,
    workers: 1,
    reporter: 'allure-playwright',
    use: {
        baseURL: EnvHelper.baseUrl,
        ignoreHTTPSErrors: true,
        trace: 'retain-on-failure',
        screenshot: 'only-on-failure',
        actionTimeout: 30000,
        navigationTimeout: 30000,
    },
    expect: {
        timeout: 30000,
    },
    projects: [
        { name: 'setup', testMatch: 'auth-android.setup.ts' },
        {
            name: 'android',
            testMatch: '*android/*/*.spec.ts',
            use: { ...devices[EnvHelper.deviceName] },
            dependencies: ['setup'],
        },
    ],
});

Ключевые моменты:

  • dependencies: ['setup'] - гарантирует, что авторизация выполнится до основных тестов;

  • testMatch - разделение тестов по типам (setup vs android);

  • EnvHelper - централизованное управление таймаутами и параметрами;

2.2 Конфигурация окружения.

.env - переменные окружения

APK_NAME_APP = FPA-debug.apk
APK_NAME_CHROME = chrome.apk
DEVICE_NAME1 = 'Infinix HOT 11S NFS'
DEVICE_NAME = 'TECNO SPARK 30 Pro'
BASE_URL = https://audit-dev3.fix-price.ru
LOGIN = '******'
PASSWORD = '******'
APPIUM_HOST=localhost
APPIUM_PORT=4723

Параметры проекта берутся из .env через support/utilities/EnvHelper.ts:

support/utilities/EnvHelper.ts

import * as process from 'node:process';


export class EnvHelper {
    static readonly baseUrl: string = String(process.env.BASE_URL);
    static readonly login: string = String(process.env.LOGIN);
    static readonly password: string = String(process.env.PASSWORD);
    static readonly apkName: string = String(process.env.APK_NAME_APP);
    static readonly apkNameChrome: string = String(process.env.APK_NAME_CHROME);
    static readonly deviceName: string = String(process.env.DEVICE_NAME);
}

2.3 Фикстуры.

tests/android/mainScreen/mainScreen.fixture.ts

import { test as base } from '@playwright/test';
import { DriverData } from '@testData/data/DriverData';
import { MainScreen } from '@screens/MainScreen';
import { Browser, remote } from 'webdriverio';


type mainScreenFixtures = {
    driver: Browser;
    mainScreen: MainScreen;
}


export const test = base.extend<mainScreenFixtures>({
    driver: async ({ }, use) => {
        const driver = await remote(DriverData);
        await use(driver);
    },
    mainScreen: async ({ driver }, use) => {
        const mainScreen = new MainScreen(driver);
        await use(mainScreen);
    }
});

Преимущества фикстур:

  • Драйвер создается один раз на тест;

  • Screen Object автоматически получает драйвер;

  • Чистая архитектура без дублирования кода;

3. Слой Page / Screen Object

support/pages/LoginPage.ts

Page Object для web-страницы авторизации:

import { Locator, Page } from '@playwright/test';
/**
 * Страница авторизации Keycloak
 */
export class LoginPage {
    private readonly page: Page;
    readonly openLoginPageButton: Locator;
    readonly loginButton: Locator;
    readonly inputUsername: Locator;
    readonly inputPassword: Locator;
    readonly textMyChecks: Locator;


    constructor(page: Page) {
        this.page = page;
        this.openLoginPageButton = page.getByRole('button', { name: 'Войти' });
        this.loginButton = page.locator('#kc-login');
        this.inputUsername = page.locator('#username');
        this.inputPassword = page.locator('#password');
        this.textMyChecks = page.getByText('Мои проверки');
    }


    async open(): Promise<void> {
        await this.page.goto('/');
    }


    async fillUsername(username: string) {
        await this.inputUsername.fill(username);
    }


    async fillPassword(password: string) {
        await this.inputPassword.fill(password);
    }
}

support/screens/MainScreen.ts

Главный экран нативного приложения на Android:

import { BaseScreen } from './BaseScreen';
import { ChainablePromiseElement } from 'webdriverio';


/**
 * Главный экран
 */
export class MainScreen extends BaseScreen {


    readonly toolbar: ChainablePromiseElement = this.driver.$('toolbar');
    readonly allowButtonDialogPermission: ChainablePromiseElement = this.driver.$('//android.widget.Button[@resource-id="com.android.permissioncontroller:id/permission_allow_button"]');
    readonly buttonSync: ChainablePromiseElement = this.driver.$('//android.widget.ImageButton[@resource-id="ru.fixprice.fpa.android:id/actionSync"]');
    readonly snackBar:ChainablePromiseElement = this.driver.$('//android.widget.TextView[@resource-id="ru.fixprice.fpa.android:id/snackbar_text"]');
}

Преимущества Screen/Page Object:

  • Изменения селекторов затрагивают только один файл;

  • Логика взаимодействия с экраном инкапсулирована;

  • Тесты читаются как бизнес-сценарии, а не работа с UI;

4. Утилиты и хелперы.

В проекте есть утилита support/utilities/AndroidBrowserManager.ts:

  • получает подключенное Android-устройство через Playwright Android API;

  • запускает Chrome на устройстве;

  • добавляет cookies из storageState;

import { _android, BrowserContext, AndroidDevice, BrowserContextOptions } from 'playwright';


export class AndroidBrowserManager {
    private device: AndroidDevice;
    private context: BrowserContext | undefined;


    constructor(device: AndroidDevice) {
        this.device = device;
    }


    async launchBrowser(options: BrowserContextOptions = {}): Promise<BrowserContext> {
        await this.device.shell('am force-stop com.android.chrome');
        this.context = await this.device.launchBrowser(options);
        return this.context;
    }


    async saveStorageState(path: string): Promise<void> {
        if (!this.context) {
            throw new Error('Browser context is not initialized');
        }
        await this.context.storageState({ path });
    }


    async closeBrowser(): Promise<void> {
        try {
            if (this.context) {
                await this.context.close();
                this.context = undefined;
                await this.device.shell('am force-stop com.android.chrome');
            }
        } catch (error) {
            throw error;
        }
    }


    static async getDevice(): Promise<AndroidDevice> {
        const devices = await _android.devices();
        if (devices.length === 0) {
            throw new Error('No Android devices found');
        }
        return devices[0];
    }
}

support/utilities/HelperAuth.ts

Функция setupAndroidBrowserWithAuth читает storageState и внедряет cookies в браузер:

import { AndroidBrowserManager } from './AndroidBrowserManager';
import fs from 'fs';


export async function setupAndroidBrowserWithAuth(storagePath: string) {
    const storageState = JSON.parse(fs.readFileSync(storagePath, 'utf-8'));
    const device = await AndroidBrowserManager.getDevice();
    const browserManager = new AndroidBrowserManager(device);
    const context = await browserManager.launchBrowser();
    await context.addCookies(storageState.cookies);
    return context;
}

Как это работает:

  • storageState читается из файла JSON;

  • Извлекаются cookies после web-авторизации;

  • Cookies добавляются в браузер на Android-устройстве;

  • Приложение при открытии видит авторизованную сессию;

Это позволяет не повторять логин на мобильном устройстве при каждом запуске теста.

5. Инструменты и данные по подключению.

Appium + WebdriverIO для native-части. WebdriverIO используется для управления Appium-драйвером. Данные по подключению и capabilities описаны в support/testData/data/DriverData.ts:

import { WebdriverIOConfig } from '@wdio/types/build/Capabilities';
import { Data } from '@testData/data/Data';
import { EnvHelper } from '@utilities/EnvHelper';


export const capabilitiesWithChrome = {
    platformName: 'Android',
    'appium:automationName': 'UiAutomator2',
    'appium:deviceName': EnvHelper.deviceName,
    'appium:adbExecTimeout': 120000,
    'appium:appWaitActivity': '*',
    'appium:chromedriverAutoDownload': true,
    'appium:allowInsecure': ['chromedriver_autodownload'],
    'appium:chromedriverExecutableDir': './node_modules/chromedriver/bin/chromedriver',
    browserName: 'Chrome',
    'goog:chromeOptions': {
        'w3c': true,
        args: [
            '--disable-zoom',
            '--force-device-scale-factor=1',
            '--disable-web-sockets',
            '--disable-bidi',
            '--no-first-run'
        ],
    },
};


export const capabilities = {
    platformName: 'Android',
    'appium:automationName': 'UiAutomator2',
    'appium:deviceName': EnvHelper.deviceName,
    'appium:grantAllPermissions': true,
    'appium:autoGrantPermissions': true,
    'appium:enforceAppInstall': true,
    'appium:allowTestPackages': true,
    'appium:appPackage': Data.pkgName,
    'appium:appActivity': Data.activity
};


export const DriverData: WebdriverIOConfig = {
    hostname: process.env.APPIUM_HOST || 'localhost',
    port: parseInt(process.env.APPIUM_PORT, 10) || 4723,
    logLevel: 'error',
    capabilities,
};


export const DriverDataWithChrome: WebdriverIOConfig = {
    hostname: process.env.APPIUM_HOST || 'localhost',
    port: parseInt(process.env.APPIUM_PORT, 10) || 4723,
    logLevel: 'error',
    capabilities: capabilitiesWithChrome
};

Ключевые параметры:

  • platformName: 'Android' - платформа для тестирования;

  • appium:automationName: 'UiAutomator2' - движок автоматизации;

  • appium:grantAllPermissions - автоматическое предоставление разрешений;

  • appium:chromedriverAutoDownload - автозагрузка ChromeDriver;

  • appium:appPackage / appium:appActivity - целевое приложение;

Эти capabilities используются в фикстуре tests/android/mainScreen/mainScreen.fixture.ts, где создаётся driver и передается в screen object.

support/testData/data/Data.ts

import { EnvHelper } from '@utilities/EnvHelper';


const pkgName = 'ru.fixprice.fpa.android';
const activityName = 'ru.mobiledimension.wrs.auth.presentation.ui.AuthActivity';


export const Data = {
    apkPath: `support/testData/files/${EnvHelper.apkName}`,
    pkgName: pkgName,
    activity: activityName,
    fullName: `${pkgName}/${activityName}`,
    keycloakServerUrl: EnvHelper.baseUrlKC.replace('https://', ''),
};

Этот файл экспортирует объект Data с тестовыми данными для Android-тестирования:

## Свойства объекта Data


1. apkPath
   - Значение: support/testData/files/${EnvHelper.apkName}
   - Назначение: Путь к APK-файлу для установки


2. pkgName
   - Значение: ru.fixprice.fpa.android
   - Назначение: Имя пакета Android-приложения


3. activity
   - Значение: ru.mobiledimension.wrs.auth.presentation.ui.AuthActivity
   - Назначение: Главный activity для экрана авторизации


4. fullName
   - Значение: ${pkgName}/${activityName}
   - Назначение: Комбинированный идентификатор package/activity


5. keycloakServerUrl
   - Значение: EnvHelper.baseUrlKC (без https://)
   - Назначение: URL сервера Keycloak для аутентификации

6. Целевые устройства и окружение.

Для запуска тестов наша инфраструктура поддерживает несколько вариантов окружения. В качестве целевого устройства Android могут выступать:

  • Реальные устройства: Подключаются через USB-кабель. Обеспечивают максимальную достоверность тестирования и производительность;

  • Локальные эмуляторы: Настраиваются через Android Studio. Удобны для быстрой отладки и не требуют физического наличия устройства;

  • Облачные фермы: Например, BrowserStack. Позволяют масштабировать тестирование на множество конфигураций, но требуют стабильного интернета и могут иметь задержки связи;

  • Docker-контейнеры: Например, budtmo/docker-android. Позволяют поднимать эмуляторы в CI/CD-среде без графического интерфейса хоста;

Каждый из этих подходов имеет свои достоинства и недостатки. На текущем этапе, чтобы обеспечить быстрый старт и гибкость отладки, мы остановились на комбинации реальных устройств и локальных эмуляторов через Android Studio

Преимущества и недостатки решения.

Любой инструмент имеет свои ограничения. Честно расскажем о плюсах и минусах нашего подхода.

Преимущества:

  1. Единый тест-раннер. Playwright выступает основой структуры тестовых файлов и главным раннером для запуска автотестов гибридного приложения. Это позволяет держать веб и мобильные тесты в одном репозитории.

  2. Переиспользование сессии. Состояние авторизации (localStorage, cookies) получается единожды и сохраняется в артефакт. Это значительно сокращает время запуска тестов, так как не требуется повторный логин через KeyCloak на каждом прогоне.

  3. Единый стиль кода. Синтаксис WebdriverIO и Playwright во многом схож (особенно в структуре тестов и использовании TypeScript). Это снижает порог входа для команды и упрощает поддержку кода.

Недостатки:

  1. Ограниченный параллелизм. Настройка workers в конфигурации Playwright отвечает за параллелизм только для веб-контекстов. Для нативных тестов через Appium параллельный запуск требует дополнительной настройки и ограничен возможностями сервера Appium.

  2. Разный подход к ожиданиям. В отличие от Playwright, WebdriverIO использует явные ожидания (таймауты) для элементов. При написании кода для нативной части нужно не забывать об этом, чтобы избежать нестабильности тестов.

  3. Отсутствие готовой интеграции с CI/CD. На текущем этапе проект настроен для локального запуска. Внедрение в пайплайн CI/CD запланировано на следующие итерации развития.

  4. Ограниченная отчетность. Сейчас нет детальной визуализации результатов прогонов. Это также планируется внедрить по мере масштабирования проекта.

Заключение

Хотелось бы подчеркнуть: данное решение представляет собой экспериментальный подход к тестированию гибридного мобильного приложения на Android с использованием связки Playwright + WebdriverIO + Appium.

Ключевые преимущества подхода:

  • Единый тестовый фреймворк для веб и мобильной частей приложения.

  • Авторизация через KeyCloak с переиспользованием сессии (storageState).

  • Четкая структура проекта (Page/Screen Objects).

  • Локальный запуск тестов на реальных устройствах и эмуляторах.

Важно понимать:

  • Подход с использованием Playwright для работы с нативными элементами Android — нестандартное решение, которое требует глубокого понимания архитектуры.

  • Параллелизм тестов ограничен возможностями связки Appium и Playwright.

  • Архитектура может эволюционировать по мере накопления опыта и изменения требований.

  • Некоторые паттерны требуют дальнейшей доработки и валидации в бою.

Текущее решение — это первый шаг к автоматизации тестирования мобильного приложения. Оно демонстрирует работоспособность подхода и готово к развитию в полноценную CI/CD-систему с качественной отчетностью и масштабированием на различные устройства и конфигурации.

Но это уже темы для новых статей. Спасибо за внимание!

Пожелания от команды

Пусть ваши тесты будут:

  • Зелёными — как можно чаще

  • Быстрыми — без лишних ожиданий

  • Надёжными — без случайных и ложных падений

  • Читаемыми — чтобы коллеги говорили «спасибо»

Помните: лучшие автотесты — не те, которые покрывают 100% кода, а те, которые находят баги до продакшена и экономят время команды.

Комментарии (0)