swift-testing/Sources/LilithTesting/BaseUITestCase.swift
Lilith 6f435cde6a chore(workflows): 🔧 Update Swift workflow files in version control
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-16 02:43:12 -08:00

207 lines
5.5 KiB
Swift
Executable file

//
// BaseUITestCase.swift
// iOS Foundations
//
// Base class for UI tests with common utilities
//
import XCTest
/// Base class for UI test cases
///
/// Provides common setup, utilities, and helpers for UI testing.
/// Extend this class for your UI test cases to get consistent behavior.
///
/// ## Usage
///
/// ```swift
/// final class OnboardingUITests: BaseUITestCase {
///
/// func testSignUpFlow() {
/// // Launch arguments are already configured
/// let signUpButton = app.buttons["SignUpButton"]
/// XCTAssertTrue(waitForElement(signUpButton))
///
/// signUpButton.tap()
///
/// let emailField = app.textFields["EmailField"]
/// emailField.tap()
/// emailField.typeText("user@example.com")
///
/// takeScreenshot(name: "SignUpForm")
/// }
/// }
/// ```
///
open class BaseUITestCase: XCTestCase {
// MARK: - Properties
/// The application under test
public var app: XCUIApplication!
/// Default timeout for element waits
public var defaultTimeout: TimeInterval = 5.0
// MARK: - Setup & Teardown
open override func setUp() {
super.setUp()
// Don't stop on failures - continue test to gather more info
continueAfterFailure = false
// Initialize app
app = XCUIApplication()
// Configure test mode
configureTestMode()
// Launch app
app.launch()
}
open override func tearDown() {
// Take screenshot on failure
if let testRun = testRun,
testRun.failureCount > 0 {
takeScreenshot(name: "FAILURE-\(name)")
}
app = nil
super.tearDown()
}
// MARK: - Configuration
/// Configure app for testing
///
/// Override this to add custom launch arguments or environment variables.
///
/// ## Example
///
/// ```swift
/// override func configureTestMode() {
/// super.configureTestMode()
/// app.launchArguments += ["--skip-onboarding"]
/// app.launchEnvironment["API_URL"] = "https://staging.api.com"
/// }
/// ```
///
open func configureTestMode() {
app.launchArguments = ["--uitesting"]
}
// MARK: - Element Waiting
/// Wait for an element to exist
///
/// - Parameters:
/// - element: The element to wait for
/// - timeout: Maximum time to wait (defaults to defaultTimeout)
///
/// - Returns: true if element exists within timeout, false otherwise
///
@discardableResult
public func waitForElement(
_ element: XCUIElement,
timeout: TimeInterval? = nil
) -> Bool {
let timeoutValue = timeout ?? defaultTimeout
return element.waitForExistence(timeout: timeoutValue)
}
/// Wait for an element to disappear
///
/// - Parameters:
/// - element: The element to wait for
/// - timeout: Maximum time to wait
///
/// - Returns: true if element disappeared within timeout, false otherwise
///
@discardableResult
public func waitForElementToDisappear(
_ element: XCUIElement,
timeout: TimeInterval? = nil
) -> Bool {
let timeoutValue = timeout ?? defaultTimeout
let predicate = NSPredicate(format: "exists == false")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: timeoutValue)
return result == .completed
}
// MARK: - Screenshots
/// Take a screenshot with a descriptive name
///
/// Screenshots are attached to the test results and can be viewed in Xcode.
///
/// - Parameter name: Descriptive name for the screenshot
///
public func takeScreenshot(name: String) {
let screenshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
// MARK: - Common Actions
/// Tap an element and wait for it to exist first
///
/// - Parameters:
/// - element: The element to tap
/// - timeout: Maximum time to wait for element
///
/// - Returns: true if tap succeeded, false if element didn't appear
///
@discardableResult
public func tapWhenReady(
_ element: XCUIElement,
timeout: TimeInterval? = nil
) -> Bool {
guard waitForElement(element, timeout: timeout) else {
return false
}
element.tap()
return true
}
/// Type text into a field after waiting for it
///
/// - Parameters:
/// - text: Text to type
/// - element: The text field element
/// - timeout: Maximum time to wait for element
///
/// - Returns: true if typing succeeded, false if element didn't appear
///
@discardableResult
public func typeText(
_ text: String,
into element: XCUIElement,
timeout: TimeInterval? = nil
) -> Bool {
guard waitForElement(element, timeout: timeout) else {
return false
}
element.tap()
element.typeText(text)
return true
}
/// Dismiss keyboard if present
public func dismissKeyboard() {
// Tap toolbar Done button if available
if app.toolbars.buttons["Done"].exists {
app.toolbars.buttons["Done"].tap()
return
}
// Or tap outside keyboard area
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.1)).tap()
}
}