axiom-ui-testing by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-ui-testing等待条件,而非任意超时。核心原则 不稳定的测试源于猜测操作所需时间。基于条件的等待可以消除竞态条件。
WWDC 2025 新增功能:录制 UI 自动化允许您录制交互操作,跨设备/语言重放,并查看测试运行的视频录像。
这些是开发者提出的真实问题,本技能旨在解答:
→ 本技能展示基于条件的等待模式,这些模式适用于不同设备/速度,消除了 CI 中的时间差异
→ 本技能演示了针对数据加载、网络请求和动画的 waitForExistence、XCTestExpectation 和轮询模式
→ 本技能涵盖视频调试工作流,用于分析录像并找到测试失败的确切步骤
→ 本技能解释了多因素测试策略和设备无关谓词,以实现稳健的跨设备测试
→ 本技能提供基于条件的等待模板、无障碍功能优先模式以及可靠测试架构的决策树
如果您看到以下任何情况,请怀疑是时间问题:
sleep() 或 Thread.sleep()(任意延迟)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
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
❌ 错误(任意超时):
func testButtonAppears() {
app.buttons["Login"].tap()
sleep(2) // ❌ 猜测需要 2 秒
XCTAssertTrue(app.buttons["Dashboard"].exists)
}
✅ 正确(等待条件):
func testButtonAppears() {
app.buttons["Login"].tap()
let dashboard = app.buttons["Dashboard"]
XCTAssertTrue(dashboard.waitForExistence(timeout: 5))
}
// 等待元素出现
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
return element.waitForExistence(timeout: timeout)
}
// 用法
XCTAssertTrue(waitForElement(app.buttons["Submit"]))
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
}
// 用法
XCTAssertTrue(waitForElementToDisappear(app.activityIndicators["Loading"]))
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
}
// 用法
let submitButton = app.buttons["Submit"]
XCTAssertTrue(waitForButton(submitButton, toBeEnabled: true))
submitButton.tap()
在应用中设置:
Button("Submit") {
// action
}
.accessibilityIdentifier("submitButton")
在测试中使用:
func testSubmitButton() {
let submitButton = app.buttons["submitButton"] // 使用标识符,而非标签
XCTAssertTrue(submitButton.waitForExistence(timeout: 5))
submitButton.tap()
}
原因:无障碍功能标识符不会随本地化而改变,在 UI 更新中保持稳定。
func testDataLoads() {
app.buttons["Refresh"].tap()
// 等待加载指示器消失
let loadingIndicator = app.activityIndicators["Loading"]
XCTAssertTrue(waitForElementToDisappear(loadingIndicator, timeout: 10))
// 现在验证数据已加载
XCTAssertTrue(app.cells.count > 0)
}
func testAnimatedTransition() {
app.buttons["Next"].tap()
// 等待目标视图出现
let destinationView = app.otherElements["DestinationView"]
XCTAssertTrue(destinationView.waitForExistence(timeout: 2))
// 可选:等待动画稍微稳定下来
// 仅在绝对必要时使用
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.3))
}
waitForExistence() 而非 sleep()func testExample() {
let app = XCUIApplication()
app.launchArguments = ["UI-Testing"]
app.launch()
}
在应用代码中:
if ProcessInfo.processInfo.arguments.contains("UI-Testing") {
// 使用模拟数据、跳过引导等。
}
override func setUpWithError() throws {
continueAfterFailure = false // 在首次失败时停止
}
func testExample() {
// 失败时截图
addUIInterruptionMonitor(withDescription: "Alert") { alert in
alert.buttons["OK"].tap()
return true
}
// 打印元素层次结构
print(app.debugDescription)
}
sleep(5) // ❌ 如果操作在 1 秒内完成,则浪费时间
app.buttons["Next"].tap()
XCTAssertTrue(app.buttons["Back"].exists) // ❌ 可能在动画期间失败
app.buttons["Submit"].tap() // ❌ 本地化时会中断
// ❌ 测试 2 假设测试 1 首先运行
func test1_Login() { /* ... */ }
func test2_ViewDashboard() { /* assumes logged in */ }
element.waitForExistence(timeout: 100) // ❌ 太长
element.waitForExistence(timeout: 0.1) // ❌ 太短
使用适当的超时时间:
之前(使用 sleep()):
之后(基于条件的等待):
关键见解:当等待实际条件而非猜测时间时,测试完成得更快且更可靠。
Xcode 26 新增功能:通过视频录像录制、重放和查看 UI 自动化测试。
三个阶段:
支持的平台:iOS、iPadOS、macOS、watchOS、tvOS、axiom-visionOS(专为 iPad 设计)
关键原则:
操作包括:
关键理解:无障碍功能直接向 UI 自动化提供信息。
无障碍功能看到的内容:
最佳实践:出色的无障碍功能体验 = 出色的 UI 自动化体验。
SwiftUI:
Button("Submit") {
// action
}
.accessibilityIdentifier("submitButton")
// 为实例设置特定的标识符
List(landmarks) { landmark in
LandmarkRow(landmark)
.accessibilityIdentifier("landmark-\(landmark.id)")
}
UIKit:
let button = UIButton()
button.accessibilityIdentifier = "submitButton"
// 为表格单元格使用索引
cell.accessibilityIdentifier = "cell-\(indexPath.row)"
良好的标识符应:
标识符重要的原因:
专业提示:使用 Xcode 编码助手添加标识符:
Prompt: "Add accessibility identifiers to the relevant parts of this view"
启动无障碍功能检查器:
功能:
需要检查的内容:
示例代码参考:提供卓越的无障碍功能体验
结果:新的 UI 测试文件夹和模板测试被添加到项目中。
录制期间:
停止录制:
func testCreateAustralianCollection() {
let app = XCUIApplication()
app.launch()
// 点击"收藏"标签页(自动录制)
app.tabBars.buttons["Collections"].tap()
// 点击"+"添加新收藏
app.navigationBars.buttons["Add"].tap()
// 点击"编辑"按钮
app.buttons["Edit"].tap()
// 输入收藏名称
app.textFields.firstMatch.tap()
app.textFields.firstMatch.typeText("Max's Australian Adventure")
// 点击"编辑地标"
app.buttons["Edit Landmarks"].tap()
// 添加地标
app.tables.cells.containing(.staticText, identifier:"Great Barrier Reef").buttons["Add"].tap()
app.tables.cells.containing(.staticText, identifier:"Uluru").buttons["Add"].tap()
// 点击对勾保存
app.navigationBars.buttons["Done"].tap()
}
录制后,查看并调整查询:
多个选项:每行都有一个下拉菜单,显示定位元素的替代方法。
选择建议:
示例:
// 文本字段的录制选项:
app.textFields["Collection Name"] // ❌ 如果标签本地化,会中断
app.textFields["collectionNameField"] // ✅ 使用标识符
app.textFields.element(boundBy: 0) // ✅ 基于位置
app.textFields.firstMatch // ✅ 通用,最短
根据您的需求选择最短、最稳定的查询。
录制后,添加断言以验证预期行为:
// 验证收藏已创建
let collection = app.buttons["Max's Australian Adventure"]
XCTAssertTrue(collection.waitForExistence(timeout: 5))
// 等待按钮变为启用状态
let submitButton = app.buttons["Submit"]
XCTAssertTrue(submitButton.wait(for: .enabled, toEqual: true, timeout: 5))
// 如果元素未出现,则测试失败
let landmark = app.staticTexts["Great Barrier Reef"]
XCTAssertTrue(landmark.waitForExistence(timeout: 5), "Landmark should appear in collection")
override func setUpWithError() throws {
let app = XCUIApplication()
// 设置设备方向
XCUIDevice.shared.orientation = .landscapeLeft
// 设置外观模式
app.launchArguments += ["-UIUserInterfaceStyle", "dark"]
// 模拟位置
let location = XCUILocation(location: CLLocation(latitude: 37.7749, longitude: -122.4194))
app.launchArguments += ["-SimulatedLocation", location.description]
app.launch()
}
func testWithMockData() {
let app = XCUIApplication()
// 向应用传递参数
app.launchArguments = ["-UI-Testing", "-UseMockData"]
// 设置环境变量
app.launchEnvironment = ["API_URL": "https://mock.api.com"]
app.launch()
}
在应用代码中:
if ProcessInfo.processInfo.arguments.contains("-UI-Testing") {
// 使用模拟数据,跳过引导
}
// 打开应用到特定 URL
let app = XCUIApplication()
app.open(URL(string: "myapp://landmark/123")!)
// 使用系统默认应用打开 URL(全局版本)
XCUIApplication.open(URL(string: "https://example.com")!)
func testAccessibility() throws {
let app = XCUIApplication()
app.launch()
// 执行无障碍功能审计
try app.performAccessibilityAudit()
}
测试计划允许您:
Configurations:
├─ English
├─ German (longer strings)
├─ Arabic (right-to-left)
└─ Hebrew (right-to-left)
每个区域设置 = 测试计划中的单独配置。
设置:
在配置标签页中:
默认:仅保留失败运行的视频/截图(用于查看)。
"开启,并保留所有"的用例:
Xcode Cloud = 内置服务,用于:
工作流配置:
查看结果:
团队访问权限:整个团队都可以看到运行历史记录并下载结果/视频。
功能:
在失败时刻:
工作流:
示例:
// 测试预期:
let button = app.buttons["Max's Australian Adventure"]
// 但叠加层显示它实际上是文本,而非按钮:
let text = app.staticTexts["Max's Australian Adventure"] // ✅ 正确
点击测试菱形标记 → 选择配置(例如,阿拉伯语)→ 观看自动化在从右到左布局中运行。
验证:相同的自动化适用于不同语言/布局。
UI 测试可能在快速网络上通过,但在 3G/LTE 上失败。网络链路调节器模拟真实世界的网络条件,以捕捉对时间敏感的崩溃。
关键场景:
安装网络链路调节器:
sudo open Network\ Link\ Conditioner.pkg验证安装:
# 检查是否已安装
ls ~/Library/Application\ Support/Network\ Link\ Conditioner/
在测试中启用:
override func setUpWithError() throws {
let app = XCUIApplication()
// 使用网络调节参数启动
app.launchArguments = ["-com.apple.CoreSimulator.CoreSimulatorService", "-networkShaping"]
app.launch()
}
3G 配置文件(大多数失败发生在这里):
override func setUpWithError() throws {
let app = XCUIApplication()
// 模拟 3G(在启动参数中输入)
app.launchEnvironment = [
"SIMULATOR_UDID": ProcessInfo.processInfo.environment["SIMULATOR_UDID"] ?? "",
"NETWORK_PROFILE": "3G"
]
app.launch()
}
手动网络调节(macOS 系统偏好设置):
❌ 没有网络调节:
func testPhotoUpload() {
app.buttons["Upload Photo"].tap()
// 在本地通过(快速网络)
XCTAssertTrue(app.staticTexts["Upload complete"].waitForExistence(timeout: 5))
}
// ✅ 在本地通过,❌ 在 3G 上因超时而失败
✅ 带网络调节:
func testPhotoUploadOn3G() {
let app = XCUIApplication()
// 网络链路调节器正在运行(3G 配置文件)
app.launch()
app.buttons["Upload Photo"].tap()
// 为 3G 增加超时时间
XCTAssertTrue(app.staticTexts["Upload complete"].waitForExistence(timeout: 30))
// 验证未发生崩溃
XCTAssertFalse(app.alerts.element.exists, "App should not crash on 3G")
}
关键区别:
测试可能在设备 A 上通过,但在设备 B 上由于布局差异 + 网络延迟而失败。多因素测试捕捉这些组合。
常见的失败模式:
在 Xcode 中创建测试计划:
示例配置矩阵:
Configurations:
├─ iPhone 14 Pro + LTE
├─ iPhone 14 Pro + 3G
├─ iPad Pro 12.9 + LTE
├─ iPad Pro 12.9 + 3G (⚠️ 大多数失败发生在这里)
└─ iPhone 12 + 3G (⚠️ 较旧的设备)
在测试计划 UI 中:
import XCTest
final class MultiFactorUITests: XCTestCase {
var deviceModel: String { UIDevice.current.model }
override func setUpWithError() throws {
let app = XCUIApplication()
app.launch()
// 根据设备调整超时时间
switch deviceModel {
case "iPad" where UIScreen.main.bounds.width > 1000:
// iPad Pro - 更大的布局,更慢的渲染
app.launchEnvironment["TEST_TIMEOUT"] = "30"
case "iPhone":
// iPhone - 紧凑,标准超时
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()
// 等待列表加载(超时时间因设备而异)
XCTAssertTrue(
app.tables.cells.count > 0,
"List should load on \(deviceModel)"
)
// 验证未发生崩溃
XCTAssertFalse(app.alerts.element.exists)
}
}
场景:应用在 iPhone 14 上工作,在通过 3G 的 iPad Pro 上崩溃。
崩溃原因:
捕捉此问题的测试:
func testLargeLayoutOn3G() {
let app = XCUIApplication()
// 使用网络链路调节器在 3G 配置文件下运行
app.launch()
// iPad Pro:大型图像网格
app.buttons["Browse"].tap()
// 在慢速网络上等待图像加载更长时间
let firstImage = app.images["photoGrid-0"]
XCTAssertTrue(
firstImage.waitForExistence(timeout: 20),
"First image must load on slow network"
)
// 验证网格已加载且未崩溃
let loadedCount = app.images.matching(identifier: NSPredicate(format: "identifier BEGINSWITH 'photoGrid'")).count
XCTAssertGreater(loadedCount, 5, "Multiple images should load on 3G")
// 没有警报(没有崩溃)
XCTAssertFalse(app.alerts.element.exists, "App should not crash on large device + slow network")
}
在 GitHub Actions 或 Xcode Cloud 中:
- name: Run tests across devices
run: |
xcodebuild -scheme MyApp \
-testPlan MultiDeviceTestPlan \
test
测试计划在以下设备上运行:
结果:在提交到 App Store 之前捕捉设备特定的崩溃。
UI 测试有时会揭示在手动测试中不会发生的崩溃。关键见解 自动化测试运行得更快,与应用交互的方式不同,并且可能暴露并发/时间错误。
崩溃发生的时间:
测试输出中的迹象:
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]
视频显示:应用明显崩溃(黑屏、立即终止)。
启用详细日志记录:
override func setUpWithError() throws {
let app = XCUIApplication()
// 启用所有日志记录
app.launchEnvironment = [
"OS_ACTIVITY_MODE": "debug",
"DYLD_PRINT_STATISTICS": "1"
]
// 启用测试诊断
if #available(iOS 17, *) {
let options = XCUIApplicationLaunchOptions()
options.captureRawLogs = true
app.launch(options)
} else {
app.launch()
}
}
func testReproduceCrash() {
let app = XCUIApplication()
app.launch()
// 运行导致崩溃的确切序列
app.buttons["Browse"].tap()
app.buttons["Photo Album"].tap()
app.buttons["Select All"].tap()
app.buttons["Upload"].tap()
// 应该在这里崩溃
let uploadButton = app.buttons["Upload"]
XCTAssertFalse(uploadButton.exists, "App crashed (expected)")
// 不要断言 - 只是让它崩溃并读取日志
}
在控制台日志可见的情况下运行测试:
位置:
查找:
示例崩溃日志:
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
这告诉我们:
PhotoViewController.reloadPhotos(_:) 中崩溃viewDidLoad 调用大多数 UI 测试崩溃是并发错误(并非特定于 UI 测试)。参考相关技能:
// 常见模式:异步图像加载中的竞态条件
class PhotoViewController: UIViewController {
var photos: [Photo] = []
override func viewDidLoad() {
super.viewDidLoad()
// ❌ 错误:从多个线程访问照片数组
Task {
let newPhotos = await fetchPhotos()
self.photos = newPhotos // 如果主线程访问,可能会崩溃
reloadPhotos() // ❌ 在这里崩溃
}
}
}
// ✅ 正确:确保在主线程
class PhotoViewController: UIViewController {
@MainActor
var photos: [Photo] = []
override func viewDidLoad() {
super
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.
These are real questions developers ask that this skill is designed to answer:
→ The skill shows condition-based waiting patterns that work across devices/speeds, eliminating CI timing differences
→ The skill demonstrates waitForExistence, XCTestExpectation, and polling patterns for data loads, network requests, and animations
→ The skill covers Video Debugging workflows to analyze recordings and find the exact step where tests fail
→ The skill explains multi-factor testing strategies and device-independent predicates for robust cross-device testing
→ The skill provides condition-based waiting templates, accessibility-first patterns, and the decision tree for reliable test architecture
If you see ANY of these, suspect timing issues:
sleep() or Thread.sleep() (arbitrary delays)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
❌ WRONG (Arbitrary Timeout) :
func testButtonAppears() {
app.buttons["Login"].tap()
sleep(2) // ❌ Guessing it takes 2 seconds
XCTAssertTrue(app.buttons["Dashboard"].exists)
}
✅ CORRECT (Wait for Condition) :
func testButtonAppears() {
app.buttons["Login"].tap()
let dashboard = app.buttons["Dashboard"]
XCTAssertTrue(dashboard.waitForExistence(timeout: 5))
}
// Wait for element to appear
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
return element.waitForExistence(timeout: timeout)
}
// Usage
XCTAssertTrue(waitForElement(app.buttons["Submit"]))
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"]))
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()
Set in app :
Button("Submit") {
// action
}
.accessibilityIdentifier("submitButton")
Use in tests :
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.
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)
}
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))
}
waitForExistence() not sleep()func testExample() {
let app = XCUIApplication()
app.launchArguments = ["UI-Testing"]
app.launch()
}
In app code:
if ProcessInfo.processInfo.arguments.contains("UI-Testing") {
// Use mock data, skip onboarding, etc.
}
override func setUpWithError() throws {
continueAfterFailure = false // Stop on first failure
}
func testExample() {
// Take screenshot on failure
addUIInterruptionMonitor(withDescription: "Alert") { alert in
alert.buttons["OK"].tap()
return true
}
// Print element hierarchy
print(app.debugDescription)
}
sleep(5) // ❌ Wastes time if operation completes in 1s
app.buttons["Next"].tap()
XCTAssertTrue(app.buttons["Back"].exists) // ❌ May fail during animation
app.buttons["Submit"].tap() // ❌ Breaks with localization
// ❌ Test 2 assumes Test 1 ran first
func test1_Login() { /* ... */ }
func test2_ViewDashboard() { /* assumes logged in */ }
element.waitForExistence(timeout: 100) // ❌ Too long
element.waitForExistence(timeout: 0.1) // ❌ Too short
Use appropriate timeouts :
Before (using sleep()):
After (condition-based waiting):
Key insight Tests finish faster AND are more reliable when waiting for actual conditions instead of guessing times.
NEW in Xcode 26 : Record, replay, and review UI automation tests with video recordings.
Three Phases :
Supported Platforms : iOS, iPadOS, macOS, watchOS, tvOS, axiom-visionOS (Designed for iPad)
Key Principles :
Actions include :
Critical Understanding : Accessibility provides information directly to UI automation.
What accessibility sees:
Best Practice : Great accessibility experience = great UI automation experience.
SwiftUI :
Button("Submit") {
// action
}
.accessibilityIdentifier("submitButton")
// Make identifiers specific to instance
List(landmarks) { landmark in
LandmarkRow(landmark)
.accessibilityIdentifier("landmark-\(landmark.id)")
}
UIKit :
let button = UIButton()
button.accessibilityIdentifier = "submitButton"
// Use index for table cells
cell.accessibilityIdentifier = "cell-\(indexPath.row)"
Good identifiers are :
Why identifiers matter :
Pro Tip : Use Xcode coding assistant to add identifiers:
Prompt: "Add accessibility identifiers to the relevant parts of this view"
Launch Accessibility Inspector :
Features :
What to check :
Sample Code Reference : Delivering an exceptional accessibility experience
Result : New UI test folder with template tests added to project.
During Recording :
Stopping Recording :
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()
}
After recording, review and adjust queries :
Multiple Options : Each line has dropdown showing alternative ways to address element.
Selection Recommendations :
Example :
// 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.
After recording, add assertions to verify expected behavior:
// Validate collection created
let collection = app.buttons["Max's Australian Adventure"]
XCTAssertTrue(collection.waitForExistence(timeout: 5))
// Wait for button to become enabled
let submitButton = app.buttons["Submit"]
XCTAssertTrue(submitButton.wait(for: .enabled, toEqual: true, timeout: 5))
// Fail test if element doesn't appear
let landmark = app.staticTexts["Great Barrier Reef"]
XCTAssertTrue(landmark.waitForExistence(timeout: 5), "Landmark should appear in collection")
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()
}
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:
if ProcessInfo.processInfo.arguments.contains("-UI-Testing") {
// Use mock data, skip onboarding
}
// 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")!)
func testAccessibility() throws {
let app = XCUIApplication()
app.launch()
// Perform accessibility audit
try app.performAccessibilityAudit()
}
Reference : Perform accessibility audits for your app — WWDC23
Test Plans let you:
Configurations:
├─ English
├─ German (longer strings)
├─ Arabic (right-to-left)
└─ Hebrew (right-to-left)
Each locale = separate configuration in test plan.
Settings :
In Configurations tab :
Defaults : Videos/screenshots kept only for failing runs (for review).
"On, and keep all" use cases :
Reference : Author fast and reliable tests for Xcode Cloud — WWDC22
Xcode Cloud = built-in service for:
Workflow configuration :
Viewing Results :
Team Access : Entire team can see run history and download results/videos.
Reference : Create practical workflows in Xcode Cloud — WWDC23
Features :
At moment of failure :
Workflow :
Example :
// 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
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
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 :
Install Network Link Conditioner :
sudo open Network\ Link\ Conditioner.pkgVerify Installation :
# Check if installed
ls ~/Library/Application\ Support/Network\ Link\ Conditioner/
Enable in Tests :
override func setUpWithError() throws {
let app = XCUIApplication()
// Launch with network conditioning argument
app.launchArguments = ["-com.apple.CoreSimulator.CoreSimulatorService", "-networkShaping"]
app.launch()
}
3G Profile (most failures occur here):
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):
❌ Without Network Conditioning :
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 :
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 :
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 :
Create Test Plan in Xcode :
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 :
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)
}
}
Scenario : App works on iPhone 14, crashes on iPad Pro over 3G.
Why it crashes :
Test that catches it :
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")
}
In GitHub Actions or Xcode Cloud :
- name: Run tests across devices
run: |
xcodebuild -scheme MyApp \
-testPlan MultiDeviceTestPlan \
test
Test Plan runs on :
Result : Catch device-specific crashes before App Store submission.
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 :
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).
Enable detailed logging :
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()
}
}
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 :
Locations :
Look for :
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 :
PhotoViewController.reloadPhotos(_:)viewDidLoadMost UI test crashes are concurrency bugs (not specific to UI testing). Reference related skills:
// 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.
After fixing :
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)
}
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")
}
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
Weekly Installs
156
Repository
GitHub Stars
674
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
opencode137
codex132
gemini-cli127
claude-code124
cursor124
github-copilot123
Skills CLI 使用指南:AI Agent 技能包管理器安装与管理教程
33,600 周安装
ISO 13485内部审计专家指南:医疗器械QMS审计计划、执行与不符合项管理
153 周安装
TikTok广告优化全攻略:从创意、技术到效果分析的完整检查清单与评分系统
154 周安装
Odoo 升级助手 v4.0:自动化模块升级,解决版本兼容性问题
154 周安装
债券相对价值分析工具 - 固定收益利差分解与情景分析 | 金融科技AI插件
154 周安装
Web设计规范检查工具 - 自动验证代码符合界面指南 | Cal.com开源
154 周安装
SwiftUI 导航 API 参考:从 NavigationStack 到 iOS 26 液态玻璃设计完整指南
154 周安装