diff --git a/Feels.xcodeproj/project.pbxproj b/Feels.xcodeproj/project.pbxproj index 57d19cf..ca10152 100644 --- a/Feels.xcodeproj/project.pbxproj +++ b/Feels.xcodeproj/project.pbxproj @@ -48,12 +48,14 @@ B8C9D0E100000000D6E7F8A9 /* MonthViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C9D0E1F2A3B4C5D6E7F8A9 /* MonthViewTests.swift */; }; BB22220022222200BBBBBBBB /* MoodLoggingEmptyStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB22222222222222BBBBBBBB /* MoodLoggingEmptyStateTests.swift */; }; C0137E33A722F405FDC457B5 /* YearViewCollapseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542B1A71F9990806CD88B285 /* YearViewCollapseTests.swift */; }; + C1A23F0250FD3380F0A0F999 /* DeepLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9A9583BE84E8DC13DB61F2B /* DeepLinkTests.swift */; }; C1D2E3F400000000E9FA0B1C /* MonthViewInteractionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D2E3F4A5B6C7D8E9FA0B1C /* MonthViewInteractionTests.swift */; }; C26D40397E1AA24816FB3751 /* TabBarScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CDDCB9C85BAE71C679C0BF /* TabBarScreen.swift */; }; C3D4E500000000E1F2A3B4C5 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D4E5F6A7B8C9D0E1F2A3B4 /* OnboardingScreen.swift */; }; C5D6E7F800000000E3F4A5B6 /* AppThemeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5D6E7F8A9B0C1D2E3F4A5B6 /* AppThemeTests.swift */; }; C9D0E1F200000000E7F8A9B0 /* SettingsActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D0E1F2A3B4C5D6E7F8A9B0 /* SettingsActionTests.swift */; }; CC33330033333300CCCCCCCC /* MoodLoggingWithDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC33333333333333CCCCCCCC /* MoodLoggingWithDataTests.swift */; }; + CFB41ED5D294B8997DB694E2 /* TrialExpirationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35AF32CC88B36CDFCB338F2C /* TrialExpirationTests.swift */; }; D0E1F2A300000000F8A9B0C1 /* CustomizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E1F2A3B4C5D6E7F8A9B0C1 /* CustomizationTests.swift */; }; D1AD0A0469EADFB1446E9B09 /* YearViewDisplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0246E9F406F872E5DEEB7269 /* YearViewDisplayTests.swift */; }; D4E5F6A700000000F2A3B4C5 /* MoodReplacementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4E5F6A7B8C9D0E1F2A3B4C5 /* MoodReplacementTests.swift */; }; @@ -148,6 +150,7 @@ 21CD463209E0909393545D62 /* TrialBannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrialBannerTests.swift; sourceTree = ""; }; 29CE4110A0D8FBBAD7F92BDF /* BaseUITestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUITestCase.swift; sourceTree = ""; }; 29E2A2FC314F88244CA946BF /* StreakTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StreakTests.swift; sourceTree = ""; }; + 35AF32CC88B36CDFCB338F2C /* TrialExpirationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrialExpirationTests.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 = ""; }; @@ -192,6 +195,7 @@ F4D304CD05CC7C662CCD7DCB /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; F6A7B8C9D0E1F2A3B4C5D6E7 /* EntryDeleteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryDeleteTests.swift; sourceTree = ""; }; F8A9B0C1D2E3F4A5B6C7D8E9 /* HeaderMoodLoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderMoodLoggingTests.swift; sourceTree = ""; }; + F9A9583BE84E8DC13DB61F2B /* DeepLinkTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeepLinkTests.swift; sourceTree = ""; }; FF66666666666666FFFFFFFF /* SecondaryTabTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryTabTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -427,6 +431,8 @@ 0246E9F406F872E5DEEB7269 /* YearViewDisplayTests.swift */, 542B1A71F9990806CD88B285 /* YearViewCollapseTests.swift */, BE4D06D4E7188339DE8BC040 /* SettingsLegalLinksTests.swift */, + F9A9583BE84E8DC13DB61F2B /* DeepLinkTests.swift */, + 35AF32CC88B36CDFCB338F2C /* TrialExpirationTests.swift */, ); path = "Tests iOS"; sourceTree = ""; @@ -831,6 +837,8 @@ D1AD0A0469EADFB1446E9B09 /* YearViewDisplayTests.swift in Sources */, C0137E33A722F405FDC457B5 /* YearViewCollapseTests.swift in Sources */, E78F98C41E83B81DAF43139E /* SettingsLegalLinksTests.swift in Sources */, + C1A23F0250FD3380F0A0F999 /* DeepLinkTests.swift in Sources */, + CFB41ED5D294B8997DB694E2 /* TrialExpirationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Tests iOS/DeepLinkTests.swift b/Tests iOS/DeepLinkTests.swift new file mode 100644 index 0000000..809b691 --- /dev/null +++ b/Tests iOS/DeepLinkTests.swift @@ -0,0 +1,73 @@ +// +// DeepLinkTests.swift +// Tests iOS +// +// TC-125, TC-126: Deep link handling. +// + +import XCTest + +final class DeepLinkTests: BaseUITestCase { + override var seedFixture: String? { "empty" } + override var bypassSubscription: Bool { false } + override var expireTrial: Bool { true } + + /// TC-126: Opening a malformed deep link does not crash the app. + func testDeepLink_MalformedURL_NoCrash() { + // Verify app launched and is on Day tab + let tabBar = TabBarScreen(app: app) + XCTAssertTrue( + tabBar.dayTab.waitForExistence(timeout: 5), + "App should launch to Day tab" + ) + + // Send a malformed deep link + let malformedURL = URL(string: "feels://invalidpath")! + app.open(malformedURL) + + // App should still be running and responsive — verify Day tab still exists + XCTAssertTrue( + tabBar.dayTab.waitForExistence(timeout: 5), + "App should remain functional after malformed deep link" + ) + + // Navigate to another tab to verify full responsiveness + tabBar.tapYear() + XCTAssertTrue( + tabBar.yearTab.waitForExistence(timeout: 3), + "App should be fully navigable after malformed deep link" + ) + + captureScreenshot(name: "deeplink_malformed_no_crash") + } + + /// TC-125: feels://subscribe opens subscription view. + func testDeepLink_Subscribe_OpensPaywall() { + // Verify app launched + let tabBar = TabBarScreen(app: app) + XCTAssertTrue( + tabBar.dayTab.waitForExistence(timeout: 5), + "App should launch to Day tab" + ) + + captureScreenshot(name: "deeplink_before_subscribe") + + // Send subscribe deep link + let subscribeURL = URL(string: "feels://subscribe")! + app.open(subscribeURL) + + // Subscription view should appear as a sheet. + // Look for common subscription UI elements. + let subscribeText = app.staticTexts.matching( + NSPredicate(format: "label CONTAINS[c] 'subscribe' OR label CONTAINS[c] 'premium' OR label CONTAINS[c] 'upgrade' OR label CONTAINS[c] 'Feels+'") + ).firstMatch + + let found = subscribeText.waitForExistence(timeout: 8) + + captureScreenshot(name: "deeplink_subscribe_result") + + XCTAssertTrue(found, + "Subscription view should appear after feels://subscribe deep link" + ) + } +} diff --git a/Tests iOS/TrialExpirationTests.swift b/Tests iOS/TrialExpirationTests.swift new file mode 100644 index 0000000..10fc63e --- /dev/null +++ b/Tests iOS/TrialExpirationTests.swift @@ -0,0 +1,34 @@ +// +// TrialExpirationTests.swift +// Tests iOS +// +// TC-078: Trial expiration state is reflected in the UI. +// + +import XCTest + +final class TrialExpirationTests: BaseUITestCase { + override var seedFixture: String? { "empty" } + override var bypassSubscription: Bool { false } + override var expireTrial: Bool { true } + + /// TC-078: When trial is expired, Settings shows "Trial expired" text + /// and the upgrade banner is visible. + func testTrialExpired_ShowsExpiredBanner() { + let tabBar = TabBarScreen(app: app) + let settingsScreen = tabBar.tapSettings() + settingsScreen.assertVisible() + + // Verify upgrade banner is visible (trial expired, not subscribed) + settingsScreen.assertUpgradeBannerVisible() + + // Check for "Trial expired" text in the banner + let expiredText = app.staticTexts["Trial expired"] + XCTAssertTrue( + expiredText.waitForExistence(timeout: 5), + "Settings should show 'Trial expired' text when trial has expired" + ) + + captureScreenshot(name: "trial_expired_banner") + } +} diff --git a/docs/Feels_QA_Test_Plan.xlsx b/docs/Feels_QA_Test_Plan.xlsx index e1122c8..ba8fed8 100644 Binary files a/docs/Feels_QA_Test_Plan.xlsx and b/docs/Feels_QA_Test_Plan.xlsx differ