158 lines
5.1 KiB
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
|
|
}
|