From 10581cc8fb567eb4bd6cec0b5a710d09e4ea05e4 Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 17 Feb 2026 13:15:21 -0600 Subject: [PATCH] Add Tests iOS/Screens/ page objects and fix gitignore The screens/ gitignore rule was matching Tests iOS/Screens/ on case-insensitive macOS. Anchored to /screens/ (repo root only) so the 7 UI test page object files are no longer ignored. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 +- Tests iOS/Screens/CustomizeScreen.swift | 65 +++++++++++++++++ Tests iOS/Screens/DayScreen.swift | 89 +++++++++++++++++++++++ Tests iOS/Screens/EntryDetailScreen.swift | 62 ++++++++++++++++ Tests iOS/Screens/NoteEditorScreen.swift | 63 ++++++++++++++++ Tests iOS/Screens/OnboardingScreen.swift | 76 +++++++++++++++++++ Tests iOS/Screens/SettingsScreen.swift | 74 +++++++++++++++++++ Tests iOS/Screens/TabBarScreen.swift | 62 ++++++++++++++++ 8 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 Tests iOS/Screens/CustomizeScreen.swift create mode 100644 Tests iOS/Screens/DayScreen.swift create mode 100644 Tests iOS/Screens/EntryDetailScreen.swift create mode 100644 Tests iOS/Screens/NoteEditorScreen.swift create mode 100644 Tests iOS/Screens/OnboardingScreen.swift create mode 100644 Tests iOS/Screens/SettingsScreen.swift create mode 100644 Tests iOS/Screens/TabBarScreen.swift diff --git a/.gitignore b/.gitignore index 0ae1bde..52986df 100644 --- a/.gitignore +++ b/.gitignore @@ -76,5 +76,5 @@ Secrets.swift **/Secrets.swift # Screenshots and promo assets -screens/ +/screens/ feels-promo/ diff --git a/Tests iOS/Screens/CustomizeScreen.swift b/Tests iOS/Screens/CustomizeScreen.swift new file mode 100644 index 0000000..dd85f99 --- /dev/null +++ b/Tests iOS/Screens/CustomizeScreen.swift @@ -0,0 +1,65 @@ +// +// CustomizeScreen.swift +// Tests iOS +// +// Screen object for the Customize sub-tab — theme, voting layout, and day view style pickers. +// + +import XCTest + +struct CustomizeScreen { + let app: XCUIApplication + + // MARK: - Theme Mode Buttons + + func themeButton(named name: String) -> XCUIElement { + app.buttons["customize_theme_\(name.lowercased())"] + } + + // MARK: - Voting Layout Buttons + + func votingLayoutButton(named name: String) -> XCUIElement { + app.buttons["customize_voting_\(name.lowercased())"] + } + + // MARK: - Day View Style Buttons + + func dayViewStyleButton(named name: String) -> XCUIElement { + app.buttons["customize_daystyle_\(name.lowercased())"] + } + + // MARK: - Actions + + func selectTheme(_ name: String) { + let button = themeButton(named: name) + button.tapWhenReady() + } + + func selectVotingLayout(_ name: String) { + let button = votingLayoutButton(named: name) + // May need to scroll horizontally to find it + if !button.isHittable { + app.swipeLeft() + } + button.tapWhenReady() + } + + func selectDayViewStyle(_ name: String) { + let button = dayViewStyleButton(named: name) + // May need to scroll horizontally to find it + if !button.isHittable { + app.swipeLeft() + } + button.tapWhenReady() + } + + // MARK: - Assertions + + func assertThemeButtonExists(_ name: String, file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue( + themeButton(named: name).waitForExistence(timeout: 5), + "Theme button '\(name)' should exist", + file: file, line: line + ) + } +} diff --git a/Tests iOS/Screens/DayScreen.swift b/Tests iOS/Screens/DayScreen.swift new file mode 100644 index 0000000..3fce93f --- /dev/null +++ b/Tests iOS/Screens/DayScreen.swift @@ -0,0 +1,89 @@ +// +// DayScreen.swift +// Tests iOS +// +// Screen object for the Day (main) view — mood logging and entry list. +// + +import XCTest + +struct DayScreen { + let app: XCUIApplication + + // MARK: - Mood Buttons (via accessibilityIdentifier) + + var greatButton: XCUIElement { app.buttons["mood_button_great"] } + var goodButton: XCUIElement { app.buttons["mood_button_good"] } + var averageButton: XCUIElement { app.buttons["mood_button_average"] } + var badButton: XCUIElement { app.buttons["mood_button_bad"] } + var horribleButton: XCUIElement { app.buttons["mood_button_horrible"] } + + /// The mood header container + var moodHeader: XCUIElement { app.otherElements["mood_header"] } + + // MARK: - Entry List + + /// Find an entry row by its date string (format: "M/d/yyyy") + func entryRow(dateString: String) -> XCUIElement { + app.descendants(matching: .any).matching(identifier: "entry_row_\(dateString)").firstMatch + } + + // MARK: - Actions + + /// Tap a mood button by mood name. Waits for the celebration animation to complete. + func logMood(_ mood: MoodChoice, file: StaticString = #file, line: UInt = #line) { + let button = moodButton(for: mood) + guard button.waitUntilHittable(timeout: 5) else { + XCTFail("Mood button '\(mood.rawValue)' not hittable", file: file, line: line) + return + } + button.tap() + + // Wait for the celebration animation to finish and entry to appear. + // The mood header disappears after logging today's mood. + // Give extra time for animation + data save. + _ = moodHeader.waitForDisappearance(timeout: 8) + } + + // MARK: - Assertions + + func assertMoodHeaderVisible(file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue( + moodHeader.waitForExistence(timeout: 5), + "Mood voting header should be visible", + file: file, line: line + ) + } + + func assertMoodHeaderHidden(file: StaticString = #file, line: UInt = #line) { + // After logging, the header should either disappear or the buttons should not be hittable + let hidden = moodHeader.waitForDisappearance(timeout: 8) + XCTAssertTrue(hidden, "Mood header should be hidden after logging today's mood", file: file, line: line) + } + + func assertEntryExists(dateString: String, file: StaticString = #file, line: UInt = #line) { + let row = entryRow(dateString: dateString) + XCTAssertTrue( + row.waitForExistence(timeout: 5), + "Entry row for \(dateString) should exist", + file: file, line: line + ) + } + + // MARK: - Private + + private func moodButton(for mood: MoodChoice) -> XCUIElement { + switch mood { + case .great: return greatButton + case .good: return goodButton + case .average: return averageButton + case .bad: return badButton + case .horrible: return horribleButton + } + } +} + +/// Represents the 5 selectable mood values for test code. +enum MoodChoice: String { + case great, good, average, bad, horrible +} diff --git a/Tests iOS/Screens/EntryDetailScreen.swift b/Tests iOS/Screens/EntryDetailScreen.swift new file mode 100644 index 0000000..17b411f --- /dev/null +++ b/Tests iOS/Screens/EntryDetailScreen.swift @@ -0,0 +1,62 @@ +// +// EntryDetailScreen.swift +// Tests iOS +// +// Screen object for the Entry Detail sheet (edit mood, notes, delete). +// + +import XCTest + +struct EntryDetailScreen { + let app: XCUIApplication + + // MARK: - Elements + + var navigationTitle: XCUIElement { app.navigationBars["Entry Details"] } + var doneButton: XCUIElement { app.buttons["entry_detail_done"] } + var deleteButton: XCUIElement { app.buttons["entry_detail_delete"] } + var moodGrid: XCUIElement { app.otherElements["entry_detail_mood_grid"] } + + /// Mood buttons inside the detail sheet's mood grid. + /// These use accessibilityLabel (the mood name text), not identifiers. + func moodButton(label: String) -> XCUIElement { + app.buttons.matching(NSPredicate(format: "label CONTAINS[cd] %@", label)).firstMatch + } + + // MARK: - Actions + + func dismiss() { + doneButton.tapWhenReady() + } + + func selectMood(_ mood: MoodChoice) { + let button = moodButton(label: mood.rawValue.capitalized) + button.tapWhenReady() + } + + func deleteEntry() { + deleteButton.tapWhenReady() + // Confirm the delete alert + let deleteAlert = app.alerts["Delete Entry"] + let confirmButton = deleteAlert.buttons["Delete"] + confirmButton.tapWhenReady() + } + + // MARK: - Assertions + + func assertVisible(file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue( + navigationTitle.waitForExistence(timeout: 5), + "Entry Detail sheet should be visible", + file: file, line: line + ) + } + + func assertDismissed(file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue( + navigationTitle.waitForDisappearance(timeout: 5), + "Entry Detail sheet should be dismissed", + file: file, line: line + ) + } +} diff --git a/Tests iOS/Screens/NoteEditorScreen.swift b/Tests iOS/Screens/NoteEditorScreen.swift new file mode 100644 index 0000000..1c17736 --- /dev/null +++ b/Tests iOS/Screens/NoteEditorScreen.swift @@ -0,0 +1,63 @@ +// +// NoteEditorScreen.swift +// Tests iOS +// +// Screen object for the Journal Note editor sheet. +// + +import XCTest + +struct NoteEditorScreen { + let app: XCUIApplication + + // MARK: - Elements + + var navigationTitle: XCUIElement { app.navigationBars["Journal Note"] } + var textEditor: XCUIElement { app.textViews["note_editor_text"] } + var saveButton: XCUIElement { app.buttons["note_editor_save"] } + var cancelButton: XCUIElement { app.buttons["note_editor_cancel"] } + + // MARK: - Actions + + func typeNote(_ text: String) { + textEditor.tapWhenReady() + textEditor.typeText(text) + } + + func clearAndTypeNote(_ text: String) { + textEditor.tapWhenReady() + // Select all and replace + textEditor.press(forDuration: 1.0) + let selectAll = app.menuItems["Select All"] + if selectAll.waitForExistence(timeout: 2) { + selectAll.tap() + } + textEditor.typeText(text) + } + + func save() { + saveButton.tapWhenReady() + } + + func cancel() { + cancelButton.tapWhenReady() + } + + // MARK: - Assertions + + func assertVisible(file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue( + navigationTitle.waitForExistence(timeout: 5), + "Note editor should be visible", + file: file, line: line + ) + } + + func assertDismissed(file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue( + navigationTitle.waitForDisappearance(timeout: 5), + "Note editor should be dismissed", + file: file, line: line + ) + } +} diff --git a/Tests iOS/Screens/OnboardingScreen.swift b/Tests iOS/Screens/OnboardingScreen.swift new file mode 100644 index 0000000..753e5b8 --- /dev/null +++ b/Tests iOS/Screens/OnboardingScreen.swift @@ -0,0 +1,76 @@ +// +// OnboardingScreen.swift +// Tests iOS +// +// Screen object for the onboarding flow — welcome, time, day, style, and subscription screens. +// + +import XCTest + +struct OnboardingScreen { + let app: XCUIApplication + + // MARK: - Screen Elements + + var welcomeScreen: XCUIElement { app.otherElements["onboarding_welcome"] } + var dayScreen: XCUIElement { app.otherElements["onboarding_day"] } + var subscriptionScreen: XCUIElement { app.otherElements["onboarding_subscription"] } + + var dayTodayButton: XCUIElement { app.buttons.matching(NSPredicate(format: "identifier == %@", "onboarding_day_today")).firstMatch } + var dayYesterdayButton: XCUIElement { app.buttons.matching(NSPredicate(format: "identifier == %@", "onboarding_day_yesterday")).firstMatch } + var subscribeButton: XCUIElement { app.buttons.matching(NSPredicate(format: "identifier == %@", "onboarding_subscribe_button")).firstMatch } + var skipButton: XCUIElement { app.buttons.matching(NSPredicate(format: "identifier == %@", "onboarding_skip_button")).firstMatch } + + // MARK: - Actions + + /// Swipe left to advance to the next onboarding page. + func swipeToNext() { + app.swipeLeft() + } + + /// Complete the full onboarding flow by swiping through all screens and tapping "Maybe Later". + func completeOnboarding() { + // Welcome -> swipe + if welcomeScreen.waitForExistence(timeout: 5) { + swipeToNext() + } + + // Time -> swipe + // Time screen doesn't have a unique identifier, just swipe + swipeToNext() + + // Day -> select Today, then swipe + if dayTodayButton.waitForExistence(timeout: 3) { + dayTodayButton.tap() + } + swipeToNext() + + // Style -> swipe + swipeToNext() + + // Subscription -> tap "Maybe Later" + if skipButton.waitForExistence(timeout: 5) { + skipButton.tap() + } + } + + // MARK: - Assertions + + func assertVisible(file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue( + welcomeScreen.waitForExistence(timeout: 5), + "Onboarding welcome screen should be visible", + file: file, line: line + ) + } + + func assertDismissed(file: StaticString = #file, line: UInt = #line) { + // After onboarding, the tab bar should be visible + let tabBar = app.tabBars.firstMatch + XCTAssertTrue( + tabBar.waitForExistence(timeout: 10), + "Tab bar should be visible after onboarding completes", + file: file, line: line + ) + } +} diff --git a/Tests iOS/Screens/SettingsScreen.swift b/Tests iOS/Screens/SettingsScreen.swift new file mode 100644 index 0000000..b183bfe --- /dev/null +++ b/Tests iOS/Screens/SettingsScreen.swift @@ -0,0 +1,74 @@ +// +// SettingsScreen.swift +// Tests iOS +// +// Screen object for the Settings tab (Customize + Settings sub-tabs). +// + +import XCTest + +struct SettingsScreen { + let app: XCUIApplication + + // MARK: - Elements + + var settingsHeader: XCUIElement { app.staticTexts["settings_header"] } + var customizeSegment: XCUIElement { app.buttons["Customize"] } + var settingsSegment: XCUIElement { app.buttons["Settings"] } + var upgradeBanner: XCUIElement { app.otherElements["upgrade_banner"] } + var subscribeButton: XCUIElement { app.buttons["subscribe_button"] } + 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 } + var analyticsToggle: XCUIElement { app.descendants(matching: .any).matching(identifier: "settings_analytics_toggle").firstMatch } + var showOnboardingButton: XCUIElement { app.buttons["settings_show_onboarding"].firstMatch } + + // MARK: - Actions + + func tapCustomizeTab() { + customizeSegment.tapWhenReady() + } + + func tapSettingsTab() { + settingsSegment.tapWhenReady() + } + + func tapClearData() { + clearDataButton.tapWhenReady() + } + + func tapAnalyticsToggle() { + // Scroll down to find the toggle if needed + let toggle = app.descendants(matching: .any).matching(identifier: "settings_analytics_toggle").firstMatch + if !toggle.isHittable { + app.swipeUp() + } + toggle.tapWhenReady() + } + + // MARK: - Assertions + + func assertVisible(file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue( + settingsHeader.waitForExistence(timeout: 5), + "Settings header should be visible", + file: file, line: line + ) + } + + func assertUpgradeBannerVisible(file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue( + upgradeBanner.waitForExistence(timeout: 5), + "Upgrade banner should be visible", + file: file, line: line + ) + } + + func assertUpgradeBannerHidden(file: StaticString = #file, line: UInt = #line) { + XCTAssertTrue( + upgradeBanner.waitForDisappearance(timeout: 5), + "Upgrade banner should be hidden (subscribed)", + file: file, line: line + ) + } +} diff --git a/Tests iOS/Screens/TabBarScreen.swift b/Tests iOS/Screens/TabBarScreen.swift new file mode 100644 index 0000000..e7d4f40 --- /dev/null +++ b/Tests iOS/Screens/TabBarScreen.swift @@ -0,0 +1,62 @@ +// +// TabBarScreen.swift +// Tests iOS +// +// Screen object for the main tab bar navigation. +// + +import XCTest + +struct TabBarScreen { + let app: XCUIApplication + + // MARK: - Tab Buttons (using localized labels) + + var dayTab: XCUIElement { app.tabBars.buttons["Day"] } + var monthTab: XCUIElement { app.tabBars.buttons["Month"] } + var yearTab: XCUIElement { app.tabBars.buttons["Year"] } + var insightsTab: XCUIElement { app.tabBars.buttons["Insights"] } + var settingsTab: XCUIElement { app.tabBars.buttons["Settings"] } + + // MARK: - Actions + + @discardableResult + func tapDay() -> DayScreen { + dayTab.tapWhenReady() + return DayScreen(app: app) + } + + @discardableResult + func tapMonth() -> TabBarScreen { + monthTab.tapWhenReady() + return self + } + + @discardableResult + func tapYear() -> TabBarScreen { + yearTab.tapWhenReady() + return self + } + + @discardableResult + func tapInsights() -> TabBarScreen { + insightsTab.tapWhenReady() + return self + } + + @discardableResult + func tapSettings() -> SettingsScreen { + settingsTab.tapWhenReady() + return SettingsScreen(app: app) + } + + // MARK: - Assertions + + func assertDayTabSelected() { + XCTAssertTrue(dayTab.isSelected, "Day tab should be selected") + } + + func assertTabBarVisible() { + XCTAssertTrue(dayTab.waitForExistence(timeout: 5), "Tab bar should be visible") + } +}