--- name: axiom-ui-testing description: Use when writing UI tests, recording interactions, tests have race conditions, timing dependencies, inconsistent pass/fail behavior, or XCTest UI tests are flaky - covers Recording UI Automation (WWDC 2025), condition-based waiting, network conditioning, multi-factor testing, crash debugging, and accessibility-first testing patterns license: MIT metadata: version: "2.1.0" last-updated: "WWDC 2025 (Updated with production debugging patterns)" --- # UI Testing ## Overview Wait for conditions, not arbitrary timeouts. **Core principle** Flaky tests come from guessing how long operations take. Condition-based waiting eliminates race conditions. **NEW in WWDC 2025**: Recording UI Automation allows you to record interactions, replay across devices/languages, and review video recordings of test runs. ## Example Prompts These are real questions developers ask that this skill is designed to answer: #### 1. "My UI tests pass locally on my Mac but fail in CI. How do I make them more reliable?" → The skill shows condition-based waiting patterns that work across devices/speeds, eliminating CI timing differences #### 2. "My tests use sleep(2) and sleep(5) but they're still flaky. How do I replace arbitrary timeouts with real conditions?" → The skill demonstrates waitForExistence, XCTestExpectation, and polling patterns for data loads, network requests, and animations #### 3. "I just recorded a test using Xcode 26's Recording UI Automation. How do I review the video and debug failures?" → The skill covers Video Debugging workflows to analyze recordings and find the exact step where tests fail #### 4. "My test is failing on iPad but passing on iPhone. How do I write tests that work across all device sizes?" → The skill explains multi-factor testing strategies and device-independent predicates for robust cross-device testing #### 5. "I want to write tests that are not flaky. What are the critical patterns I need to know?" → The skill provides condition-based waiting templates, accessibility-first patterns, and the decision tree for reliable test architecture --- ## Red Flags — Test Reliability Issues If you see ANY of these, suspect timing issues: - Tests pass locally, fail in CI (timing differences) - Tests sometimes pass, sometimes fail (race conditions) - Tests use `sleep()` or `Thread.sleep()` (arbitrary delays) - Tests fail with "UI element not found" then pass on retry - Long test runs (waiting for worst-case scenarios) ## Quick Decision Tree ``` Test failing? ├─ Element not found? │ └─ Use waitForExistence(timeout:) not sleep() ├─ Passes locally, fails CI? │ └─ Replace sleep() with condition polling ├─ Animation causing issues? │ └─ Wait for animation completion, don't disable └─ Network request timing? └─ Use XCTestExpectation or waitForExistence ``` ## Core Pattern: Condition-Based Waiting **❌ WRONG (Arbitrary Timeout)**: ```swift func testButtonAppears() { app.buttons["Login"].tap() sleep(2) // ❌ Guessing it takes 2 seconds XCTAssertTrue(app.buttons["Dashboard"].exists) } ``` **✅ CORRECT (Wait for Condition)**: ```swift func testButtonAppears() { app.buttons["Login"].tap() let dashboard = app.buttons["Dashboard"] XCTAssertTrue(dashboard.waitForExistence(timeout: 5)) } ``` ## Common UI Testing Patterns ### Pattern 1: Waiting for Elements ```swift // Wait for element to appear func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool { return element.waitForExistence(timeout: timeout) } // Usage XCTAssertTrue(waitForElement(app.buttons["Submit"])) ``` ### Pattern 2: Waiting for Element to Disappear ```swift func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool { let predicate = NSPredicate(format: "exists == false") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element) let result = XCTWaiter().wait(for: [expectation], timeout: timeout) return result == .completed } // Usage XCTAssertTrue(waitForElementToDisappear(app.activityIndicators["Loading"])) ``` ### Pattern 3: Waiting for Specific State ```swift func waitForButton(_ button: XCUIElement, toBeEnabled enabled: Bool, timeout: TimeInterval = 5) -> Bool { let predicate = NSPredicate(format: "isEnabled == %@", NSNumber(value: enabled)) let expectation = XCTNSPredicateExpectation(predicate: predicate, object: button) let result = XCTWaiter().wait(for: [expectation], timeout: timeout) return result == .completed } // Usage let submitButton = app.buttons["Submit"] XCTAssertTrue(waitForButton(submitButton, toBeEnabled: true)) submitButton.tap() ``` ### Pattern 4: Accessibility Identifiers **Set in app**: ```swift Button("Submit") { // action } .accessibilityIdentifier("submitButton") ``` **Use in tests**: ```swift func testSubmitButton() { let submitButton = app.buttons["submitButton"] // Uses identifier, not label XCTAssertTrue(submitButton.waitForExistence(timeout: 5)) submitButton.tap() } ``` **Why**: Accessibility identifiers don't change with localization, remain stable across UI updates. ### Pattern 5: Network Request Delays ```swift func testDataLoads() { app.buttons["Refresh"].tap() // Wait for loading indicator to disappear let loadingIndicator = app.activityIndicators["Loading"] XCTAssertTrue(waitForElementToDisappear(loadingIndicator, timeout: 10)) // Now verify data loaded XCTAssertTrue(app.cells.count > 0) } ``` ### Pattern 6: Animation Handling ```swift func testAnimatedTransition() { app.buttons["Next"].tap() // Wait for destination view to appear let destinationView = app.otherElements["DestinationView"] XCTAssertTrue(destinationView.waitForExistence(timeout: 2)) // Optional: Wait a bit more for animation to settle // Only if absolutely necessary RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.3)) } ``` ## Testing Checklist ### Before Writing Tests - [ ] Use accessibility identifiers for all interactive elements - [ ] Avoid hardcoded labels (use identifiers instead) - [ ] Plan for network delays and animations - [ ] Choose appropriate timeouts (2s UI, 10s network) ### When Writing Tests - [ ] Use `waitForExistence()` not `sleep()` - [ ] Use predicates for complex conditions - [ ] Test both success and failure paths - [ ] Make tests independent (can run in any order) ### After Writing Tests - [ ] Run tests 10 times locally (catch flakiness) - [ ] Run tests on slowest supported device - [ ] Run tests in CI environment - [ ] Check test duration (if >30s per test, optimize) ## Xcode UI Testing Tips ### Launch Arguments for Testing ```swift func testExample() { let app = XCUIApplication() app.launchArguments = ["UI-Testing"] app.launch() } ``` In app code: ```swift if ProcessInfo.processInfo.arguments.contains("UI-Testing") { // Use mock data, skip onboarding, etc. } ``` ### Faster Test Execution ```swift override func setUpWithError() throws { continueAfterFailure = false // Stop on first failure } ``` ### Debugging Failing Tests ```swift func testExample() { // Take screenshot on failure addUIInterruptionMonitor(withDescription: "Alert") { alert in alert.buttons["OK"].tap() return true } // Print element hierarchy print(app.debugDescription) } ``` ## Common Mistakes ### ❌ Using sleep() for Everything ```swift sleep(5) // ❌ Wastes time if operation completes in 1s ``` ### ❌ Not Handling Animations ```swift app.buttons["Next"].tap() XCTAssertTrue(app.buttons["Back"].exists) // ❌ May fail during animation ``` ### ❌ Hardcoded Text Labels ```swift app.buttons["Submit"].tap() // ❌ Breaks with localization ``` ### ❌ Tests Depend on Each Other ```swift // ❌ Test 2 assumes Test 1 ran first func test1_Login() { /* ... */ } func test2_ViewDashboard() { /* assumes logged in */ } ``` ### ❌ No Timeout Strategy ```swift element.waitForExistence(timeout: 100) // ❌ Too long element.waitForExistence(timeout: 0.1) // ❌ Too short ``` **Use appropriate timeouts**: - UI animations: 2-3 seconds - Network requests: 10 seconds - Complex operations: 30 seconds max ## Real-World Impact **Before** (using sleep()): - Test suite: 15 minutes (waiting for worst-case) - Flaky tests: 20% failure rate - CI failures: 50% require retry **After** (condition-based waiting): - Test suite: 5 minutes (waits only as needed) - Flaky tests: <2% failure rate - CI failures: <5% require retry **Key insight** Tests finish faster AND are more reliable when waiting for actual conditions instead of guessing times. --- ## Recording UI Automation ### Overview **NEW in Xcode 26**: Record, replay, and review UI automation tests with video recordings. **Three Phases**: 1. **Record** — Capture interactions (taps, swipes, hardware button presses) as Swift code 2. **Replay** — Run across multiple devices, languages, regions, orientations 3. **Review** — Watch video recordings, analyze failures, view UI element overlays **Supported Platforms**: iOS, iPadOS, macOS, watchOS, tvOS, axiom-visionOS (Designed for iPad) ### How UI Automation Works **Key Principles**: - UI automation interacts with your app **as a person does** using gestures and hardware events - Runs **completely independently** from your app (app models/data not directly accessible) - Uses **accessibility framework** as underlying technology - Tells OS which gestures to perform, then waits for completion **synchronously** one at a time **Actions include**: - Launching your app - Interacting with buttons and navigation - Setting system state (Dark Mode, axiom-localization, etc.) - Setting simulated location ### Accessibility is the Foundation **Critical Understanding**: Accessibility provides information directly to UI automation. What accessibility sees: - Element types (button, text, image, etc.) - Labels (visible text) - Values (current state for checkboxes, etc.) - Frames (element positions) - **Identifiers** (accessibility identifiers — NOT localized) **Best Practice**: Great accessibility experience = great UI automation experience. ### Preparing Your App for Recording #### Step 1: Add Accessibility Identifiers **SwiftUI**: ```swift Button("Submit") { // action } .accessibilityIdentifier("submitButton") // Make identifiers specific to instance List(landmarks) { landmark in LandmarkRow(landmark) .accessibilityIdentifier("landmark-\(landmark.id)") } ``` **UIKit**: ```swift let button = UIButton() button.accessibilityIdentifier = "submitButton" // Use index for table cells cell.accessibilityIdentifier = "cell-\(indexPath.row)" ``` **Good identifiers are**: - ✅ Unique within entire app - ✅ Descriptive of element contents - ✅ Static (don't react to content changes) - ✅ Not localized (same across languages) **Why identifiers matter**: - Titles/descriptions may change, identifiers remain stable - Work across localized strings - Uniquely identify elements with dynamic content **Pro Tip**: Use Xcode coding assistant to add identifiers: ``` Prompt: "Add accessibility identifiers to the relevant parts of this view" ``` #### Step 2: Review Accessibility with Accessibility Inspector **Launch Accessibility Inspector**: - Xcode menu → Open Developer Tool → Accessibility Inspector - Or: Launch from Spotlight **Features**: 1. **Element Inspector** — List accessibility values for any view 2. **Property details** — Click property name for documentation 3. **Platform support** — Works on all Apple platforms **What to check**: - Elements have labels - Interactive elements have types (button, not just text) - Values set for stateful elements (checkboxes, toggles) - Identifiers set for elements with dynamic/localized content **Sample Code Reference**: [Delivering an exceptional accessibility experience](https://developer.apple.com/documentation/accessibility/delivering_an_exceptional_accessibility_experience) #### Step 3: Add UI Testing Target 1. Open project settings in Xcode 2. Click "+" below targets list 3. Select **UI Testing Bundle** 4. Click Finish **Result**: New UI test folder with template tests added to project. ### Recording Interactions #### Starting a Recording (Xcode 26) 1. Open UI test source file 2. **Popover appears** explaining how to start recording (first time only) 3. Click **"Start Recording"** button in editor gutter 4. Xcode builds and launches app in Simulator/device **During Recording**: - Interact with app normally (taps, swipes, text entry, etc.) - Code representing interactions appears in source editor in real-time - Recording updates as you type (e.g., text field entries) **Stopping Recording**: - Click **"Stop Run"** button in Xcode #### Example Recording Session ```swift func testCreateAustralianCollection() { let app = XCUIApplication() app.launch() // Tap "Collections" tab (recorded automatically) app.tabBars.buttons["Collections"].tap() // Tap "+" to add new collection app.navigationBars.buttons["Add"].tap() // Tap "Edit" button app.buttons["Edit"].tap() // Type collection name app.textFields.firstMatch.tap() app.textFields.firstMatch.typeText("Max's Australian Adventure") // Tap "Edit Landmarks" app.buttons["Edit Landmarks"].tap() // Add landmarks app.tables.cells.containing(.staticText, identifier:"Great Barrier Reef").buttons["Add"].tap() app.tables.cells.containing(.staticText, identifier:"Uluru").buttons["Add"].tap() // Tap checkmark to save app.navigationBars.buttons["Done"].tap() } ``` #### Reviewing Recorded Code After recording, **review and adjust queries**: **Multiple Options**: Each line has dropdown showing alternative ways to address element. **Selection Recommendations**: 1. **For localized strings** (text, button labels): Choose accessibility identifier if available 2. **For deeply nested views**: Choose shortest query (stays resilient as app changes) 3. **For dynamic content** (timestamps, temperature): Use generic query or identifier **Example**: ```swift // Recorded options for text field: app.textFields["Collection Name"] // ❌ Breaks if label localizes app.textFields["collectionNameField"] // ✅ Uses identifier app.textFields.element(boundBy: 0) // ✅ Position-based app.textFields.firstMatch // ✅ Generic, shortest ``` **Choose shortest, most stable query** for your needs. ### Adding Validations After recording, **add assertions** to verify expected behavior: #### Wait for Existence ```swift // Validate collection created let collection = app.buttons["Max's Australian Adventure"] XCTAssertTrue(collection.waitForExistence(timeout: 5)) ``` #### Wait for Property Changes ```swift // Wait for button to become enabled let submitButton = app.buttons["Submit"] XCTAssertTrue(submitButton.wait(for: .enabled, toEqual: true, timeout: 5)) ``` #### Combine with XCTAssert ```swift // Fail test if element doesn't appear let landmark = app.staticTexts["Great Barrier Reef"] XCTAssertTrue(landmark.waitForExistence(timeout: 5), "Landmark should appear in collection") ``` ### Advanced Automation APIs #### Setup Device State ```swift override func setUpWithError() throws { let app = XCUIApplication() // Set device orientation XCUIDevice.shared.orientation = .landscapeLeft // Set appearance mode app.launchArguments += ["-UIUserInterfaceStyle", "dark"] // Simulate location let location = XCUILocation(location: CLLocation(latitude: 37.7749, longitude: -122.4194)) app.launchArguments += ["-SimulatedLocation", location.description] app.launch() } ``` #### Launch Arguments & Environment ```swift func testWithMockData() { let app = XCUIApplication() // Pass arguments to app app.launchArguments = ["-UI-Testing", "-UseMockData"] // Set environment variables app.launchEnvironment = ["API_URL": "https://mock.api.com"] app.launch() } ``` In app code: ```swift if ProcessInfo.processInfo.arguments.contains("-UI-Testing") { // Use mock data, skip onboarding } ``` #### Custom URL Schemes ```swift // Open app to specific URL let app = XCUIApplication() app.open(URL(string: "myapp://landmark/123")!) // Open URL with system default app (global version) XCUIApplication.open(URL(string: "https://example.com")!) ``` #### Accessibility Audits in Tests ```swift func testAccessibility() throws { let app = XCUIApplication() app.launch() // Perform accessibility audit try app.performAccessibilityAudit() } ``` **Reference**: [Perform accessibility audits for your app — WWDC23](https://developer.apple.com/videos/play/wwdc2023/10035/) ### Test Plans for Multiple Configurations **Test Plans** let you: - Include/exclude individual tests - Set system settings (language, region, appearance) - Configure test properties (timeouts, repetitions, parallelization) - Associate with schemes for specific build settings #### Creating Test Plan 1. Create new or use existing test plan 2. Add/remove tests on first screen 3. Switch to **Configurations** tab #### Adding Multiple Languages ``` Configurations: ├─ English ├─ German (longer strings) ├─ Arabic (right-to-left) └─ Hebrew (right-to-left) ``` **Each locale** = separate configuration in test plan. **Settings**: - Focused for specific locale - Shared across all configurations #### Video & Screenshot Capture **In Configurations tab**: - **Capture screenshots**: On/Off - **Capture video**: On/Off - **Keep media**: "Only failures" or "On, and keep all" **Defaults**: Videos/screenshots kept only for failing runs (for review). **"On, and keep all" use cases**: - Documentation - Tutorials - Marketing materials **Reference**: [Author fast and reliable tests for Xcode Cloud — WWDC22](https://developer.apple.com/videos/play/wwdc2022/110371/) ### Replaying Tests in Xcode Cloud **Xcode Cloud** = built-in service for: - Building app - Running tests - Uploading to App Store - All in cloud without using team devices **Workflow configuration**: - Same test plan used locally - Runs on multiple devices and configurations - Videos/results available in App Store Connect **Viewing Results**: - Xcode: Xcode Cloud section - App Store Connect: Xcode Cloud section - See build info, logs, failure descriptions, video recordings **Team Access**: Entire team can see run history and download results/videos. **Reference**: [Create practical workflows in Xcode Cloud — WWDC23](https://developer.apple.com/videos/play/wwdc2023/10269/) ### Reviewing Test Results with Videos #### Accessing Test Report 1. Click **Test** button in Xcode 2. Double-click failing run to see video + description **Features**: - **Runs dropdown** — Switch between video recordings of different configurations (languages, devices) - **Save video** — Secondary click → Save - **Play/pause** — Video playback with UI interaction overlays - **Timeline dots** — UI interactions shown as dots on timeline - **Jump to failure** — Click failure diamond on timeline #### UI Element Overlay at Failure **At moment of failure**: - Click timeline failure point - **Overlay shows all UI elements** present on screen - Click any element to see code recommendations for addressing it - **Show All** — See alternative examples **Workflow**: 1. Identify what was actually present (vs what test expected) 2. Click element to get query code 3. Secondary click → Copy code 4. **View Source** → Go directly to test 5. Paste corrected code **Example**: ```swift // Test expected: let button = app.buttons["Max's Australian Adventure"] // But overlay shows it's actually text, not button: let text = app.staticTexts["Max's Australian Adventure"] // ✅ Correct ``` #### Running Test in Different Language Click test diamond → Select configuration (e.g., Arabic) → Watch automation run in right-to-left layout. **Validates**: Same automation works across languages/layouts. **Reference**: [Fix failures faster with Xcode test reports — WWDC23](https://developer.apple.com/videos/play/wwdc2023/10175/) ### Recording UI Automation Checklist #### Before Recording - [ ] Add accessibility identifiers to interactive elements - [ ] Review app with Accessibility Inspector - [ ] Add UI Testing Bundle target to project - [ ] Plan workflow to record (user journey) #### During Recording - [ ] Interact naturally with app - [ ] Record complete user journeys (not individual taps) - [ ] Check code generates as you interact - [ ] Stop recording when workflow complete #### After Recording - [ ] Review recorded code options (dropdown on each line) - [ ] Choose stable queries (identifiers > labels) - [ ] Add validations (waitForExistence, XCTAssert) - [ ] Add setup code (device state, launch arguments) - [ ] Run test to verify it passes #### Test Plan Configuration - [ ] Create/update test plan - [ ] Add multiple language configurations - [ ] Include right-to-left languages (Arabic, Hebrew) - [ ] Configure video/screenshot capture settings - [ ] Set appropriate timeouts for network tests #### Running & Reviewing - [ ] Run test locally across configurations - [ ] Review video recordings for failures - [ ] Use UI element overlay to debug failures - [ ] Run in Xcode Cloud for team visibility - [ ] Download and share videos if needed ## Network Conditioning in Tests ### Overview UI tests can pass on fast networks but fail on 3G/LTE. **Network Link Conditioner** simulates real-world network conditions to catch timing-sensitive crashes. **Critical scenarios**: - ❌ iPad Pro over Wi-Fi (fast) → pass - ❌ iPad Pro over 3G (slow) → crash - ✅ Test both to catch device-specific failures ### Setup Network Link Conditioner **Install Network Link Conditioner**: 1. Download from [Apple's Additional Tools for Xcode](https://developer.apple.com/download/all/) 2. Search: "Network Link Conditioner" 3. Install: `sudo open Network\ Link\ Conditioner.pkg` **Verify Installation**: ```bash # Check if installed ls ~/Library/Application\ Support/Network\ Link\ Conditioner/ ``` **Enable in Tests**: ```swift override func setUpWithError() throws { let app = XCUIApplication() // Launch with network conditioning argument app.launchArguments = ["-com.apple.CoreSimulator.CoreSimulatorService", "-networkShaping"] app.launch() } ``` ### Common Network Profiles **3G Profile** (most failures occur here): ```swift override func setUpWithError() throws { let app = XCUIApplication() // Simulate 3G (type in launch arguments) app.launchEnvironment = [ "SIMULATOR_UDID": ProcessInfo.processInfo.environment["SIMULATOR_UDID"] ?? "", "NETWORK_PROFILE": "3G" ] app.launch() } ``` **Manual Network Conditioning** (macOS System Preferences): 1. Open System Preferences → Network 2. Click "Network Link Conditioner" (installed above) 3. Select profile: 3G, LTE, WiFi 4. Click "Start" 5. Run tests (they'll use throttled network) ### Real-World Example: Photo Upload with Network Throttling **❌ Without Network Conditioning**: ```swift func testPhotoUpload() { app.buttons["Upload Photo"].tap() // Passes locally (fast network) XCTAssertTrue(app.staticTexts["Upload complete"].waitForExistence(timeout: 5)) } // ✅ Passes locally, ❌ FAILS on 3G with timeout ``` **✅ With Network Conditioning**: ```swift func testPhotoUploadOn3G() { let app = XCUIApplication() // Network Link Conditioner running (3G profile) app.launch() app.buttons["Upload Photo"].tap() // Increase timeout for 3G XCTAssertTrue(app.staticTexts["Upload complete"].waitForExistence(timeout: 30)) // Verify no crash occurred XCTAssertFalse(app.alerts.element.exists, "App should not crash on 3G") } ``` **Key differences**: - Longer timeout (30s instead of 5s) - Check for crashes - Run on slowest expected network --- ## Multi-Factor Testing: Device Size + Network Speed ### The Problem Tests can pass on device A but fail on device B due to layout differences + network delays. **Multi-factor testing** catches these combinations. **Common failure patterns**: - ✅ iPhone 14 Pro (compact, fast network) - ❌ iPad Pro 12.9 (large, 3G network) → crashes - ✅ iPhone 15 (compact, LTE) - ❌ iPhone 12 (older GPU, 3G) → timeout ### Test Plan Configuration for Multiple Devices **Create Test Plan in Xcode**: 1. File → New → Test Plan 2. Select tests to include 3. Click "Configurations" tab 4. Add configurations for each device/network combo **Example Configuration Matrix**: ``` Configurations: ├─ iPhone 14 Pro + LTE ├─ iPhone 14 Pro + 3G ├─ iPad Pro 12.9 + LTE ├─ iPad Pro 12.9 + 3G (⚠️ Most failures here) └─ iPhone 12 + 3G (⚠️ Older device) ``` **In Test Plan UI**: - Device: iPhone 14 Pro / iPad Pro 12.9 - OS Version: Latest - Locale: English - Network Profile: LTE / 3G ### Programmatic Device-Specific Testing ```swift import XCTest final class MultiFactorUITests: XCTestCase { var deviceModel: String { UIDevice.current.model } override func setUpWithError() throws { let app = XCUIApplication() app.launch() // Adjust timeouts based on device switch deviceModel { case "iPad" where UIScreen.main.bounds.width > 1000: // iPad Pro - larger layout, slower rendering app.launchEnvironment["TEST_TIMEOUT"] = "30" case "iPhone": // iPhone - compact, standard timeout app.launchEnvironment["TEST_TIMEOUT"] = "10" default: app.launchEnvironment["TEST_TIMEOUT"] = "15" } } func testListLoadingAcrossDevices() { let app = XCUIApplication() let timeout = Double(app.launchEnvironment["TEST_TIMEOUT"] ?? "10") ?? 10 app.buttons["Refresh"].tap() // Wait for list to load (timeout varies by device) XCTAssertTrue( app.tables.cells.count > 0, "List should load on \(deviceModel)" ) // Verify no crashes XCTAssertFalse(app.alerts.element.exists) } } ``` ### Real-World Example: iPad Pro + 3G Crash **Scenario**: App works on iPhone 14, crashes on iPad Pro over 3G. **Why it crashes**: 1. iPad Pro has larger layout (landscape) 2. 3G network is slow (latency 100ms+) 3. Images don't load in time, layout engine crashes 4. Single-device testing misses this combo **Test that catches it**: ```swift func testLargeLayoutOn3G() { let app = XCUIApplication() // Running with Network Link Conditioner on 3G profile app.launch() // iPad Pro: Large grid of images app.buttons["Browse"].tap() // Wait longer for images on slow network let firstImage = app.images["photoGrid-0"] XCTAssertTrue( firstImage.waitForExistence(timeout: 20), "First image must load on slow network" ) // Verify grid loaded without crash let loadedCount = app.images.matching(identifier: NSPredicate(format: "identifier BEGINSWITH 'photoGrid'")).count XCTAssertGreater(loadedCount, 5, "Multiple images should load on 3G") // No alerts (no crashes) XCTAssertFalse(app.alerts.element.exists, "App should not crash on large device + slow network") } ``` ### Running Multi-Factor Tests in CI **In GitHub Actions or Xcode Cloud**: ```yaml - name: Run tests across devices run: | xcodebuild -scheme MyApp \ -testPlan MultiDeviceTestPlan \ test ``` **Test Plan runs on**: - iPhone 14 Pro + LTE - iPhone 14 Pro + 3G - iPad Pro + LTE - iPad Pro + 3G **Result**: Catch device-specific crashes before App Store submission. --- ## Debugging Crashes Revealed by UI Tests ### Overview UI tests sometimes reveal crashes that don't happen in manual testing. **Key insight** Automated tests run faster, interact with app differently, and can expose concurrency/timing bugs. **When crashes happen**: - ❌ Manual testing: Can't reproduce (works when you run it) - ✅ UI Test: Crashes every time (automated repetition finds race condition) ### Recognizing Test-Revealed Crashes **Signs in test output**: ``` Failing test: testPhotoUpload Error: The app crashed while responding to a UI event App died from an uncaught exception Stack trace: [EXC_BAD_ACCESS in PhotoViewController] ``` **Video shows**: App visibly crashes (black screen, immediate termination). ### Systematic Debugging Approach #### Step 1: Capture Crash Details **Enable detailed logging**: ```swift override func setUpWithError() throws { let app = XCUIApplication() // Enable all logging app.launchEnvironment = [ "OS_ACTIVITY_MODE": "debug", "DYLD_PRINT_STATISTICS": "1" ] // Enable test diagnostics if #available(iOS 17, *) { let options = XCUIApplicationLaunchOptions() options.captureRawLogs = true app.launch(options) } else { app.launch() } } ``` #### Step 2: Reproduce Locally ```swift func testReproduceCrash() { let app = XCUIApplication() app.launch() // Run exact sequence that causes crash app.buttons["Browse"].tap() app.buttons["Photo Album"].tap() app.buttons["Select All"].tap() app.buttons["Upload"].tap() // Should crash here let uploadButton = app.buttons["Upload"] XCTAssertFalse(uploadButton.exists, "App crashed (expected)") // Don't assert - just let it crash and read logs } ``` **Run test with Console logs visible**: - Xcode: View → Navigators → Show Console - Watch for exception messages #### Step 3: Analyze Crash Logs **Locations**: 1. Xcode Console (real-time, less detail) 2. ~/Library/Logs/DiagnosticMessages/crash_*.log (full details) 3. Device Settings → Privacy → Analytics → Analytics Data **Look for**: - Thread that crashed - Exception type (EXC_BAD_ACCESS, EXC_CRASH, etc.) - Stack trace showing which method crashed **Example crash log**: ``` Exception Type: EXC_BAD_ACCESS (SIGSEGV) Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000000 Thread 0 Crashed: 0 MyApp 0x0001a234 -[PhotoViewController reloadPhotos:] + 234 1 MyApp 0x0001a123 -[PhotoViewController viewDidLoad] + 180 ``` **This tells us**: - Crash in `PhotoViewController.reloadPhotos(_:)` - Likely null pointer dereference - Called from `viewDidLoad` #### Step 4: Connection to Swift Concurrency Issues **Most UI test crashes are concurrency bugs** (not specific to UI testing). Reference related skills: ```swift // Common pattern: Race condition in async image loading class PhotoViewController: UIViewController { var photos: [Photo] = [] override func viewDidLoad() { super.viewDidLoad() // ❌ WRONG: Accessing photos array from multiple threads Task { let newPhotos = await fetchPhotos() self.photos = newPhotos // May crash if main thread access reloadPhotos() // ❌ Crash here } } } // ✅ CORRECT: Ensure main thread class PhotoViewController: UIViewController { @MainActor var photos: [Photo] = [] override func viewDidLoad() { super.viewDidLoad() Task { let newPhotos = await fetchPhotos() await MainActor.run { [weak self] in self?.photos = newPhotos self?.reloadPhotos() // ✅ Safe } } } } ``` **For deep crash analysis**: See `axiom-swift-concurrency` skill for @MainActor patterns and `axiom-memory-debugging` skill for thread-safety issues. #### Step 5: Add Crash-Prevention Tests **After fixing**: ```swift func testPhotosLoadWithoutCrash() { let app = XCUIApplication() app.launch() // Rapid fire interactions that previously caused crash app.buttons["Browse"].tap() app.buttons["Photo Album"].tap() // Load should complete without crash let photoGrid = app.otherElements["photoGrid"] XCTAssertTrue(photoGrid.waitForExistence(timeout: 10)) // No alerts (no crash dialogs) XCTAssertFalse(app.alerts.element.exists) } ``` #### Step 6: Stress Test to Verify Fix ```swift func testPhotosLoadUnderStress() { let app = XCUIApplication() app.launch() // Repeat the crash-causing action multiple times for iteration in 0..<10 { app.buttons["Browse"].tap() // Wait for load let grid = app.otherElements["photoGrid"] XCTAssertTrue(grid.waitForExistence(timeout: 10), "Iteration \(iteration)") // Go back app.navigationBars.buttons["Back"].tap() app.buttons["Refresh"].tap() } // Completed without crash XCTAssertTrue(true, "Stress test passed") } ``` ### Prevention Checklist #### Before releasing - [ ] Run UI tests on slowest network (3G) - [ ] Run on largest device (iPad Pro) - [ ] Run on oldest supported device (iPhone 12) - [ ] Record video of test runs (saves debugging time) - [ ] Check for crashes in logs - [ ] Run stress tests (10x repeated actions) - [ ] Verify @MainActor on UI properties - [ ] Check for race conditions in async code --- ## Resources **WWDC**: 2025-344, 2024-10179, 2023-10175, 2023-10035 **Docs**: /xctest, /xcuiautomation/recording-ui-automation-for-testing, /xctest/xctwaiter, /accessibility/delivering_an_exceptional_accessibility_experience, /accessibility/performing_accessibility_testing_for_your_app **Note**: This skill focuses on reliability patterns and Recording UI Automation. For TDD workflow, see superpowers:test-driven-development. --- **History:** See git log for changes