xcuitest-suite
Authors XCUIest UI tests for iOS / iPadOS / tvOS and macOS desktop apps - uses the three-class XCUIApplication / XCUIElement / XCUIElementQuery pattern, sets accessibility identifiers on production code, runs via `xcodebuild test` with destination, parses the `xcresult` bundle. The macOS desktop delta (platform=macOS destination flags, TCC privacy-permission resets, per-device performance baselines) is in references/macos.md. Use when an iOS or macOS app needs UI tests in Apple's first-party framework (no external runtime; native to Xcode).
Install with skills.sh (any agent)
npx skills add testland/qa --skill xcuitest-suitexcuitest-suite
Overview
XCUITest is Apple's first-party UI testing framework, integrated with Xcode and built on the XCTest framework (xcui-fundamentals (opens in new window)).
"UI testing in Xcode is built on three fundamental classes: XCUIApplication, XCUIElement, XCUIElementQuery." (xcui-fundamentals (opens in new window))
The general pattern: Query → Synthesize → Assert.
"1. Query - Use XCUIElementQuery to find an XCUIElement 2. Synthesize - Synthesize an event and send it to the XCUIElement 3. Assert - Use an assertion to compare the element's state against expected reference state" (xcui-fundamentals (opens in new window))
When to use
If the app is React Native, see detox-testing. For cross-platform Appium-style coverage, see appium-testing.
macOS desktop apps
The same framework tests macOS desktop apps (AppKit, SwiftUI, Catalyst) - every step below carries over. The macOS delta - -destination 'platform=macOS' flags, TCC privacy-permission resets (tccutil reset), per-device performance baselines, and headless-runner constraints - is in references/macos.md.
Step 1 - Add a UI test target
In Xcode: File → New → Target → UI Testing Bundle. The template generates a .swift test class with a default setUp:
import XCTest
final class CartUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false // critical (default)
XCUIApplication().launch()
}
}Per xcui-fundamentals (opens in new window):
"
continueAfterFailure = NOis the default (recommended) because UI test steps are dependent on previous steps."
Step 2 - Set accessibility identifiers in production code
XCUITest finds elements via the accessibility tree. Hard-coded labels / text are brittle - set explicit identifiers in the SUT:
// Production code
let placeOrderButton = UIButton()
placeOrderButton.accessibilityIdentifier = "place-order-button"Then in tests:
let app = XCUIApplication()
app.buttons["place-order-button"].tap()accessibilityIdentifier (not accessibilityLabel) - labels are user-facing and translated; identifiers are dev-only and stable.
Step 3 - Query patterns
let app = XCUIApplication()
// By accessibility identifier (preferred)
app.buttons["place-order-button"].tap()
app.textFields["email-field"].typeText("user@example.com")
// By type + text
app.staticTexts["Welcome"].swipeUp()
// Predicate-based query
let cells = app.tables.cells.matching(
NSPredicate(format: "label CONTAINS[c] 'BOOK-001'")
)
cells.element(boundBy: 0).tap()
// Wait for an element
XCTAssert(app.staticTexts["Order confirmed"].waitForExistence(timeout: 5))Step 4 - Synthesize events
// Tap
app.buttons["submit"].tap()
// Type
app.textFields["email"].typeText("user@example.com")
// Swipe / drag
app.cells.element(boundBy: 0).swipeLeft()
// Pinch
app.images["map"].pinch(withScale: 2.0, velocity: 1.0)
// Press for duration (long-press)
app.buttons["context-menu"].press(forDuration: 1.0)
// System keyboard return
app.keyboards.buttons["return"].tap()Step 5 - Assert state
let confirmation = app.staticTexts["Order confirmed"]
XCTAssertTrue(confirmation.exists)
XCTAssertEqual(app.staticTexts["order-id"].label, "ORD-12345")
XCTAssertTrue(app.buttons["submit"].isEnabled)exists returns immediately; waitForExistence(timeout:) waits up to N seconds. Use waitForExistence for any post-tap state that depends on async work.
Step 6 - Run
# From the project directory:
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
-resultBundlePath TestResults.xcresultPer-destination patterns:
| Use | Destination string |
|---|---|
| Latest sim | 'platform=iOS Simulator,name=iPhone 15,OS=latest' |
| Specific OS | 'platform=iOS Simulator,name=iPhone 14,OS=17.4' |
| Connected device | 'platform=iOS,id=<UDID>' |
| Multi-device matrix | Pass -destination multiple times. |
Step 7 - Parse .xcresult
The result bundle is binary. Extract via xcresulttool:
xcrun xcresulttool get test-results summary --path TestResults.xcresult --format json > results.jsonThen use junit-xml-analysis (in the qa-test-reporting plugin) on the JUnit-equivalent shape (or directly on the JSON for richer data).
Step 8 - CI integration
# .github/workflows/ios-tests.yml
jobs:
ui-tests:
runs-on: macos-15
steps:
- uses: actions/checkout@v5
- run: xcodebuild test -project MyApp.xcodeproj -scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
-resultBundlePath TestResults.xcresult
- uses: actions/upload-artifact@v4
if: always()
with:
name: xcresult
path: TestResults.xcresultGitHub Actions provides macos-15 runners with Xcode pre-installed.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Querying by accessibilityLabel | Labels are translated; tests fail in non-English locales. | Use accessibilityIdentifier (Step 2-3). |
Thread.sleep for async waits | Flaky on slow runners; slow on fast. | waitForExistence(timeout:) (Step 5). |
continueAfterFailure = true | One failure cascades into N false positives. | Default false (Step 1). |
| Hard-coded text in queries | Copy changes break tests. | accessibility identifiers (Step 2). |
| Running against wrong destination ("latest" without pin) | Test passes on Xcode 16, fails on 15; reproducibility issues. | Pin OS version explicitly in CI. |
Skipping xcresult upload | Failure debugging needs the screenshots / videos in the bundle. | Always upload (Step 8). |
Limitations
References
XCUITest on macOS desktop apps - the macOS delta
View source (opens in new window)XCUITest on macOS desktop apps - the macOS delta
XCTest UI testing for macOS desktop apps (AppKit, SwiftUI, Catalyst) uses the same three-class XCUIApplication / XCUIElement / XCUIElementQuery pattern, the same accessibility-identifier locator strategy, and the same XCTAssert* macros as the iOS workflow in SKILL.md (opens in new window). Per Apple's Testing with Xcode - UI Testing chapter (opens in new window), "UI Testing in Xcode rests on two core technologies: the XCTest framework and Accessibility." This reference covers only what differs on macOS.
Destination flags
There is no simulator: -destination targets the host Mac directly.
# Run the full test bundle on the host Mac
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=macOS' \
-resultBundlePath build/result.xcresult
# Run a single test class / method
xcodebuild test -project MyApp.xcodeproj -scheme MyApp \
-destination 'platform=macOS' \
-only-testing:MyAppUITests/CheckoutUITests/testCheckoutHappyPathSetting identifiers in macOS app code
// SwiftUI
Button("Sign In") { … }
.accessibilityIdentifier("signInButton")
// AppKit
signInButton.setAccessibilityIdentifier("signInButton")TCC privacy permissions
macOS gates Automation, Accessibility, and Screen Recording behind TCC (Transparency, Consent, and Control) consent prompts. The prompts are out-of-process - an XCUITest cannot click through them. Reset consent state before launch (per Jamf's TCC reset guide (opens in new window)), or pre-grant via an MDM PPPC (Privacy Preferences Policy Control) profile on managed CI fleets:
for s in Automation Accessibility ScreenCapture; do
tccutil reset "$s" "$BUNDLE_ID" || true
doneCI on macOS runners
Hosted GitHub macOS runners are interactive sessions, so XCUIApplication launches need no extra display setup. Self-hosted headless Macs need an attached console or VNC session - XCTest UI cannot run under launchd alone.
# .github/workflows/macos-xctest.yml
jobs:
test:
runs-on: macos-14 # Apple Silicon
steps:
- uses: actions/checkout@v5
- uses: maxim-lobanov/setup-xcode@v1
with: { xcode-version: '15.4' }
- run: |
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=macOS' \
-resultBundlePath build/result.xcresult \
-enableCodeCoverage YES
- uses: actions/upload-artifact@v4
if: always()
with:
name: xcresult
path: build/result.xcresultResult parsing is identical to iOS (Step 7 in SKILL.md (opens in new window)): xcrun xcresulttool for JSON summaries and attachments; the open-source xcresultparser converts .xcresult to JUnit XML for junit-xml-analysis.
macOS-specific gotchas
Related skills
appium-testing
Runs and repairs mobile UI suites driven by Appium - sessions that stop launching after an Appium server upgrade, an iOS or Android half switched off in CI because the simulator or emulator will not boot, unstable waits around one-time codes and other out-of-app steps, and locators that break on every redesign. Covers the WebDriver protocol, driver choice per platform (XCUITest for iOS, UiAutomator2 / Espresso for Android, Mac2 for macOS, Windows for desktop), `desiredCapabilities`, client bindings in JS / Python / Java / Ruby / .NET, and running against simulators, emulators, and device farms. Use when a mobile suite fails, flakes, or only runs on one platform, or when one suite must cover both iOS and Android.
detox-testing
Authors React Native E2E tests with Detox (Wix) - gray-box architecture (runs in-process with the app), `element(by.id|by.text|by.label)` matchers, `waitFor()` for explicit sync beyond Detox's automatic async tracking, Jest runner. Use when the app is React Native and speed matters. For Flutter use flutter-testing; for black-box cross-platform use appium-testing; for YAML-declarative flows use maestro-flows; for non-RN native use xcuitest-suite or espresso-suite.
espresso-suite
Authors Espresso UI tests for Android - uses `onView(withId(...)).perform(...).check(matches(...))`, leans on Espresso's automatic synchronization (no `Thread.sleep`), wires `IdlingResource` for app-specific async, runs via `./gradlew connectedAndroidTest` and parses the JUnit XML output. Use when an Android app needs UI tests in Google's first-party framework.
flutter-testing
Authors Flutter tests across the three-layer pyramid - unit (`flutter test`), widget (`testWidgets` + `WidgetTester`), integration (`integration_test` on simulator/emulator/device). Picks the right layer per change, mocks via `mockito` + `build_runner`, LCOV coverage, CI with the Flutter Action. Use when the app is Flutter and the team wants its first-party stack. For React Native use detox-testing; for black-box cross-platform use appium-testing; for YAML-declarative flows use maestro-flows.
maestro-flows
Authors Maestro YAML flow files (`.maestro/*.yaml`) for mobile + web UI automation: declarative `tapOn`, `inputText`, `assertVisible`, `swipe`, supported targets (iOS, Android, Flutter, React Native, web), nested flow imports, JavaScript hooks for complex conditions. Use when the team has already chosen Maestro, is coming from an existing `.maestro/` directory, or explicitly wants YAML-declarative tests readable by non-engineers without a compile step. For framework selection or authoring tests in XCUITest / Espresso / Detox / Appium / Flutter, use a mobile driver-selection or per-flow mobile test-authoring step instead.
mobile-a11y-test-author
Authors native mobile accessibility tests covering iOS (Accessibility Inspector, XCUITest `performAccessibilityAudit()` introduced in iOS 17, VoiceOver label/trait/hint verification) and Android (Espresso `AccessibilityChecks.enable()`, Accessibility Scanner, TalkBack traversal, `contentDescription` labelling) with WCAG-aligned checks for element labels, 44pt/48dp touch targets, contrast ratios, and focus order. Use when an iOS or Android app needs automated and manual accessibility test coverage beyond what `xcuitest-suite` or `espresso-suite` provide.
mobile-device-matrix-toolkit
Dispatches mobile UI test runs across a 3-tier device matrix (smoke per-PR, regression per-merge, full farm at release) to control CI cost: generates per-target Appium capability configs from a central YAML, parallelises via GitHub Actions matrix strategy, and aggregates JUnit XML into a cross-device pass/fail table. Use when deciding which iOS / Android devices and OS versions to run tests on and at which stage (smoke / regression / full farm), not how to configure a specific test framework (for that, use xcuitest-suite, espresso-suite, etc.).