E2E тестирование
Инструмент: Maestro
Заголовок раздела «Инструмент: Maestro»Для мобильных E2E тестов используется Maestro — YAML-based фреймворк для автоматизации мобильных приложений.
| Характеристика | Значение |
|---|---|
| Скорость | ~10-15 сек/тест |
| Формат | Декларативные YAML-файлы |
| Детерминизм | Да (в отличие от AI-driven Mobile MCP) |
| Совместимость | Expo dev build (не Expo Go) |
| Платформа | iOS Simulator, Android Emulator |
Почему не Mobile MCP?
Заголовок раздела «Почему не Mobile MCP?»Mobile MCP (через Claude) хорош для ad-hoc проверок (“посмотри что на экране”), но для регрессии не подходит:
- ~30-60 сек/тест из-за цикла screenshot + AI reasoning
- Недетерминистичный — AI может интерпретировать экран по-разному
- Не подходит для CI/CD
Установка
Заголовок раздела «Установка»1. Maestro CLI
Заголовок раздела «1. Maestro CLI»curl -Ls "https://get.maestro.mobile.dev" | bash2. Expo dev build
Заголовок раздела «2. Expo dev build»Maestro не работает с Expo Go — нужен нативный dev build:
cd mobile && npx expo run:ios3. Backend + тестовые данные
Заголовок раздела «3. Backend + тестовые данные»bash scripts/e2e-setup.sh# или вручную:docker compose up -d --waitcd backend && npm run seedSeed создаёт тестового пользователя:
- Телефон:
+7 (700) 123-45-67 - Пароль:
test1234
Запуск тестов
Заголовок раздела «Запуск тестов»cd mobile
# Все E2E тестыnpm run test:e2e
# Только login flownpm run test:e2e:loginПокрытие
Заголовок раздела «Покрытие»| Тест | Файл | Что проверяет |
|---|---|---|
| Login happy path | .maestro/login-flow.yaml | Ввод телефона, пароль, авторизация, переход на главную |
Что ещё стоит покрыть (TODO)
Заголовок раздела «Что ещё стоит покрыть (TODO)»- Login с неверным паролем (проверка ошибки)
- Logout flow
- Просмотр проекта (read-only данные)
- Чат — отправка сообщения
Структура файлов
Заголовок раздела «Структура файлов»mobile/├── .maestro/│ └── login-flow.yaml # Login happy-path тест├── package.json # Скрипты test:e2e, test:e2e:loginscripts/└── e2e-setup.sh # Подготовка Docker + seedtestID в компонентах
Заголовок раздела «testID в компонентах»Для Maestro нужны testID на React Native компонентах. Maestro находит элементы по id: селектору, который маппится на testID.
Где добавлены testID
Заголовок раздела «Где добавлены testID»| Компонент | testID | Файл |
|---|---|---|
| Login screen container | login-screen | app/(auth)/login.tsx |
| Phone input | login-phone-input | app/(auth)/login.tsx |
| Password input | login-password-input | app/(auth)/login.tsx |
| Login button | login-submit-button | app/(auth)/login.tsx |
| Login error text | login-error-text | app/(auth)/login.tsx |
| Home screen container | home-screen | app/(app)/index.tsx |
| Logout button | logout-button | app/(app)/index.tsx |
Поддержка testID в UI-компонентах
Заголовок раздела «Поддержка testID в UI-компонентах»testID проброшен в базовые компоненты:
- Button (
components/ui/Button.tsx) —testIDна<Pressable> - Typography (
components/ui/Typography.tsx) —testIDна<Text> - Input (
components/ui/Input.tsx) —testIDпроходит через...restна<TextInput>(extendsTextInputProps)
Подводные камни
Заголовок раздела «Подводные камни»1. iOS Accessibility Grouping
Заголовок раздела «1. iOS Accessibility Grouping»Проблема: React Native на iOS группирует текстовые элементы в один accessibility-узел. Maestro видит один блок вместо отдельных текстов:
# Maestro видит ЭТО:text='Войдите в личный кабинет, Пароль, Войти, Данные для входа...'
# А НЕ отдельные элементыРешение: Использовать testID + id: селектор вместо текстового матчинга. Добавить accessible={false} на контейнерные <View>, чтобы дочерние элементы были видны в accessibility-дереве.
// login.tsx — контейнеры с accessible={false}<TouchableWithoutFeedback accessible={false}> <KeyboardAvoidingView accessible={false}> <View accessible={false} style={styles.header}> <View accessible={false} style={styles.form}>// Input.tsx — wrapper с accessible={false}<View accessible={false}> <View accessible={false} style={styles.container}> <TextInput testID={testID} />2. clearState vs clearKeychain
Заголовок раздела «2. clearState vs clearKeychain»Проблема: expo-secure-store хранит JWT токены в iOS Keychain, а не в sandbox. clearState очищает только sandbox.
| Команда | Что чистит | Эффект |
|---|---|---|
clearState | App sandbox (UserDefaults, файлы) | Токены в Keychain остаются |
clearKeychain | iOS Keychain | Ломает Expo runtime (очищает всё) |
Решение: Не использовать clearKeychain. Вместо этого — если приложение уже залогинено, нажать кнопку “Выйти” через optional tap:
- clearState- launchApp- waitForAnimationToEnd- waitForAnimationToEnd# Если уже залогинены — выйти- tapOn: id: "logout-button" optional: true3. Phone-pad клавиатура
Заголовок раздела «3. Phone-pad клавиатура»Проблема: Поле телефона использует keyboardType="phone-pad" (только цифры). hideKeyboard не работает с phone-pad на iOS.
Решение: Тап по пустой области экрана (logo area) для dismiss:
- tapOn: point: "50%,15%"- waitForAnimationToEnd4. Русская клавиатура
Заголовок раздела «4. Русская клавиатура»Проблема: Если клавиатура симулятора в русской раскладке, Maestro inputText не может ввести латинские символы (пароль “test1234” → вводится только “1234”).
Решение: Перед вводом пароля тапнуть иконку глобуса (🌐) для переключения на английскую раскладку:
- tapOn: id: "login-password-input"- waitForAnimationToEnd# Переключить клавиатуру на English- tapOn: point: "12%,96%"- waitForAnimationToEnd- inputText: "test1234"5. Pre-filled phone field
Заголовок раздела «5. Pre-filled phone field»Проблема: Поле телефона стартует со значением “7” (отображается как “+7”). При eraseText → поле пустеет → handlePhoneChange("") сбрасывает state обратно на “7” → поле снова “+7”.
Решение: Не стирать, а дописывать оставшиеся 10 цифр:
- tapOn: id: "login-phone-input"- inputText: "7001234567" # Только оставшиеся 10 цифр6. AuthProvider
Заголовок раздела «6. AuthProvider»В процессе настройки E2E обнаружен баг: после directus.login() AuthGate не обновлял состояние authenticated, что приводило к redirect обратно на login.
Решение: Создан AuthProvider (providers/AuthProvider.tsx) с методами signIn() / signOut(), который управляет auth-состоянием через React Context. Login screen вызывает signIn() после успешного логина, AuthProvider обновляет state и роутер перенаправляет на /(app).
Maestro: справочник команд
Заголовок раздела «Maestro: справочник команд»Часто используемые команды в наших тестах:
# Запуск и очистка- clearState # Очистка sandbox (не keychain!)- launchApp # Запуск приложения
# Ожидание- waitForAnimationToEnd # Ждать окончания анимации- extendedWaitUntil: # Ждать появления элемента visible: id: "element-id" timeout: 15000
# Взаимодействие- tapOn: id: "test-id" # Тап по testID- tapOn: point: "50%,15%" # Тап по координатам (% экрана)- tapOn: id: "element" optional: true # Пропустить если не найден
# Ввод текста- inputText: "текст" # Ввод через клавиатуру- eraseText: 20 # Удалить N символов (backspace)Отладка
Заголовок раздела «Отладка»Debug-артефакты сохраняются в ~/.maestro/tests/YYYY-MM-DD_HHMMSS/:
screenshot-*.png— скриншот в момент ошибкиcommands-*.json— иерархия элементов для каждого шагаmaestro.log— подробный лог
Для просмотра accessibility-иерархии из JSON:
cat commands-*.json | python3 -c "import json, sysdata = json.load(sys.stdin)for cmd in data: if cmd.get('metadata',{}).get('status') == 'FAILED': def find(node, depth=0): a = node.get('attributes', {}) t, i = a.get('accessibilityText',''), a.get('resource-id','') if t or i: print(f\"{' '*depth}text={t!r} id={i!r}\") for c in node.get('children', []): find(c, depth+1) find(cmd['metadata']['error']['hierarchyRoot'])"