diff --git a/Feels.xcodeproj/project.pbxproj b/Feels.xcodeproj/project.pbxproj index 18d9431..0c816ec 100644 --- a/Feels.xcodeproj/project.pbxproj +++ b/Feels.xcodeproj/project.pbxproj @@ -28,13 +28,16 @@ 1CDEFBBF2F3B8736006AE6A1 /* Configuration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 1CDEFBBE2F3B8736006AE6A1 /* Configuration.storekit */; }; 1CDEFBC02F3B8736006AE6A1 /* Configuration.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 1CDEFBBE2F3B8736006AE6A1 /* Configuration.storekit */; }; 2EE4D94530F6BF39B26FB4D4 /* DayScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427CD9C91D43AB6A0302B4DD /* DayScreen.swift */; }; + 343D472E5524E2E8ED59A7CC /* DateLocaleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF843FEBE18F8FF570CC4CCB /* DateLocaleTests.swift */; }; 39C43652C41F5459788A604D /* SpanishLocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C2982F0B879A0C57273F0E /* SpanishLocalizationTests.swift */; }; 46F07FA9D330456697C9AC29 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD90B47278C7E7A001C4FEA /* WidgetKit.framework */; }; 4F1C717B7747918A459322CB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4D304CD05CC7C662CCD7DCB /* Foundation.framework */; }; 54259F7B3F4E959B3F4055E4 /* StreakTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29E2A2FC314F88244CA946BF /* StreakTests.swift */; }; + 624CA4AB557BB0C30A0E2198 /* LongTranslationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7E6C56A47EB49419BFA77C /* LongTranslationTests.swift */; }; 6F9C9C4B50CF8C1769171FF9 /* NoteEditTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 469470483072085BE9E04E12 /* NoteEditTests.swift */; }; 756B9857B0657D2DB2D6D4E2 /* AppResumeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0359E1D32D936859E5A0C9F3 /* AppResumeTests.swift */; }; 85EF4702AE378AB3198E67D3 /* AccessibilityTextSizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C033EE00E7E7B3448FB862DA /* AccessibilityTextSizeTests.swift */; }; + 8F39BFEBFC387DBDA42CBDA5 /* OnboardingVotingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D5EECC086A9E7F469B5873 /* OnboardingVotingTests.swift */; }; 92C1523E0398F866DB4CA027 /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881CA8B21231D67DED575502 /* SettingsScreen.swift */; }; 9559409B5AEEAB40EBCB6AF9 /* VoteLogicsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD717F91BD65382B7DDFE3C4 /* VoteLogicsTests.swift */; }; A018FE95582C04ED0F1806DC /* BaseUITestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CE4110A0D8FBBAD7F92BDF /* BaseUITestCase.swift */; }; @@ -159,6 +162,7 @@ 2C8D04ACF01F539EA572EEB8 /* ReduceMotionTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReduceMotionTests.swift; sourceTree = ""; }; 31C2982F0B879A0C57273F0E /* SpanishLocalizationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SpanishLocalizationTests.swift; sourceTree = ""; }; 35AF32CC88B36CDFCB338F2C /* TrialExpirationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrialExpirationTests.swift; sourceTree = ""; }; + 37D5EECC086A9E7F469B5873 /* OnboardingVotingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OnboardingVotingTests.swift; sourceTree = ""; }; 427CD9C91D43AB6A0302B4DD /* DayScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DayScreen.swift; sourceTree = ""; }; 469470483072085BE9E04E12 /* NoteEditTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteEditTests.swift; sourceTree = ""; }; 5354C23DD5FC67C1C97482F2 /* WaitHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitHelpers.swift; sourceTree = ""; }; @@ -196,8 +200,10 @@ D6E7F8A9B0C1D2E3F4A5B6C7 /* IconPackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconPackTests.swift; sourceTree = ""; }; D9475CC3818201762FA57D91 /* YearViewHeatmapTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = YearViewHeatmapTests.swift; sourceTree = ""; }; DA0D74ACDD741CFA1F14F50F /* FeelsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FeelsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DA7E6C56A47EB49419BFA77C /* LongTranslationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LongTranslationTests.swift; sourceTree = ""; }; DD44444444444444DDDDDDDD /* EntryDetailTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryDetailTests.swift; sourceTree = ""; }; DD717F91BD65382B7DDFE3C4 /* VoteLogicsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = VoteLogicsTests.swift; sourceTree = ""; }; + DF843FEBE18F8FF570CC4CCB /* DateLocaleTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DateLocaleTests.swift; sourceTree = ""; }; E1F2A3B4C5D6E7F8A9B0C1D2 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = ""; }; E5F6A7B8C9D0E1F2A3B4C5D6 /* EmptyStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateTests.swift; sourceTree = ""; }; E7F8A9B0C1D2E3F4A5B6C7D8 /* PremiumCustomizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumCustomizationTests.swift; sourceTree = ""; }; @@ -451,6 +457,9 @@ D9475CC3818201762FA57D91 /* YearViewHeatmapTests.swift */, 2C8D04ACF01F539EA572EEB8 /* ReduceMotionTests.swift */, F5A135CC76572BAD0445B0DD /* HighContrastTests.swift */, + 37D5EECC086A9E7F469B5873 /* OnboardingVotingTests.swift */, + DF843FEBE18F8FF570CC4CCB /* DateLocaleTests.swift */, + DA7E6C56A47EB49419BFA77C /* LongTranslationTests.swift */, ); path = "Tests iOS"; sourceTree = ""; @@ -863,6 +872,9 @@ 19F8D7CA5D384E82A4BB4BCA /* YearViewHeatmapTests.swift in Sources */, 141C5C51CA0658F682E984F5 /* ReduceMotionTests.swift in Sources */, F5C77B3C81A9180E138BF226 /* HighContrastTests.swift in Sources */, + 8F39BFEBFC387DBDA42CBDA5 /* OnboardingVotingTests.swift in Sources */, + 343D472E5524E2E8ED59A7CC /* DateLocaleTests.swift in Sources */, + 624CA4AB557BB0C30A0E2198 /* LongTranslationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tests iOS/DateLocaleTests.swift b/Tests iOS/DateLocaleTests.swift new file mode 100644 index 0000000..67728de --- /dev/null +++ b/Tests iOS/DateLocaleTests.swift @@ -0,0 +1,83 @@ +// +// DateLocaleTests.swift +// Tests iOS +// +// TC-139: Date formatting matches locale (German locale uses DD.MM.YYYY format). +// + +import XCTest + +final class DateLocaleTests: BaseUITestCase { + override var seedFixture: String? { "week_of_moods" } + override var bypassSubscription: Bool { true } + + override func setUp() { + // Do NOT call super — we need custom locale launch args + continueAfterFailure = false + + let application = XCUIApplication() + let args: [String] = [ + "--ui-testing", "--disable-animations", + "--reset-state", + "--bypass-subscription", + "--skip-onboarding", + "-AppleLanguages", "(de)", + "-AppleLocale", "de_DE" + ] + application.launchArguments = args + application.launchEnvironment = ["UI_TEST_FIXTURE": "week_of_moods"] + application.launch() + app = application + } + + /// TC-139: German locale displays German month/weekday names. + func testGermanLocale_DateFormattingMatchesLocale() { + // Tab bar should load + let tabBar = app.tabBars.firstMatch + XCTAssertTrue(tabBar.waitForExistence(timeout: 5), "Tab bar should exist") + + captureScreenshot(name: "german_locale_day_tab") + + // Navigate to Year View via tab bar + // In German, Year tab may be labeled "Jahr" or use accessibility ID + let yearTabButton = app.tabBars.buttons["Jahr"] + if yearTabButton.waitForExistence(timeout: 3) { + yearTabButton.tap() + } else { + // Fallback: tap by index (year is the 3rd tab) + let allButtons = app.tabBars.buttons.allElementsBoundByIndex + if allButtons.count >= 3 { + allButtons[2].tap() + } + } + + // Year view should show German month abbreviations + // German months: Jan, Feb, Mär, Apr, Mai, Jun, Jul, Aug, Sep, Okt, Nov, Dez + let germanMonth = app.staticTexts.matching( + NSPredicate(format: "label CONTAINS[c] 'Feb' OR label CONTAINS[c] 'Mär' OR label CONTAINS[c] 'Okt' OR label CONTAINS[c] 'Dez'") + ).firstMatch + + let hasGermanDate = germanMonth.waitForExistence(timeout: 5) + + captureScreenshot(name: "german_locale_year_tab") + + // Navigate to Settings to verify German "Einstellungen" text + let settingsButton = app.tabBars.buttons["Einstellungen"] + if settingsButton.waitForExistence(timeout: 3) { + settingsButton.tap() + } else { + let allButtons = app.tabBars.buttons.allElementsBoundByIndex + if allButtons.count >= 5 { + allButtons[4].tap() + } + } + + let settingsHeader = app.element(UITestID.Settings.header) + XCTAssertTrue( + settingsHeader.waitForExistence(timeout: 5), + "Settings header should be visible in German locale" + ) + + captureScreenshot(name: "german_locale_settings") + } +} diff --git a/Tests iOS/LongTranslationTests.swift b/Tests iOS/LongTranslationTests.swift new file mode 100644 index 0000000..311976a --- /dev/null +++ b/Tests iOS/LongTranslationTests.swift @@ -0,0 +1,76 @@ +// +// LongTranslationTests.swift +// Tests iOS +// +// TC-138: Long translations (German) don't truncate critical UI text. +// + +import XCTest + +final class LongTranslationTests: BaseUITestCase { + override var seedFixture: String? { "single_mood" } + override var bypassSubscription: Bool { true } + + override func setUp() { + // Do NOT call super — we need German locale (known for long compound words) + continueAfterFailure = false + + let application = XCUIApplication() + let args: [String] = [ + "--ui-testing", "--disable-animations", + "--reset-state", + "--bypass-subscription", + "--skip-onboarding", + "-AppleLanguages", "(de)", + "-AppleLocale", "de_DE" + ] + application.launchArguments = args + application.launchEnvironment = ["UI_TEST_FIXTURE": "single_mood"] + application.launch() + app = application + } + + /// TC-138: German locale with long compound words renders without crashes. + /// Navigates through all tabs to ensure no layout truncation causes issues. + func testLongTranslations_GermanLocale_NoLayoutCrash() { + // Day tab should load + let tabBar = app.tabBars.firstMatch + XCTAssertTrue(tabBar.waitForExistence(timeout: 5), "Tab bar should exist") + + captureScreenshot(name: "german_long_day") + + // Navigate to Month view + let monthTab = app.tabBars.buttons.element(boundBy: 1) + monthTab.tap() + _ = app.waitForExistence(timeout: 2) + captureScreenshot(name: "german_long_month") + + // Navigate to Year view + let yearTab = app.tabBars.buttons.element(boundBy: 2) + yearTab.tap() + _ = app.waitForExistence(timeout: 2) + captureScreenshot(name: "german_long_year") + + // Navigate to Settings + let settingsTab = app.tabBars.buttons.element(boundBy: 4) + settingsTab.tap() + + let settingsHeader = app.element(UITestID.Settings.header) + XCTAssertTrue( + settingsHeader.waitForExistence(timeout: 5), + "Settings header should be visible in German locale" + ) + + captureScreenshot(name: "german_long_settings") + + // Verify no truncation indicators ("..." / ellipsis) in key labels + // Check that "Einstellungen" (Settings) text is fully rendered + let einstellungenText = app.staticTexts.matching( + NSPredicate(format: "label == %@", "Einstellungen") + ).firstMatch + XCTAssertTrue( + einstellungenText.waitForExistence(timeout: 3), + "Full German 'Einstellungen' text should be visible (not truncated)" + ) + } +} diff --git a/Tests iOS/OnboardingVotingTests.swift b/Tests iOS/OnboardingVotingTests.swift new file mode 100644 index 0000000..cdcaf37 --- /dev/null +++ b/Tests iOS/OnboardingVotingTests.swift @@ -0,0 +1,85 @@ +// +// OnboardingVotingTests.swift +// Tests iOS +// +// TC-122: Onboarding day voting — Today vs Yesterday selection. +// + +import XCTest + +final class OnboardingVotingTests: BaseUITestCase { + override var seedFixture: String? { "empty" } + override var skipOnboarding: Bool { false } + + /// TC-122: Tapping Today and Yesterday buttons toggles the selection. + func testOnboarding_DayVoting_TodayAndYesterday() { + let onboarding = OnboardingScreen(app: app) + + // Wait for welcome screen + XCTAssertTrue( + onboarding.welcomeScreen.waitForExistence(timeout: 10), + "Onboarding welcome screen should appear" + ) + + // Swipe exactly 2 times: Welcome → Time → Day + swipeToNext() + swipeToNext() + + // Look for the "Which day should" title text to confirm we're on the day page + let dayTitle = app.staticTexts.matching( + NSPredicate(format: "label CONTAINS[c] 'Which day'") + ).firstMatch + + // If not found, try one more swipe (may need 3 depending on animation) + if !dayTitle.waitForExistence(timeout: 3) { + swipeToNext() + } + + XCTAssertTrue( + dayTitle.waitForExistence(timeout: 5), + "Day screen title 'Which day should you rate?' should be visible" + ) + + captureScreenshot(name: "onboarding_day_screen") + + // Tap the Yesterday card by looking for its text + let yesterdayText = app.staticTexts["Yesterday, Rate the previous day"] + let todayText = app.staticTexts["Today, Rate the current day"] + + // Fallback: try the button by accessibility identifier + let yesterdayButton = app.element(UITestID.Onboarding.dayYesterday) + let todayButton = app.element(UITestID.Onboarding.dayToday) + + // Try tapping Yesterday via text label or accessibility ID + if yesterdayButton.exists { + yesterdayButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() + } else if yesterdayText.exists { + yesterdayText.tap() + } else { + // Fallback: tap coordinate at roughly the "Yesterday" card position + app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.72)).tap() + } + + captureScreenshot(name: "onboarding_day_yesterday_tapped") + + // Tap Today + if todayButton.exists { + todayButton.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap() + } else if todayText.exists { + todayText.tap() + } else { + app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.60)).tap() + } + + captureScreenshot(name: "onboarding_day_today_tapped") + } + + // MARK: - Private + + private func swipeToNext() { + let start = app.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.18)) + let end = app.coordinate(withNormalizedOffset: CGVector(dx: 0.1, dy: 0.18)) + start.press(forDuration: 0.05, thenDragTo: end) + _ = app.waitForExistence(timeout: 1.0) + } +} diff --git a/docs/Feels_QA_Test_Plan.xlsx b/docs/Feels_QA_Test_Plan.xlsx index 8b3d510..7d3b9b0 100644 Binary files a/docs/Feels_QA_Test_Plan.xlsx and b/docs/Feels_QA_Test_Plan.xlsx differ