detox-mobile-test

安装量: 52
排名: #14187

安装

npx skills add https://github.com/dengineproblem/agents-monorepo --skill 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 : [ '/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 && 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 — видео/скриншоты при падении

返回排行榜