Use when setting up UI test recording in Xcode 26, enhancing recorded tests for stability, or configuring test plans for multi-configuration replay. Based on WWDC 2025-344 "Record, replay, and review".
BY CharlesWiltgenUPDATED Oct 27, 2026
INTENDED FOR◈OpenClawInstall in a listed agent.
instructions.md
---
name: axiom-ui-recording
description: "Use when setting up UI test recording in Xcode 26, enhancing recorded tests for stability, or configuring test plans for multi-configuration replay. Based on WWDC 2025-344 \"Record, replay, and review\"."
---
# 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
1. Open your UI test file in Xcode
2. Place cursor inside a test method
3. **Editor → Start Recording UI Test** (or use the record button)
4. App launches in Simulator
5. Perform interactions - Xcode generates code
6. 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
```swift
// Xcode generates the interactions below. `@MainActor` is what makes them build
// cleanly under Swift 6 — XCUI types are main-actor isolated.
@MainActor
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:
```swift
// RECORDED (fragile - breaks with localization)
app.buttons["Login"].tap()
// ENHANCED (stable - uses identifier)
app.buttons["loginButton"].tap()
```
**Add identifiers in your app code:**
```swift
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"
```
### 2. Add waitForExistence
Recorded code assumes elements exist immediately:
```swift
// 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()
```
### 3. Add Assertions
Recorded code just performs actions without verification:
```swift
// 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")
```
### 4. Use Shorter Queries
Recorded code may have overly specific queries:
```swift
// 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
1. **Prefer identifiers over labels**
2. **Use the shortest query that works**
3. **Avoid index-based queries** (`element(boundBy: 0)`)
4. **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
1. **File → New → File → Test Plan**
2. Add test targets
3. Configure configurations
### Test Plan Structure
```json
{
"configurations": [
{
"name": "iPhone - English",
"options": {
"targetForVariableExpansion": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyApp",
"name": "MyApp"
},
"language": "en",
"region": "US"
}
},
{
"name": "iPhone - Spanish",
"options": {
"language": "es",
"region": "ES"
}
},
{
"name": "iPad - Landscape",
"options": {
"defaultTestExecutionTimeAllowance": 120,
"testTimeoutsEnabled": true
}
}
],
"defaultOptions": {
"targetForVariableExpansion": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyApp",
"name": "MyApp"
}
},
"testTargets": [
{
"target": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyAppUITests",
"name": "MyAppUITests"
}
}
],
"version": 1
}
```
### Configuration Options
| Option | Purpose |
|--------|---------|
| `language` | Test localization |
| `region` | Test regional formatting |
| `targetForVariableExpansion` | App target for configuration |
| `testTimeoutsEnabled` | Enable timeout enforcement |
| `defaultTestExecutionTimeAllowance` | Timeout in seconds |
Appearance is not a test-plan option - the app follows the simulator's appearance. Set it at the device level before the run:
```bash
xcrun simctl ui <device-udid> appearance dark
```
### Running with Test Plan
```bash
# Command line
xcodebuild test \
-scheme "MyApp" \
-testPlan "MyTestPlan" \
-destination "platform=iOS Simulator,name=iPhone 18 Pro" \
-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:
1. **View test results** in Report Navigator
2. **Watch video recordings** of each failing run (passing runs keep them only with the retention setting below)
3. **See screenshots** at failure points
4. **Analyze timeline** of actions
### Enabling Attachments
In test plan or scheme:
```json
"options": {
"systemAttachmentLifetime": "keepAlways",
"userAttachmentLifetime": "keepAlways"
}
```
### Capturing Custom Screenshots
```swift
@MainActor
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
```swift
@MainActor
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
```swift
@MainActor
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.buttons["Account"].tap()
XCTAssertTrue(app.navigationBars["Account"].exists)
}
```
### Form Validation Template
```swift
@MainActor
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
1. Ensure you're in a test method
2. Check simulator is available
3. Verify app builds and runs
4. Try restarting Xcode
### Recorded Code Doesn't Work
1. **Add waitForExistence** before interactions
2. **Check accessibility identifiers** are set
3. **Simplify queries** to shortest form
4. **Run app manually** to verify flow works
### Tests Pass Locally, Fail in CI
1. **Increase timeouts** for slower CI machines
2. **Add explicit waits** for animations
3. **Check simulator configuration** matches
4. **Disable animations** in test setup - the app must read this flag itself:
```swift
app.launchArguments = ["--disable-animations"]
```
```swift
// App code
if CommandLine.arguments.contains("--disable-animations") {
UIView.setAnimationsEnabled(false)
}
```
## Anti-Patterns
### Don't Use Raw Recorded Code in CI
```swift
// 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
```swift
// 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
```swift
// 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, 2023-10175, 2019-413
**Docs**: /xcuiautomation/recording-ui-automation-for-testing, /xctest/xcuiapplication
**Skills**: See `skills/xctest-automation.md`, `skills/ui-testing.md`
---
name: axiom-ui-recording
description: "Use when setting up UI test recording in Xcode 26, enhancing recorded tests for stability, or configuring test plans for multi-configuration replay. Based on WWDC 2025-344 \"Record, replay, and review\"."
---
# 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
1. Open your UI test file in Xcode
2. Place cursor inside a test method
3. **Editor → Start Recording UI Test** (or use the record button)
4. App launches in Simulator
5. Perform interactions - Xcode generates code
6. 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
```swift
// Xcode generates the interactions below. `@MainActor` is what makes them build
// cleanly under Swift 6 — XCUI types are main-actor isolated.
@MainActor
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:
```swift
// RECORDED (fragile - breaks with localization)
app.buttons["Login"].tap()
// ENHANCED (stable - uses identifier)
app.buttons["loginButton"].tap()
```
**Add identifiers in your app code:**
```swift
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"
```
### 2. Add waitForExistence
Recorded code assumes elements exist immediately:
```swift
// 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()
```
### 3. Add Assertions
Recorded code just performs actions without verification:
```swift
// 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")
```
### 4. Use Shorter Queries
Recorded code may have overly specific queries:
```swift
// 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
1. **Prefer identifiers over labels**
2. **Use the shortest query that works**
3. **Avoid index-based queries** (`element(boundBy: 0)`)
4. **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
1. **File → New → File → Test Plan**
2. Add test targets
3. Configure configurations
### Test Plan Structure
```json
{
"configurations": [
{
"name": "iPhone - English",
"options": {
"targetForVariableExpansion": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyApp",
"name": "MyApp"
},
"language": "en",
"region": "US"
}
},
{
"name": "iPhone - Spanish",
"options": {
"language": "es",
"region": "ES"
}
},
{
"name": "iPad - Landscape",
"options": {
"defaultTestExecutionTimeAllowance": 120,
"testTimeoutsEnabled": true
}
}
],
"defaultOptions": {
"targetForVariableExpansion": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyApp",
"name": "MyApp"
}
},
"testTargets": [
{
"target": {
"containerPath": "container:MyApp.xcodeproj",
"identifier": "MyAppUITests",
"name": "MyAppUITests"
}
}
],
"version": 1
}
```
### Configuration Options
| Option | Purpose |
|--------|---------|
| `language` | Test localization |
| `region` | Test regional formatting |
| `targetForVariableExpansion` | App target for configuration |
| `testTimeoutsEnabled` | Enable timeout enforcement |
| `defaultTestExecutionTimeAllowance` | Timeout in seconds |
Appearance is not a test-plan option - the app follows the simulator's appearance. Set it at the device level before the run:
```bash
xcrun simctl ui <device-udid> appearance dark
```
### Running with Test Plan
```bash
# Command line
xcodebuild test \
-scheme "MyApp" \
-testPlan "MyTestPlan" \
-destination "platform=iOS Simulator,name=iPhone 18 Pro" \
-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:
1. **View test results** in Report Navigator
2. **Watch video recordings** of each failing run (passing runs keep them only with the retention setting below)
3. **See screenshots** at failure points
4. **Analyze timeline** of actions
### Enabling Attachments
In test plan or scheme:
```json
"options": {
"systemAttachmentLifetime": "keepAlways",
"userAttachmentLifetime": "keepAlways"
}
```
### Capturing Custom Screenshots
```swift
@MainActor
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
```swift
@MainActor
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
```swift
@MainActor
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.buttons["Account"].tap()
XCTAssertTrue(app.navigationBars["Account"].exists)
}
```
### Form Validation Template
```swift
@MainActor
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
1. Ensure you're in a test method
2. Check simulator is available
3. Verify app builds and runs
4. Try restarting Xcode
### Recorded Code Doesn't Work
1. **Add waitForExistence** before interactions
2. **Check accessibility identifiers** are set
3. **Simplify queries** to shortest form
4. **Run app manually** to verify flow works
### Tests Pass Locally, Fail in CI
1. **Increase timeouts** for slower CI machines
2. **Add explicit waits** for animations
3. **Check simulator configuration** matches
4. **Disable animations** in test setup - the app must read this flag itself:
```swift
app.launchArguments = ["--disable-animations"]
```
```swift
// App code
if CommandLine.arguments.contains("--disable-animations") {
UIView.setAnimationsEnabled(false)
}
```
## Anti-Patterns
### Don't Use Raw Recorded Code in CI
```swift
// 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
```swift
// 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
```swift
// 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, 2023-10175, 2019-413
**Docs**: /xcuiautomation/recording-ui-automation-for-testing, /xctest/xcuiapplication
**Skills**: See `skills/xctest-automation.md`, `skills/ui-testing.md`
COMPATIBILITY & REFERENCES
Platform guidance
Imported from the listed AgentSkill.sh category. Complete supplied package files and notices are retained. Required external tools and credentials are not installed. No bundled code was executed during import. Individual agent task behavior has not been tested.
Resource notes updated 2026-10-27 · Original instructions by CharlesWiltgen
Creates comprehensive frontend UI/UX designs including user interfaces, design systems, component libraries, responsive layouts, and accessibility implementations. Produces wireframes, mockups, design specifications, and implementation guidelines. Use when designing user interfaces, creating desi...
Expert in building scalable design systems with Atomic Design, design tokens, and theming infrastructure. Use when creating component libraries, implementing dark mode, or establishing typography and color systems. Covers multi-brand support, Storybook-driven development, and accessibility-first ...
Deep CSS analysis tool for understanding specificity, cascade order, inheritance chains, and computed styles. Visualizes CSS complexity and suggests simplifications. Use when debugging specificity issues or understanding CSS behavior.