ワンクリックで
ui-testing
Write, review, or orchestrate XCUITest UI tests — accessibility identifiers, page objects, test helpers, and test execution.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Write, review, or orchestrate XCUITest UI tests — accessibility identifiers, page objects, test helpers, and test execution.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Implement UI from Figma designs via MCP with a closed-loop pixel-parity check. Extract exact values (never eyeball), translate to SwiftUI, then render→diff→fix against the Figma export until it matches. Use when implementing or auditing UI against Figma.
HTTP networking layer reference — TargetType, HTTPClient, services, error handling.
Fetch CodeRabbit review comments from batch PRs and auto-fix the issues. Pushes fixes and re-requests review.
Run multiple tasks overnight as parallel PRs. Produces a PRD per task, selects skills/rules, spawns isolated agents, and reports PR links.
Run multiple tasks overnight as parallel PRs. Produces a PRD per task, selects skills/rules, spawns isolated agents, and reports PR links.
Run SwiftLint and xcodebuild to verify the project compiles cleanly.
| name | ui-testing |
| description | Write, review, or orchestrate XCUITest UI tests — accessibility identifiers, page objects, test helpers, and test execution. |
| user-invocable | false |
UI tests live in the VultisigAppUITests target and use Apple's XCUITest framework. The testing architecture follows three layers:
┌─────────────────────────────────────────┐
│ Test Cases (XCTestCase subclasses) │ — what to verify
├─────────────────────────────────────────┤
│ Page Objects (Screen abstractions) │ — how to interact
├─────────────────────────────────────────┤
│ Accessibility IDs (in app source) │ — where to find elements
└─────────────────────────────────────────┘
VultisigApp/VultisigAppUITests/
├── Helpers/
│ ├── XCUIApplication+Launch.swift # Launch helpers & environment config
│ └── XCUIElement+Helpers.swift # waitForExistence, tap-if-exists, etc.
├── Pages/
│ ├── HomePage.swift # Home screen interactions
│ ├── VaultSelectorPage.swift # Vault selector overlay
│ ├── SendPage.swift # Send transaction flow
│ ├── SettingsPage.swift # Settings screen
│ └── ... # One page per screen
├── Tests/
│ ├── HomeScreenTests.swift # Home screen test cases
│ ├── SendFlowTests.swift # Send transaction tests
│ ├── VaultCreationTests.swift # Vault creation tests
│ ├── SettingsTests.swift # Settings tests
│ └── LaunchTests.swift # App launch & screenshot tests
└── VultisigAppUITests.swift # Base test class (shared setup)
AccessibilityID EnumAll identifiers are defined in a single enum in the main app target (not the test target), organized by screen:
File: VultisigApp/VultisigApp/Utils/AccessibilityID.swift
import Foundation
enum AccessibilityID {
enum Home {
static let walletTab = "home.walletTab"
static let defiTab = "home.defiTab"
static let agentTab = "home.agentTab"
static let settingsButton = "home.settingsButton"
static let historyButton = "home.historyButton"
static let vaultSelector = "home.vaultSelector"
static let cameraButton = "home.cameraButton"
}
enum VaultSelector {
static let container = "vaultSelector.container"
static let addVaultButton = "vaultSelector.addVaultButton"
static func vaultCell(name: String) -> String {
"vaultSelector.vault.\(name)"
}
}
enum Send {
static let amountField = "send.amountField"
static let addressField = "send.addressField"
static let memoField = "send.memoField"
static let continueButton = "send.continueButton"
static let coinSelector = "send.coinSelector"
static let maxButton = "send.maxButton"
}
enum Verify {
static let confirmButton = "verify.confirmButton"
static let amountLabel = "verify.amountLabel"
static let addressLabel = "verify.addressLabel"
static let feeLabel = "verify.feeLabel"
}
enum Settings {
static let container = "settings.container"
static let languageCell = "settings.languageCell"
static let currencyCell = "settings.currencyCell"
static let vaultSettingsCell = "settings.vaultSettingsCell"
static let faqCell = "settings.faqCell"
}
enum Onboarding {
static let createVaultButton = "onboarding.createVaultButton"
static let importVaultButton = "onboarding.importVaultButton"
static let vaultNameField = "onboarding.vaultNameField"
}
// Add more screens as tests expand
}
Add .accessibilityIdentifier() to interactive and assertable elements:
// Buttons
PrimaryButton(title: "continue".localized, type: .primary, size: .medium) {
onContinue()
}
.accessibilityIdentifier(AccessibilityID.Send.continueButton)
// Text fields
CommonTextField(text: $amount, label: "amount".localized, placeholder: "0.0")
.accessibilityIdentifier(AccessibilityID.Send.amountField)
// Tab items
Button { selectedTab = .wallet } label: { ... }
.accessibilityIdentifier(AccessibilityID.Home.walletTab)
// Navigation elements
Button(action: onSettings) { Image(systemName: "gear") }
.accessibilityIdentifier(AccessibilityID.Home.settingsButton)
// Dynamic cells (use a function with the unique identifier)
VaultCell(vault: vault)
.accessibilityIdentifier(AccessibilityID.VaultSelector.vaultCell(name: vault.name))
"screen.element" format (e.g., "home.settingsButton")AccessibilityID — never use inline string literals in .accessibilityIdentifier()Each screen gets a page object that encapsulates element queries and interactions. Page objects live in the test target.
import XCTest
struct HomePage {
let app: XCUIApplication
// MARK: - Elements
var walletTab: XCUIElement {
app.buttons[AccessibilityID.Home.walletTab]
}
var defiTab: XCUIElement {
app.buttons[AccessibilityID.Home.defiTab]
}
var settingsButton: XCUIElement {
app.buttons[AccessibilityID.Home.settingsButton]
}
var vaultSelector: XCUIElement {
app.buttons[AccessibilityID.Home.vaultSelector]
}
// MARK: - Actions
@discardableResult
func tapWalletTab() -> Self {
walletTab.tap()
return self
}
@discardableResult
func tapSettings() -> SettingsPage {
settingsButton.tap()
return SettingsPage(app: app)
}
@discardableResult
func tapVaultSelector() -> VaultSelectorPage {
vaultSelector.tap()
return VaultSelectorPage(app: app)
}
// MARK: - Assertions
func assertVisible(timeout: TimeInterval = 5) {
XCTAssertTrue(walletTab.waitForExistence(timeout: timeout), "Home screen not visible")
}
}
self or the next page — enables fluent chaining: homePage.tapSettings().assertVisible()@discardableResult — callers can ignore the return when they don't chainAccessibilityID constants — the page never contains string literalsassertVisible(), assertAmount(equals:), etc.*Screen.swift naming in the appSince AccessibilityID is defined in the main app target, the test target needs access. Two options:
Option A (recommended): Add AccessibilityID.swift to both targets in the project file (main + UI test target membership).
Option B: Import the app module in the test target:
@testable import VultisigApp
// VultisigAppUITests/Helpers/XCUIApplication+Launch.swift
import XCTest
extension XCUIApplication {
/// Launch with standard test configuration
func launchForTesting() {
launchArguments += ["-UITesting"]
launchArguments += ["-disableAnimations"]
launchEnvironment["UI_TESTING"] = "1"
launch()
}
/// Launch with a specific vault pre-selected (via launch environment)
func launchWithVault(named name: String) {
launchEnvironment["TEST_VAULT_NAME"] = name
launchForTesting()
}
/// Launch skipping authentication (for tests that don't test auth)
func launchSkippingAuth() {
launchArguments += ["-skipAuthentication"]
launchForTesting()
}
}
The app must check for these arguments to configure test mode:
// In VultisigApp.swift or AppViewModel
#if DEBUG
if CommandLine.arguments.contains("-UITesting") {
// Disable analytics, push notifications, etc.
}
if CommandLine.arguments.contains("-skipAuthentication") {
// Bypass biometric auth
}
if CommandLine.arguments.contains("-disableAnimations") {
UIView.setAnimationsEnabled(false)
}
if let testVault = ProcessInfo.processInfo.environment["TEST_VAULT_NAME"] {
// Pre-select vault by name
}
#endif
// VultisigAppUITests/Helpers/XCUIElement+Helpers.swift
import XCTest
extension XCUIElement {
/// Wait for element to exist, then tap
func waitAndTap(timeout: TimeInterval = 5, file: StaticString = #file, line: UInt = #line) {
let exists = waitForExistence(timeout: timeout)
XCTAssertTrue(exists, "Element \(identifier) not found after \(timeout)s", file: file, line: line)
tap()
}
/// Tap only if the element exists (no assertion failure)
func tapIfExists(timeout: TimeInterval = 2) {
if waitForExistence(timeout: timeout) {
tap()
}
}
/// Clear text field and type new text
func clearAndType(_ text: String) {
guard exists else { return }
tap()
// Select all + delete
if let value = value as? String, !value.isEmpty {
let selectAll = XCUIApplication().menuItems["Select All"]
if selectAll.exists {
selectAll.tap()
}
typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: value.count))
}
typeText(text)
}
/// Assert element label contains expected text
func assertLabelContains(_ text: String, file: StaticString = #file, line: UInt = #line) {
let labelValue = label
XCTAssertTrue(labelValue.contains(text),
"Expected label to contain '\(text)', got '\(labelValue)'",
file: file, line: line)
}
}
// VultisigAppUITests/VultisigAppUITests.swift
import XCTest
class VultisigUITestCase: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
}
override func tearDownWithError() throws {
app = nil
}
// MARK: - Page Factories
var homePage: HomePage { HomePage(app: app) }
var sendPage: SendPage { SendPage(app: app) }
var settingsPage: SettingsPage { SettingsPage(app: app) }
var vaultSelectorPage: VaultSelectorPage { VaultSelectorPage(app: app) }
// MARK: - Common Flows
/// Launch app and navigate to home (skipping auth)
func launchToHome() {
app.launchSkippingAuth()
homePage.assertVisible()
}
/// Take a screenshot and attach to the test report
func takeScreenshot(name: String) {
let screenshot = app.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
}
// VultisigAppUITests/Tests/HomeScreenTests.swift
import XCTest
final class HomeScreenTests: VultisigUITestCase {
func testHomeScreenShowsTabs() throws {
launchToHome()
homePage.assertVisible()
XCTAssertTrue(homePage.walletTab.exists)
XCTAssertTrue(homePage.defiTab.exists)
}
func testNavigateToSettings() throws {
launchToHome()
let settings = homePage.tapSettings()
settings.assertVisible()
}
func testSwitchTabs() throws {
launchToHome()
homePage.tapWalletTab()
// Assert wallet content visible
homePage.tapDefiTab()
// Assert DeFi content visible
}
func testVaultSelectorOpens() throws {
launchToHome()
let selector = homePage.tapVaultSelector()
selector.assertVisible()
}
}
// VultisigAppUITests/Tests/SendFlowTests.swift
import XCTest
final class SendFlowTests: VultisigUITestCase {
func testSendFlowNavigatesToVerify() throws {
launchToHome()
// Navigate to send
// (depends on how send is accessed — coin tap, toolbar button, etc.)
sendPage.assertVisible()
sendPage
.enterAmount("0.001")
.enterAddress("0x1234567890abcdef1234567890abcdef12345678")
.tapContinue()
// Should navigate to verify screen
let verifyPage = VerifyPage(app: app)
verifyPage.assertVisible()
}
}
UI tests have their own dedicated scheme (VultisigAppUITests) and test plan (VultisigAppUITests.xctestplan), separate from unit tests. This keeps CI fast — unit tests run on the main VultisigApp scheme, UI tests run on VultisigAppUITests.
Prefer the Makefile target (see /make skill) for full runs:
# Run all UI tests (dedicated scheme)
make ui_test
# Override the simulator if needed
make ui_test DESTINATION='platform=iOS Simulator,name=iPhone 16 Pro,OS=18.5'
Drop to raw xcodebuild only when you need -only-testing: filtering. Prepend make generate so newly added test files are in the project:
# Run a specific test class
make generate
cd VultisigApp && xcodebuild test \
-project VultisigApp.xcodeproj \
-scheme VultisigAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \
-only-testing:VultisigAppUITests/HomeScreenTests \
-skipPackagePluginValidation
# Run a specific test method
make generate
cd VultisigApp && xcodebuild test \
-project VultisigApp.xcodeproj \
-scheme VultisigAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \
-only-testing:VultisigAppUITests/HomeScreenTests/testHomeScreenShowsTabs \
-skipPackagePluginValidation
| Scheme | Test Plan | Targets | Purpose |
|---|---|---|---|
VultisigApp | VultisigApp.xctestplan | VultisigAppTests | Unit + integration tests (CI) |
VultisigAppUITests | VultisigAppUITests.xctestplan | VultisigAppUITests | UI tests (separate CI step) |
Follow this checklist when adding UI tests for a screen:
VultisigApp/VultisigApp/Utils/AccessibilityID.swiftenum Swap { ... })static let constants for each interactive/assertable element.accessibilityIdentifier(AccessibilityID.Screen.element) to each tagged elementVultisigAppUITests/Pages/{Screen}Page.swiftapp.buttons[...], app.textFields[...], etc.self or the destination pageassertVisible() and any screen-specific assertionsVultisigAppUITests/Tests/{Screen}Tests.swiftVultisigUITestCaseAlways regenerate the project first — new Swift files aren't in VultisigApp.xcodeproj until xcodegen picks them up.
Filtered runs need raw xcodebuild; full runs go through make ui_test:
make generate
cd VultisigApp && xcodebuild test \
-project VultisigApp.xcodeproj \
-scheme VultisigAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \
-only-testing:VultisigAppUITests/{TestClass} \
-skipPackagePluginValidation
For UI tests to work reliably, the app needs test-mode hooks. Add these checks wrapped in #if DEBUG:
VultisigApp.swift — Check for -UITesting launch argument:
#if DEBUG
private var isUITesting: Bool {
CommandLine.arguments.contains("-UITesting")
}
#endif
AppViewModel.swift — Skip authentication in test mode:
#if DEBUG
if CommandLine.arguments.contains("-skipAuthentication") {
isAuthenticated = true
showSplashView = false
return
}
#endif
Animations — Disable for test stability:
#if DEBUG
if CommandLine.arguments.contains("-disableAnimations") {
UIView.setAnimationsEnabled(false)
}
#endif
| Type | Pattern | Example |
|---|---|---|
| Test classes | {Feature}Tests.swift | HomeScreenTests.swift |
| Page objects | {Screen}Page.swift | HomePage.swift |
| Test methods | test{Behavior} | testNavigateToSettings |
| Accessibility IDs | {screen}.{element} | "home.settingsButton" |
| Helpers | Descriptive name | XCUIElement+Helpers.swift |
| SwiftUI Element | XCUITest Query |
|---|---|
Button | app.buttons[id] |
TextField | app.textFields[id] |
SecureField | app.secureTextFields[id] |
Text | app.staticTexts[id] |
Image | app.images[id] |
Toggle | app.switches[id] |
NavigationLink | app.buttons[id] |
List / ForEach cell | app.cells[id] or app.buttons[id] |
TabView item | app.buttons[id] |
ScrollView | app.scrollViews[id] |
Alert button | app.alerts.buttons["OK"] |
Sheet content | app.sheets.firstMatch |
waitForExistence(timeout:) before interacting — SwiftUI renders asynchronouslytakeScreenshot(name:)) at key checkpoints for debugging failureswaitForExistence or XCTNSPredicateExpectation insteadXCUIDevice.shared.orientation = .landscapeLeft