Enable parallel UI test execution via per-session data isolation
Each test class now gets a unique session ID (UUID) passed to the app via UI_TEST_SESSION_ID environment variable. The app uses this to: - Route GroupUserDefaults to a session-specific UserDefaults suite, preventing tests from clobbering each other's AppStorage state - Create an in-memory SwiftData container instead of the shared on-disk App Group store, eliminating SQLite contention Refactored 8 test classes that bypassed BaseUITestCase.setUp() with custom launch args — they now use overridable `localeArguments` and `extraLaunchArguments` properties, keeping session ID injection centralized. Added `relaunchApp(resetState:bypassSubscription:)` to BaseUITestCase for tests that need mid-test relaunch with different subscription state. Includes a ParallelUITests.xctestplan with class-level parallelism enabled and random execution ordering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,25 +10,8 @@ import XCTest
|
||||
final class AccessibilityTextSizeTests: BaseUITestCase {
|
||||
override var seedFixture: String? { "single_mood" }
|
||||
override var bypassSubscription: Bool { true }
|
||||
|
||||
override func setUp() {
|
||||
// Do NOT call super — we need custom content size launch args
|
||||
continueAfterFailure = false
|
||||
|
||||
let application = XCUIApplication()
|
||||
var args: [String] = [
|
||||
"--ui-testing", "--disable-animations",
|
||||
"--reset-state",
|
||||
"--bypass-subscription",
|
||||
"--skip-onboarding",
|
||||
"-AppleLanguages", "(en)",
|
||||
"-AppleLocale", "en_US",
|
||||
"-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityXXL"
|
||||
]
|
||||
application.launchArguments = args
|
||||
application.launchEnvironment = ["UI_TEST_FIXTURE": "single_mood"]
|
||||
application.launch()
|
||||
app = application
|
||||
override var extraLaunchArguments: [String] {
|
||||
["-UIPreferredContentSizeCategoryName", "UICTContentSizeCategoryAccessibilityXXL"]
|
||||
}
|
||||
|
||||
/// TC-142: App launches and is navigable at largest accessibility text size.
|
||||
|
||||
@@ -10,25 +10,7 @@ import XCTest
|
||||
final class DateLocaleTests: BaseUITestCase {
|
||||
override var seedFixture: String? { "week_of_moods" }
|
||||
override var bypassSubscription: Bool { true }
|
||||
|
||||
override func setUp() {
|
||||
// Do NOT call super — we need custom locale launch args
|
||||
continueAfterFailure = false
|
||||
|
||||
let application = XCUIApplication()
|
||||
let args: [String] = [
|
||||
"--ui-testing", "--disable-animations",
|
||||
"--reset-state",
|
||||
"--bypass-subscription",
|
||||
"--skip-onboarding",
|
||||
"-AppleLanguages", "(de)",
|
||||
"-AppleLocale", "de_DE"
|
||||
]
|
||||
application.launchArguments = args
|
||||
application.launchEnvironment = ["UI_TEST_FIXTURE": "week_of_moods"]
|
||||
application.launch()
|
||||
app = application
|
||||
}
|
||||
override var localeArguments: [String] { ["-AppleLanguages", "(de)", "-AppleLocale", "de_DE"] }
|
||||
|
||||
/// TC-139: German locale displays German month/weekday names.
|
||||
func testGermanLocale_DateFormattingMatchesLocale() {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// Tests iOS
|
||||
//
|
||||
// Base class for all UI tests. Handles launch arguments,
|
||||
// state reset, and screenshot capture on failure.
|
||||
// state reset, screenshot capture on failure, and parallel
|
||||
// test isolation via per-session data sandboxing.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@@ -12,6 +13,13 @@ class BaseUITestCase: XCTestCase {
|
||||
|
||||
var app: XCUIApplication!
|
||||
|
||||
// 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.
|
||||
@@ -26,6 +34,15 @@ class BaseUITestCase: XCTestCase {
|
||||
/// Whether to force the trial to be expired. Default: false.
|
||||
var expireTrial: Bool { false }
|
||||
|
||||
/// Override to change the test locale/language.
|
||||
/// Default: English (US). Locale tests override this instead of setUp().
|
||||
var localeArguments: [String] { ["-AppleLanguages", "(en)", "-AppleLocale", "en_US"] }
|
||||
|
||||
/// Extra launch arguments for tests needing special settings
|
||||
/// (accessibility sizes, reduce motion, high contrast, etc.).
|
||||
/// Override in subclasses instead of overriding setUp().
|
||||
var extraLaunchArguments: [String] { [] }
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func setUp() {
|
||||
@@ -46,7 +63,8 @@ class BaseUITestCase: XCTestCase {
|
||||
// MARK: - Launch Configuration
|
||||
|
||||
private func buildLaunchArguments(resetState: Bool) -> [String] {
|
||||
var args = ["--ui-testing", "--disable-animations", "-AppleLanguages", "(en)", "-AppleLocale", "en_US"]
|
||||
var args = ["--ui-testing", "--disable-animations"]
|
||||
args.append(contentsOf: localeArguments)
|
||||
if resetState {
|
||||
args.append("--reset-state")
|
||||
}
|
||||
@@ -59,11 +77,13 @@ class BaseUITestCase: XCTestCase {
|
||||
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
|
||||
}
|
||||
@@ -90,6 +110,26 @@ class BaseUITestCase: XCTestCase {
|
||||
return application
|
||||
}
|
||||
|
||||
/// Relaunch the app with custom bypass setting, preserving the session ID.
|
||||
/// Use when a test needs to toggle subscription bypass mid-test.
|
||||
@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()
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import XCTest
|
||||
|
||||
class HierarchyDumpTest: XCTestCase {
|
||||
class HierarchyDumpTest: BaseUITestCase {
|
||||
override var seedFixture: String? { nil }
|
||||
|
||||
func testDumpAccessibilityTree() {
|
||||
let app = XCUIApplication()
|
||||
app.launchArguments = ["--ui-testing", "--reset-state", "--disable-animations", "--bypass-subscription", "--skip-onboarding"]
|
||||
app.launch()
|
||||
sleep(3)
|
||||
|
||||
print("\n=== ELEMENT QUERIES ===")
|
||||
print("otherElements[mood_header]: \(app.otherElements[\"mood_header\"].exists)")
|
||||
print("descendants[mood_header]: \(app.descendants(matching: .any)[\"mood_header\"].firstMatch.exists)")
|
||||
print("groups[mood_header]: \(app.groups[\"mood_header\"].exists)")
|
||||
print("scrollViews[mood_header]: \(app.scrollViews[\"mood_header\"].exists)")
|
||||
print("staticTexts[mood_header]: \(app.staticTexts[\"mood_header\"].exists)")
|
||||
print("buttons[mood_button_great]: \(app.buttons[\"mood_button_great\"].exists)")
|
||||
print("otherElements[mood_header]: \(app.otherElements["mood_header"].exists)")
|
||||
print("descendants[mood_header]: \(app.descendants(matching: .any)["mood_header"].firstMatch.exists)")
|
||||
print("groups[mood_header]: \(app.groups["mood_header"].exists)")
|
||||
print("scrollViews[mood_header]: \(app.scrollViews["mood_header"].exists)")
|
||||
print("staticTexts[mood_header]: \(app.staticTexts["mood_header"].exists)")
|
||||
print("buttons[mood_button_great]: \(app.buttons["mood_button_great"].exists)")
|
||||
print("tabBars count: \(app.tabBars.count)")
|
||||
if app.tabBars.count > 0 {
|
||||
let tb = app.tabBars.firstMatch
|
||||
@@ -21,15 +20,15 @@ class HierarchyDumpTest: XCTestCase {
|
||||
print(" tab button: \(b.identifier) label=\(b.label)")
|
||||
}
|
||||
}
|
||||
print("otherElements[settings_header]: \(app.otherElements[\"settings_header\"].exists)")
|
||||
|
||||
print("otherElements[settings_header]: \(app.otherElements["settings_header"].exists)")
|
||||
|
||||
print("\n=== HIERARCHY (first 200 lines) ===")
|
||||
let desc = app.debugDescription
|
||||
let lines = desc.components(separatedBy: "\n")
|
||||
for (i, line) in lines.prefix(200).enumerated() {
|
||||
print("\(i): \(line)")
|
||||
}
|
||||
|
||||
|
||||
XCTAssertTrue(true) // always pass
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,25 +10,8 @@ import XCTest
|
||||
final class HighContrastTests: BaseUITestCase {
|
||||
override var seedFixture: String? { "single_mood" }
|
||||
override var bypassSubscription: Bool { true }
|
||||
|
||||
override func setUp() {
|
||||
// Do NOT call super — we need custom accessibility launch args
|
||||
continueAfterFailure = false
|
||||
|
||||
let application = XCUIApplication()
|
||||
let args: [String] = [
|
||||
"--ui-testing", "--disable-animations",
|
||||
"--reset-state",
|
||||
"--bypass-subscription",
|
||||
"--skip-onboarding",
|
||||
"-AppleLanguages", "(en)",
|
||||
"-AppleLocale", "en_US",
|
||||
"-UIAccessibilityDarkerSystemColorsEnabled", "YES"
|
||||
]
|
||||
application.launchArguments = args
|
||||
application.launchEnvironment = ["UI_TEST_FIXTURE": "single_mood"]
|
||||
application.launch()
|
||||
app = application
|
||||
override var extraLaunchArguments: [String] {
|
||||
["-UIAccessibilityDarkerSystemColorsEnabled", "YES"]
|
||||
}
|
||||
|
||||
/// TC-144: App is navigable with High Contrast mode enabled.
|
||||
|
||||
@@ -10,25 +10,7 @@ import XCTest
|
||||
final class LongTranslationTests: BaseUITestCase {
|
||||
override var seedFixture: String? { "single_mood" }
|
||||
override var bypassSubscription: Bool { true }
|
||||
|
||||
override func setUp() {
|
||||
// Do NOT call super — we need German locale (known for long compound words)
|
||||
continueAfterFailure = false
|
||||
|
||||
let application = XCUIApplication()
|
||||
let args: [String] = [
|
||||
"--ui-testing", "--disable-animations",
|
||||
"--reset-state",
|
||||
"--bypass-subscription",
|
||||
"--skip-onboarding",
|
||||
"-AppleLanguages", "(de)",
|
||||
"-AppleLocale", "de_DE"
|
||||
]
|
||||
application.launchArguments = args
|
||||
application.launchEnvironment = ["UI_TEST_FIXTURE": "single_mood"]
|
||||
application.launch()
|
||||
app = application
|
||||
}
|
||||
override var localeArguments: [String] { ["-AppleLanguages", "(de)", "-AppleLocale", "de_DE"] }
|
||||
|
||||
/// TC-138: German locale with long compound words renders without crashes.
|
||||
/// Navigates through all tabs to ensure no layout truncation causes issues.
|
||||
|
||||
@@ -10,25 +10,8 @@ import XCTest
|
||||
final class ReduceMotionTests: BaseUITestCase {
|
||||
override var seedFixture: String? { "single_mood" }
|
||||
override var bypassSubscription: Bool { true }
|
||||
|
||||
override func setUp() {
|
||||
// Do NOT call super — we need custom accessibility launch args
|
||||
continueAfterFailure = false
|
||||
|
||||
let application = XCUIApplication()
|
||||
let args: [String] = [
|
||||
"--ui-testing", "--disable-animations",
|
||||
"--reset-state",
|
||||
"--bypass-subscription",
|
||||
"--skip-onboarding",
|
||||
"-AppleLanguages", "(en)",
|
||||
"-AppleLocale", "en_US",
|
||||
"-UIReduceMotionPreference", "YES"
|
||||
]
|
||||
application.launchArguments = args
|
||||
application.launchEnvironment = ["UI_TEST_FIXTURE": "single_mood"]
|
||||
application.launch()
|
||||
app = application
|
||||
override var extraLaunchArguments: [String] {
|
||||
["-UIReduceMotionPreference", "YES"]
|
||||
}
|
||||
|
||||
/// TC-143: App is navigable with Reduce Motion enabled.
|
||||
|
||||
@@ -10,25 +10,7 @@ import XCTest
|
||||
final class SpanishLocalizationTests: BaseUITestCase {
|
||||
override var seedFixture: String? { "week_of_moods" }
|
||||
override var bypassSubscription: Bool { true }
|
||||
|
||||
override func setUp() {
|
||||
// Do NOT call super — we need custom language launch args
|
||||
continueAfterFailure = false
|
||||
|
||||
let application = XCUIApplication()
|
||||
let args: [String] = [
|
||||
"--ui-testing", "--disable-animations",
|
||||
"--reset-state",
|
||||
"--bypass-subscription",
|
||||
"--skip-onboarding",
|
||||
"-AppleLanguages", "(es)",
|
||||
"-AppleLocale", "es_ES"
|
||||
]
|
||||
application.launchArguments = args
|
||||
application.launchEnvironment = ["UI_TEST_FIXTURE": "week_of_moods"]
|
||||
application.launch()
|
||||
app = application
|
||||
}
|
||||
override var localeArguments: [String] { ["-AppleLanguages", "(es)", "-AppleLocale", "es_ES"] }
|
||||
|
||||
/// TC-137: Key Spanish strings appear when launched in Spanish locale.
|
||||
func testSpanishLocale_DisplaysSpanishStrings() {
|
||||
|
||||
@@ -20,20 +20,8 @@ final class TrialBannerTests: BaseUITestCase {
|
||||
settingsScreen.assertVisible()
|
||||
|
||||
// With default settings (bypassSubscription = true), the banner is hidden.
|
||||
// We need to launch without bypass to see the banner.
|
||||
// Re-launch with bypass disabled.
|
||||
app.terminate()
|
||||
|
||||
let freshApp = XCUIApplication()
|
||||
var args = ["--ui-testing", "--reset-state", "--disable-animations", "--skip-onboarding",
|
||||
"-AppleLanguages", "(en)", "-AppleLocale", "en_US"]
|
||||
// Do NOT add --bypass-subscription
|
||||
freshApp.launchArguments = args
|
||||
if let fixture = seedFixture {
|
||||
freshApp.launchEnvironment = ["UI_TEST_FIXTURE": fixture]
|
||||
}
|
||||
freshApp.launch()
|
||||
app = freshApp
|
||||
// Re-launch without bypass to see the banner.
|
||||
relaunchApp(resetState: true, bypassSubscription: false)
|
||||
|
||||
// Navigate to Settings
|
||||
let freshTabBar = TabBarScreen(app: app)
|
||||
|
||||
Reference in New Issue
Block a user