This skill should be used when the user asks to "write UI tests", "add XCUITest", "test accessibility", "automate macOS UI", or needs guidance on XCTest patterns, accessibility identifiers, keyboard...
Comprehensive patterns for testing macOS SwiftUI apps using XCTest and XCUITest.
@Test): Model logic, algorithms, pure functionsXCTestCase): User interactions, menu items, keyboard shortcuts// Unit/Integration - Use Swift Testing
import Testing
@Test func confidencePropagation() {
// Fast, isolated tests
}
// UI Automation - Use XCTest (required for XCUIApplication)
import XCTest
class MyAppUITests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
}
CRITICAL: Every testable element needs an accessibility identifier.
// SwiftUI View
Canvas { ... }
.accessibilityIdentifier("graphCanvas")
Button("Save") { ... }
.accessibilityIdentifier("saveButton")
// Node in Canvas - use node ID
.accessibilityIdentifier("node-\(node.id)")
"graphCanvas""node-{id}""edge-{id}""inspector-document", "inspector-element", etc."toolbar-{action}"func testNewEntityMenuItem() throws {
let menuBar = app.menuBars
menuBar.menuBarItems["Entity"].click()
let newEntity = menuBar.menuItems["New Entity"]
XCTAssertTrue(newEntity.waitForExistence(timeout: 2))
// Actually click it and verify behavior
newEntity.click()
// Verify a node was created (check inspector or canvas)
let elementInspector = app.groups["inspector-element"]
XCTAssertTrue(elementInspector.waitForExistence(timeout: 2))
}
func testNewEntityShortcut() throws {
// Create new document first
app.typeKey("n", modifierFlags: .command)
sleep(1)
// Get node count before
let canvas = app.otherElements["graphCanvas"]
// Press Cmd+E for new entity
app.typeKey("e", modifierFlags: .command)
sleep(1)
// Verify entity was created via inspector or element count
let elementInspector = app.groups["inspector-element"]
XCTAssertTrue(elementInspector.exists, "Element inspector should show selected entity")
}
func testDragNodeOnCanvas() throws {
let canvas = app.otherElements["graphCanvas"]
XCTAssertTrue(canvas.waitForExistence(timeout: 5))
// Create a node first
app.typeKey("e", modifierFlags: .command)
sleep(1)
// Drag from center to offset position
let startPoint = canvas.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
let endPoint = startPoint.withOffset(CGVector(dx: 100, dy: 50))
startPoint.press(forDuration: 0.1, thenDragTo: endPoint)
}
func testCreateEdgeByDrag() throws {
let canvas = app.otherElements["graphCanvas"]
// Create two nodes
app.typeKey("e", modifierFlags: .command)
sleep(1)
app.typeKey("e", modifierFlags: .command)
sleep(1)
// Option+drag from first node to second (edge creation)
let node1 = app.otherElements["node-1"] // needs accessibilityIdentifier
let node2 = app.otherElements["node-2"]
let start = node1.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
let end = node2.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
// Hold Option while dragging
start.press(forDuration: 0.1, thenDragTo: end, withVelocity: .default, thenHoldForDuration: 0.1)
}
func testDocumentInspectorFields() throws {
// Cmd+1 to show Document Inspector
app.typeKey("1", modifierFlags: .command)
sleep(1)
let titleField = app.textFields["document-title"]
XCTAssertTrue(titleField.waitForExistence(timeout: 2))
// Edit title
titleField.click()
titleField.typeText("My Document")
// Verify it took
XCTAssertEqual(titleField.value as? String, "My Document")
}
func testUndoRedoEntity() throws {
// Create entity
app.typeKey("e", modifierFlags: .command)
sleep(1)
// Verify entity exists
let inspector = app.groups["inspector-element"]
XCTAssertTrue(inspector.exists)
// Undo
app.typeKey("z", modifierFlags: .command)
sleep(1)
// Entity should be gone
XCTAssertFalse(inspector.exists, "Entity should be removed after undo")
// Redo
app.typeKey("z", modifierFlags: [.command, .shift])
sleep(1)
// Entity should be back
XCTAssertTrue(inspector.waitForExistence(timeout: 2), "Entity should return after redo")
}
func testCopyPasteEntity() throws {
// Create and select entity
app.typeKey("e", modifierFlags: .command)
sleep(1)
// Copy
app.typeKey("c", modifierFlags: .command)
sleep(1)
// Paste
app.typeKey("v", modifierFlags: .command)
sleep(1)
// Should now have 2 nodes
// Verify via graph state or UI element count
}
extension XCUIElement {
/// Wait for element and tap
func waitAndTap(timeout: TimeInterval = 5) {
XCTAssertTrue(waitForExistence(timeout: timeout))
tap()
}
/// Clear text field
func clearAndType(_ text: String) {
guard let stringValue = value as? String else { return }
tap()
let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count)
typeText(deleteString)
typeText(text)
}
}
# Generate Xcode project
xcodegen generate
# Run all UI tests
xcodebuild test -scheme MyApp -destination 'platform=macOS' -only-testing MyAppUITests
# Run specific test
xcodebuild test -scheme MyApp -destination 'platform=macOS' -only-testing MyAppUITests/MyAppUITests/testNewEntityMenuItem
# Or use script
./scripts/run-ui-tests.sh --test testNewEntityMenuItem
When implementing a new feature, ensure: