Fix 23 failing UI tests: accessibility hierarchy, test mode, and interaction issues
App fixes: - Remove empty_state identifier from EmptyHomeView VStack (was overriding mood_header) - Fix resetAppState to set needsOnboarding=true (fresh state) instead of false - Set bypassSubscription explicitly based on launch arg presence (was defaulting to true in DEBUG) Test fixes: - TabBarScreen: use coordinate tap to avoid iOS 26 Liquid Glass hittability issues - SettingsScreen: use coordinate tap for segments, handle Settings label ambiguity with tab bar - EntryDetailScreen: use mood_button_ identifiers instead of label matching (was matching entry rows) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,11 +66,9 @@ enum UITestMode {
|
|||||||
GroupUserDefaults.groupDefaults.set(false, forKey: UserDefaultsStore.Keys.needsOnboarding.rawValue)
|
GroupUserDefaults.groupDefaults.set(false, forKey: UserDefaultsStore.Keys.needsOnboarding.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if bypassSubscription {
|
#if DEBUG
|
||||||
#if DEBUG
|
IAPManager.shared.bypassSubscription = bypassSubscription
|
||||||
IAPManager.shared.bypassSubscription = true
|
#endif
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
if expireTrial {
|
if expireTrial {
|
||||||
// Set firstLaunchDate to 31 days ago so the 30-day trial is expired
|
// Set firstLaunchDate to 31 days ago so the 30-day trial is expired
|
||||||
@@ -93,8 +91,8 @@ enum UITestMode {
|
|||||||
if let bundleId = Bundle.main.bundleIdentifier {
|
if let bundleId = Bundle.main.bundleIdentifier {
|
||||||
defaults.removePersistentDomain(forName: bundleId)
|
defaults.removePersistentDomain(forName: bundleId)
|
||||||
}
|
}
|
||||||
// Reset key defaults explicitly
|
// Reset key defaults explicitly (true = fresh install state where onboarding is needed)
|
||||||
defaults.set(false, forKey: UserDefaultsStore.Keys.needsOnboarding.rawValue)
|
defaults.set(true, forKey: UserDefaultsStore.Keys.needsOnboarding.rawValue)
|
||||||
defaults.set(0, forKey: UserDefaultsStore.Keys.votingLayoutStyle.rawValue) // horizontal
|
defaults.set(0, forKey: UserDefaultsStore.Keys.votingLayoutStyle.rawValue) // horizontal
|
||||||
defaults.synchronize()
|
defaults.synchronize()
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ struct EmptyHomeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.accessibilityIdentifier(AccessibilityID.DayView.emptyState)
|
|
||||||
}
|
}
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ struct EntryDetailScreen {
|
|||||||
var moodGrid: XCUIElement { app.otherElements["entry_detail_mood_grid"] }
|
var moodGrid: XCUIElement { app.otherElements["entry_detail_mood_grid"] }
|
||||||
|
|
||||||
/// Mood buttons inside the detail sheet's mood grid.
|
/// Mood buttons inside the detail sheet's mood grid.
|
||||||
/// These use accessibilityLabel (the mood name text), not identifiers.
|
/// Match by the mood_button_ identifier prefix to avoid matching entry rows.
|
||||||
func moodButton(label: String) -> XCUIElement {
|
func moodButton(for mood: MoodChoice) -> XCUIElement {
|
||||||
app.buttons.matching(NSPredicate(format: "label CONTAINS[cd] %@", label)).firstMatch
|
app.buttons["mood_button_\(mood.rawValue)"]
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
@@ -30,7 +30,7 @@ struct EntryDetailScreen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func selectMood(_ mood: MoodChoice) {
|
func selectMood(_ mood: MoodChoice) {
|
||||||
let button = moodButton(label: mood.rawValue.capitalized)
|
let button = moodButton(for: mood)
|
||||||
button.tapWhenReady()
|
button.tapWhenReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ struct SettingsScreen {
|
|||||||
|
|
||||||
var settingsHeader: XCUIElement { app.staticTexts["settings_header"] }
|
var settingsHeader: XCUIElement { app.staticTexts["settings_header"] }
|
||||||
var customizeSegment: XCUIElement { app.buttons["Customize"] }
|
var customizeSegment: XCUIElement { app.buttons["Customize"] }
|
||||||
var settingsSegment: XCUIElement { app.buttons["Settings"] }
|
|
||||||
var upgradeBanner: XCUIElement { app.otherElements["upgrade_banner"] }
|
var upgradeBanner: XCUIElement { app.otherElements["upgrade_banner"] }
|
||||||
var subscribeButton: XCUIElement { app.buttons["subscribe_button"] }
|
var subscribeButton: XCUIElement { app.buttons["subscribe_button"] }
|
||||||
var whyUpgradeButton: XCUIElement { app.buttons["why_upgrade_button"] }
|
var whyUpgradeButton: XCUIElement { app.buttons["why_upgrade_button"] }
|
||||||
@@ -26,24 +25,44 @@ struct SettingsScreen {
|
|||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
|
|
||||||
func tapCustomizeTab() {
|
func tapCustomizeTab() {
|
||||||
customizeSegment.tapWhenReady()
|
let segment = customizeSegment
|
||||||
|
_ = segment.waitForExistence(timeout: 5)
|
||||||
|
segment.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||||
}
|
}
|
||||||
|
|
||||||
func tapSettingsTab() {
|
func tapSettingsTab() {
|
||||||
settingsSegment.tapWhenReady()
|
// Find the "Settings" segment in the segmented control (not the tab bar button).
|
||||||
|
// Try segmentedControls first, then fall back to finding by exclusion.
|
||||||
|
let segCtrl = app.segmentedControls.buttons["Settings"]
|
||||||
|
if segCtrl.waitForExistence(timeout: 3) {
|
||||||
|
segCtrl.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Fallback: find a "Settings" button that is NOT the tab bar button
|
||||||
|
let candidates = app.buttons.matching(NSPredicate(format: "label == 'Settings'")).allElementsBoundByIndex
|
||||||
|
let tabBarBtn = app.tabBars.buttons["Settings"]
|
||||||
|
for candidate in candidates where candidate.frame != tabBarBtn.frame {
|
||||||
|
candidate.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tapClearData() {
|
func tapClearData() {
|
||||||
clearDataButton.tapWhenReady()
|
let button = clearDataButton
|
||||||
|
if button.exists && !button.isHittable {
|
||||||
|
app.swipeUp()
|
||||||
|
}
|
||||||
|
_ = button.waitForExistence(timeout: 5)
|
||||||
|
button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||||
}
|
}
|
||||||
|
|
||||||
func tapAnalyticsToggle() {
|
func tapAnalyticsToggle() {
|
||||||
// Scroll down to find the toggle if needed
|
let toggle = analyticsToggle
|
||||||
let toggle = app.descendants(matching: .any).matching(identifier: "settings_analytics_toggle").firstMatch
|
if toggle.exists && !toggle.isHittable {
|
||||||
if !toggle.isHittable {
|
|
||||||
app.swipeUp()
|
app.swipeUp()
|
||||||
}
|
}
|
||||||
toggle.tapWhenReady()
|
_ = toggle.waitForExistence(timeout: 5)
|
||||||
|
toggle.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Assertions
|
// MARK: - Assertions
|
||||||
|
|||||||
@@ -22,31 +22,31 @@ struct TabBarScreen {
|
|||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func tapDay() -> DayScreen {
|
func tapDay() -> DayScreen {
|
||||||
dayTab.tapWhenReady()
|
tapTab(dayTab)
|
||||||
return DayScreen(app: app)
|
return DayScreen(app: app)
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func tapMonth() -> TabBarScreen {
|
func tapMonth() -> TabBarScreen {
|
||||||
monthTab.tapWhenReady()
|
tapTab(monthTab)
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func tapYear() -> TabBarScreen {
|
func tapYear() -> TabBarScreen {
|
||||||
yearTab.tapWhenReady()
|
tapTab(yearTab)
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func tapInsights() -> TabBarScreen {
|
func tapInsights() -> TabBarScreen {
|
||||||
insightsTab.tapWhenReady()
|
tapTab(insightsTab)
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func tapSettings() -> SettingsScreen {
|
func tapSettings() -> SettingsScreen {
|
||||||
settingsTab.tapWhenReady()
|
tapTab(settingsTab)
|
||||||
return SettingsScreen(app: app)
|
return SettingsScreen(app: app)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,4 +59,13 @@ struct TabBarScreen {
|
|||||||
func assertTabBarVisible() {
|
func assertTabBarVisible() {
|
||||||
XCTAssertTrue(dayTab.waitForExistence(timeout: 5), "Tab bar should be visible")
|
XCTAssertTrue(dayTab.waitForExistence(timeout: 5), "Tab bar should be visible")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
/// Tap a tab bar button. Uses coordinate tap to avoid iOS 26 Liquid Glass
|
||||||
|
/// overlay elements reporting buttons as not hittable.
|
||||||
|
private func tapTab(_ tab: XCUIElement) {
|
||||||
|
_ = tab.waitForExistence(timeout: 5)
|
||||||
|
tab.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user