// // WaitHelpers.swift // Tests iOS // // Centralized wait helpers and element extensions. No sleep() allowed. // Follows fail-fast principles: if an element isn't there, fail immediately. // import XCTest // MARK: - Test Accessibility Identifiers (mirrors AccessibilityID in app target) enum UITestID { enum Tab { static let day = "tab_day" static let month = "tab_month" static let year = "tab_year" static let insights = "tab_insights" static let settings = "tab_settings" } enum Day { static let moodHeader = "mood_header" static let entryRowPrefix = "entry_row_" static let sectionPrefix = "day_section_" static let emptyStateNoData = "empty_state_no_data" } enum Settings { static let header = "settings_header" static let customizeTab = "settings_tab_customize" static let settingsTab = "settings_tab_settings" static let upgradeBanner = "upgrade_banner" static let subscribeButton = "subscribe_button" static let whyUpgradeButton = "why_upgrade_button" static let browseThemesButton = "browse_themes_button" static let clearDataButton = "settings_clear_data" static let analyticsToggle = "settings_analytics_toggle" static let eulaButton = "settings_eula" static let privacyPolicyButton = "settings_privacy_policy" } enum Customize { static func themeButton(_ name: String) -> String { "customize_theme_\(name.lowercased())" } static func votingLayoutButton(_ name: String) -> String { "customize_voting_\(name.lowercased())" } static func dayStyleButton(_ name: String) -> String { "customize_daystyle_\(name.lowercased())" } static func iconPackButton(_ name: String) -> String { "customize_iconpack_\(name.lowercased())" } static func personalityPackButton(_ name: String) -> String { "customize_personality_\(name.lowercased())" } static func appThemeCard(_ name: String) -> String { "apptheme_card_\(name.lowercased())" } static let pickerDoneButton = "apptheme_picker_done" static let previewCancelButton = "apptheme_preview_cancel" static let previewApplyButton = "apptheme_preview_apply" } enum EntryDetail { static let sheet = "entry_detail_sheet" static let doneButton = "entry_detail_done" static let deleteButton = "entry_detail_delete" static let noteButton = "entry_detail_note_button" static let noteArea = "entry_detail_note_area" } enum NoteEditor { static let text = "note_editor_text" static let save = "note_editor_save" static let cancel = "note_editor_cancel" } enum Onboarding { static let welcome = "onboarding_welcome" static let time = "onboarding_time" static let day = "onboarding_day" static let dayToday = "onboarding_day_today" static let dayYesterday = "onboarding_day_yesterday" static let style = "onboarding_style" static let subscription = "onboarding_subscription" static let subscribe = "onboarding_subscribe_button" static let skip = "onboarding_skip_button" } enum Paywall { static let monthOverlay = "paywall_month_overlay" static let yearOverlay = "paywall_year_overlay" static let insightsOverlay = "paywall_insights_overlay" } enum Insights { static let header = "insights_header" static let monthSection = "insights_month_section" static let yearSection = "insights_year_section" static let allTimeSection = "insights_all_time_section" } enum Year { static let heatmap = "year_heatmap" static let donutChart = "year_donut_chart" static let barChart = "year_bar_chart" static let statsSection = "year_stats_section" static func cardHeader(year: Int) -> String { "year_card_header_\(year)" } static let shareButton = "year_share_button" } enum Month { static let grid = "month_grid" static let shareButton = "month_share_button" } } // MARK: - XCUIElement Extensions (fail-fast, no retry loops) extension XCUIElement { /// Wait for element to exist; XCTFail if it doesn't. @discardableResult func waitForExistenceOrFail( timeout: TimeInterval, message: String? = nil, file: StaticString = #filePath, line: UInt = #line ) -> XCUIElement { if !waitForExistence(timeout: timeout) { XCTFail(message ?? "Expected element to exist: \(self)", file: file, line: line) } return self } /// Wait for element to become hittable; XCTFail if it doesn't. @discardableResult func waitUntilHittableOrFail( timeout: TimeInterval, message: String? = nil, file: StaticString = #filePath, line: UInt = #line ) -> XCUIElement { let predicate = NSPredicate(format: "exists == true AND isHittable == true") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self) let result = XCTWaiter().wait(for: [expectation], timeout: timeout) if result != .completed { XCTFail(message ?? "Expected element to become hittable: \(self)", file: file, line: line) } return self } /// Wait for element to disappear; XCTFail if it doesn't. @discardableResult func waitForNonExistence( timeout: TimeInterval, message: String? = nil, file: StaticString = #filePath, line: UInt = #line ) -> Bool { let predicate = NSPredicate(format: "exists == false") let expectation = XCTNSPredicateExpectation(predicate: predicate, object: self) let result = XCTWaiter().wait(for: [expectation], timeout: timeout) if result != .completed { XCTFail(message ?? "Expected element to disappear: \(self)", file: file, line: line) return false } return true } /// Scroll element into view within a scrollable container. Fail-fast if not found. func scrollIntoView( in container: XCUIElement, direction: SwipeDirection = .up, maxSwipes: Int = 5, file: StaticString = #filePath, line: UInt = #line ) { if exists && isHittable { return } for _ in 0.. XCUIElement { descendants(matching: .any).matching(identifier: identifier).firstMatch } var entryRows: XCUIElementQuery { descendants(matching: .any).matching(NSPredicate(format: "identifier BEGINSWITH %@", UITestID.Day.entryRowPrefix)) } var firstEntryRow: XCUIElement { entryRows.firstMatch } /// Tap a tab by identifier, falling back to labels. func tapTab(identifier: String, labels: [String], timeout: TimeInterval = 5, file: StaticString = #filePath, line: UInt = #line) { let idMatch = tabBars.buttons[identifier] if idMatch.waitForExistence(timeout: 1) { idMatch.forceTap(file: file, line: line) return } for label in labels { let labelMatch = tabBars.buttons[label] if labelMatch.waitForExistence(timeout: 1) { labelMatch.forceTap(file: file, line: line) return } } XCTFail("Unable to find tab by id \(identifier) or labels \(labels)", file: file, line: line) } } enum SwipeDirection { case up, down, left, right }