axiom-ui-recording

Recording UI Automation (Xcode 26+)

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 "axiom-ui-recording" with this command: npx skills add charleswiltgen/axiom/charleswiltgen-axiom-axiom-ui-recording

Recording UI Automation (Xcode 26+)

Guide to Xcode 26's Recording UI Automation feature for creating UI tests through user interaction recording.

The Three-Phase Workflow

From WWDC 2025-344:

┌─────────────────────────────────────────────────────────────┐ │ UI Automation Workflow │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. RECORD ──────► Interact with app in Simulator │ │ Xcode captures as Swift test code │ │ │ │ 2. REPLAY ──────► Run across devices, languages, configs │ │ Using test plans for multi-config │ │ │ │ 3. REVIEW ──────► Watch video recordings in test report │ │ Analyze failures with screenshots │ │ │ └─────────────────────────────────────────────────────────────┘

Phase 1: Recording

Starting a Recording

  • Open your UI test file in Xcode

  • Place cursor inside a test method

  • Debug → Record UI Automation (or use the record button)

  • App launches in Simulator

  • Perform interactions - Xcode generates code

  • Stop recording when done

What Gets Recorded

  • Taps on buttons, cells, controls

  • Text input into text fields

  • Swipes and scrolling

  • Gestures (pinch, rotate)

  • Hardware button presses (Home, volume)

Generated Code Example

// Xcode generates this from your interactions func testLoginFlow() { let app = XCUIApplication() app.launch()

// Recorded: Tap email field, type email
app.textFields["Email"].tap()
app.textFields["Email"].typeText("user@example.com")

// Recorded: Tap password field, type password
app.secureTextFields["Password"].tap()
app.secureTextFields["Password"].typeText("password123")

// Recorded: Tap login button
app.buttons["Login"].tap()

}

Enhancing Recorded Code

Critical: Recorded code is often fragile. Always enhance it for stability.

  1. Add Accessibility Identifiers

Recorded code uses labels which break with localization:

// RECORDED (fragile - breaks with localization) app.buttons["Login"].tap()

// ENHANCED (stable - uses identifier) app.buttons["loginButton"].tap()

Add identifiers in your app code:

// SwiftUI Button("Login") { ... } .accessibilityIdentifier("loginButton")

// UIKit loginButton.accessibilityIdentifier = "loginButton"

  1. Add waitForExistence

Recorded code assumes elements exist immediately:

// RECORDED (may fail if app is slow) app.buttons["Login"].tap()

// ENHANCED (waits for element) let loginButton = app.buttons["loginButton"] XCTAssertTrue(loginButton.waitForExistence(timeout: 5)) loginButton.tap()

  1. Add Assertions

Recorded code just performs actions without verification:

// RECORDED (no verification) app.buttons["Login"].tap()

// ENHANCED (with assertion) app.buttons["loginButton"].tap() let welcomeLabel = app.staticTexts["welcomeLabel"] XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 10), "Welcome screen should appear after login")

  1. Use Shorter Queries

Recorded code may have overly specific queries:

// RECORDED (too specific) app.tables.cells.element(boundBy: 0).buttons["Action"].tap()

// ENHANCED (simpler) app.buttons["actionButton"].tap()

Query Selection Guidelines

From WWDC 2025-344:

Scenario Problem Solution

Localized strings "Login" changes by language Use accessibilityIdentifier

Deeply nested views Long query chains break easily Use shortest possible query

Dynamic content Cell content changes Use identifier or generic query

Multiple matches Query returns many elements Add unique identifier

Best Practices

  • Prefer identifiers over labels

  • Use the shortest query that works

  • Avoid index-based queries (element(boundBy: 0) )

  • Add identifiers to dynamic content

Phase 2: Replay with Test Plans

Test plans allow running the same tests across multiple configurations.

Creating a Test Plan

  • File → New → File → Test Plan

  • Add test targets

  • Configure configurations

Test Plan Structure

{ "configurations": [ { "name": "iPhone - English", "options": { "targetForVariableExpansion": { "containerPath": "container:MyApp.xcodeproj", "identifier": "MyApp" }, "language": "en", "region": "US" } }, { "name": "iPhone - Spanish", "options": { "language": "es", "region": "ES" } }, { "name": "iPhone - Dark Mode", "options": { "userInterfaceStyle": "dark" } }, { "name": "iPad - Landscape", "options": { "defaultTestExecutionTimeAllowance": 120, "testTimeoutsEnabled": true } } ], "defaultOptions": { "targetForVariableExpansion": { "containerPath": "container:MyApp.xcodeproj", "identifier": "MyApp" } }, "testTargets": [ { "target": { "containerPath": "container:MyApp.xcodeproj", "identifier": "MyAppUITests", "name": "MyAppUITests" } } ], "version": 1 }

Configuration Options

Option Purpose

language

Test localization

region

Test regional formatting

userInterfaceStyle

Test dark/light mode

targetForVariableExpansion

App target for configuration

testTimeoutsEnabled

Enable timeout enforcement

defaultTestExecutionTimeAllowance

Timeout in seconds

Running with Test Plan

Command line

xcodebuild test
-scheme "MyApp"
-testPlan "MyTestPlan"
-destination "platform=iOS Simulator,name=iPhone 16"
-resultBundlePath /tmp/results.xcresult

In Xcode

Product → Test Plan → Select your plan

Then Cmd+U to run tests

Phase 3: Review

Test Report Features

After tests complete:

  • View test results in Report Navigator

  • Watch video recordings of each test

  • See screenshots at failure points

  • Analyze timeline of actions

Enabling Attachments

In test plan or scheme:

"options": { "systemAttachmentLifetime": "keepAlways", "userAttachmentLifetime": "keepAlways" }

Capturing Custom Screenshots

func testCheckout() { // ... actions ...

// Manual screenshot at specific point
let screenshot = app.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Checkout Confirmation"
attachment.lifetime = .keepAlways
add(attachment)

}

Common Patterns

Login Flow Template

func testLoginWithValidCredentials() throws { let app = XCUIApplication() app.launch()

// Navigate to login
let showLoginButton = app.buttons["showLoginButton"]
XCTAssertTrue(showLoginButton.waitForExistence(timeout: 5))
showLoginButton.tap()

// Enter credentials
let emailField = app.textFields["emailTextField"]
XCTAssertTrue(emailField.waitForExistence(timeout: 5))
emailField.tap()
emailField.typeText("test@example.com")

let passwordField = app.secureTextFields["passwordTextField"]
passwordField.tap()
passwordField.typeText("password123")

// Submit
app.buttons["loginButton"].tap()

// Verify success
let welcomeScreen = app.staticTexts["welcomeLabel"]
XCTAssertTrue(welcomeScreen.waitForExistence(timeout: 10))

}

Navigation Flow Template

func testNavigateToSettings() throws { let app = XCUIApplication() app.launch()

// Open tab bar item
app.tabBars.buttons["Settings"].tap()

// Verify navigation
let settingsTitle = app.navigationBars["Settings"]
XCTAssertTrue(settingsTitle.waitForExistence(timeout: 5))

// Navigate deeper
app.tables.cells["Account"].tap()
XCTAssertTrue(app.navigationBars["Account"].exists)

}

Form Validation Template

func testFormValidation() throws { let app = XCUIApplication() app.launch()

// Submit empty form
app.buttons["submitButton"].tap()

// Verify error appears
let errorAlert = app.alerts["Error"]
XCTAssertTrue(errorAlert.waitForExistence(timeout: 5))
XCTAssertTrue(errorAlert.staticTexts["Please fill all fields"].exists)

// Dismiss alert
errorAlert.buttons["OK"].tap()

}

Troubleshooting

Recording Doesn't Start

  • Ensure you're in a test method

  • Check simulator is available

  • Verify app builds and runs

  • Try restarting Xcode

Recorded Code Doesn't Work

  • Add waitForExistence before interactions

  • Check accessibility identifiers are set

  • Simplify queries to shortest form

  • Run app manually to verify flow works

Tests Pass Locally, Fail in CI

  • Increase timeouts for slower CI machines

  • Add explicit waits for animations

  • Check simulator configuration matches

  • Disable animations in test setup: app.launchArguments = ["--disable-animations"]

Anti-Patterns

Don't Use Raw Recorded Code in CI

// BAD - Raw recorded code app.buttons["Login"].tap() app.textFields["Email"].typeText("user@example.com")

// GOOD - Enhanced for CI let loginButton = app.buttons["loginButton"] XCTAssertTrue(loginButton.waitForExistence(timeout: 10)) loginButton.tap()

Don't Hardcode Coordinates

// BAD - Coordinates from recording app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()

// GOOD - Use element queries app.buttons["centerButton"].tap()

Don't Skip Assertions

// BAD - Actions only app.buttons["Login"].tap() sleep(2) // Hope it works

// GOOD - Verify outcomes app.buttons["loginButton"].tap() XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 10))

Resources

WWDC: 2025-344, 2024-10206, 2019-413

Docs: /xcode/testing/recording-ui-tests, /xctest/xcuiapplication

Skills: axiom-xctest-automation, axiom-ui-testing

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.

Coding

axiom-xcode-debugging

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

axiom-xcode-mcp

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

axiom-xcode-mcp-setup

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

axiom-xcode-mcp-ref

No summary provided by upstream source.

Repository SourceNeeds Review