Files
Sportstime/SportsTimeUITests/Framework/BaseUITestCase.swift

158 lines
5.1 KiB
Swift

//
// BaseUITestCase.swift
// SportsTimeUITests
//
// Base class for all UI tests. Provides launch configuration,
// screenshot-on-failure, and centralized wait helpers.
//
import XCTest
// MARK: - Base Test Case
class BaseUITestCase: XCTestCase {
/// The application under test. Configured in setUp.
var app: XCUIApplication!
/// Standard timeout for element existence checks.
static let defaultTimeout: TimeInterval = 15
/// Short timeout for elements expected to appear quickly.
static let shortTimeout: TimeInterval = 5
/// Extended timeout for bootstrap / planning engine operations.
static let longTimeout: TimeInterval = 30
override func setUpWithError() throws {
continueAfterFailure = false
// Keep UI tests in a consistent orientation to avoid layout-dependent flakiness.
XCUIDevice.shared.orientation = .portrait
app = XCUIApplication()
app.launchArguments = [
"--ui-testing",
"--disable-animations",
"--reset-state"
]
app.launch()
}
override func tearDownWithError() throws {
// Capture a screenshot on test failure for post-mortem debugging.
if let failure = testRun, failure.failureCount > 0 {
let screenshot = XCTAttachment(screenshot: app.screenshot())
screenshot.name = "Failure-\(name)"
screenshot.lifetime = .keepAlways
add(screenshot)
}
app = nil
}
// MARK: - Screenshot Helpers
/// Captures a named screenshot attached to the test report.
func captureScreenshot(named name: String) {
let screenshot = XCTAttachment(screenshot: app.screenshot())
screenshot.name = name
screenshot.lifetime = .keepAlways
add(screenshot)
}
}
// MARK: - Wait Helpers
extension XCUIElement {
/// Waits until the element exists, failing with a descriptive message.
@discardableResult
func waitForExistenceOrFail(
timeout: TimeInterval = BaseUITestCase.defaultTimeout,
_ message: String? = nil,
file: StaticString = #filePath,
line: UInt = #line
) -> XCUIElement {
let msg = message ?? "Expected \(self) to exist within \(timeout)s"
XCTAssertTrue(waitForExistence(timeout: timeout), msg, file: file, line: line)
return self
}
/// Waits until the element exists AND is hittable.
@discardableResult
func waitUntilHittable(
timeout: TimeInterval = BaseUITestCase.defaultTimeout,
_ message: String? = nil,
file: StaticString = #filePath,
line: UInt = #line
) -> XCUIElement {
waitForExistenceOrFail(timeout: timeout, message, file: file, line: line)
let predicate = NSPredicate(format: "isHittable == true")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self)
let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
let msg = message ?? "Expected \(self) to be hittable within \(timeout)s"
XCTAssertEqual(result, .completed, msg, file: file, line: line)
return self
}
/// Waits until the element no longer exists.
func waitForNonExistence(
timeout: TimeInterval = BaseUITestCase.defaultTimeout,
_ message: String? = nil,
file: StaticString = #filePath,
line: UInt = #line
) {
let predicate = NSPredicate(format: "exists == false")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self)
let result = XCTWaiter.wait(for: [expectation], timeout: timeout)
let msg = message ?? "Expected \(self) to disappear within \(timeout)s"
XCTAssertEqual(result, .completed, msg, file: file, line: line)
}
/// Scrolls a scroll view until this element is hittable, or times out.
@discardableResult
func scrollIntoView(
in scrollView: XCUIElement,
direction: ScrollDirection = .down,
maxScrolls: Int = 10,
file: StaticString = #filePath,
line: UInt = #line
) -> XCUIElement {
if exists && isHittable { return self }
func attemptScroll(_ scrollDirection: ScrollDirection, attempts: Int) -> Bool {
var remaining = attempts
while (!exists || !isHittable) && remaining > 0 {
switch scrollDirection {
case .down:
scrollView.swipeUp(velocity: .slow)
case .up:
scrollView.swipeDown(velocity: .slow)
}
remaining -= 1
}
return exists && isHittable
}
if attemptScroll(direction, attempts: maxScrolls) {
return self
}
let reverseDirection: ScrollDirection = direction == .down ? .up : .down
if attemptScroll(reverseDirection, attempts: maxScrolls) {
return self
}
XCTFail(
"Could not scroll \(self) into view after \(maxScrolls) scrolls in either direction",
file: file,
line: line
)
return self
}
}
enum ScrollDirection: Equatable {
case up, down
}