From 08275d0301bdee16a40aa34bd39a0073e016966f Mon Sep 17 00:00:00 2001 From: Amy Date: Mon, 13 Jul 2026 22:29:22 -0400 Subject: [PATCH 1/3] Move tracking-related tests from ProjectPageViewModelTests into new file, ProjectPageViewModel_TrackingTests --- Kickstarter.xcodeproj/project.pbxproj | 4 + .../ProjectPageViewModelTests.swift | 485 ---------------- .../ProjectPageViewModel_TrackingTests.swift | 519 ++++++++++++++++++ 3 files changed, 523 insertions(+), 485 deletions(-) create mode 100644 LibraryTests/Library/ViewModels/ProjectPageViewModel_TrackingTests.swift diff --git a/Kickstarter.xcodeproj/project.pbxproj b/Kickstarter.xcodeproj/project.pbxproj index 97d9b05845..6c900d505f 100644 --- a/Kickstarter.xcodeproj/project.pbxproj +++ b/Kickstarter.xcodeproj/project.pbxproj @@ -313,6 +313,7 @@ E108416C2DC27F24000E4F17 /* (null) in Sources */ = {isa = PBXBuildFile; }; E10BE8D02B02975C00F73DC9 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10BE8CF2B02975C00F73DC9 /* NotificationService.swift */; }; E10BE8D42B02975C00F73DC9 /* RichPushNotifications.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E10BE8CD2B02975C00F73DC9 /* RichPushNotifications.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + E10FED153005D38600EAF400 /* ProjectPageViewModel_TrackingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10FED143005D38600EAF400 /* ProjectPageViewModel_TrackingTests.swift */; }; E113BD842B7D255000D3A809 /* Library_Keychain_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113BD832B7D255000D3A809 /* Library_Keychain_iOSTests.swift */; }; E113BD8D2B7D255A00D3A809 /* KeychainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E113BD772B7D1B7700D3A809 /* KeychainTests.swift */; }; E182E5BA2B8CDFDE0008DD69 /* AppEnvironmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F121E830FDC00BFFA01 /* AppEnvironmentTests.swift */; }; @@ -649,6 +650,7 @@ E10BE8CD2B02975C00F73DC9 /* RichPushNotifications.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = RichPushNotifications.appex; sourceTree = BUILT_PRODUCTS_DIR; }; E10BE8CF2B02975C00F73DC9 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; E10BE8D12B02975C00F73DC9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E10FED143005D38600EAF400 /* ProjectPageViewModel_TrackingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectPageViewModel_TrackingTests.swift; sourceTree = ""; }; E113BD772B7D1B7700D3A809 /* KeychainTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainTests.swift; sourceTree = ""; }; E113BD812B7D255000D3A809 /* Library-Keychain-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Library-Keychain-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; E113BD832B7D255000D3A809 /* Library_Keychain_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Library_Keychain_iOSTests.swift; sourceTree = ""; }; @@ -1103,6 +1105,7 @@ AAF5486C2F2445C0001DB5AC /* ProjectNotificationCellViewModelTests.swift */, AAF5486D2F2445C0001DB5AC /* ProjectNotificationsViewModelTest.swift */, AAF5486E2F2445C0001DB5AC /* ProjectPageViewModelTests.swift */, + E10FED143005D38600EAF400 /* ProjectPageViewModel_TrackingTests.swift */, AAF5486F2F2445C0001DB5AC /* ProjectPamphletContentViewModelTests.swift */, AAF548702F2445C0001DB5AC /* ProjectPamphletCreatorHeaderCellViewModelTests.swift */, AAF548712F2445C0001DB5AC /* ProjectPamphletMainCellViewModelTests.swift */, @@ -1754,6 +1757,7 @@ AAF5491B2F2445C0001DB5AC /* HelpViewModelTests.swift in Sources */, AAF5491C2F2445C0001DB5AC /* KSRSearchBarViewModelTests.swift in Sources */, AAF5491D2F2445C0001DB5AC /* LoadingBarButtonItemViewModelTests.swift in Sources */, + E10FED153005D38600EAF400 /* ProjectPageViewModel_TrackingTests.swift in Sources */, AAF5491E2F2445C0001DB5AC /* LoadingButtonViewModelTests.swift in Sources */, AAF5491F2F2445C0001DB5AC /* LoginSignupUseCaseTests.swift in Sources */, AAF549202F2445C0001DB5AC /* LoginToutViewModelTests.swift in Sources */, diff --git a/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift b/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift index 44f9440a21..b97658df46 100644 --- a/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift +++ b/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift @@ -531,337 +531,6 @@ final class ProjectPageViewModelTests: TestCase { } } - // Tests that ref tags and referral credit cookies are tracked and saved like we expect. - func testTracksRefTag() { - let project = Project.template - let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) - - withEnvironment(apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([ - Reward.noReward, - Reward.template - ]) - )) { - self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.category)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) - - self.scheduler.advance() - - XCTAssertEqual( - ["Page Viewed"], - self.segmentTrackingClient.events, "A project page event is tracked." - ) - - XCTAssertEqual( - [RefTag.category.stringTag], - self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, - "The ref tag is tracked in the event." - ) - XCTAssertEqual( - 1, self.cookieStorage.cookies?.count, - "A single cookie is set" - ) - XCTAssertEqual( - "ref_\(project.id)", self.cookieStorage.cookies?.last?.name, - "A referral cookie is set for the project." - ) - XCTAssertEqual( - "category?", - (self.cookieStorage.cookies?.last?.value.prefix(9)).map(String.init), - "A referral cookie is set for the category ref tag." - ) - - // Start up another view model with the same project - let newVm: ProjectPageViewModelType = ProjectPageViewModel() - newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.recommended)) - newVm.inputs.viewDidLoad() - newVm.inputs.viewDidAppear(animated: true) - - self.scheduler.advance() - - XCTAssertEqual( - [ - "Page Viewed", "Page Viewed" - ], - self.segmentTrackingClient.events, "A project page event is tracked." - ) - - XCTAssertEqual( - [ - RefTag.category.stringTag, - RefTag.recommended.stringTag - ], - self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, - "The new ref tag is tracked in an event." - ) - XCTAssertEqual( - 1, self.cookieStorage.cookies?.count, - "A single cookie has been set." - ) - } - } - - // Tests that ref tags for similar projects and referral credit cookies are tracked and saved like we expect. - func testTracksRefTag_SimilarProjects() { - let project = Project.template - let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) - - withEnvironment(apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([ - Reward.noReward, - Reward.template - ]) - )) { - self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.similarProjects)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) - - self.scheduler.advance() - - XCTAssertEqual( - ["Page Viewed"], - self.segmentTrackingClient.events, "A project page event is tracked." - ) - - XCTAssertEqual( - [RefTag.similarProjects.stringTag], - self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, - "The ref tag is tracked in the event." - ) - XCTAssertEqual( - 1, self.cookieStorage.cookies?.count, - "A single cookie is set" - ) - XCTAssertEqual( - "ref_\(project.id)", self.cookieStorage.cookies?.last?.name, - "A referral cookie is set for the project." - ) - - // Start up another view model with the same project - let newVm: ProjectPageViewModelType = ProjectPageViewModel() - newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.recommended)) - newVm.inputs.viewDidLoad() - newVm.inputs.viewDidAppear(animated: true) - - self.scheduler.advance() - - XCTAssertEqual( - [ - "Page Viewed", "Page Viewed" - ], - self.segmentTrackingClient.events, "A project page event is tracked." - ) - - XCTAssertEqual( - [ - RefTag.similarProjects.stringTag, - RefTag.recommended.stringTag - ], - self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, - "The new ref tag is tracked in an event." - ) - XCTAssertEqual( - 1, self.cookieStorage.cookies?.count, - "A single cookie has been set." - ) - } - } - - func testProjectPageViewed_Tracking_OnError() { - let service = MockService(fetchProjectPamphletResult: .failure(.couldNotParseJSON)) - - withEnvironment(apiService: service) { - self.configureInitialState(.init(left: .template)) - - self.scheduler.advance() - - XCTAssertEqual( - [], - self.segmentTrackingClient.events, - "Project Page Viewed doesnt track if the request fails" - ) - } - } - - func testProjectPageViewed_OnViewDidAppear() { - let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) - - withEnvironment(apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([Reward.noReward, Reward.template]) - )) { - XCTAssertEqual([], self.segmentTrackingClient.events) - - self.configureInitialState(.init(left: .template)) - - self.scheduler.advance() - - XCTAssertEqual(["Page Viewed"], self.segmentTrackingClient.events) - - XCTAssertEqual(["project"], self.segmentTrackingClient.properties(forKey: "context_page")) - XCTAssertEqual(["overview"], self.segmentTrackingClient.properties(forKey: "context_section")) - XCTAssertEqual(["discovery"], self.segmentTrackingClient.properties(forKey: "session_ref_tag")) - } - } - - func testMockCookieStorageSet_SeparateSchedulers() { - let project = Project.template - let scheduler1 = TestScheduler(startDate: MockDate().date) - let scheduler2 = TestScheduler(startDate: scheduler1.currentDate.addingTimeInterval(1)) - let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) - - withEnvironment( - apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([.template]) - ), - scheduler: scheduler1 - ) { - let newVm: ProjectPageViewModelType = ProjectPageViewModel() - newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.category)) - newVm.inputs.viewDidLoad() - newVm.inputs.viewDidAppear(animated: true) - - scheduler1.advance() - - XCTAssertEqual(1, self.cookieStorage.cookies?.count, "A single cookie has been set.") - } - - withEnvironment( - apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([.template]) - ), - scheduler: scheduler2 - ) { - let newVm: ProjectPageViewModelType = ProjectPageViewModel() - newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.recommended)) - newVm.inputs.viewDidLoad() - newVm.inputs.viewDidAppear(animated: true) - - scheduler2.advance() - - XCTAssertEqual(2, self.cookieStorage.cookies?.count, "Two cookies are set on separate schedulers.") - } - } - - func testMockCookieStorageSet_SameScheduler() { - let project = Project.template - let scheduler1 = TestScheduler(startDate: MockDate().date) - - withEnvironment(scheduler: scheduler1) { - let newVm: ProjectPageViewModelType = ProjectPageViewModel() - newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.category)) - newVm.inputs.viewDidLoad() - newVm.inputs.viewDidAppear(animated: true) - - scheduler1.advance() - - XCTAssertEqual(1, self.cookieStorage.cookies?.count, "A single cookie has been set.") - } - - withEnvironment(scheduler: scheduler1) { - let newVm: ProjectPageViewModelType = ProjectPageViewModel() - newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.recommended)) - newVm.inputs.viewDidLoad() - newVm.inputs.viewDidAppear(animated: true) - - scheduler1.advance() - - XCTAssertEqual( - 1, self.cookieStorage.cookies?.count, - "A single cookie has been set on the same scheduler." - ) - } - } - - func testTracksRefTag_WithBadData() { - let project = Project.template - let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) - - withEnvironment(apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectFriendsResult: .success([.template]), - fetchProjectRewardsResult: .success([.template]) - )) { - self.vm.inputs.configureWith( - projectOrParam: .left(project), refInfo: RefInfo(.unrecognized("category%3F1232")) - ) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) - - self.scheduler.advance() - - XCTAssertEqual( - ["Page Viewed"], - self.segmentTrackingClient.events, "A project page event is tracked." - ) - - XCTAssertEqual( - [RefTag.category.stringTag], - self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, - "The ref tag is tracked in the event." - ) - XCTAssertEqual( - 1, self.cookieStorage.cookies?.count, - "A single cookie is set" - ) - XCTAssertEqual( - "ref_\(project.id)", self.cookieStorage.cookies?.last?.name, - "A referral cookie is set for the project." - ) - XCTAssertEqual( - "category?", - (self.cookieStorage.cookies?.last?.value.prefix(9)).map(String.init), - "A referral cookie is set for the category ref tag." - ) - - // Start up another view model with the same project - let newVm: ProjectPageViewModelType = ProjectPageViewModel() - newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.recommended)) - newVm.inputs.viewDidLoad() - newVm.inputs.viewDidAppear(animated: true) - - self.scheduler.advance() - - XCTAssertEqual( - [ - "Page Viewed", "Page Viewed" - ], - self.segmentTrackingClient.events, "A project page event is tracked." - ) - - XCTAssertEqual( - [ - RefTag.category.stringTag, - RefTag.recommended.stringTag - ], - self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, - "The new ref tag is tracked in an event." - ) - XCTAssertEqual( - 1, self.cookieStorage.cookies?.count, - "A single cookie has been set." - ) - } - } - - func testTrackingDoesNotOccurOnLoad() { - let project = Project.template - - self.vm.inputs.configureWith( - projectOrParam: .left(project), refInfo: RefInfo(.unrecognized("category%3F1232")) - ) - self.vm.inputs.viewDidLoad() - - self.scheduler.advance() - - XCTAssertEqual([], self.segmentTrackingClient.events) - } - func testGoToComments() { self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) @@ -1589,115 +1258,6 @@ final class ProjectPageViewModelTests: TestCase { self.dismissManagePledgeAndShowMessageBannerWithMessage.assertValues(["Your changes have been saved"]) } - func testTrackingProjectPageViewed_LoggedIn_AdvertisingConsentNotAllowed_EventsNotTracked() { - let segmentClient = MockTrackingClient() - let appTrackingTransparency = MockAppTrackingTransparency() - appTrackingTransparency.shouldRequestAuthStatus = false - let ksrAnalytics = KSRAnalytics( - config: .template, - loggedInUser: User.template, - segmentClient: segmentClient, - appTrackingTransparency: appTrackingTransparency - ) - - let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) - - withEnvironment( - apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([Reward.noReward, Reward.template]) - ), - currentUser: User.template, - ksrAnalytics: ksrAnalytics - ) { - self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) - - self.scheduler.advance() - - XCTAssertEqual(segmentClient.events, []) - } - } - - func testTrackingProjectPageViewed_LoggedIn_AdvertisingConsentAllowed_EventsTracked() { - let segmentClient = MockTrackingClient() - let ksrAnalytics = KSRAnalytics( - config: .template, - loggedInUser: User.template, - segmentClient: segmentClient, - appTrackingTransparency: MockAppTrackingTransparency() - ) - - let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) - - withEnvironment( - apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([Reward.noReward, Reward.template]) - ), - currentUser: User.template, - ksrAnalytics: ksrAnalytics - ) { - self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) - - self.scheduler.advance() - - XCTAssertEqual(segmentClient.events, ["Page Viewed"]) - - XCTAssertEqual(segmentClient.properties(forKey: "session_user_is_logged_in", as: Bool.self), [true]) - XCTAssertEqual(segmentClient.properties(forKey: "user_uid", as: String.self), ["1"]) - XCTAssertEqual(segmentClient.properties(forKey: "session_ref_tag"), ["discovery"]) - XCTAssertEqual(segmentClient.properties(forKey: "project_subcategory"), ["Ceramics"]) - XCTAssertEqual(segmentClient.properties(forKey: "project_category"), ["Art"]) - XCTAssertEqual(segmentClient.properties(forKey: "project_country"), ["US"]) - XCTAssertEqual(segmentClient.properties(forKey: "project_user_has_watched", as: Bool.self), [nil]) - } - } - - func testTrackingProjectPageViewed_LoggedOut() { - let config = Config.template - |> \.countryCode .~ "GB" - - let segmentClient = MockTrackingClient() - let ksrAnalytics = KSRAnalytics( - config: config, - loggedInUser: nil, - segmentClient: segmentClient, - appTrackingTransparency: MockAppTrackingTransparency() - ) - - let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) - - withEnvironment( - apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([Reward.noReward, Reward.template]) - ), - currentUser: nil, - ksrAnalytics: ksrAnalytics - ) { - self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) - - self.scheduler.advance() - - XCTAssertEqual(segmentClient.events, ["Page Viewed"]) - - XCTAssertEqual(segmentClient.properties(forKey: "session_ref_tag"), ["discovery"]) - - XCTAssertEqual(segmentClient.properties(forKey: "session_user_is_logged_in", as: Bool.self), [false]) - XCTAssertEqual(segmentClient.properties(forKey: "user_uid", as: Int.self), [nil]) - XCTAssertEqual(segmentClient.properties(forKey: "project_subcategory"), ["Ceramics"]) - XCTAssertEqual(segmentClient.properties(forKey: "project_category"), ["Art"]) - XCTAssertEqual(segmentClient.properties(forKey: "project_country"), ["US"]) - XCTAssertEqual(segmentClient.properties(forKey: "project_user_has_watched", as: Bool.self), [nil]) - } - } - func testnavigateBackToProjectPage() { self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: nil) self.vm.inputs.viewDidLoad() @@ -2305,51 +1865,6 @@ final class ProjectPageViewModelTests: TestCase { self.goToURL.assertValue(url) } - func testTrackUserBlockedFromProject_EventsEmitted() { - let segmentClient = MockTrackingClient() - let ksrAnalytics = KSRAnalytics( - config: .template, - loggedInUser: User.template, - segmentClient: segmentClient, - appTrackingTransparency: MockAppTrackingTransparency() - ) - - let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) - - withEnvironment( - apiService: MockService( - blockUserResult: .success(EmptyResponseEnvelope()), - fetchProjectPamphletResult: .success(projectPamphletData) - ), - currentUser: User.template, - ksrAnalytics: ksrAnalytics - ) { - self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) - self.vm.inputs.blockUser(id: 111) - - self.scheduler.advance() - - XCTAssertEqual(segmentClient.events, ["CTA Clicked", "CTA Clicked"]) - - XCTAssertEqual( - segmentClient.properties(forKey: "session_user_is_logged_in", as: Bool.self), - [true, true] - ) - XCTAssertEqual(segmentClient.properties(forKey: "user_uid", as: String.self), ["1", "1"]) - XCTAssertEqual(segmentClient.properties(forKey: "context_cta"), ["block_user", "block_user"]) - XCTAssertEqual( - segmentClient.properties(forKey: "context_location"), - ["creator_details_menu", "creator_details_menu"] - ) - XCTAssertEqual(segmentClient.properties(forKey: "context_page"), ["project", "project"]) - XCTAssertEqual(segmentClient.properties(forKey: "context_section"), ["overview", "overview"]) - XCTAssertEqual(segmentClient.properties(forKey: "context_type"), ["initiate", "confirm"]) - XCTAssertEqual(segmentClient.properties(forKey: "interaction_target_uid"), ["111", "111"]) - } - } - func testPrefetchImageURLsOnFirstLoad_LoadingViaParam_Success() { // Given a mock API that returns a project with an image in its HTML content let imageUrl = URL(string: "https://placecats.com/millie/300/150")! diff --git a/LibraryTests/Library/ViewModels/ProjectPageViewModel_TrackingTests.swift b/LibraryTests/Library/ViewModels/ProjectPageViewModel_TrackingTests.swift new file mode 100644 index 0000000000..a114eac66c --- /dev/null +++ b/LibraryTests/Library/ViewModels/ProjectPageViewModel_TrackingTests.swift @@ -0,0 +1,519 @@ +import AVFoundation +@testable import KsApi +@testable import KsApiTestHelpers +@testable import Library +@testable import LibraryTestHelpers +import Prelude +import ReactiveExtensions_TestHelpers +import ReactiveSwift +import XCTest + +final class ProjectPageViewModel_TrackingTests: TestCase { + fileprivate var vm: ProjectPageViewModelType! + + internal override func setUp() { + super.setUp() + + self.vm = ProjectPageViewModel() + } + + // Tests that ref tags and referral credit cookies are tracked and saved like we expect. + func testTracksRefTag() { + let project = Project.template + let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) + + withEnvironment(apiService: MockService( + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectRewardsResult: .success([ + Reward.noReward, + Reward.template + ]) + )) { + self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.category)) + self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidAppear(animated: false) + + self.scheduler.advance() + + XCTAssertEqual( + ["Page Viewed"], + self.segmentTrackingClient.events, "A project page event is tracked." + ) + + XCTAssertEqual( + [RefTag.category.stringTag], + self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, + "The ref tag is tracked in the event." + ) + XCTAssertEqual( + 1, self.cookieStorage.cookies?.count, + "A single cookie is set" + ) + XCTAssertEqual( + "ref_\(project.id)", self.cookieStorage.cookies?.last?.name, + "A referral cookie is set for the project." + ) + XCTAssertEqual( + "category?", + (self.cookieStorage.cookies?.last?.value.prefix(9)).map(String.init), + "A referral cookie is set for the category ref tag." + ) + + // Start up another view model with the same project + let newVm: ProjectPageViewModelType = ProjectPageViewModel() + newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.recommended)) + newVm.inputs.viewDidLoad() + newVm.inputs.viewDidAppear(animated: true) + + self.scheduler.advance() + + XCTAssertEqual( + [ + "Page Viewed", "Page Viewed" + ], + self.segmentTrackingClient.events, "A project page event is tracked." + ) + + XCTAssertEqual( + [ + RefTag.category.stringTag, + RefTag.recommended.stringTag + ], + self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, + "The new ref tag is tracked in an event." + ) + XCTAssertEqual( + 1, self.cookieStorage.cookies?.count, + "A single cookie has been set." + ) + } + } + + // Tests that ref tags for similar projects and referral credit cookies are tracked and saved like we expect. + func testTracksRefTag_SimilarProjects() { + let project = Project.template + let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) + + withEnvironment(apiService: MockService( + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectRewardsResult: .success([ + Reward.noReward, + Reward.template + ]) + )) { + self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.similarProjects)) + self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidAppear(animated: false) + + self.scheduler.advance() + + XCTAssertEqual( + ["Page Viewed"], + self.segmentTrackingClient.events, "A project page event is tracked." + ) + + XCTAssertEqual( + [RefTag.similarProjects.stringTag], + self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, + "The ref tag is tracked in the event." + ) + XCTAssertEqual( + 1, self.cookieStorage.cookies?.count, + "A single cookie is set" + ) + XCTAssertEqual( + "ref_\(project.id)", self.cookieStorage.cookies?.last?.name, + "A referral cookie is set for the project." + ) + + // Start up another view model with the same project + let newVm: ProjectPageViewModelType = ProjectPageViewModel() + newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.recommended)) + newVm.inputs.viewDidLoad() + newVm.inputs.viewDidAppear(animated: true) + + self.scheduler.advance() + + XCTAssertEqual( + [ + "Page Viewed", "Page Viewed" + ], + self.segmentTrackingClient.events, "A project page event is tracked." + ) + + XCTAssertEqual( + [ + RefTag.similarProjects.stringTag, + RefTag.recommended.stringTag + ], + self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, + "The new ref tag is tracked in an event." + ) + XCTAssertEqual( + 1, self.cookieStorage.cookies?.count, + "A single cookie has been set." + ) + } + } + + func testProjectPageViewed_Tracking_OnError() { + let service = MockService(fetchProjectPamphletResult: .failure(.couldNotParseJSON)) + + withEnvironment(apiService: service) { + self.configureInitialState(.init(left: .template)) + + self.scheduler.advance() + + XCTAssertEqual( + [], + self.segmentTrackingClient.events, + "Project Page Viewed doesnt track if the request fails" + ) + } + } + + func testProjectPageViewed_OnViewDidAppear() { + let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) + + withEnvironment(apiService: MockService( + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectRewardsResult: .success([Reward.noReward, Reward.template]) + )) { + XCTAssertEqual([], self.segmentTrackingClient.events) + + self.configureInitialState(.init(left: .template)) + + self.scheduler.advance() + + XCTAssertEqual(["Page Viewed"], self.segmentTrackingClient.events) + + XCTAssertEqual(["project"], self.segmentTrackingClient.properties(forKey: "context_page")) + XCTAssertEqual(["overview"], self.segmentTrackingClient.properties(forKey: "context_section")) + XCTAssertEqual(["discovery"], self.segmentTrackingClient.properties(forKey: "session_ref_tag")) + } + } + + func testMockCookieStorageSet_SeparateSchedulers() { + let project = Project.template + let scheduler1 = TestScheduler(startDate: MockDate().date) + let scheduler2 = TestScheduler(startDate: scheduler1.currentDate.addingTimeInterval(1)) + let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) + + withEnvironment( + apiService: MockService( + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectRewardsResult: .success([.template]) + ), + scheduler: scheduler1 + ) { + let newVm: ProjectPageViewModelType = ProjectPageViewModel() + newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.category)) + newVm.inputs.viewDidLoad() + newVm.inputs.viewDidAppear(animated: true) + + scheduler1.advance() + + XCTAssertEqual(1, self.cookieStorage.cookies?.count, "A single cookie has been set.") + } + + withEnvironment( + apiService: MockService( + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectRewardsResult: .success([.template]) + ), + scheduler: scheduler2 + ) { + let newVm: ProjectPageViewModelType = ProjectPageViewModel() + newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.recommended)) + newVm.inputs.viewDidLoad() + newVm.inputs.viewDidAppear(animated: true) + + scheduler2.advance() + + XCTAssertEqual(2, self.cookieStorage.cookies?.count, "Two cookies are set on separate schedulers.") + } + } + + func testMockCookieStorageSet_SameScheduler() { + let project = Project.template + let scheduler1 = TestScheduler(startDate: MockDate().date) + + withEnvironment(scheduler: scheduler1) { + let newVm: ProjectPageViewModelType = ProjectPageViewModel() + newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.category)) + newVm.inputs.viewDidLoad() + newVm.inputs.viewDidAppear(animated: true) + + scheduler1.advance() + + XCTAssertEqual(1, self.cookieStorage.cookies?.count, "A single cookie has been set.") + } + + withEnvironment(scheduler: scheduler1) { + let newVm: ProjectPageViewModelType = ProjectPageViewModel() + newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.recommended)) + newVm.inputs.viewDidLoad() + newVm.inputs.viewDidAppear(animated: true) + + scheduler1.advance() + + XCTAssertEqual( + 1, self.cookieStorage.cookies?.count, + "A single cookie has been set on the same scheduler." + ) + } + } + + func testTracksRefTag_WithBadData() { + let project = Project.template + let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) + + withEnvironment(apiService: MockService( + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectFriendsResult: .success([.template]), + fetchProjectRewardsResult: .success([.template]) + )) { + self.vm.inputs.configureWith( + projectOrParam: .left(project), refInfo: RefInfo(.unrecognized("category%3F1232")) + ) + self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidAppear(animated: false) + + self.scheduler.advance() + + XCTAssertEqual( + ["Page Viewed"], + self.segmentTrackingClient.events, "A project page event is tracked." + ) + + XCTAssertEqual( + [RefTag.category.stringTag], + self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, + "The ref tag is tracked in the event." + ) + XCTAssertEqual( + 1, self.cookieStorage.cookies?.count, + "A single cookie is set" + ) + XCTAssertEqual( + "ref_\(project.id)", self.cookieStorage.cookies?.last?.name, + "A referral cookie is set for the project." + ) + XCTAssertEqual( + "category?", + (self.cookieStorage.cookies?.last?.value.prefix(9)).map(String.init), + "A referral cookie is set for the category ref tag." + ) + + // Start up another view model with the same project + let newVm: ProjectPageViewModelType = ProjectPageViewModel() + newVm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.recommended)) + newVm.inputs.viewDidLoad() + newVm.inputs.viewDidAppear(animated: true) + + self.scheduler.advance() + + XCTAssertEqual( + [ + "Page Viewed", "Page Viewed" + ], + self.segmentTrackingClient.events, "A project page event is tracked." + ) + + XCTAssertEqual( + [ + RefTag.category.stringTag, + RefTag.recommended.stringTag + ], + self.segmentTrackingClient.properties.compactMap { $0["session_ref_tag"] as? String }, + "The new ref tag is tracked in an event." + ) + XCTAssertEqual( + 1, self.cookieStorage.cookies?.count, + "A single cookie has been set." + ) + } + } + + func testTrackingDoesNotOccurOnLoad() { + let project = Project.template + + self.vm.inputs.configureWith( + projectOrParam: .left(project), refInfo: RefInfo(.unrecognized("category%3F1232")) + ) + self.vm.inputs.viewDidLoad() + + self.scheduler.advance() + + XCTAssertEqual([], self.segmentTrackingClient.events) + } + + func testTrackingProjectPageViewed_LoggedIn_AdvertisingConsentNotAllowed_EventsNotTracked() { + let segmentClient = MockTrackingClient() + let appTrackingTransparency = MockAppTrackingTransparency() + appTrackingTransparency.shouldRequestAuthStatus = false + let ksrAnalytics = KSRAnalytics( + config: .template, + loggedInUser: User.template, + segmentClient: segmentClient, + appTrackingTransparency: appTrackingTransparency + ) + + let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) + + withEnvironment( + apiService: MockService( + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectRewardsResult: .success([Reward.noReward, Reward.template]) + ), + currentUser: User.template, + ksrAnalytics: ksrAnalytics + ) { + self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) + self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidAppear(animated: false) + + self.scheduler.advance() + + XCTAssertEqual(segmentClient.events, []) + } + } + + func testTrackingProjectPageViewed_LoggedIn_AdvertisingConsentAllowed_EventsTracked() { + let segmentClient = MockTrackingClient() + let ksrAnalytics = KSRAnalytics( + config: .template, + loggedInUser: User.template, + segmentClient: segmentClient, + appTrackingTransparency: MockAppTrackingTransparency() + ) + + let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) + + withEnvironment( + apiService: MockService( + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectRewardsResult: .success([Reward.noReward, Reward.template]) + ), + currentUser: User.template, + ksrAnalytics: ksrAnalytics + ) { + self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) + self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidAppear(animated: false) + + self.scheduler.advance() + + XCTAssertEqual(segmentClient.events, ["Page Viewed"]) + + XCTAssertEqual(segmentClient.properties(forKey: "session_user_is_logged_in", as: Bool.self), [true]) + XCTAssertEqual(segmentClient.properties(forKey: "user_uid", as: String.self), ["1"]) + XCTAssertEqual(segmentClient.properties(forKey: "session_ref_tag"), ["discovery"]) + XCTAssertEqual(segmentClient.properties(forKey: "project_subcategory"), ["Ceramics"]) + XCTAssertEqual(segmentClient.properties(forKey: "project_category"), ["Art"]) + XCTAssertEqual(segmentClient.properties(forKey: "project_country"), ["US"]) + XCTAssertEqual(segmentClient.properties(forKey: "project_user_has_watched", as: Bool.self), [nil]) + } + } + + func testTrackingProjectPageViewed_LoggedOut() { + let config = Config.template + |> \.countryCode .~ "GB" + + let segmentClient = MockTrackingClient() + let ksrAnalytics = KSRAnalytics( + config: config, + loggedInUser: nil, + segmentClient: segmentClient, + appTrackingTransparency: MockAppTrackingTransparency() + ) + + let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) + + withEnvironment( + apiService: MockService( + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectRewardsResult: .success([Reward.noReward, Reward.template]) + ), + currentUser: nil, + ksrAnalytics: ksrAnalytics + ) { + self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) + self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidAppear(animated: false) + + self.scheduler.advance() + + XCTAssertEqual(segmentClient.events, ["Page Viewed"]) + + XCTAssertEqual(segmentClient.properties(forKey: "session_ref_tag"), ["discovery"]) + + XCTAssertEqual(segmentClient.properties(forKey: "session_user_is_logged_in", as: Bool.self), [false]) + XCTAssertEqual(segmentClient.properties(forKey: "user_uid", as: Int.self), [nil]) + XCTAssertEqual(segmentClient.properties(forKey: "project_subcategory"), ["Ceramics"]) + XCTAssertEqual(segmentClient.properties(forKey: "project_category"), ["Art"]) + XCTAssertEqual(segmentClient.properties(forKey: "project_country"), ["US"]) + XCTAssertEqual(segmentClient.properties(forKey: "project_user_has_watched", as: Bool.self), [nil]) + } + } + + func testTrackUserBlockedFromProject_EventsEmitted() { + let segmentClient = MockTrackingClient() + let ksrAnalytics = KSRAnalytics( + config: .template, + loggedInUser: User.template, + segmentClient: segmentClient, + appTrackingTransparency: MockAppTrackingTransparency() + ) + + let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) + + withEnvironment( + apiService: MockService( + blockUserResult: .success(EmptyResponseEnvelope()), + fetchProjectPamphletResult: .success(projectPamphletData) + ), + currentUser: User.template, + ksrAnalytics: ksrAnalytics + ) { + self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) + self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidAppear(animated: false) + self.vm.inputs.blockUser(id: 111) + + self.scheduler.advance() + + XCTAssertEqual(segmentClient.events, ["CTA Clicked", "CTA Clicked"]) + + XCTAssertEqual( + segmentClient.properties(forKey: "session_user_is_logged_in", as: Bool.self), + [true, true] + ) + XCTAssertEqual(segmentClient.properties(forKey: "user_uid", as: String.self), ["1", "1"]) + XCTAssertEqual(segmentClient.properties(forKey: "context_cta"), ["block_user", "block_user"]) + XCTAssertEqual( + segmentClient.properties(forKey: "context_location"), + ["creator_details_menu", "creator_details_menu"] + ) + XCTAssertEqual(segmentClient.properties(forKey: "context_page"), ["project", "project"]) + XCTAssertEqual(segmentClient.properties(forKey: "context_section"), ["overview", "overview"]) + XCTAssertEqual(segmentClient.properties(forKey: "context_type"), ["initiate", "confirm"]) + XCTAssertEqual(segmentClient.properties(forKey: "interaction_target_uid"), ["111", "111"]) + } + } + + // MARK: - Functions + + private func configureInitialState( + _ projectOrParam: Either, + secretRewardToken: String? = nil + ) { + self.vm.inputs.configureWith( + projectOrParam: projectOrParam, + refInfo: RefInfo(.discovery), + secretRewardToken: secretRewardToken + ) + self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidAppear(animated: false) + } +} From 47e0d19f669fd10df3165e752c9fb3ba7972662b Mon Sep 17 00:00:00 2001 From: Amy Date: Tue, 14 Jul 2026 15:06:52 -0400 Subject: [PATCH 2/3] Add helper methods to ProjectPageViewModelTests --- .../ProjectPageViewModelTests.swift | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift b/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift index b97658df46..e8a5eb3495 100644 --- a/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift +++ b/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift @@ -562,7 +562,7 @@ final class ProjectPageViewModelTests: TestCase { withEnvironment(config: .template, currentUser: .template, mainBundle: self.releaseBundle) { let project = Project.template - self.configureInitialState(.left(project)) + self.vm.configureAndLoad(.left(project)) self.goToRewardsProject.assertDidNotEmitValue() self.goToRewardsRefTag.assertDidNotEmitValue() @@ -600,7 +600,7 @@ final class ProjectPageViewModelTests: TestCase { withEnvironment(config: .template, currentUser: nil, mainBundle: self.releaseBundle) { let project = Project.template - self.configureInitialState(.left(project)) + self.vm.configureAndLoad(.left(project)) self.goToRewardsProject.assertDidNotEmitValue() self.goToRewardsRefTag.assertDidNotEmitValue() @@ -653,7 +653,7 @@ final class ProjectPageViewModelTests: TestCase { let initialData = Either.right(param) withEnvironment(apiService: mockService, currentUser: .template) { - self.configureInitialState(initialData) + self.vm.configureAndLoad(initialData) self.configureChildViewControllersWithProject.assertDidNotEmitValue() @@ -691,7 +691,7 @@ final class ProjectPageViewModelTests: TestCase { let initialData = Either.right(param) withEnvironment(apiService: mockService, currentUser: .template) { - self.configureInitialState(initialData, secretRewardToken: "foobar") + self.vm.configureAndLoad(initialData, secretRewardToken: "foobar") self.configureChildViewControllersWithProject.assertDidNotEmitValue() @@ -728,7 +728,7 @@ final class ProjectPageViewModelTests: TestCase { let initialData = Either.right(param) withEnvironment(apiService: mockService, currentUser: .template) { - self.configureInitialState(initialData) + self.vm.configureAndLoad(initialData) self.configureChildViewControllersWithProject.assertDidNotEmitValue() @@ -746,7 +746,7 @@ final class ProjectPageViewModelTests: TestCase { currentUser: .template, mainBundle: self.releaseBundle ) { - self.configureInitialState(.left(project), secretRewardToken: "secret-reward-token") + self.vm.configureAndLoad(.left(project), secretRewardToken: "secret-reward-token") self.goToRewardsProject.assertDidNotEmitValue() self.goToRewardsRefTag.assertDidNotEmitValue() @@ -770,7 +770,7 @@ final class ProjectPageViewModelTests: TestCase { ) { let project = Project.template - self.configureInitialState(.left(project), secretRewardToken: "secret-reward-token") + self.vm.configureAndLoad(.left(project), secretRewardToken: "secret-reward-token") self.goToRewardsProject.assertDidNotEmitValue() self.goToRewardsRefTag.assertDidNotEmitValue() @@ -794,7 +794,7 @@ final class ProjectPageViewModelTests: TestCase { |> \.watchesCount .~ 10 |> \.personalization.isStarred .~ true - self.configureInitialState(.left(project)) + self.vm.configureAndLoad(.left(project)) self.updateWatchProjectWithPrelaunchProjectState.assertDidNotEmitValue() @@ -818,7 +818,7 @@ final class ProjectPageViewModelTests: TestCase { |> Project.lens.personalization.backing .~ backing |> Project.lens.personalization.isBacking .~ true - self.configureInitialState(.left(project)) + self.vm.configureAndLoad(.left(project)) self.goToManagePledgeProjectParam.assertDidNotEmitValue() self.goToManagePledgeBackingParam.assertDidNotEmitValue() @@ -844,7 +844,7 @@ final class ProjectPageViewModelTests: TestCase { |> Project.lens.personalization.backing .~ backing |> Project.lens.personalization.isBacking .~ true - self.configureInitialState(.left(project)) + self.vm.configureAndLoad(.left(project)) self.goToManagePledgeProjectParam.assertDidNotEmitValue() self.goToManagePledgeBackingParam.assertDidNotEmitValue() @@ -871,7 +871,7 @@ final class ProjectPageViewModelTests: TestCase { let backingDetailsPageURL = backing.backingDetailsPageRoute - self.configureInitialState(.left(project)) + self.vm.configureAndLoad(.left(project)) self.goToManagePledgeProjectParam.assertDidNotEmitValue() self.goToManagePledgeBackingParam.assertDidNotEmitValue() @@ -899,7 +899,7 @@ final class ProjectPageViewModelTests: TestCase { let backingDetailsPageURL = backing.backingDetailsPageRoute - self.configureInitialState(.left(project)) + self.vm.configureAndLoad(.left(project)) self.goToManagePledgeProjectParam.assertDidNotEmitValue() self.goToManagePledgeBackingParam.assertDidNotEmitValue() @@ -920,7 +920,7 @@ final class ProjectPageViewModelTests: TestCase { AppEnvironment.current.apiService.serverConfig.webBaseUrl.absoluteString + project.redemptionPageUrl - self.configureInitialState(.left(project)) + self.vm.configureAndLoad(.left(project)) self.goToPledgeManager.assertDidNotEmitValue() @@ -964,7 +964,7 @@ final class ProjectPageViewModelTests: TestCase { self.configurePledgeCTAViewIsLoading.assertDidNotEmitValue() self.configurePledgeCTAViewRefTag.assertValues([]) - self.configureInitialState(.left(project)) + self.vm.configureAndLoad(.left(project)) self.configurePledgeCTAViewProject.assertValues([project]) self.configurePledgeCTAViewIsLoading.assertValues([true]) @@ -993,7 +993,7 @@ final class ProjectPageViewModelTests: TestCase { self.configurePledgeCTAViewIsLoading.assertDidNotEmitValue() self.configurePledgeCTAViewRefTag.assertDidNotEmitValue() - self.configureInitialState(.left(project)) + self.vm.configureAndLoad(.left(project)) self.configurePledgeCTAViewProject.assertValues([project]) self.configurePledgeCTAViewIsLoading.assertValues([true]) @@ -1916,18 +1916,56 @@ final class ProjectPageViewModelTests: TestCase { } } - // MARK: - Functions + static func mockNetworkRequests( + project: Project = Project.template, + rewards: [Reward] = [Reward.noReward, Reward.template], + backing: Backing? = nil, + action: () -> Void + ) { + if let backing { + let projectPamphletData = Project.ProjectPamphletData(project: project, backingId: backing.id) + let projectAndBacking = ProjectAndBackingEnvelope(project: project, backing: backing) + + AppEnvironment.pushEnvironment( + apiService: MockService( + addUserToSecretRewardGroup: .success(EmptyResponseEnvelope()), + fetchProjectAndBackingResult: .success(projectAndBacking), + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectRewardsResult: .success(rewards), + ) + ) + + } else { + let projectPamphletData = Project.ProjectPamphletData(project: project, backingId: nil) + + AppEnvironment.pushEnvironment( + apiService: MockService( + addUserToSecretRewardGroup: .success(EmptyResponseEnvelope()), + fetchProjectPamphletResult: .success(projectPamphletData), + fetchProjectRewardsResult: .success(rewards) + ) + ) + } - private func configureInitialState( - _ projectOrParam: Either, - secretRewardToken: String? = nil + action() + + AppEnvironment.popEnvironment() + } +} + +extension ProjectPageViewModelType { + /// Convenience method which calls `configureWith`, `viewDidLoad` and `viewDidAppear`. + func configureAndLoad( + _ either: Either, + secretRewardToken: String? = nil, + refTag: RefTag? = .discovery ) { - self.vm.inputs.configureWith( - projectOrParam: projectOrParam, - refInfo: RefInfo(.discovery), + self.inputs.configureWith( + projectOrParam: either, + refInfo: RefInfo(refTag), secretRewardToken: secretRewardToken ) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) + self.inputs.viewDidLoad() + self.inputs.viewDidAppear(animated: false) } } From 7af71c679ae8d63f70f77403467cd383e55af43b Mon Sep 17 00:00:00 2001 From: Amy Date: Tue, 14 Jul 2026 15:44:31 -0400 Subject: [PATCH 3/3] DISC-369: Handle network stubs in ProjectPageViewModelTests more gracefully --- .../ProjectPageViewModelTests.swift | 732 ++++++++---------- 1 file changed, 339 insertions(+), 393 deletions(-) diff --git a/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift b/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift index e8a5eb3495..8d186a13a6 100644 --- a/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift +++ b/LibraryTests/Library/ViewModels/ProjectPageViewModelTests.swift @@ -143,99 +143,43 @@ final class ProjectPageViewModelTests: TestCase { .observe(self.updateWatchProjectWithPrelaunchProjectState.observer) } - func testConfigureChildViewControllersWithProject_WithFriendsNoBacking_ConfiguredWithProject() { + func testConfigureChildViewControllersWithProject_ConfiguredWithProject() { + let stubProject = self.projectWithEmptyProperties let project = Project.template - let friends = [User.template] let refTag = RefTag.category - let projectPamphletData = Project.ProjectPamphletData(project: project, backingId: nil) - withEnvironment(apiService: MockService( - fetchProjectAndBackingResult: .success(.template), - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectFriendsResult: .success(friends), - fetchProjectRewardsResult: .success([.template]) - )) { - self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(refTag)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) + ProjectPageViewModelTests.mockNetworkRequests(project: project) { + self.vm.configureAndLoad(.left(stubProject), refTag: refTag) - self.configureChildViewControllersWithProject.assertValues([project]) + self.configureChildViewControllersWithProject.assertValues([stubProject]) self.configureChildViewControllersWithRefTag.assertValues([refTag]) self.scheduler.advance() - self.configureChildViewControllersWithProject.assertValues([project, project]) + self.configureChildViewControllersWithProject.assertValues([stubProject, project]) self.configureChildViewControllersWithRefTag.assertValues([refTag, refTag]) self.vm.inputs.didBackProject() self.scheduler.advance() - self.configureChildViewControllersWithProject.assertValues([project, project, project]) + self.configureChildViewControllersWithProject.assertValues([stubProject, project, project]) self.configureChildViewControllersWithRefTag.assertValues([refTag, refTag, refTag]) self.vm.inputs.managePledgeViewControllerFinished(with: nil) self.scheduler.advance() - self.configureChildViewControllersWithProject.assertValues([project, project, project, project]) + self.configureChildViewControllersWithProject.assertValues([stubProject, project, project, project]) self.configureChildViewControllersWithRefTag.assertValues([refTag, refTag, refTag, refTag]) } } - func testConfigureChildViewControllersWithProject_FailedProjectFriendsNoBacking_ConfiguredWithProject() { - let project = Project.template - let refTag = RefTag.category - let projectPamphletData = Project.ProjectPamphletData(project: project, backingId: nil) - - withEnvironment(apiService: MockService( - fetchProjectAndBackingResult: .success(.template), - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectFriendsResult: .failure(.couldNotParseJSON), - fetchProjectRewardsResult: .success([.template]) - )) { - self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(refTag)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) - - self.configureChildViewControllersWithProject.assertValues([project]) - self.configureChildViewControllersWithRefTag.assertValues([refTag]) - - self.scheduler.advance() - - self.configureChildViewControllersWithProject.assertValues([project, project]) - self.configureChildViewControllersWithRefTag.assertValues([refTag, refTag]) - - self.vm.inputs.didBackProject() - - self.scheduler.advance() - - self.configureChildViewControllersWithProject.assertValues([project, project, project]) - self.configureChildViewControllersWithRefTag.assertValues([refTag, refTag, refTag]) - - self.vm.inputs.managePledgeViewControllerFinished(with: nil) - - self.scheduler.advance() - - self.configureChildViewControllersWithProject.assertValues([project, project, project, project]) - self.configureChildViewControllersWithRefTag.assertValues([refTag, refTag, refTag, refTag]) - } - } - - func testConfigureChildViewControllersWithProject_WithFriendsNoBacking_ConfiguredWithParam() { + func testConfigureChildViewControllersWithProject_ConfiguredWithParam() { let project = .template |> Project.lens.id .~ 42 - let projectPamphletData = Project.ProjectPamphletData(project: project, backingId: nil) - let friends = [User.template] - withEnvironment(apiService: MockService( - fetchProjectAndBackingResult: .success(.template), - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectFriendsResult: .success(friends), - fetchProjectRewardsResult: .success([.template]) - )) { - self.vm.inputs.configureWith(projectOrParam: .right(Param.id(project.id)), refInfo: nil) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) + ProjectPageViewModelTests.mockNetworkRequests(project: project) { + self.vm.configureAndLoad(.right(Param.id(project.id)), refTag: nil) self.configureChildViewControllersWithProject.assertValues([]) self.configureChildViewControllersWithRefTag.assertValues([]) @@ -272,56 +216,58 @@ final class ProjectPageViewModelTests: TestCase { self.configureDataSourceNavigationSection.assertValues([.overview]) } - func testConfigureProjectPageViewControllerDataSourceProject() { - self.vm.inputs - .configureWith(projectOrParam: .left(self.projectWithEmptyProperties), refInfo: RefInfo(.category)) + func testConfigureProjectPageViewControllerDataSource_loadsInitialProject_andRefreshedProject() { + ProjectPageViewModelTests.mockNetworkRequests { + self.vm.inputs + .configureWith(projectOrParam: .left(self.projectWithEmptyProperties), refInfo: RefInfo(.category)) - self.configureDataSourceProject.assertDidNotEmitValue() + self.configureDataSourceProject.assertDidNotEmitValue() - self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidLoad() - self.configureDataSourceProject.assertDidEmitValue() + self.configureDataSourceProject.assertValueCount(1, "Loaded initial project preview") + + self.scheduler.advance() + + self.configureDataSourceProject.assertValueCount(2, "Loaded actual refreshed project") + } } - func testConfigureProjectPageViewControllerDataSourceProject_US_ProjectCurrency_US_ProjectCountry() { - let USCurrencyProject = self.projectWithEmptyProperties + func testConfigureProjectPageViewControllerDataSource_LoadsFullProject() { + let projectStub = self.projectWithEmptyProperties + |> Project.lens.country .~ .us + |> Project.lens.stats.projectCurrency .~ Project.Country.us.currencyCode + + let fullProject = Project.template |> Project.lens.country .~ .us |> Project.lens.stats.projectCurrency .~ Project.Country.us.currencyCode let backing = Backing.template |> Backing.lens.id .~ 543 - let projectPamphletData = Project - .ProjectPamphletData(project: USCurrencyProject, backingId: backing.id) - - let projectFullAndEnvelope = ProjectAndBackingEnvelope(project: USCurrencyProject, backing: backing) - - withEnvironment(apiService: MockService( - fetchProjectAndBackingResult: .success(projectFullAndEnvelope), - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([.template]) - )) { - self.vm.inputs.configureWith(projectOrParam: .left(USCurrencyProject), refInfo: RefInfo(.category)) + ProjectPageViewModelTests.mockNetworkRequests(project: fullProject, backing: backing) { + self.vm.inputs.configureWith(projectOrParam: .left(projectStub), refInfo: RefInfo(.category)) self.configureDataSourceProject.assertDidNotEmitValue() self.vm.inputs.viewDidLoad() - self.configureDataSourceProject.assertValueCount(1) + self.configureDataSourceProject.assertValueCount(1, "Loaded project preview") self.vm.inputs.pledgeRetryButtonTapped() - self.configureDataSourceProject.assertValueCount(1) + self.configureDataSourceProject.assertValueCount(1, "Loaded project preview") + + XCTAssertEqual( + self.configureDataSourceProject.lastValue?.left, projectStub + ) self.scheduler.advance() + self.configureDataSourceProject.assertValueCount(2, "Loaded full project") + XCTAssertEqual( - self.configureDataSourceProject.lastValue?.left?.stats.projectCurrency, - Project.Country.us.currencyCode - ) - XCTAssertEqual( - self.configureDataSourceProject.lastValue?.left?.country, - Project.Country.us + self.configureDataSourceProject.lastValue?.left, fullProject ) } } @@ -381,16 +327,7 @@ final class ProjectPageViewModelTests: TestCase { let backing = Backing.template |> Backing.lens.id .~ 543 - let projectPamphletData = Project - .ProjectPamphletData(project: USCurrencyProject, backingId: backing.id) - - let projectFullAndEnvelope = ProjectAndBackingEnvelope(project: USCurrencyProject, backing: backing) - - withEnvironment(apiService: MockService( - fetchProjectAndBackingResult: .success(projectFullAndEnvelope), - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([.template]) - )) { + ProjectPageViewModelTests.mockNetworkRequests(project: USCurrencyProject, backing: backing) { self.vm.inputs.configureWith(projectOrParam: .left(USCurrencyProject), refInfo: RefInfo(.category)) self.configureDataSourceProject.assertDidNotEmitValue() @@ -417,155 +354,149 @@ final class ProjectPageViewModelTests: TestCase { } func testConfigureProjectNavigationSelectorView_ExtendedPropertiesEmpty_CreatesNavigationSelector_Success() { - let projectPamphletData = Project - .ProjectPamphletData(project: self.projectWithEmptyProperties, backingId: nil) + let stubProject = self.projectWithEmptyProperties + let fullProject = Project.cosmicSurgery - withEnvironment(apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([.template]) - )) { + ProjectPageViewModelTests.mockNetworkRequests(project: fullProject) { self.vm.inputs - .configureWith(projectOrParam: .left(self.projectWithEmptyProperties), refInfo: RefInfo(.category)) + .configureWith(projectOrParam: .left(stubProject), refInfo: RefInfo(.category)) self.configureProjectNavigationSelectorView.assertDidNotEmitValue() self.vm.inputs.viewDidLoad() - self.configureProjectNavigationSelectorView.assertDidEmitValue() + guard let configuredProject = self.configureProjectNavigationSelectorView.lastValue?.0 else { + XCTFail() + return + } + + XCTAssertEqual(configuredProject, stubProject) + + self.scheduler.advance() + + guard let configuredProject = self.configureProjectNavigationSelectorView.lastValue?.0 else { + XCTFail() + return + } + + XCTAssertEqual(configuredProject, fullProject) } } func testConfigureProjectNavigationSelectorView_ExtendedProjectPropertiesNil_CreatesNavigationSelector_Success( ) { - let projectPamphletData = Project.ProjectPamphletData(project: .template, backingId: nil) - - withEnvironment(apiService: MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([.template]) - )) { + ProjectPageViewModelTests.mockNetworkRequests { self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.category)) self.configureProjectNavigationSelectorView.assertDidNotEmitValue() self.vm.inputs.viewDidLoad() + self.scheduler.advance() + self.configureProjectNavigationSelectorView.assertDidEmitValue() } } - func testConfiguredProject_WithNoBacking_Succcessfully() { - let project = Project.template - let projectPamphletData = Project.ProjectPamphletData(project: project, backingId: nil) - let refTag = RefTag.category + func testConfiguredProject_WithNoBacking_Successfully() { + let stubProject = self.projectWithEmptyProperties + let fullProject = Project.template - withEnvironment(apiService: MockService( - fetchProjectAndBackingResult: .success(.template), - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([.template]) - )) { - self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(refTag)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) + ProjectPageViewModelTests.mockNetworkRequests(project: fullProject, backing: nil) { + self.vm.configureAndLoad(.left(stubProject)) - self.scheduler.advance() + self.configureChildViewControllersWithProject.assertValues([stubProject]) - XCTAssertNil( - self.configureChildViewControllersWithProject.values.last!.personalization.friends, - ) + self.scheduler.advance() - self.configureChildViewControllersWithProject.assertValues([project, project]) + self.configureChildViewControllersWithProject.assertValues([stubProject, fullProject]) } } func testConfiguredProject_WithNoBacking_Unsuccessfully() { let project = Project.template - let refTag = RefTag.category - let projectPamphletData = Project.ProjectPamphletData(project: project, backingId: nil) withEnvironment(apiService: MockService( - // If there's no backingID, we won't fetch a backing. - // fetchProjectAndBackingResult: .success(.template), - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([.template]) + fetchProjectPamphletResult: .failure(ErrorEnvelope.couldNotParseJSON), )) { - self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(refTag)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) + self.vm.configureAndLoad(.left(project)) + + self.configureChildViewControllersWithProject.assertLastValue(project) self.scheduler.advance() - XCTAssertNil( - self.configureChildViewControllersWithProject.values.last!.personalization.friends + self.configureChildViewControllersWithProject.assertLastValue( + project, + "Should only have emitted the initial project data" ) - self.configureChildViewControllersWithProject.assertValues([project, project]) + self.configurePledgeCTAViewErrorEnvelope.assertDidEmitValue("Should have emitted error") } } - func testConfiguredProject_WithBacking_Succcessfully() { + func testConfiguredProject_WithBacking_Successfully() { let project = Project.template - let refTag = RefTag.category - let projectPamphletData = Project.ProjectPamphletData(project: project, backingId: 1) - - withEnvironment(apiService: MockService( - fetchProjectAndBackingResult: .success(ProjectAndBackingEnvelope.template), - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([Reward.template]) - )) { - self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(refTag)) - self.vm.inputs.viewDidLoad() - self.vm.inputs.viewDidAppear(animated: false) + let backing = Backing.template - let projectWithBacking = project |> \.personalization.backing .~ .template - |> \.personalization.isBacking .~ true + ProjectPageViewModelTests.mockNetworkRequests(project: project, backing: backing) { + self.vm.configureAndLoad(.left(project)) self.scheduler.advance() - XCTAssertEqual( - self.configureChildViewControllersWithProject.values.last!.personalization.backing, - .template - ) - XCTAssertTrue( - self.configureChildViewControllersWithProject.values.last!.personalization.isBacking! - ) - self.configureChildViewControllersWithProject.assertValues([projectWithBacking, projectWithBacking]) + guard let fetchedProject = self.configureChildViewControllersWithProject.values.last else { + XCTFail("Expected project") + return + } + + XCTAssertEqual(fetchedProject.personalization.backing, backing) + XCTAssertTrue(fetchedProject.personalization.isBacking!) } } func testGoToComments() { - self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) + ProjectPageViewModelTests.mockNetworkRequests { + self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) - self.vm.inputs.viewDidLoad() + self.vm.inputs.viewDidLoad() - self.goToComments.assertDidNotEmitValue() + self.scheduler.advance() + + self.goToComments.assertDidNotEmitValue() - self.vm.inputs.tappedComments() + self.vm.inputs.tappedComments() - self.goToComments.assertValues([.template]) + self.goToComments.assertValues([.template]) + } } func testGoToReportProject() { let project = Project.template - self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.discovery)) - self.vm.inputs.viewDidLoad() + ProjectPageViewModelTests.mockNetworkRequests(project: project) { + self.vm.inputs.configureWith(projectOrParam: .left(project), refInfo: RefInfo(.discovery)) - self.goToReportProject.assertDidNotEmitValue() + self.vm.inputs.viewDidLoad() + + self.scheduler.advance() - self.vm.inputs.tappedReportProject() + self.goToReportProject.assertDidNotEmitValue() - XCTAssertEqual(self.goToReportProject.lastValue?.0, false) - XCTAssertEqual(self.goToReportProject.lastValue?.1, project.graphID) - XCTAssertEqual(self.goToReportProject.lastValue?.2, project.urls.web.project) + self.vm.inputs.tappedReportProject() + + XCTAssertEqual(self.goToReportProject.lastValue?.0, false) + XCTAssertEqual(self.goToReportProject.lastValue?.1, project.graphID) + XCTAssertEqual(self.goToReportProject.lastValue?.2, project.urls.web.project) + } } func testGoToRewards_withUserLoggedIn() { - withEnvironment(config: .template, currentUser: .template, mainBundle: self.releaseBundle) { - let project = Project.template + let project = Project.template + ProjectPageViewModelTests.mockNetworkRequests(project: project) { self.vm.configureAndLoad(.left(project)) + self.scheduler.advance() + self.goToRewardsProject.assertDidNotEmitValue() - self.goToRewardsRefTag.assertDidNotEmitValue() self.vm.inputs.pledgeCTAButtonTapped(with: .pledge) @@ -597,115 +528,108 @@ final class ProjectPageViewModelTests: TestCase { } func testGoToRewards_withUserLoggedOut() { - withEnvironment(config: .template, currentUser: nil, mainBundle: self.releaseBundle) { - let project = Project.template + let project = Project.template - self.vm.configureAndLoad(.left(project)) + ProjectPageViewModelTests.mockNetworkRequests(project: project) { + withEnvironment(config: .template, currentUser: nil, mainBundle: self.releaseBundle) { + self.vm.configureAndLoad(.left(project)) - self.goToRewardsProject.assertDidNotEmitValue() - self.goToRewardsRefTag.assertDidNotEmitValue() + self.scheduler.advance() - self.vm.inputs.pledgeCTAButtonTapped(with: .pledge) + self.goToRewardsProject.assertDidNotEmitValue() + self.goToRewardsRefTag.assertDidNotEmitValue() - self.goToRewardsProject.assertValues([project], "Tapping 'Back this project' emits the project") - self.goToRewardsRefTag.assertValues([.discovery], "Tapping 'Back this project' emits the refTag") + self.vm.inputs.pledgeCTAButtonTapped(with: .pledge) - self.vm.inputs.pledgeCTAButtonTapped(with: .viewRewards) + self.goToRewardsProject.assertValues([project], "Tapping 'Back this project' emits the project") + self.goToRewardsRefTag.assertValues([.discovery], "Tapping 'Back this project' emits the refTag") - self.goToRewardsProject.assertValues( - [project, project], - "Tapping 'View rewards' emits the project" - ) - self.goToRewardsRefTag.assertValues( - [.discovery, .discovery], - "Tapping 'View rewards' emits the refTag" - ) + self.vm.inputs.pledgeCTAButtonTapped(with: .viewRewards) - self.vm.inputs.pledgeCTAButtonTapped(with: .viewYourRewards) + self.goToRewardsProject.assertValues( + [project, project], + "Tapping 'View rewards' emits the project" + ) + self.goToRewardsRefTag.assertValues( + [.discovery, .discovery], + "Tapping 'View rewards' emits the refTag" + ) - self.goToRewardsProject.assertValues( - [project, project, project], - "Tapping 'View your rewards' emits the project" - ) - self.goToRewardsRefTag.assertValues( - [.discovery, .discovery, .discovery], - "Tapping 'View your rewards' emits the refTag" - ) + self.vm.inputs.pledgeCTAButtonTapped(with: .viewYourRewards) + + self.goToRewardsProject.assertValues( + [project, project, project], + "Tapping 'View your rewards' emits the project" + ) + self.goToRewardsRefTag.assertValues( + [.discovery, .discovery, .discovery], + "Tapping 'View your rewards' emits the refTag" + ) + } } } func test_loadingFromParameter_loadsAllData() { - let project = Project.template - + let projectStub = self.projectWithEmptyProperties let projectFull = Project.template - |> Project.lens.rewardData.rewards .~ [] + let rewards = [Reward.noReward, Reward.template] - let projectAndBacking = ProjectAndBackingEnvelope(project: projectFull, backing: Backing.template) - let initialProject = Project.ProjectPamphletData(project: projectFull, backingId: 1) + let param = ProjectPageParamBox(param: .id(projectStub.id), initialProject: projectStub) - let mockService = MockService( - fetchProjectAndBackingResult: .success(projectAndBacking), - fetchProjectPamphletResult: .success(initialProject), - fetchProjectRewardsResult: .success([Reward.noReward, Reward.template]), - ) - - let param = ProjectPageParamBox(param: .id(project.id), initialProject: nil) - let initialData = Either.right(param) + ProjectPageViewModelTests.mockNetworkRequests( + project: projectFull, + rewards: rewards, + backing: Backing.template + ) { + withEnvironment(currentUser: .template) { + self.vm.configureAndLoad(.right(param)) - withEnvironment(apiService: mockService, currentUser: .template) { - self.vm.configureAndLoad(initialData) + self.configureChildViewControllersWithProject.assertDidNotEmitValue() - self.configureChildViewControllersWithProject.assertDidNotEmitValue() + self.scheduler.advance() - self.scheduler.advance() + self.configureChildViewControllersWithProject.assertDidEmitValue() + guard let loadedProject = self.configureChildViewControllersWithProject.lastValue else { + XCTFail("Expected project to have loaded") + return + } - self.configureChildViewControllersWithProject.assertDidEmitValue() - guard let loadedProject = self.configureChildViewControllersWithProject.lastValue else { - XCTFail("Expected project to have loaded") - return + XCTAssertEqual(loadedProject.rewards.count, 2) + XCTAssertEqual(loadedProject.rewards.first, Reward.noReward) + XCTAssertEqual(loadedProject.personalization.backing, Backing.template) } - - XCTAssertEqual(loadedProject.rewards.count, 2) - XCTAssertEqual(loadedProject.rewards.first, Reward.noReward) - XCTAssertEqual(loadedProject.personalization.backing, Backing.template) } } func test_loadingFromParameter_withSecretRewards_loadsAllData() { - let project = Project.template - + let projectStub = self.projectWithEmptyProperties let projectFull = Project.template - |> Project.lens.rewardData.rewards .~ [] - - let projectAndBacking = ProjectAndBackingEnvelope(project: projectFull, backing: Backing.template) - let initialProject = Project.ProjectPamphletData(project: projectFull, backingId: 1) + let rewards = [Reward.noReward, Reward.secretRewardTemplate, Reward.template] - let mockService = MockService( - addUserToSecretRewardGroup: .success(EmptyResponseEnvelope.init()), - fetchProjectAndBackingResult: .success(projectAndBacking), - fetchProjectPamphletResult: .success(initialProject), - fetchProjectRewardsResult: .success([Reward.secretRewardTemplate, Reward.noReward, Reward.template]), - ) + let param = ProjectPageParamBox(param: .id(projectStub.id), initialProject: projectStub) - let param = ProjectPageParamBox(param: .id(project.id), initialProject: nil) - let initialData = Either.right(param) + ProjectPageViewModelTests.mockNetworkRequests( + project: projectFull, + rewards: rewards, + backing: Backing.template + ) { + withEnvironment(currentUser: .template) { + self.vm.configureAndLoad(.right(param), secretRewardToken: "foobar") - withEnvironment(apiService: mockService, currentUser: .template) { - self.vm.configureAndLoad(initialData, secretRewardToken: "foobar") + self.configureChildViewControllersWithProject.assertDidNotEmitValue() - self.configureChildViewControllersWithProject.assertDidNotEmitValue() + self.scheduler.advance() - self.scheduler.advance() + self.configureChildViewControllersWithProject.assertDidEmitValue() + guard let loadedProject = self.configureChildViewControllersWithProject.lastValue else { + XCTFail("Expected project to have loaded") + return + } - self.configureChildViewControllersWithProject.assertDidEmitValue() - guard let loadedProject = self.configureChildViewControllersWithProject.lastValue else { - XCTFail("Expected project to have loaded") - return + XCTAssertEqual(loadedProject.rewards.count, 3) + XCTAssertEqual(loadedProject.rewards[1], Reward.secretRewardTemplate) + XCTAssertEqual(loadedProject.personalization.backing, Backing.template) } - - XCTAssertEqual(loadedProject.rewards.count, 3) - XCTAssertEqual(loadedProject.rewards.first, Reward.secretRewardTemplate) - XCTAssertEqual(loadedProject.personalization.backing, Backing.template) } } @@ -725,10 +649,9 @@ final class ProjectPageViewModelTests: TestCase { ) let param = ProjectPageParamBox(param: .id(project.id), initialProject: nil) - let initialData = Either.right(param) withEnvironment(apiService: mockService, currentUser: .template) { - self.vm.configureAndLoad(initialData) + self.vm.configureAndLoad(.right(param)) self.configureChildViewControllersWithProject.assertDidNotEmitValue() @@ -741,85 +664,87 @@ final class ProjectPageViewModelTests: TestCase { func testSecretRewards_GoToRewards() { let project = Project.template - withEnvironment( - config: .template, - currentUser: .template, - mainBundle: self.releaseBundle - ) { - self.vm.configureAndLoad(.left(project), secretRewardToken: "secret-reward-token") + ProjectPageViewModelTests.mockNetworkRequests(project: project) { + withEnvironment(currentUser: .template) { + self.vm.configureAndLoad(.left(project), secretRewardToken: "secret-reward-token") - self.goToRewardsProject.assertDidNotEmitValue() - self.goToRewardsRefTag.assertDidNotEmitValue() - self.goToLoginWithIntent.assertDidNotEmitValue() + self.goToRewardsProject.assertDidNotEmitValue() + self.goToRewardsRefTag.assertDidNotEmitValue() + self.goToLoginWithIntent.assertDidNotEmitValue() - self.scheduler.advance() + self.scheduler.advance() - self.vm.inputs.pledgeCTAButtonTapped(with: .pledge) + self.vm.inputs.pledgeCTAButtonTapped(with: .pledge) - self.goToRewardsProject.assertValues([project], "Tapping 'Back this project' emits the project") - self.goToRewardsRefTag.assertValues([.discovery], "Tapping 'Back this project' emits the refTag") - self.goToLoginWithIntent.assertDidNotEmitValue() + self.goToRewardsProject.assertValues([project], "Tapping 'Back this project' emits the project") + self.goToRewardsRefTag.assertValues([.discovery], "Tapping 'Back this project' emits the refTag") + self.goToLoginWithIntent.assertDidNotEmitValue() + } } } func testSecretRewards_GoToLogin() { - withEnvironment( - config: .template, - currentUser: nil, - mainBundle: self.releaseBundle - ) { - let project = Project.template + ProjectPageViewModelTests.mockNetworkRequests { + withEnvironment(currentUser: nil) { + let project = Project.template - self.vm.configureAndLoad(.left(project), secretRewardToken: "secret-reward-token") + self.vm.configureAndLoad(.left(project), secretRewardToken: "secret-reward-token") - self.goToRewardsProject.assertDidNotEmitValue() - self.goToRewardsRefTag.assertDidNotEmitValue() - self.goToLoginWithIntent.assertDidNotEmitValue() + self.scheduler.advance() - self.vm.inputs.pledgeCTAButtonTapped(with: .pledge) + self.goToRewardsProject.assertDidNotEmitValue() + self.goToRewardsRefTag.assertDidNotEmitValue() + self.goToLoginWithIntent.assertDidNotEmitValue() - self.self.goToLoginWithIntent.assertValues( - [.backProject], - "Tapping 'Back this project' emits the project" - ) - self.goToRewardsProject.assertDidNotEmitValue() - self.goToRewardsRefTag.assertDidNotEmitValue() + self.vm.inputs.pledgeCTAButtonTapped(with: .pledge) + + self.self.goToLoginWithIntent.assertValues( + [.backProject], + "Tapping 'Back this project' emits the project" + ) + self.goToRewardsProject.assertDidNotEmitValue() + self.goToRewardsRefTag.assertDidNotEmitValue() + } } } func testUpdateWatchProjectWithPrelaunchState() { - withEnvironment(config: .template, mainBundle: self.releaseBundle) { - let project = Project.template - |> \.displayPrelaunch .~ true - |> \.watchesCount .~ 10 - |> \.personalization.isStarred .~ true + let project = Project.template + |> \.displayPrelaunch .~ true + |> \.watchesCount .~ 10 + |> \.personalization.isStarred .~ true - self.vm.configureAndLoad(.left(project)) + self.vm.configureAndLoad(.left(project)) - self.updateWatchProjectWithPrelaunchProjectState.assertDidNotEmitValue() + self.updateWatchProjectWithPrelaunchProjectState.assertDidNotEmitValue() - self.vm.inputs.pledgeCTAButtonTapped(with: .prelaunch(saved: true, watchCount: 10)) + self.vm.inputs.pledgeCTAButtonTapped(with: .prelaunch(saved: true, watchCount: 10)) - XCTAssertEqual(self.updateWatchProjectWithPrelaunchProjectState.values.count, 1) - XCTAssertEqual(self.updateWatchProjectWithPrelaunchProjectState.values.last!.prelaunch, true) - XCTAssertEqual(self.updateWatchProjectWithPrelaunchProjectState.values.last!.saved, true) - XCTAssertEqual(self.updateWatchProjectWithPrelaunchProjectState.values.last!.watchesCount, 10) - } + XCTAssertEqual(self.updateWatchProjectWithPrelaunchProjectState.values.count, 1) + XCTAssertEqual(self.updateWatchProjectWithPrelaunchProjectState.values.last!.prelaunch, true) + XCTAssertEqual(self.updateWatchProjectWithPrelaunchProjectState.values.last!.saved, true) + XCTAssertEqual(self.updateWatchProjectWithPrelaunchProjectState.values.last!.watchesCount, 10) } func testGoToManageViewPledge_ManagingPledge() { - withEnvironment(config: .template) { - let reward = Project.cosmicSurgery.rewards.first! - let backing = Backing.template - |> Backing.lens.reward .~ reward - |> Backing.lens.rewardId .~ reward.id + let reward = Project.cosmicSurgery.rewards.first! + let backing = Backing.template + |> Backing.lens.reward .~ reward + |> Backing.lens.rewardId .~ reward.id - let project = Project.cosmicSurgery - |> Project.lens.personalization.backing .~ backing - |> Project.lens.personalization.isBacking .~ true + let project = Project.cosmicSurgery + |> Project.lens.personalization.backing .~ backing + |> Project.lens.personalization.isBacking .~ true + ProjectPageViewModelTests.mockNetworkRequests( + project: project, + rewards: Project.cosmicSurgery.rewards, + backing: backing + ) { self.vm.configureAndLoad(.left(project)) + self.scheduler.advance() + self.goToManagePledgeProjectParam.assertDidNotEmitValue() self.goToManagePledgeBackingParam.assertDidNotEmitValue() self.goToPledgeManagementViewPledge.assertDidNotEmitValue() @@ -833,46 +758,59 @@ final class ProjectPageViewModelTests: TestCase { } func testGoToManageViewPledge_ViewingPledge() { - withEnvironment(config: .template, currentUser: .template) { - let reward = Project.cosmicSurgery.rewards.first! - let backing = Backing.template - |> Backing.lens.reward .~ reward - |> Backing.lens.rewardId .~ reward.id + let reward = Project.cosmicSurgery.rewards.first! + let backing = Backing.template + |> Backing.lens.reward .~ reward + |> Backing.lens.rewardId .~ reward.id - let project = Project.cosmicSurgery - |> Project.lens.state .~ .successful - |> Project.lens.personalization.backing .~ backing - |> Project.lens.personalization.isBacking .~ true + let project = Project.cosmicSurgery + |> Project.lens.state .~ .successful + |> Project.lens.personalization.backing .~ backing + |> Project.lens.personalization.isBacking .~ true - self.vm.configureAndLoad(.left(project)) + ProjectPageViewModelTests.mockNetworkRequests( + project: project, + rewards: Project.cosmicSurgery.rewards, + backing: backing + ) { + withEnvironment(currentUser: .template) { + self.vm.configureAndLoad(.left(project)) + self.scheduler.advance() - self.goToManagePledgeProjectParam.assertDidNotEmitValue() - self.goToManagePledgeBackingParam.assertDidNotEmitValue() - self.goToPledgeManagementViewPledge.assertDidNotEmitValue() + self.goToManagePledgeProjectParam.assertDidNotEmitValue() + self.goToManagePledgeBackingParam.assertDidNotEmitValue() + self.goToPledgeManagementViewPledge.assertDidNotEmitValue() - self.vm.inputs.pledgeCTAButtonTapped(with: .viewBacking) + self.vm.inputs.pledgeCTAButtonTapped(with: .viewBacking) - self.goToManagePledgeProjectParam.assertValues([.slug(project.slug)]) - self.goToManagePledgeBackingParam.assertValues([.id(backing.id)]) - self.goToPledgeManagementViewPledge.assertDidNotEmitValue() + self.goToManagePledgeProjectParam.assertValues([.slug(project.slug)]) + self.goToManagePledgeBackingParam.assertValues([.id(backing.id)]) + self.goToPledgeManagementViewPledge.assertDidNotEmitValue() + } } } func testGoToPledgeManagementWebview_ManagingPledge() { - withEnvironment(config: .template) { - let reward = Project.cosmicSurgery.rewards.first! - let backing = Backing.templateMadeWithPledgeManagment - |> Backing.lens.reward .~ reward - |> Backing.lens.rewardId .~ reward.id + let reward = Project.cosmicSurgery.rewards.first! + let backing = Backing.templateMadeWithPledgeManagment + |> Backing.lens.reward .~ reward + |> Backing.lens.rewardId .~ reward.id - let project = Project.cosmicSurgery - |> Project.lens.personalization.backing .~ backing - |> Project.lens.personalization.isBacking .~ true + let project = Project.cosmicSurgery + |> Project.lens.personalization.backing .~ backing + |> Project.lens.personalization.isBacking .~ true + ProjectPageViewModelTests.mockNetworkRequests( + project: project, + rewards: Project.cosmicSurgery.rewards, + backing: backing + ) { let backingDetailsPageURL = backing.backingDetailsPageRoute self.vm.configureAndLoad(.left(project)) + self.scheduler.advance() + self.goToManagePledgeProjectParam.assertDidNotEmitValue() self.goToManagePledgeBackingParam.assertDidNotEmitValue() self.goToPledgeManagementViewPledge.assertDidNotEmitValue() @@ -886,30 +824,36 @@ final class ProjectPageViewModelTests: TestCase { } func testGoToPledgeManagementWebview_ViewingPledge() { - withEnvironment(config: .template, currentUser: .template) { - let reward = Project.cosmicSurgery.rewards.first! - let backing = Backing.templateMadeWithPledgeManagment - |> Backing.lens.reward .~ reward - |> Backing.lens.rewardId .~ reward.id - - let project = Project.cosmicSurgery - |> Project.lens.state .~ .successful - |> Project.lens.personalization.backing .~ backing - |> Project.lens.personalization.isBacking .~ true + let reward = Project.cosmicSurgery.rewards.first! + let backing = Backing.templateMadeWithPledgeManagment + |> Backing.lens.reward .~ reward + |> Backing.lens.rewardId .~ reward.id + + let project = Project.cosmicSurgery + |> Project.lens.state .~ .successful + |> Project.lens.personalization.backing .~ backing + |> Project.lens.personalization.isBacking .~ true - let backingDetailsPageURL = backing.backingDetailsPageRoute + ProjectPageViewModelTests.mockNetworkRequests( + project: project, + rewards: Project.cosmicSurgery.rewards, + backing: backing + ) { + withEnvironment(currentUser: .template) { + let backingDetailsPageURL = backing.backingDetailsPageRoute - self.vm.configureAndLoad(.left(project)) + self.vm.configureAndLoad(.left(project)) - self.goToManagePledgeProjectParam.assertDidNotEmitValue() - self.goToManagePledgeBackingParam.assertDidNotEmitValue() - self.goToPledgeManagementViewPledge.assertDidNotEmitValue() + self.goToManagePledgeProjectParam.assertDidNotEmitValue() + self.goToManagePledgeBackingParam.assertDidNotEmitValue() + self.goToPledgeManagementViewPledge.assertDidNotEmitValue() - self.vm.inputs.pledgeCTAButtonTapped(with: .viewBacking) + self.vm.inputs.pledgeCTAButtonTapped(with: .viewBacking) - self.goToManagePledgeProjectParam.assertDidNotEmitValue() - self.goToManagePledgeBackingParam.assertDidNotEmitValue() - self.goToPledgeManagementViewPledge.assertLastValue(backingDetailsPageURL) + self.goToManagePledgeProjectParam.assertDidNotEmitValue() + self.goToManagePledgeBackingParam.assertDidNotEmitValue() + self.goToPledgeManagementViewPledge.assertLastValue(backingDetailsPageURL) + } } } @@ -920,61 +864,63 @@ final class ProjectPageViewModelTests: TestCase { AppEnvironment.current.apiService.serverConfig.webBaseUrl.absoluteString + project.redemptionPageUrl - self.vm.configureAndLoad(.left(project)) + ProjectPageViewModelTests.mockNetworkRequests(project: project) { + self.vm.configureAndLoad(.left(project)) - self.goToPledgeManager.assertDidNotEmitValue() + self.scheduler.advance() - self.vm.inputs.pledgeCTAButtonTapped(with: .pledgeManager) + self.goToPledgeManager.assertDidNotEmitValue() - self.goToPledgeManager.assertLastValue(redemptionPageUrl) + self.vm.inputs.pledgeCTAButtonTapped(with: .pledgeManager) + + self.goToPledgeManager.assertLastValue(redemptionPageUrl) + } } func testGoToUpdates() { - self.vm.inputs.configureWith(projectOrParam: .left(.template), refInfo: RefInfo(.discovery)) - - self.vm.inputs.viewDidLoad() + let project = Project.template + ProjectPageViewModelTests.mockNetworkRequests(project: project) { + self.vm.configureAndLoad(.left(project)) + self.scheduler.advance() - self.goToUpdates.assertDidNotEmitValue() + self.goToUpdates.assertDidNotEmitValue() - self.vm.inputs.tappedUpdates() + self.vm.inputs.tappedUpdates() - self.goToUpdates.assertValues([.template]) + self.goToUpdates.assertValues([project]) + } } func testConfigurePledgeCTAView_FetchProjectSuccess() { - let project = Project.template + let projectStub = self.projectWithEmptyProperties + |> \.id .~ 2 + let projectFull = Project.template |> \.id .~ 2 |> Project.lens.personalization.isBacking .~ true - let projectPamphletData = Project.ProjectPamphletData(project: projectFull, backingId: nil) - - let mockService = MockService( - fetchProjectPamphletResult: .success(projectPamphletData), - fetchProjectRewardsResult: .success([Reward.noReward, Reward.template]) - ) + ProjectPageViewModelTests.mockNetworkRequests(project: projectFull, backing: .template) { + withEnvironment( + apiDelayInterval: .seconds(1), + ) { + self.configurePledgeCTAViewProject.assertDidNotEmitValue() + self.configurePledgeCTAViewIsLoading.assertDidNotEmitValue() + self.configurePledgeCTAViewRefTag.assertValues([]) - withEnvironment( - apiService: mockService, - apiDelayInterval: .seconds(1), - config: .template, - mainBundle: self.releaseBundle - ) { - self.configurePledgeCTAViewProject.assertDidNotEmitValue() - self.configurePledgeCTAViewIsLoading.assertDidNotEmitValue() - self.configurePledgeCTAViewRefTag.assertValues([]) + self.vm.configureAndLoad(.left(projectStub), refTag: .discovery) - self.vm.configureAndLoad(.left(project)) + self.scheduler.advance() - self.configurePledgeCTAViewProject.assertValues([project]) - self.configurePledgeCTAViewIsLoading.assertValues([true]) - self.configurePledgeCTAViewRefTag.assertValues([.discovery]) + self.configurePledgeCTAViewProject.assertValues([projectStub]) + self.configurePledgeCTAViewIsLoading.assertValues([true]) + self.configurePledgeCTAViewRefTag.assertValues([.discovery]) - self.scheduler.run() + self.scheduler.run() - self.configurePledgeCTAViewProject.assertValues([project, projectFull, projectFull]) - self.configurePledgeCTAViewIsLoading.assertValues([true, true, false]) - self.configurePledgeCTAViewRefTag.assertValues([.discovery, .discovery, .discovery]) + self.configurePledgeCTAViewProject.assertValues([projectStub, projectFull, projectFull]) + self.configurePledgeCTAViewIsLoading.assertValues([true, true, false]) + self.configurePledgeCTAViewRefTag.assertValues([.discovery, .discovery, .discovery]) + } } }