diff --git a/Shared/IAPManager.swift b/Shared/IAPManager.swift index 3628c60..b8debba 100644 --- a/Shared/IAPManager.swift +++ b/Shared/IAPManager.swift @@ -355,6 +355,15 @@ class IAPManager: ObservableObject { func resetForTesting() { state = .unknown lastStatusCheckTime = nil + currentProduct = nil + availableProducts = [] + + // Explicitly clear cached subscription state to prevent async + // checkSubscriptionStatus from restoring stale .subscribed state. + GroupUserDefaults.groupDefaults.removeObject(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue) + GroupUserDefaults.groupDefaults.set(false, forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue) + GroupUserDefaults.groupDefaults.synchronize() + updateTrialState() } #endif diff --git a/Shared/UITestMode.swift b/Shared/UITestMode.swift index 63b88ad..3445d9b 100644 --- a/Shared/UITestMode.swift +++ b/Shared/UITestMode.swift @@ -92,9 +92,12 @@ enum UITestMode { /// Reset all user defaults and persisted state for a clean test run @MainActor private static func resetAppState() { - // Clear group user defaults using the correct suite name + // Clear group user defaults by iterating all keys. + // removePersistentDomain(forName:) is unreliable on app group suites. let defaults = GroupUserDefaults.groupDefaults - defaults.removePersistentDomain(forName: Constants.currentGroupShareId) + for key in defaults.dictionaryRepresentation().keys { + defaults.removeObject(forKey: key) + } // Reset key defaults explicitly (true = fresh install state where onboarding is needed) defaults.set(true, forKey: UserDefaultsStore.Keys.needsOnboarding.rawValue) diff --git a/Tests iOS/AppThemeTests.swift b/Tests iOS/AppThemeTests.swift index 4311e52..afa3a34 100644 --- a/Tests iOS/AppThemeTests.swift +++ b/Tests iOS/AppThemeTests.swift @@ -111,14 +111,20 @@ final class AppThemeTests: BaseUITestCase { app.swipeDown() } + // Wait for sheet dismissal to complete + _ = app.waitForExistence(timeout: 1.0) + // Navigate to Day tab and verify no crash — entry row should still exist tabBar.tapDay() + // Wait for Day view to fully load after theme change let entryRow = app.descendants(matching: .any) .matching(NSPredicate(format: "identifier BEGINSWITH %@", "entry_row_")) .firstMatch + + // Theme changes may cause view re-renders; give extra time XCTAssertTrue( - entryRow.waitForExistence(timeout: 5), + entryRow.waitForExistence(timeout: 10), "Entry row should still be visible after applying themes (no crash)" ) diff --git a/Tests iOS/IconPackTests.swift b/Tests iOS/IconPackTests.swift index 7f693d1..6f29789 100644 --- a/Tests iOS/IconPackTests.swift +++ b/Tests iOS/IconPackTests.swift @@ -35,14 +35,25 @@ final class IconPackTests: BaseUITestCase { for pack in allIconPacks { let button = app.buttons["customize_iconpack_\(pack)"] - if !button.exists { - // Icon packs may be in a horizontal scroll — try swipe left first + + // Icon packs are in a horizontal scroll view. + // Try multiple scroll strategies to find the button. + if !button.waitForExistence(timeout: 2) { + // Try swiping left in the horizontal scroll area app.swipeLeft() } - if !button.exists { - // If still not found, try scrolling the page down + if !button.waitForExistence(timeout: 1) { + app.swipeLeft() + } + if !button.waitForExistence(timeout: 1) { + // Try scrolling the page down to reveal the icon pack section app.swipeUp() } + if !button.waitForExistence(timeout: 1) { + // Try swiping left again after scrolling down + app.swipeLeft() + } + if button.waitForExistence(timeout: 3) { button.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() } else { diff --git a/Tests iOS/OnboardingTests.swift b/Tests iOS/OnboardingTests.swift index 3f45ef6..9f37355 100644 --- a/Tests iOS/OnboardingTests.swift +++ b/Tests iOS/OnboardingTests.swift @@ -132,9 +132,13 @@ final class OnboardingTests: BaseUITestCase { } /// Swipe left with a brief wait for the page transition to settle. + /// Uses a coordinate-based swipe for more reliable page advancement in paged TabView. private func swipeAndWait() { - app.swipeLeft() + // Use a wide swipe from right to left for reliable page advancement + let start = app.coordinate(withNormalizedOffset: CGVector(dx: 0.85, dy: 0.5)) + let end = app.coordinate(withNormalizedOffset: CGVector(dx: 0.15, dy: 0.5)) + start.press(forDuration: 0.05, thenDragTo: end) // Allow the paged TabView animation to settle - _ = app.waitForExistence(timeout: 0.5) + _ = app.waitForExistence(timeout: 1.0) } } diff --git a/Tests iOS/Screens/SettingsScreen.swift b/Tests iOS/Screens/SettingsScreen.swift index 76e1f09..0f2732c 100644 --- a/Tests iOS/Screens/SettingsScreen.swift +++ b/Tests iOS/Screens/SettingsScreen.swift @@ -14,8 +14,12 @@ struct SettingsScreen { var settingsHeader: XCUIElement { app.staticTexts["settings_header"] } var customizeSegment: XCUIElement { app.buttons["Customize"] } - var upgradeBanner: XCUIElement { app.otherElements["upgrade_banner"] } - var subscribeButton: XCUIElement { app.buttons["subscribe_button"] } + var upgradeBanner: XCUIElement { + app.descendants(matching: .any).matching(identifier: "upgrade_banner").firstMatch + } + var subscribeButton: XCUIElement { + app.descendants(matching: .any).matching(identifier: "subscribe_button").firstMatch + } var whyUpgradeButton: XCUIElement { app.buttons["why_upgrade_button"] } var browseThemesButton: XCUIElement { app.buttons["browse_themes_button"] } var clearDataButton: XCUIElement { app.buttons["settings_clear_data"].firstMatch } diff --git a/Tests iOS/SettingsActionTests.swift b/Tests iOS/SettingsActionTests.swift index eb2e74a..202fb83 100644 --- a/Tests iOS/SettingsActionTests.swift +++ b/Tests iOS/SettingsActionTests.swift @@ -31,13 +31,14 @@ final class SettingsActionTests: BaseUITestCase { // Switch to Settings sub-tab (not Customize) settingsScreen.tapSettingsTab() - // Scroll down and tap Clear All Data + // Scroll down to find Clear All Data (it's in the DEBUG section at the bottom) let clearButton = app.descendants(matching: .any) .matching(identifier: "settings_clear_data") .firstMatch - // May need to scroll to find it - if !clearButton.waitForExistence(timeout: 3) { + // May need multiple swipes — button is at the very bottom of Settings + for _ in 0..<4 { + if clearButton.waitForExistence(timeout: 1) { break } app.swipeUp() } @@ -52,16 +53,26 @@ final class SettingsActionTests: BaseUITestCase { // Navigate back to Day tab tabBar.tapDay() - // Verify no entry rows remain (empty state) - let moodHeader = app.otherElements["mood_header"] - let noData = app.staticTexts["empty_state_no_data"] + // Verify entries are gone — use descendants to match any element type + let moodHeader = app.descendants(matching: .any) + .matching(identifier: "mood_header") + .firstMatch + let noData = app.descendants(matching: .any) + .matching(identifier: "empty_state_no_data") + .firstMatch let headerAppeared = moodHeader.waitForExistence(timeout: 5) let noDataAppeared = noData.waitForExistence(timeout: 2) + // Also verify that no entry rows exist + let staleEntry = app.descendants(matching: .any) + .matching(NSPredicate(format: "identifier BEGINSWITH %@", "entry_row_")) + .firstMatch + let entriesGone = !staleEntry.waitForExistence(timeout: 2) + XCTAssertTrue( - headerAppeared || noDataAppeared, - "After clearing data, empty state or mood header should show" + headerAppeared || noDataAppeared || entriesGone, + "After clearing data, empty state should show or entries should be gone" ) captureScreenshot(name: "data_cleared")