Loading...
Loading...
API reference: XCUITest. Query for element queries, waiting patterns, Swift 6 @MainActor, assertions, screenshots, launch arguments.
npx skill4agent add vabole/apple-skills xcuitest// Basic test structure
@MainActor
final class MyUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testExample() {
let button = app.buttons["Submit"]
XCTAssertTrue(button.waitForExistence(timeout: 5))
button.tap()
}
}let app = XCUIApplication()
// Launch configuration
app.launchArguments = ["-UITest", "-DisableAnimations"]
app.launchEnvironment["API_URL"] = "https://test.example.com"
// Lifecycle
app.launch() // Start the app
app.terminate() // Stop the app
app.activate() // Bring to foreground
// State checking
app.state == .runningForeground
app.state == .runningBackground
app.state == .notRunninglet element = app.buttons["Submit"]
// Properties
element.exists // Bool - element is in hierarchy
element.isHittable // Bool - element can receive taps
element.isEnabled // Bool - element is enabled
element.isSelected // Bool - element is selected
element.label // String - accessibility label
element.value // Any? - current value
element.identifier // String - accessibility identifier
element.frame // CGRect - frame in screen coordinates
element.elementType // XCUIElement.ElementType// Type-based queries (convenience)
app.buttons // All buttons
app.staticTexts // All text labels
app.textFields // All text inputs
app.secureTextFields // Password fields
app.switches // Toggle switches
app.sliders // Slider controls
app.tables // Table views
app.cells // Table/collection cells
app.scrollViews // Scroll views
app.images // Image views
app.alerts // Alert dialogs
app.sheets // Action sheets
app.navigationBars // Navigation bars
app.tabBars // Tab bars
app.toolbars // Toolbars
// Querying by identifier (subscript)
app.buttons["Submit"]
app.staticTexts["Welcome"]
// Descendants query (any element type)
app.descendants(matching: .any)
app.descendants(matching: .button)
app.descendants(matching: .staticText)
// Chained queries
app.descendants(matching: .any).matching(identifier: "my-id").firstMatch
// Predicate queries
app.buttons.matching(NSPredicate(format: "label CONTAINS[c] 'Save'"))
app.buttons.matching(NSPredicate(format: "identifier == 'submit-btn'"))
app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH 'Error'"))
// Query results
query.count // Number of matches
query.element // Single element (fails if not exactly 1)
query.firstMatch // First matching element
query.element(boundBy: 0) // Element at index
query.allElementsBoundByIndex // Array of all elements// Normalized offset (0,0 = top-left, 1,1 = bottom-right)
let center = element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
let topLeft = element.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
// Absolute offset from normalized point
let point = app.coordinate(withNormalizedOffset: .zero)
.withOffset(CGVector(dx: 100, dy: 200))
// Screen coordinates
let screenCenter = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))element.tap() // Single tap
element.doubleTap() // Double tap
element.twoFingerTap() // Two finger tap (iOS only)
element.tap(withNumberOfTaps: 3, numberOfTouches: 1) // Triple tapelement.press(forDuration: 1.0) // Long press
element.press(forDuration: 0.5, thenDragTo: otherElement) // Press and dragtextField.tap() // Focus first
textField.typeText("Hello") // Type text
textField.clearAndEnterText("New text") // Custom helper needed
// Clear text field
textField.tap()
textField.press(forDuration: 1.0)
app.menuItems["Select All"].tap()
textField.typeText("") // Or use delete keyelement.swipeUp()
element.swipeDown()
element.swipeLeft()
element.swipeRight()
// With velocity (iOS 16+)
element.swipeUp(velocity: .fast)
element.swipeUp(velocity: .slow)// Pull to refresh
let start = cell.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0))
let end = cell.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 6))
start.press(forDuration: 0, thenDragTo: end)
// Custom swipe
let from = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8))
let to = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2))
from.press(forDuration: 0.1, thenDragTo: to)
// Tap at specific point
let point = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
point.tap()element.pinch(withScale: 0.5, velocity: -1) // Pinch in
element.pinch(withScale: 2.0, velocity: 1) // Pinch out
element.rotate(0.5, withVelocity: 1) // Rotate
// Sliders
slider.adjust(toNormalizedSliderPosition: 0.7)
// Pickers
picker.adjust(toPickerWheelValue: "Option 3")// Returns Bool - does not fail test automatically
let exists = element.waitForExistence(timeout: 5)
XCTAssertTrue(exists, "Element did not appear")
// Common pattern
if button.waitForExistence(timeout: 3) {
button.tap()
}// Wait with result handling
let predicate = NSPredicate(format: "exists == true")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: 5)
switch result {
case .completed:
// Element found
case .timedOut:
XCTFail("Element did not appear within timeout")
case .incorrectOrder:
// Multiple expectations fulfilled out of order
case .invertedFulfillment:
// Inverted expectation was fulfilled (unexpected)
case .interrupted:
// Wait was interrupted
@unknown default:
break
}// Native API (Xcode 16+) - preferred
let loadingIndicator = app.activityIndicators["loading"]
XCTAssertTrue(loadingIndicator.waitForNonExistence(withTimeout: 10), "Loading should complete")
// Legacy approach (pre-Xcode 16)
func waitForNonExistence(_ element: XCUIElement, timeout: TimeInterval) -> 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
}// Wait for element to become enabled
let predicate = NSPredicate(format: "isEnabled == true")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: button)
XCTWaiter().wait(for: [expectation], timeout: 5)
// Wait for label to change
let predicate = NSPredicate(format: "label == 'Done'")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: statusLabel)
XCTWaiter().wait(for: [expectation], timeout: 10)let exp1 = XCTNSPredicateExpectation(predicate: pred1, object: element1)
let exp2 = XCTNSPredicateExpectation(predicate: pred2, object: element2)
XCTWaiter().wait(for: [exp1, exp2], timeout: 10, enforceOrder: false)// New KeyPath-based waiting - wait for any property to equal a value
let favoriteButton = app.buttons["Favorite"]
favoriteButton.tap()
// Wait for value property to become true
XCTAssertTrue(
favoriteButton.wait(for: \.value, toEqual: true, timeout: 10),
"Button should show favorited state"
)
// Wait for label to change
XCTAssertTrue(
statusLabel.wait(for: \.label, toEqual: "Complete", timeout: 5),
"Status should update to Complete"
)
// Wait for element to become enabled
XCTAssertTrue(
submitButton.wait(for: \.isEnabled, toEqual: true, timeout: 3),
"Submit button should become enabled"
)wait(for:toEqual:timeout:)truefalseapp.launch()override func setUp() {
super.setUp()
let app = XCUIApplication()
// Reset permissions before launch
app.resetAuthorizationStatus(for: .location)
app.resetAuthorizationStatus(for: .camera)
app.resetAuthorizationStatus(for: .photos)
app.resetAuthorizationStatus(for: .health)
app.launch()
}// Available protected resources
.contacts // Contacts access
.calendar // Calendar access
.reminders // Reminders access
.photos // Photo library access
.microphone // Microphone access
.camera // Camera access
.mediaLibrary // Media library access
.homeKit // HomeKit access
.bluetooth // Bluetooth access
.keyboardNetwork // Network keyboard access
.location // Location services
.health // HealthKit accessfunc handleHealthKitDialog(allow: Bool = false) {
let healthAccessText = app.staticTexts["Health Access"]
if healthAccessText.waitForExistence(timeout: 5) {
// Scroll down to reveal buttons (iOS 26 sheet is scrollable)
let from = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.8))
let to = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.3))
from.press(forDuration: 0.1, thenDragTo: to)
// Tap the appropriate button
let buttonLabel = allow ? "Allow" : "Don't Allow"
let button = app.buttons[buttonLabel]
if button.waitForExistence(timeout: 5) {
button.tap()
} else {
// Fallback: find by partial match
let fallback = app.buttons.matching(
NSPredicate(format: "label CONTAINS[c] '\(buttonLabel)'")
).firstMatch
if fallback.exists { fallback.tap() }
}
}
}override func setUp() {
super.setUp()
// Handle location permission
addUIInterruptionMonitor(withDescription: "Location Permission") { alert -> Bool in
if alert.buttons["Allow While Using App"].exists {
alert.buttons["Allow While Using App"].tap()
return true
}
if alert.buttons["Don't Allow"].exists {
alert.buttons["Don't Allow"].tap()
return true
}
return false
}
// Handle HealthKit permission
addUIInterruptionMonitor(withDescription: "Health Permission") { alert -> Bool in
// Note: HealthKit uses a sheet, not a system alert
// This may not trigger the interruption monitor
return false
}
app.launch()
}
func testWithPermissions() {
// Trigger action that shows permission dialog
app.buttons["Enable Location"].tap()
// IMPORTANT: Must interact with app to trigger the monitor
app.tap()
// Continue test...
}func handleSpringboardAlert(buttonLabel: String) {
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let alertButton = springboard.buttons[buttonLabel]
if alertButton.waitForExistence(timeout: 3) {
alertButton.tap()
}
}
// Usage
handleSpringboardAlert(buttonLabel: "Allow")
handleSpringboardAlert(buttonLabel: "Don't Allow")XCTestCase@MainActorCall to main actor-isolated initializer 'init()' in a synchronous nonisolated context@MainActor
final class MyUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
super.setUp()
app = XCUIApplication()
app.launch()
}
override func tearDown() {
app = nil
super.tearDown()
}
func testSomething() {
// All code runs on main actor
}
}final class MyUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() async throws {
try await super.setUp()
await MainActor.run {
app = XCUIApplication()
app.launch()
}
}
override func tearDown() async throws {
await MainActor.run {
app = nil
}
try await super.tearDown()
}
}@MainActor
final class MyUITests: XCTestCase {
nonisolated var testUserID: String {
ProcessInfo.processInfo.environment["TEST_USER_ID"] ?? "default"
}
}@MainActor
func testAsyncOperation() async throws {
app.buttons["Start"].tap()
// Await async operation
try await Task.sleep(nanoseconds: 1_000_000_000)
XCTAssertTrue(app.staticTexts["Complete"].exists)
}// Screenshot of entire screen
let screenshot = XCUIScreen.main.screenshot()
// Screenshot of specific element
let elementShot = element.screenshot()
// Create attachment
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Login Screen"
attachment.lifetime = .keepAlways // Don't delete after test
add(attachment)override func tearDown() {
if let failureCount = testRun?.failureCount, failureCount > 0 {
let screenshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Failure-\(name)"
attachment.lifetime = .keepAlways
add(attachment)
}
super.tearDown()
}.xcresultxcparsebrew install xcparse
xcparse screenshots /path/to/Test.xcresult /output/directorylet app = XCUIApplication()
// Launch arguments (appear in ProcessInfo.processInfo.arguments)
app.launchArguments = ["-UITest", "-DisableAnimations"]
app.launchArguments.append("-SkipOnboarding")
// Launch environment (appear in ProcessInfo.processInfo.environment)
app.launchEnvironment["API_URL"] = "https://test.example.com"
app.launchEnvironment["TEST_USER_ID"] = "test-user-123"
app.launch() // Must be set before launch!// Check for launch argument
if ProcessInfo.processInfo.arguments.contains("-UITest") {
UIView.setAnimationsEnabled(false)
}
// Read environment variable
if let testUserID = ProcessInfo.processInfo.environment["TEST_USER_ID"] {
UserDefaults.standard.set(testUserID, forKey: "testUserID")
}-// In test
app.launchArguments = ["-myKey", "myValue"]
// In app - reads from UserDefaults
UserDefaults.standard.string(forKey: "myKey") // "myValue"app.launchArguments = [
"-AppleLanguages", "(es)",
"-AppleLocale", "es_ES"
]app.launchArguments = [
"-UIPreferredContentSizeCategoryName",
UIContentSizeCategory.accessibilityExtraExtraExtraLarge.rawValue
]XCTAssertTrue(element.exists)
XCTAssertFalse(element.exists)
XCTAssertEqual(element.label, "Expected")
XCTAssertNotEqual(element.value as? String, "Wrong")
XCTAssertNil(element.value)
XCTAssertNotNil(element.value)XCTAssertTrue(element.exists, "Submit button should be visible")
XCTAssertEqual(element.label, "Done", "Button label should be 'Done' after completion")// Element must exist (fails test if not)
XCTAssertTrue(element.waitForExistence(timeout: 5), "Element not found")
// Element should not exist
XCTAssertFalse(app.alerts["Error"].exists, "Unexpected error alert")
// Element state
XCTAssertTrue(button.isEnabled, "Button should be enabled")
XCTAssertTrue(button.isHittable, "Button should be tappable")override func setUp() {
super.setUp()
// Set up before launching app
addUIInterruptionMonitor(withDescription: "System Alert") { alert -> Bool in
// Handle location permission
if alert.buttons["Allow While Using App"].exists {
alert.buttons["Allow While Using App"].tap()
return true
}
// Handle notification permission
if alert.buttons["Allow"].exists {
alert.buttons["Allow"].tap()
return true
}
// Handle "Don't Allow"
if alert.buttons["Don't Allow"].exists {
alert.buttons["Don't Allow"].tap()
return true
}
return false
}
app.launch()
}
func testWithSystemAlert() {
// Trigger action that shows system alert
app.buttons["Request Permission"].tap()
// IMPORTANT: Must interact with app to trigger handler
app.tap()
// Continue test...
}// For app alerts (not system)
let alert = app.alerts["Confirm Delete"]
if alert.waitForExistence(timeout: 3) {
alert.buttons["Delete"].tap()
}
// For springboard alerts (system)
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let systemAlert = springboard.alerts.firstMatch
if systemAlert.waitForExistence(timeout: 3) {
systemAlert.buttons["Allow"].tap()
}protocol Screen {
var app: XCUIApplication { get }
func waitForScreen(timeout: TimeInterval) -> Bool
}
struct LoginScreen: Screen {
let app: XCUIApplication
var usernameField: XCUIElement { app.textFields["username"] }
var passwordField: XCUIElement { app.secureTextFields["password"] }
var loginButton: XCUIElement { app.buttons["Login"] }
var errorLabel: XCUIElement { app.staticTexts["error-message"] }
func waitForScreen(timeout: TimeInterval = 5) -> Bool {
usernameField.waitForExistence(timeout: timeout)
}
func login(username: String, password: String) -> HomeScreen {
usernameField.tap()
usernameField.typeText(username)
passwordField.tap()
passwordField.typeText(password)
loginButton.tap()
return HomeScreen(app: app)
}
}extension XCUIApplication {
func waitForElement(_ identifier: String, timeout: TimeInterval = 5) -> XCUIElement {
let element = descendants(matching: .any).matching(identifier: identifier).firstMatch
XCTAssertTrue(element.waitForExistence(timeout: timeout),
"Element '\(identifier)' not found within \(timeout)s")
return element
}
func tapTab(_ identifier: String) {
let tab = buttons[identifier]
XCTAssertTrue(tab.waitForExistence(timeout: 5), "Tab '\(identifier)' not found")
tab.tap()
}
}
extension XCUIElement {
func clearAndType(_ text: String) {
guard let currentValue = value as? String, !currentValue.isEmpty else {
tap()
typeText(text)
return
}
tap()
let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: currentValue.count)
typeText(deleteString)
typeText(text)
}
}enum TestTimeout: TimeInterval {
case short = 2
case medium = 5
case long = 10
case networkLoad = 30
}
// Usage
button.waitForExistence(timeout: TestTimeout.medium.rawValue)print(app.debugDescription)waitForExistencesleepcontinueAfterFailure = false@MainActornonisolatedattachment.lifetime = .keepAlways.xcresultxcparseresetAuthorizationStatus(for:)app.launch()addUIInterruptionMonitorwait(for:toEqual:timeout:)waitForNonExistence(withTimeout:).mdSKILL.mdsosumi.ai/documentation/xcuiautomation/xcuielementhttps://sosumi.ai/documentation/xcuiautomation/xcuielement