axiom-ui-recording by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-ui-recording关于 Xcode 26 的录制 UI 自动化功能指南,用于通过用户交互录制创建 UI 测试。
来自 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 │
│ │
└─────────────────────────────────────────────────────────────┘
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// 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()
}
关键提示:录制的代码通常很脆弱。请务必增强其稳定性。
录制的代码使用标签,这些标签在本地化时会失效:
// RECORDED (fragile - breaks with localization)
app.buttons["Login"].tap()
// ENHANCED (stable - uses identifier)
app.buttons["loginButton"].tap()
在你的应用代码中添加标识符:
// SwiftUI
Button("Login") { ... }
.accessibilityIdentifier("loginButton")
// UIKit
loginButton.accessibilityIdentifier = "loginButton"
录制的代码假设元素立即存在:
// 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()
录制的代码只执行操作而不进行验证:
// 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")
录制的代码可能包含过于具体的查询:
// RECORDED (too specific)
app.tables.cells.element(boundBy: 0).buttons["Action"].tap()
// ENHANCED (simpler)
app.buttons["actionButton"].tap()
来自 WWDC 2025-344:
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 本地化字符串 | "Login" 随语言变化 | 使用 accessibilityIdentifier |
| 深度嵌套视图 | 长查询链容易中断 | 使用尽可能短的查询 |
| 动态内容 | 单元格内容变化 | 使用标识符或通用查询 |
| 多个匹配项 | 查询返回多个元素 | 添加唯一标识符 |
element(boundBy: 0))测试计划允许在多种配置下运行相同的测试。
{
"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
}
| 选项 | 用途 |
|---|---|
language | 测试本地化 |
region | 测试区域格式化 |
userInterfaceStyle | 测试深色/浅色模式 |
targetForVariableExpansion | 用于配置的应用目标 |
testTimeoutsEnabled | 启用超时强制执行 |
defaultTestExecutionTimeAllowance | 超时时间(秒) |
# 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
测试完成后:
在测试计划或方案中:
"options": {
"systemAttachmentLifetime": "keepAlways",
"userAttachmentLifetime": "keepAlways"
}
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)
}
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))
}
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)
}
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()
}
为较慢的 CI 机器增加超时时间
为动画添加显式等待
检查模拟器配置是否匹配
在测试设置中禁用动画:
app.launchArguments = ["--disable-animations"]
// 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()
// BAD - Coordinates from recording
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
// GOOD - Use element queries
app.buttons["centerButton"].tap()
// 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))
WWDC : 2025-344, 2024-10206, 2019-413
文档 : /xcode/testing/recording-ui-tests, /xctest/xcuiapplication
技能 : axiom-xctest-automation, axiom-ui-testing
每周安装量
91
仓库
GitHub 星标数
606
首次出现
2026年1月21日
安全审计
安装于
opencode75
claude-code71
codex70
gemini-cli68
cursor68
github-copilot65
Guide to Xcode 26's Recording UI Automation feature for creating UI tests through user interaction recording.
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 │
│ │
└─────────────────────────────────────────────────────────────┘
// 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()
}
Critical : Recorded code is often fragile. Always enhance it for stability.
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"
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()
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")
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()
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 |
element(boundBy: 0))Test plans allow running the same tests across multiple configurations.
{
"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
}
| Option | Purpose |
|---|---|
language | Test localization |
region | Test regional formatting |
userInterfaceStyle | Test dark/light mode |
targetForVariableExpansion | App target for configuration |
testTimeoutsEnabled | Enable timeout enforcement |
defaultTestExecutionTimeAllowance |
# 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
After tests complete:
In test plan or scheme:
"options": {
"systemAttachmentLifetime": "keepAlways",
"userAttachmentLifetime": "keepAlways"
}
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)
}
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))
}
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)
}
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()
}
Increase timeouts for slower CI machines
Add explicit waits for animations
Check simulator configuration matches
Disable animations in test setup:
app.launchArguments = ["--disable-animations"]
// 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()
// BAD - Coordinates from recording
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
// GOOD - Use element queries
app.buttons["centerButton"].tap()
// 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))
WWDC : 2025-344, 2024-10206, 2019-413
Docs : /xcode/testing/recording-ui-tests, /xctest/xcuiapplication
Skills : axiom-xctest-automation, axiom-ui-testing
Weekly Installs
91
Repository
GitHub Stars
606
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode75
claude-code71
codex70
gemini-cli68
cursor68
github-copilot65
Skills CLI 使用指南:AI Agent 技能包管理器安装与管理教程
40,000 周安装
Google Workspace CLI gws-chat 命令:管理 Google Chat 空间、表情符号与媒体文件
8,000 周安装
Sentry CLI 使用指南:命令行工具与Sentry监控平台交互的最佳实践
10,000 周安装
Vercel CLI 令牌认证部署指南 - 无需登录的自动化项目部署
9,900 周安装
Next.js 自动升级工具 - 安全高效升级 Next.js 版本,遵循官方迁移指南
10,000 周安装
Firestore Enterprise Native Mode 完整指南:配置、数据模型、安全规则与SDK使用
10,800 周安装
Genkit Dart AI SDK 开发指南:Dart 代码生成、AI 智能体与结构化输出
10,700 周安装
| Timeout in seconds |