diff --git a/Kickstarter.xcodeproj/project.pbxproj b/Kickstarter.xcodeproj/project.pbxproj index 8fbaee413a..5e207bc1e1 100644 --- a/Kickstarter.xcodeproj/project.pbxproj +++ b/Kickstarter.xcodeproj/project.pbxproj @@ -1492,6 +1492,7 @@ E10BE8E72B151D2700F73DC9 /* BlockUserInputTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10BE8E52B151CC800F73DC9 /* BlockUserInputTests.swift */; }; E10D06632ACF385E00470B5C /* FetchBackerProjectsQuery.json in Resources */ = {isa = PBXBuildFile; fileRef = E10D06622ACF385E00470B5C /* FetchBackerProjectsQuery.json */; }; E10D06652AD48C9C00470B5C /* FetchBackerProjectsQueryRequestForTests.graphql_test in Resources */ = {isa = PBXBuildFile; fileRef = E10D06642AD48C9C00470B5C /* FetchBackerProjectsQueryRequestForTests.graphql_test */; }; + E170B9112B20E83B001BEDD7 /* MockGraphQLClient+CombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E170B9102B20E83B001BEDD7 /* MockGraphQLClient+CombineTests.swift */; }; E1A1491E2ACDD76800F49709 /* FetchBackerProjectsQuery.graphql in Resources */ = {isa = PBXBuildFile; fileRef = E1A1491D2ACDD76700F49709 /* FetchBackerProjectsQuery.graphql */; }; E1A149202ACDD7BF00F49709 /* FetchProjectsEnvelope+FetchBackerProjectsQueryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A1491F2ACDD7BF00F49709 /* FetchProjectsEnvelope+FetchBackerProjectsQueryData.swift */; }; E1A149222ACE013100F49709 /* FetchProjectsEnvelope.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A149212ACE013100F49709 /* FetchProjectsEnvelope.swift */; }; @@ -3068,6 +3069,7 @@ E10BE8E52B151CC800F73DC9 /* BlockUserInputTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockUserInputTests.swift; sourceTree = ""; }; E10D06622ACF385E00470B5C /* FetchBackerProjectsQuery.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = FetchBackerProjectsQuery.json; sourceTree = ""; }; E10D06642AD48C9C00470B5C /* FetchBackerProjectsQueryRequestForTests.graphql_test */ = {isa = PBXFileReference; lastKnownFileType = text; path = FetchBackerProjectsQueryRequestForTests.graphql_test; sourceTree = ""; }; + E170B9102B20E83B001BEDD7 /* MockGraphQLClient+CombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockGraphQLClient+CombineTests.swift"; sourceTree = ""; }; E1A1491D2ACDD76700F49709 /* FetchBackerProjectsQuery.graphql */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = FetchBackerProjectsQuery.graphql; sourceTree = ""; }; E1A1491F2ACDD7BF00F49709 /* FetchProjectsEnvelope+FetchBackerProjectsQueryData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FetchProjectsEnvelope+FetchBackerProjectsQueryData.swift"; sourceTree = ""; }; E1A149212ACE013100F49709 /* FetchProjectsEnvelope.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchProjectsEnvelope.swift; sourceTree = ""; }; @@ -6556,6 +6558,7 @@ 8AF34C772342D6A5000B211D /* Encodable+Dictionary.swift */, 8AF34C792342D864000B211D /* Encodable+DictionaryTests.swift */, 06FE2D7526CD851300A4C0F4 /* MockGraphQLClient.swift */, + E170B9102B20E83B001BEDD7 /* MockGraphQLClient+CombineTests.swift */, D015876A1EEB2ED6006E7684 /* NSURLSession.swift */, ); path = extensions; @@ -8716,6 +8719,7 @@ 0665C74B26E930BC00A0EDA1 /* FetchProjectQueryTemplate.swift in Sources */, 06D23BBF26F2484500F76122 /* Project+FetchProjectFriendsQueryDataTests.swift in Sources */, 8AC3E0592697ABD900168BF8 /* Project.Category+CategoryFragmentTests.swift in Sources */, + E170B9112B20E83B001BEDD7 /* MockGraphQLClient+CombineTests.swift in Sources */, D0E78C3822A981EE00AAB645 /* ClearUserUnseenActivityEnvelopeTests.swift in Sources */, 8AC3E128269F5B9700168BF8 /* UpdateBackingEnvelope+UpdateBackingMutation.DataTests.swift in Sources */, 19047FC12889B11F00BDD1A8 /* CreateSetupIntentMutationTests.swift in Sources */, diff --git a/KsApi/combine/CombineTestObserver.swift b/KsApi/combine/CombineTestObserver.swift index 4faa14fc4d..a685001373 100644 --- a/KsApi/combine/CombineTestObserver.swift +++ b/KsApi/combine/CombineTestObserver.swift @@ -2,15 +2,28 @@ import Combine import Foundation public final class CombineTestObserver { - public private(set) var events: [Value] = [] + public enum Event { + case value(Value) + case error(Error) + case finished + } + + public private(set) var events: [Event] = [] + private var subscriptions = Set() public func observe(_ publisher: any Publisher) { - publisher.sink { _ in - // TODO(MBL-1017) implement this as part of writing a new test observer for Combine - fatalError("Errors haven't been handled here yet.") + publisher.sink { [weak self] completion in + + switch completion { + case let .failure(error): + self?.events.append(.error(error)) + case .finished: + self?.events.append(.finished) + } + } receiveValue: { [weak self] value in - self?.events.append(value) + self?.events.append(.value(value)) } .store(in: &self.subscriptions) } diff --git a/KsApi/extensions/MockGraphQLClient+CombineTests.swift b/KsApi/extensions/MockGraphQLClient+CombineTests.swift new file mode 100644 index 0000000000..7af8abca13 --- /dev/null +++ b/KsApi/extensions/MockGraphQLClient+CombineTests.swift @@ -0,0 +1,59 @@ +import Combine +@testable import KsApi +import XCTest + +final class MockGraphQLClient_CombineTests: XCTestCase { + func testSuccess() { + let mockClient = MockGraphQLClient() + let observer = CombineTestObserver, ErrorEnvelope>() + + let fetchGraphUserEmailQuery = GraphAPI.FetchUserEmailQuery() + let fetchUserEmailQueryData = GraphAPI.FetchUserEmailQuery + .Data(unsafeResultMap: GraphUserEnvelopeTemplates.userJSONDict) + + guard let envelope = UserEnvelope.userEnvelope(from: fetchUserEmailQueryData) else { + XCTFail() + return + } + + let publisher: AnyPublisher, ErrorEnvelope> = + mockClient.fetchWithResult(query: fetchGraphUserEmailQuery, result: .success(envelope)) + + observer.observe(publisher) + + XCTAssertEqual(observer.events.count, 1) + + if case let .value(observedEnvelope) = observer.events.last { + XCTAssertEqual(observedEnvelope.me, envelope.me) + } else { + XCTFail() + } + } + + func testFailure() { + let mockClient = MockGraphQLClient() + let observer = CombineTestObserver, ErrorEnvelope>() + + let fetchGraphUserEmailQuery = GraphAPI.FetchUserEmailQuery() + let error = ErrorEnvelope( + errorMessages: ["Something went wrong"], + ksrCode: .GraphQLError, + httpCode: 503, + exception: nil + ) + + let publisher: AnyPublisher, ErrorEnvelope> = + mockClient.fetchWithResult(query: fetchGraphUserEmailQuery, result: .failure(error)) + + observer.observe(publisher) + + XCTAssertEqual(observer.events.count, 1) + + if case let .error(observedError) = observer.events.last { + XCTAssertEqual(observedError.ksrCode, error.ksrCode) + XCTAssertEqual(observedError.errorMessages, error.errorMessages) + } else { + XCTFail() + } + } +} diff --git a/KsApi/extensions/MockGraphQLClient.swift b/KsApi/extensions/MockGraphQLClient.swift index 620ddbcf58..259852d248 100644 --- a/KsApi/extensions/MockGraphQLClient.swift +++ b/KsApi/extensions/MockGraphQLClient.swift @@ -90,14 +90,9 @@ private func producer(for property: Result?) -> SignalProducer private func producer(for property: Result?) -> AnyPublisher { switch property { - case let .success(data): - return CurrentValueSubject(data).eraseToAnyPublisher() - case .failure: - // TODO(MBL-1015) Implement this as part of further networking code updates for SwiftUI. - assertionFailure("Need to implement this behavior. I think the Fail() subject is what we want, possibly with a deferred?") - return Empty(completeImmediately: false).eraseToAnyPublisher() - case .none: - return Empty(completeImmediately: false).eraseToAnyPublisher() + case let .success(data): return CurrentValueSubject(data).eraseToAnyPublisher() + case let .failure(error): return Fail(error: error).eraseToAnyPublisher() + case .none: return Empty(completeImmediately: false).eraseToAnyPublisher() } } diff --git a/Library/ViewModels/ReportProjectFormViewModelTests.swift b/Library/ViewModels/ReportProjectFormViewModelTests.swift index 1ef17805d6..4b40d535ca 100644 --- a/Library/ViewModels/ReportProjectFormViewModelTests.swift +++ b/Library/ViewModels/ReportProjectFormViewModelTests.swift @@ -31,13 +31,21 @@ final class ReportProjectFormViewModelTests: TestCase { userEmail.observe(vm.$retrievedEmail) XCTAssertEqual(userEmail.events.count, 1) - XCTAssertEqual(userEmail.events.last, Optional.some(nil)) + + if case let .value(value) = userEmail.events.last { + XCTAssertEqual(value, nil) + } vm.inputs.viewDidLoad() self.scheduler.advance() XCTAssertEqual(userEmail.events.count, 2) - XCTAssertEqual(userEmail.events.last, Optional("nativesquad@ksr.com")) + + if case let .value(value) = userEmail.events.last { + XCTAssertEqual(value, "nativesquad@ksr.com") + } else { + XCTFail() + } } } @@ -50,14 +58,28 @@ final class ReportProjectFormViewModelTests: TestCase { saveButtonEnabled.observe(vm.$saveButtonEnabled) XCTAssertEqual(saveButtonEnabled.events.count, 1) - XCTAssertEqual(saveButtonEnabled.events.last, false) + + if case let .value(value) = saveButtonEnabled.events.last { + XCTAssertEqual(value, false) + } else { + XCTFail() + } vm.detailsText = "This is my report. I don't like this project very much." XCTAssertEqual(saveButtonEnabled.events.count, 2) - XCTAssertEqual(saveButtonEnabled.events.last, true) + if case let .value(value) = saveButtonEnabled.events.last { + XCTAssertEqual(value, true) + } else { + XCTFail() + } vm.detailsText = "" + XCTAssertEqual(saveButtonEnabled.events.count, 3) - XCTAssertEqual(saveButtonEnabled.events.last, false) + if case let .value(value) = saveButtonEnabled.events.last { + XCTAssertEqual(value, false) + } else { + XCTFail() + } } }