detox-mobile-test

Detox Mobile Testing Expert

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "detox-mobile-test" with this command: npx skills add dengineproblem/agents-monorepo/dengineproblem-agents-monorepo-detox-mobile-test

Detox Mobile Testing Expert

Эксперт по E2E тестированию React Native приложений с Detox.

Core Testing Principles

Synchronization

  • Автоматическая синхронизация с React Native bridge

  • Синхронизация с анимациями и сетевыми запросами

  • waitFor() для явных ожиданий

  • toBeVisible() вместо toExist() для стабильности

Test Organization

  • AAA pattern (Arrange, Act, Assert)

  • Изоляция через beforeEach() и afterEach()

  • describe() для группировки

  • Page Object pattern для сложного UI

Configuration

.detoxrc.json

{ "testRunner": { "args": { "$0": "jest", "config": "e2e/jest.config.js" }, "jest": { "setupTimeout": 120000 } }, "apps": { "ios.debug": { "type": "ios.app", "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/MyApp.app", "build": "xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build" }, "ios.release": { "type": "ios.app", "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/MyApp.app", "build": "xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -derivedDataPath ios/build" }, "android.debug": { "type": "android.apk", "binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk", "build": "cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug" }, "android.release": { "type": "android.apk", "binaryPath": "android/app/build/outputs/apk/release/app-release.apk", "build": "cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release" } }, "devices": { "simulator": { "type": "ios.simulator", "device": { "type": "iPhone 14" } }, "emulator": { "type": "android.emulator", "device": { "avdName": "Pixel_4_API_30" } } }, "configurations": { "ios.sim.debug": { "device": "simulator", "app": "ios.debug" }, "ios.sim.release": { "device": "simulator", "app": "ios.release" }, "android.emu.debug": { "device": "emulator", "app": "android.debug" }, "android.emu.release": { "device": "emulator", "app": "android.release" } } }

Jest Config

// e2e/jest.config.js module.exports = { rootDir: '..', testMatch: ['<rootDir>/e2e/**/*.test.js'], testTimeout: 120000, maxWorkers: 1, globalSetup: 'detox/runners/jest/globalSetup', globalTeardown: 'detox/runners/jest/globalTeardown', reporters: ['detox/runners/jest/reporter'], testEnvironment: 'detox/runners/jest/testEnvironment', verbose: true };

Basic Test Structure

describe('Login Flow', () => { beforeAll(async () => { await device.launchApp(); });

beforeEach(async () => { await device.reloadReactNative(); });

afterAll(async () => { await device.terminateApp(); });

it('should login with valid credentials', async () => { // Arrange const email = 'test@example.com'; const password = 'password123';

// Act
await element(by.id('email-input')).typeText(email);
await element(by.id('password-input')).typeText(password);
await element(by.id('login-button')).tap();

// Assert
await expect(element(by.id('home-screen'))).toBeVisible();

});

it('should show error for invalid credentials', async () => { // Arrange const email = 'wrong@example.com'; const password = 'wrongpassword';

// Act
await element(by.id('email-input')).typeText(email);
await element(by.id('password-input')).typeText(password);
await element(by.id('login-button')).tap();

// Assert
await expect(element(by.id('error-message'))).toBeVisible();
await expect(element(by.text('Invalid credentials'))).toBeVisible();

}); });

Element Matchers

// By testID element(by.id('submit-button'))

// By text element(by.text('Submit'))

// By label (accessibility) element(by.label('Submit form'))

// By type element(by.type('RCTTextInput'))

// By traits (iOS) element(by.traits(['button']))

// Combining matchers element(by.id('item').withAncestor(by.id('list'))) element(by.id('item').withDescendant(by.text('Title')))

// Index for multiple matches element(by.id('list-item')).atIndex(0)

Actions

// Tap await element(by.id('button')).tap(); await element(by.id('button')).multiTap(2); await element(by.id('button')).longPress(); await element(by.id('button')).longPress(2000); // 2 seconds

// Text input await element(by.id('input')).typeText('Hello'); await element(by.id('input')).replaceText('New text'); await element(by.id('input')).clearText();

// Scroll await element(by.id('scrollView')).scroll(200, 'down'); await element(by.id('scrollView')).scroll(200, 'up'); await element(by.id('scrollView')).scrollTo('bottom'); await element(by.id('scrollView')).scrollTo('top');

// Scroll until visible await waitFor(element(by.id('item'))) .toBeVisible() .whileElement(by.id('scrollView')) .scroll(200, 'down');

// Swipe await element(by.id('card')).swipe('left'); await element(by.id('card')).swipe('right', 'fast', 0.9);

// Pinch await element(by.id('map')).pinch(1.5); // zoom in await element(by.id('map')).pinch(0.5); // zoom out

Expectations

// Visibility await expect(element(by.id('view'))).toBeVisible(); await expect(element(by.id('view'))).not.toBeVisible(); await expect(element(by.id('view'))).toExist(); await expect(element(by.id('view'))).not.toExist();

// Focus await expect(element(by.id('input'))).toBeFocused();

// Text await expect(element(by.id('label'))).toHaveText('Hello'); await expect(element(by.id('input'))).toHaveValue('input value');

// Toggle state await expect(element(by.id('switch'))).toHaveToggleValue(true);

// Slider await expect(element(by.id('slider'))).toHaveSliderPosition(0.5);

// ID await expect(element(by.id('view'))).toHaveId('view');

// Label await expect(element(by.id('button'))).toHaveLabel('Submit');

waitFor API

// Wait for element to be visible await waitFor(element(by.id('loading'))) .not.toBeVisible() .withTimeout(10000);

// Wait for element to exist await waitFor(element(by.id('data'))) .toExist() .withTimeout(5000);

// Wait while scrolling await waitFor(element(by.id('item-50'))) .toBeVisible() .whileElement(by.id('list')) .scroll(100, 'down');

// Custom polling await waitFor(element(by.id('result'))) .toHaveText('Success') .withTimeout(30000);

Page Object Pattern

// e2e/pages/LoginPage.js class LoginPage { get emailInput() { return element(by.id('email-input')); }

get passwordInput() { return element(by.id('password-input')); }

get loginButton() { return element(by.id('login-button')); }

get errorMessage() { return element(by.id('error-message')); }

async login(email, password) { await this.emailInput.typeText(email); await this.passwordInput.typeText(password); await this.loginButton.tap(); }

async assertErrorVisible(message) { await expect(this.errorMessage).toBeVisible(); if (message) { await expect(element(by.text(message))).toBeVisible(); } } }

module.exports = new LoginPage();

// e2e/tests/login.test.js const LoginPage = require('../pages/LoginPage'); const HomePage = require('../pages/HomePage');

describe('Login', () => { it('should login successfully', async () => { await LoginPage.login('user@test.com', 'password123'); await expect(HomePage.welcomeMessage).toBeVisible(); }); });

Debugging

Verbose Logging

// In test await device.launchApp({ launchArgs: { detoxPrintBusyIdleResources: 'YES' } });

Screenshots

// Take screenshot await device.takeScreenshot('login-screen');

// On failure (in jest setup) afterEach(async () => { if (jasmine.currentTest.failedExpectations.length > 0) { await device.takeScreenshot(failed-${jasmine.currentTest.fullName}); } });

Element Debugging

// Get element attributes const attributes = await element(by.id('button')).getAttributes(); console.log(attributes); // { text: 'Submit', visible: true, enabled: true, ... }

Handling Common Issues

Disable Synchronization

// For non-React Native screens (WebViews, etc.) await device.disableSynchronization(); await element(by.id('webview-button')).tap(); await device.enableSynchronization();

Permission Dialogs

// iOS await device.launchApp({ permissions: { notifications: 'YES', camera: 'YES', photos: 'YES', location: 'always' } });

// Android - handle at runtime await element(by.text('Allow')).tap();

Keyboard Issues

// Dismiss keyboard await element(by.id('input')).typeText('text\n'); // or await device.pressBack(); // Android

// Avoid keyboard overlap await element(by.id('input')).tap(); await element(by.id('input')).typeText('text'); await element(by.id('submit')).tap();

CI/CD Integration

GitHub Actions

name: E2E Tests

on: push: branches: [main] pull_request: branches: [main]

jobs: ios-e2e: runs-on: macos-latest steps: - uses: actions/checkout@v4

  - name: Setup Node
    uses: actions/setup-node@v4
    with:
      node-version: '18'
      cache: 'npm'

  - name: Install dependencies
    run: npm ci

  - name: Install pods
    run: cd ios &#x26;&#x26; pod install

  - name: Build app
    run: npx detox build --configuration ios.sim.release

  - name: Run tests
    run: npx detox test --configuration ios.sim.release --cleanup

  - name: Upload artifacts
    if: failure()
    uses: actions/upload-artifact@v3
    with:
      name: detox-artifacts
      path: artifacts/

android-e2e: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4

  - name: Setup Node
    uses: actions/setup-node@v4
    with:
      node-version: '18'

  - name: Setup Java
    uses: actions/setup-java@v3
    with:
      distribution: 'zulu'
      java-version: '11'

  - name: Install dependencies
    run: npm ci

  - name: Build app
    run: npx detox build --configuration android.emu.release

  - name: Start emulator
    uses: reactivecircus/android-emulator-runner@v2
    with:
      api-level: 30
      target: google_apis
      script: npx detox test --configuration android.emu.release --cleanup

Performance Tips

// Use reloadReactNative instead of launchApp beforeEach(async () => { await device.reloadReactNative(); // Fast // await device.launchApp({ newInstance: true }); // Slow });

// Record videos only on failure // In detoxrc.json { "artifacts": { "plugins": { "video": { "enabled": true, "keepOnlyFailedTestsArtifacts": true } } } }

// Test sharding for parallel execution // jest.config.js module.exports = { maxWorkers: process.env.CI ? 2 : 1, // ... };

Лучшие практики

  • Stable selectors — используйте testID, не text

  • Proper waits — waitFor вместо sleep

  • Page Objects — переиспользуемые абстракции

  • Isolated tests — каждый тест независим

  • CI/CD first — тесты должны работать в CI

  • Record on failure — видео/скриншоты при падении

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

social-media-marketing

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

video-marketing

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review