---
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
