// // SettingsScreen.swift // Tests iOS // // Screen object for the Settings tab (Customize + Settings sub-tabs). // import XCTest struct SettingsScreen { let app: XCUIApplication private let defaultTimeout: TimeInterval = 2 private let navigationTimeout: TimeInterval = 5 // MARK: - Elements var settingsHeader: XCUIElement { app.element(UITestID.Settings.header) } var customizeSegment: XCUIElement { app.element(UITestID.Settings.customizeTab) } var settingsSegment: XCUIElement { app.element(UITestID.Settings.settingsTab) } var upgradeBanner: XCUIElement { app.element(UITestID.Settings.upgradeBanner) } var subscribeButton: XCUIElement { app.element(UITestID.Settings.subscribeButton) } var whyUpgradeButton: XCUIElement { app.element(UITestID.Settings.whyUpgradeButton) } var browseThemesButton: XCUIElement { app.element(UITestID.Settings.browseThemesButton) } var clearDataButton: XCUIElement { app.element(UITestID.Settings.clearDataButton) } var analyticsToggle: XCUIElement { app.element(UITestID.Settings.analyticsToggle) } var eulaButton: XCUIElement { app.element(UITestID.Settings.eulaButton) } var privacyPolicyButton: XCUIElement { app.element(UITestID.Settings.privacyPolicyButton) } // MARK: - Actions func tapCustomizeTab(file: StaticString = #filePath, line: UInt = #line) { tapSegment(identifier: UITestID.Settings.customizeTab, fallbackLabel: "Customize", file: file, line: line) } func tapSettingsTab(file: StaticString = #filePath, line: UInt = #line) { tapSegment(identifier: UITestID.Settings.settingsTab, fallbackLabel: "Settings", file: file, line: line) } private func tapSegment(identifier: String, fallbackLabel: String, file: StaticString, line: UInt) { // On iOS 26, segmented controls may expose buttons by label or by ID. // Try multiple strategies in order of reliability. // Strategy 1: Segmented control button by label (most reliable) let segButton = app.segmentedControls.buttons[fallbackLabel] if segButton.waitForExistence(timeout: defaultTimeout) && segButton.isHittable { segButton.tap() return } // Strategy 2: Find a non-tab-bar button with matching label let tabBarButton = app.tabBars.buttons[fallbackLabel] let allButtons = app.buttons.matching(NSPredicate(format: "label == %@", fallbackLabel)).allElementsBoundByIndex for button in allButtons { if button.isHittable && button.frame != tabBarButton.frame { button.tap() return } } // Strategy 3: Accessibility ID with coordinate tap fallback let byID = app.element(identifier) if byID.waitForExistence(timeout: defaultTimeout) { byID.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() return } XCTFail("Could not find segment '\(fallbackLabel)' by ID or label", file: file, line: line) } func tapClearData(file: StaticString = #filePath, line: UInt = #line) { scrollToSettingsElement(clearDataButton, maxSwipes: 20, file: file, line: line) clearDataButton.forceTap(file: file, line: line) } func tapAnalyticsToggle(file: StaticString = #filePath, line: UInt = #line) { scrollToSettingsElement(analyticsToggle, maxSwipes: 15, file: file, line: line) analyticsToggle.forceTap(file: file, line: line) } /// Scroll within the settings content to find a deeply nested element. /// Uses aggressive swipes on the main app surface since the ScrollView /// hierarchy varies by iOS version. private func scrollToSettingsElement( _ element: XCUIElement, maxSwipes: Int, file: StaticString, line: UInt ) { if element.exists && element.isHittable { return } for _ in 0.. SettingsScreen { settingsHeader.waitForExistenceOrFail(timeout: navigationTimeout, message: "Settings header should be visible", file: file, line: line) return self } func assertUpgradeBannerVisible(file: StaticString = #filePath, line: UInt = #line) { upgradeBanner.waitForExistenceOrFail(timeout: defaultTimeout, message: "Upgrade banner should be visible", file: file, line: line) } func assertUpgradeBannerHidden(file: StaticString = #filePath, line: UInt = #line) { upgradeBanner.waitForNonExistence(timeout: navigationTimeout, message: "Upgrade banner should be hidden (subscribed)", file: file, line: line) } }