feat: add XCUITest suite with 10 critical flow tests and QA test plan
Add comprehensive UI test infrastructure with Page Object pattern, accessibility identifiers, UI test mode (--ui-testing, --reset-state, --disable-animations), and 10 passing tests covering app launch, tab navigation, trip wizard, trip saving, settings, schedule, and accessibility at XXXL Dynamic Type. Also adds a 229-case QA test plan Excel workbook for manual QA handoff. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
139
SportsTimeUITests/Framework/BaseUITestCase.swift
Normal file
139
SportsTimeUITests/Framework/BaseUITestCase.swift
Normal file
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
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 {
|
||||
var scrollsRemaining = maxScrolls
|
||||
while !exists || !isHittable {
|
||||
guard scrollsRemaining > 0 else {
|
||||
XCTFail("Could not scroll \(self) into view after \(maxScrolls) scrolls",
|
||||
file: file, line: line)
|
||||
return self
|
||||
}
|
||||
switch direction {
|
||||
case .down:
|
||||
scrollView.swipeUp(velocity: .slow)
|
||||
case .up:
|
||||
scrollView.swipeDown(velocity: .slow)
|
||||
}
|
||||
scrollsRemaining -= 1
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
enum ScrollDirection {
|
||||
case up, down
|
||||
}
|
||||
Reference in New Issue
Block a user