Files
Reflect/Tests iOS/Helpers/BaseUITestCase.swift
Trey T d97db4910e Rewrite all UI tests following fail-fast TEST_RULES patterns
Rewrote 60+ test files to follow honeydue-style test guidelines:
- defaultTimeout=2s, navigationTimeout=5s — fail fast, no long waits
- No coordinate taps (except onboarding paged TabView swipes)
- No sleep(), no retry loops
- No guard...else { return } silent passes — XCTFail everywhere
- All elements by accessibility ID via UITestID constants
- Screen objects for all navigation/actions/assertions
- One logical assertion per test method

Added missing accessibility identifiers to app views:
- MonthView.swift: added AccessibilityID.MonthView.grid to ScrollView
- YearView.swift: added AccessibilityID.YearView.heatmap to ScrollView

Framework rewrites:
- BaseUITestCase: added session ID, localeArguments, extraLaunchArguments
- WaitHelpers: waitForExistenceOrFail, waitUntilHittableOrFail,
  waitForNonExistence, scrollIntoView, forceTap
- All 7 screen objects rewritten with fail-fast semantics
- TEST_RULES.md added with non-negotiable rules

Known remaining issues:
- OnboardingTests: paged TabView swipes unreliable on iOS 26 simulator
- SettingsLegalLinksTests: EULA/Privacy buttons too deep in DEBUG scroll
- Customization horizontal picker scrolling needs further tuning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:00:30 -05:00

131 lines
4.3 KiB
Swift

//
// BaseUITestCase.swift
// Tests iOS
//
// Base class for all UI tests. Handles launch arguments,
// parallel test isolation, and screenshot capture on failure.
//
import XCTest
class BaseUITestCase: XCTestCase {
var app: XCUIApplication!
/// Element on current screen if it's not there in 2s, the app is broken
let defaultTimeout: TimeInterval = 2
/// Screen transitions, tab switches
let navigationTimeout: TimeInterval = 5
// MARK: - Parallel Test Isolation
/// Unique session ID for this test class instance.
/// Passed to the app via environment so each parallel runner gets
/// its own UserDefaults suite and in-memory SwiftData container.
private(set) var testSessionID: String = UUID().uuidString
// MARK: - Configuration (override in subclasses)
/// Fixture to seed. Override to use a specific data set.
var seedFixture: String? { nil }
/// Whether to bypass the subscription paywall. Default: true.
var bypassSubscription: Bool { true }
/// Whether to skip onboarding. Default: true.
var skipOnboarding: Bool { true }
/// Whether to force the trial to be expired. Default: false.
var expireTrial: Bool { false }
/// Override to change the test locale/language. Default: English (US).
var localeArguments: [String] { ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"] }
/// Extra launch arguments (accessibility sizes, reduce motion, etc.).
var extraLaunchArguments: [String] { [] }
// MARK: - Lifecycle
override func setUp() {
super.setUp()
continueAfterFailure = false
app = launchApp(resetState: true)
}
override func tearDown() {
if let failure = testRun?.failureCount, failure > 0 {
captureScreenshot(name: "FAILURE-\(name)")
}
app = nil
super.tearDown()
}
// MARK: - Launch Configuration
private func buildLaunchArguments(resetState: Bool) -> [String] {
var args = ["--ui-testing", "--disable-animations"]
args.append(contentsOf: localeArguments)
if resetState { args.append("--reset-state") }
if bypassSubscription { args.append("--bypass-subscription") }
if skipOnboarding { args.append("--skip-onboarding") }
if expireTrial { args.append("--expire-trial") }
args.append(contentsOf: extraLaunchArguments)
return args
}
private func buildLaunchEnvironment() -> [String: String] {
var env = [String: String]()
env["UI_TEST_SESSION_ID"] = testSessionID
if let fixture = seedFixture { env["UI_TEST_FIXTURE"] = fixture }
return env
}
// MARK: - Screenshots
func captureScreenshot(name: String) {
let screenshot = XCTAttachment(screenshot: app.screenshot())
screenshot.name = name
screenshot.lifetime = .keepAlways
add(screenshot)
}
// MARK: - Launch Helpers
@discardableResult
func launchApp(resetState: Bool) -> XCUIApplication {
let application = XCUIApplication()
application.launchArguments = buildLaunchArguments(resetState: resetState)
application.launchEnvironment = buildLaunchEnvironment()
application.launch()
return application
}
/// Relaunch with a different bypass setting, preserving session ID.
@discardableResult
func relaunchApp(resetState: Bool, bypassSubscription overrideBypass: Bool) -> XCUIApplication {
app.terminate()
let application = XCUIApplication()
var args = ["--ui-testing", "--disable-animations"]
args.append(contentsOf: localeArguments)
if resetState { args.append("--reset-state") }
if overrideBypass { args.append("--bypass-subscription") }
if skipOnboarding { args.append("--skip-onboarding") }
if expireTrial { args.append("--expire-trial") }
args.append(contentsOf: extraLaunchArguments)
application.launchArguments = args
application.launchEnvironment = buildLaunchEnvironment()
application.launch()
app = application
return application
}
@discardableResult
func relaunchPreservingState() -> XCUIApplication {
app.terminate()
let relaunched = launchApp(resetState: false)
app = relaunched
return relaunched
}
}